Skip to content

第八十五章:文件之形

斩灵期

涉及内核源码:

林小源在森林中遇到了一群半透明的光影。

光影们在古树之间穿梭,没有固定的形态,像一团团凝固的烟雾。每个光影体内都闪烁着几个数字——有的是"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 的投影。打开时诞生,关闭时消散,仅此而已。


c
/*
 * 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");

道藏笔记

内核启示

file 是进程打开文件时创建的对象——open 的时候诞生,close 的时候消亡。

它身上最关键的是 (读到哪了)、(用什么权限打开的)和 (引用计数)。注意区分 file 和 inode:一个文件只有一个 inode,但可以被打开无数次,每次打开都是一个独立的 file 对象,各有各的读写位置。

fd 和 file 的关系也值得琢磨。fd 是进程对 file 的引用,多个 fd 可以指向同一个 file——fork 之后就是这样,父子进程共享同一个 file 对象,f_pos 是共享的,父进程读了 100 字节,子进程接着读就从 100 开始。如果不想互相影响,就得 open 一个新的或者用 dup()。


破关试炼

file 之试

进程打开文件后,内核为这次打开实例创建的对象在本章称为什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道