第八十五章:文件之形
斩灵期涉及内核源码:
一
林小源在森林中遇到了一群半透明的光影。
光影们在古树之间穿梭,没有固定的形态,像一团团凝固的烟雾。每个光影体内都闪烁着几个数字——有的是"fd 3",有的是"fd 4",有的是"fd 7"。
"你们是什么?"林小源拦住一个光影。
光影停了下来,烟雾般的身体微微旋转:"我是 file——进程打开文件时诞生的形体。你每次调用 open(),就有一个新的我出现。"
林小源伸手触摸光影。指尖穿过烟雾,但他感知到了一组信息:读写位置停在 1024,标志是只读,引用计数是 1。
"你和 inode 有什么区别?"他问。
"inode 是树的躯干,我是打开的化身,"光影说,"一棵树只有一个 inode,但可以被打开无数次。每次打开都是一个独立的我——有自己的读写位置,有自己的打开标志。"
旁边另一个光影凑了过来,烟雾中闪烁着"fd 6,count = 2"的字样。"我和它不同,"第二个光影说,"我是 fork 后共享的。父子进程都指向我——所以引用计数是 2。父进程读了 100 字节,我的 f_pos 就变成 100。子进程接着读,也是从 100 开始。"
"共享同一个 file 对象?"林小源惊讶地问。
"对,"第二个光影说,"fork 的时候,子进程复制了父进程的文件描述符表——但指向的是同一个 file 对象。f_pos 是共享的。如果父子进程同时读文件,会互相影响。"
林小源皱起眉头:"那如果不想共享呢?"
"open 一个新的,"第一个光影说,"或者用 dup() 复制一个新的 file 对象。每个 file 有自己的 f_pos——互不干扰。"
二
林小源在光影群中穿行,来到了一棵古树跟前。
古树的树干上嵌着一个小小的金属牌,牌上刻着"fd table"。金属牌下方有三条光线,分别连接着三个光影。
"这是进程的文件描述符表,"古树开口了,声音平静而稳重,"fd 0 是 stdin,fd 1 是 stdout,fd 2 是 stderr。从 fd 3 开始才是用户打开的文件。"
林小源看着那三条光线。fd 0 的光线连着一个金色光影,fd 1 连着银色光影,fd 2 连着暗红色光影。三个光影都在微微颤动,像是随时准备传输数据。
"fd 是整数,"古树说,"进程用 fd 来引用 file 对象。你调用 read(fd, buf, count),内核通过 fd 找到 file 对象,再通过 file 找到 inode,最后从磁盘读数据。三层——fd、file、inode。"
林小源点了点头。他伸手触摸 fd 3 对应的光影,感知到了更详细的信息:f_path 指向一个 dentry,dentry 指向 inode,inode 的 i_fop 指向具体的文件操作函数表。
"所以 read 的时候,"他低声说,"内核先查 fd 表找到 file,再查 file 找到 inode 和 f_pos,最后调用 inode 的 i_fop->read。"
"不错,"古树说,"这就是 VFS 的层次。fd 是进程视角,file 是打开视角,inode 是文件视角。三层各司其职。"
三
林小源在森林深处发现了一个即将消散的光影。
光影的烟雾已经稀薄得几乎透明,体内的数字"fd 5,count = 0"忽明忽暗。
"你要消散了?"林小源问。
光影发出一声微弱的叹息:"引用计数归零了。最后一个进程关闭了 fd——我没有主人了。等内核的垃圾回收来,我就会彻底消失。"
"你会带走什么?"
"什么都不带,"光影说,"f_pos、f_flags——都是我的临时状态。我消散了,inode 还在,数据还在。下一次有人调用 open(),会诞生一个新的 file 对象——新的 f_pos,新的 f_flags,但指向同一棵古树。"
林小源看着光影缓缓消散。烟雾散去后,古树的树干上多了一道浅浅的痕迹——那是 file 曾经存在过的证据。但痕迹很快就淡去了。
"file 是短暂的,"古树说,"open 的时候诞生,close 的时候消亡。它只是一次打开的记录——读到哪里了,用什么权限打开的。文件本身不关心这些。"
林小源转身离开。身后,又有新的光影从远处飘来——某个进程刚刚调用了 open(),一个新的 file 对象诞生了。
他忽然明白过来——所谓形体,不过是一次 open 的投影。打开时诞生,关闭时消散,仅此而已。
/*
* struct file 的关键字段:
*
* f_path — 路径(dentry + vfsmount)
* f_inode — 指向的 inode
* f_op — 文件操作函数表
* f_pos — 当前读写位置
* f_flags — 打开标志(O_RDONLY, O_WRONLY...)
* f_count — 引用计数
*
* file vs inode:
* inode — 文件的元数据(一个文件一个)
* file — 打开的文件(可以有多个)
*
* 一个文件可以被多次打开:
* fd1 = open("file", O_RDONLY) → file1
* fd2 = open("file", O_WRONLY) → file2
* 两个 file 对象指向同一个 inode
*
* fork 后共享 file:
* fork() 后,父子进程共享同一个 file
* f_pos 是共享的
*/
struct file {
int fd;
int inode_no;
long f_pos;
int f_flags;
int f_count;
};
printf("=== file — 打开的文件对象 ===\n\n");
/* 模拟打开文件 */
struct file files[4] = {
{3, 100, 0, 0x0000, 1}, /* O_RDONLY */
{4, 100, 0, 0x0001, 1}, /* O_WRONLY */
{5, 200, 1024, 0x0002, 1}, /* O_RDWR, 已读 1024 */
{6, 100, 512, 0x0000, 2}, /* fork 后共享 */
};
printf("打开的文件:\n");
printf("%-4s %-8s %-8s %-8s %-8s\n",
"fd", "inode", "pos", "flags", "count");
printf("%-4s %-8s %-8s %-8s %-8s\n",
"---", "---", "---", "---", "---");
for (int i = 0; i < 4; i++) {
printf("%-4d %-8d %-8ld %-8x %-8d\n",
files[i].fd, files[i].inode_no,
files[i].f_pos, files[i].f_flags,
files[i].f_count);
}
printf("\n--- file vs inode ---\n");
printf("inode:\n");
printf(" 文件的元数据\n");
printf(" 一个文件一个 inode\n");
printf(" 包含权限、大小、数据块位置\n\n");
printf("file:\n");
printf(" 打开的文件\n");
printf(" 可以有多个 file 指向同一个 inode\n");
printf(" 包含读写位置、打开标志\n\n");
printf("--- fork 后共享 file ---\n");
printf("fork() 后:\n");
printf(" 父子进程共享同一个 file 对象\n");
printf(" f_pos 是共享的\n");
printf(" 父进程读了 100 字节\n");
printf(" 子进程从 100 开始读\n\n");
printf("--- 文件描述符表 ---\n");
printf("进程的 fd 表:\n");
printf(" fd 0 → file (stdin)\n");
printf(" fd 1 → file (stdout)\n");
printf(" fd 2 → file (stderr)\n");
printf(" fd 3 → file (用户打开)\n");#include <stdio.h>
/*
* struct file 的关键字段:
*
* f_path — 路径(dentry + vfsmount)
* f_inode — 指向的 inode
* f_op — 文件操作函数表
* f_pos — 当前读写位置
* f_flags — 打开标志(O_RDONLY, O_WRONLY...)
* f_count — 引用计数
*
* file vs inode:
* inode — 文件的元数据(一个文件一个)
* file — 打开的文件(可以有多个)
*
* 一个文件可以被多次打开:
* fd1 = open("file", O_RDONLY) → file1
* fd2 = open("file", O_WRONLY) → file2
* 两个 file 对象指向同一个 inode
*
* fork 后共享 file:
* fork() 后,父子进程共享同一个 file
* f_pos 是共享的
*/
struct file {
int fd;
int inode_no;
long f_pos;
int f_flags;
int f_count;
};
int main() {
printf("=== file — 打开的文件对象 ===\n\n");
/* 模拟打开文件 */
struct file files[4] = {
{3, 100, 0, 0x0000, 1}, /* O_RDONLY */
{4, 100, 0, 0x0001, 1}, /* O_WRONLY */
{5, 200, 1024, 0x0002, 1}, /* O_RDWR, 已读 1024 */
{6, 100, 512, 0x0000, 2}, /* fork 后共享 */
};
printf("打开的文件:\n");
printf("%-4s %-8s %-8s %-8s %-8s\n",
"fd", "inode", "pos", "flags", "count");
printf("%-4s %-8s %-8s %-8s %-8s\n",
"---", "---", "---", "---", "---");
for (int i = 0; i < 4; i++) {
printf("%-4d %-8d %-8ld %-8x %-8d\n",
files[i].fd, files[i].inode_no,
files[i].f_pos, files[i].f_flags,
files[i].f_count);
}
printf("\n--- file vs inode ---\n");
printf("inode:\n");
printf(" 文件的元数据\n");
printf(" 一个文件一个 inode\n");
printf(" 包含权限、大小、数据块位置\n\n");
printf("file:\n");
printf(" 打开的文件\n");
printf(" 可以有多个 file 指向同一个 inode\n");
printf(" 包含读写位置、打开标志\n\n");
printf("--- fork 后共享 file ---\n");
printf("fork() 后:\n");
printf(" 父子进程共享同一个 file 对象\n");
printf(" f_pos 是共享的\n");
printf(" 父进程读了 100 字节\n");
printf(" 子进程从 100 开始读\n\n");
printf("--- 文件描述符表 ---\n");
printf("进程的 fd 表:\n");
printf(" fd 0 → file (stdin)\n");
printf(" fd 1 → file (stdout)\n");
printf(" fd 2 → file (stderr)\n");
printf(" fd 3 → file (用户打开)\n");
return 0;
}道藏笔记
内核启示
file 是进程打开文件时创建的对象——open 的时候诞生,close 的时候消亡。
它身上最关键的是 (读到哪了)、(用什么权限打开的)和 (引用计数)。注意区分 file 和 inode:一个文件只有一个 inode,但可以被打开无数次,每次打开都是一个独立的 file 对象,各有各的读写位置。
fd 和 file 的关系也值得琢磨。fd 是进程对 file 的引用,多个 fd 可以指向同一个 file——fork 之后就是这样,父子进程共享同一个 file 对象,f_pos 是共享的,父进程读了 100 字节,子进程接着读就从 100 开始。如果不想互相影响,就得 open 一个新的或者用 dup()。
file 之试
进程打开文件后,内核为这次打开实例创建的对象在本章称为什么?