Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← 系统与高性能 / Systems & Performance

xv6 源码学习 / xv6 Source Learning

1. xv6 源码学习专题 / xv6 Source Learning

2. xv6 学习路线:从 C 指针到操作系统源码 / xv6 Learning Roadmap

3. 读 xv6 必备的 C 语言和指针基础 / C And Pointers For xv6

4. 读 xv6 必备的 RISC-V 基础 / RISC-V Basics For xv6

5. 如何阅读 xv6 源码 / How To Read xv6 Source

6. xv6 启动与内核入口 / xv6 Boot And Kernel Entry

7. xv6 内存布局与页表 / xv6 Memory Layout And Page Tables

8. xv6 trap、中断与系统调用 / xv6 Traps Interrupts And System Calls

9. xv6 进程与 struct proc / xv6 Process And struct proc

10. xv6 上下文切换与调度器 / xv6 Context Switch And Scheduler

11. xv6 sleep/wakeup、wait 与 exit / xv6 Sleep Wakeup Wait And Exit

12. xv6 锁与并发 / xv6 Locks And Concurrency

13. xv6 文件描述符与 inode / xv6 File Descriptor And Inode

14. xv6 文件系统与日志 / xv6 File System And Log

15. xv6 设备驱动、console、UART 与磁盘 / xv6 Device Driver Console UART And Disk

16. xv6 用户程序与 shell / xv6 User Programs And Shell

17. 用 QEMU/GDB 调试 xv6 / Debugging xv6 With QEMU And GDB

18. 从 xv6 过渡到 Linux 0.11 / Bridge From xv6 To Linux 0.11

19. xv6 专题总结与实践项目 / xv6 Summary And Practice Projects

本页目录

xv6 文件描述符与 inode / xv6 File Descriptor And Inode ​

1. 这篇文章解决什么问题 ​

用户程序不会直接拿到 inode 指针。

用户程序也不会直接操作磁盘块。

它拿到的是一个很小的整数。

这个整数叫文件描述符。

英文是 file descriptor,常写成 fd。

例如:

c
int fd = open("README", 0);
read(fd, buf, sizeof(buf));
close(fd);
1
2
3

fd 看起来像文件本身。

但它不是文件。

它只是当前进程里的一张表的下标。

真正的层次更长:

text
user fd number
  |
  v
proc.ofile[fd]
  |
  v
struct file
  |
  +-- FD_INODE  -> struct inode -> disk inode -> data blocks
  |
  +-- FD_PIPE   -> struct pipe  -> memory circular buffer
  |
  +-- FD_DEVICE -> devsw[major] -> console / other device driver
1
2
3
4
5
6
7
8
9
10
11
12
13

这篇文章要把这条链路讲清楚。

重点问题包括:

  • fd 到底是什么。
  • proc.ofile[] 为什么属于进程。
  • struct file 为什么是全局对象。
  • dup() 后两个 fd 为什么共享 offset。
  • inode 和 struct file 的区别是什么。
  • pipe 和 device 为什么也能用 read/write。
  • close() 到底释放了哪一层。

本篇基于标准 MIT xv6-riscv 的文件系统结构。

本地 E:\Github\xv6-riscv 可读,本文列出的源码文件与本地仓库中的标准文件名一致。

1.1 本文基础词小注释 ​

文件描述符 fd:用户程序手里的小整数。它不是文件本体,只是当前进程打开文件表 ofile[] 的下标。比如 fd=0 通常是标准输入,fd=1 通常是标准输出。

proc.ofile[]:每个进程自己的“打开文件槽位数组”。同样是 fd=3,在不同进程里可以指向完全不同的打开文件对象。

text
------------- current process -------------+
| ofile[0] -> stdin                         |
| ofile[1] -> stdout                        |
| ofile[2] -> stderr                        |
| ofile[3] -> struct file for README        |
+-------------------------------------------+
1
2
3
4
5
6

struct file:内核中的“打开文件对象”。它记录这个打开动作的状态,比如读写权限、当前 offset、引用计数,以及背后连接的是 inode、pipe 还是 device。

offset:当前读写位置。对普通文件来说,连续 read() 会从上次读完的位置继续读,就是因为 struct file 里保存了 offset。

inode:文件系统里的“文件身份和元数据”。inode 记录类型、大小、数据块地址等。文件名不是 inode 的本体,目录项才把名字映射到 inode 编号。

pipe:管道是一段内核内存缓冲区,用来让一个进程写、另一个进程读。它没有磁盘 inode,但可以伪装成文件一样用 read/write。

device:设备也可以被包装成文件接口。比如 console 可以用 read/write,但底层不是磁盘块,而是驱动程序。

2. 最简单心智模型 ​

先把文件 I/O 想成三层翻译。

第一层是用户看到的整数。

第二层是内核的打开文件对象。

第三层才是具体后端。

图:

text
Process A

  fd table
  +----+----------------+
  | 0  | struct file *  | ---> console device
  | 1  | struct file *  | ---> console device
  | 2  | struct file *  | ---> console device
  | 3  | struct file *  | ---> inode of "a.txt"
  +----+----------------+

Global file table

  struct file
  +------------------+
  | type = FD_INODE  |
  | readable = 1     |
  | writable = 0     |
  | off = 128        |
  | ip = inode *     |
  +------------------+

Inode cache

  struct inode
  +------------------+
  | dev              |
  | inum             |
  | type = T_FILE    |
  | size             |
  | addrs[]          |
  +------------------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

一句话:

text
fd 是进程私有的编号,struct file 是打开实例,inode 是文件本体的元数据。
1

这句话很重要。

后面所有误解几乎都来自把这三者混成一个东西。

3. 涉及的 xv6 源码文件 ​

本篇主要涉及这些内核源码:

text
kernel/proc.h
kernel/file.h
kernel/file.c
kernel/sysfile.c
kernel/fs.h
kernel/fs.c
kernel/pipe.c
kernel/stat.h
kernel/fcntl.h
kernel/defs.h
kernel/param.h
kernel/console.c
1
2
3
4
5
6
7
8
9
10
11
12

kernel/proc.h 定义进程结构。

其中 struct proc 里有 ofile[NOFILE]。

kernel/file.h 定义 struct file、struct inode、struct devsw。

kernel/file.c 实现通用文件对象操作。

包括 filealloc、filedup、fileclose、fileread、filewrite。

kernel/sysfile.c 实现文件相关系统调用。

包括 open、read、write、close、dup、link、unlink、mkdir、mknod、pipe。

kernel/fs.h 定义磁盘上的文件系统格式。

包括 superblock、dinode、dirent、块号计算宏。

kernel/fs.c 实现 inode、目录、路径解析、读写 inode 内容。

kernel/pipe.c 实现内存管道。

kernel/console.c 把 console 注册成设备文件的读写函数。

kernel/stat.h 定义用户可见的文件类型和 struct stat。

kernel/fcntl.h 定义 O_RDONLY、O_WRONLY、O_RDWR、O_CREATE、O_TRUNC 等 open 标志。

kernel/param.h 定义 NOFILE、NFILE、NINODE 等容量限制。

4. 什么是 fd ​

fd 是 file descriptor 的缩写。

中文常译为文件描述符。

它在 xv6 中是一个整数。

这个整数只对当前进程有意义。

例如进程 A 的 fd=3 和进程 B 的 fd=3 可以指向完全不同的文件。

原因是每个进程都有自己的打开文件表。

这张表在 struct proc 里。

概念上像这样:

text
struct proc
  |
  +-- ofile[0]
  +-- ofile[1]
  +-- ofile[2]
  +-- ofile[3]
  ...
1
2
3
4
5
6
7

ofile 中每一项是 struct file *。

所以 fd 的本质是:

text
fd = index into current process's ofile array
1

当用户调用:

c
read(fd, buf, n);
1

内核会先检查 fd 是否合法。

然后用它取出 myproc()->ofile[fd]。

如果这一项为空,说明这个 fd 没有打开。

如果这一项不为空,才继续进入 fileread()。

5. 为什么 fd 是进程私有的 ​

进程之间需要隔离。

一个进程关闭自己的 fd=3,不应该自动关闭另一个进程的 fd=3。

因此 fd 不能是全局编号。

它必须是进程本地编号。

图:

text
Process A                  Process B

ofile[3] ----> file X      ofile[3] ----> file Y
1
2
3

同一个数字,在不同进程里可以指向不同 struct file。

这和虚拟地址有点像。

同一个虚拟地址,在不同进程中也可以映射到不同物理页。

区别是 fd 映射到内核中的 file 对象。

6. proc.ofile[] 的职责 ​

proc.ofile[] 是进程的打开文件指针数组。

它不保存文件内容。

它不保存 inode 里的磁盘地址。

它只保存当前进程能访问哪些打开文件对象。

职责可以写成:

text
proc.ofile[]
  |
  +-- define fd namespace for this process
  +-- point fd numbers to struct file objects
  +-- provide inheritance across fork
  +-- clear entries on close and exit
1
2
3
4
5
6

open() 会填入一项。

close() 会清空一项。

dup() 会找一个新空位,指向同一个 struct file。

fork() 会让子进程复制父进程的 ofile 指针,并增加引用计数。

exit() 会关闭进程剩余打开的文件。

这张表是进程资源的一部分。

不是文件系统全局状态。

7. struct file 是什么 ​

struct file 表示一次“打开文件”的内核对象。

它定义在 kernel/file.h。

它大致包含:

text
struct file
  |
  +-- type
  +-- ref
  +-- readable
  +-- writable
  +-- pipe
  +-- ip
  +-- off
  +-- major
1
2
3
4
5
6
7
8
9
10

字段含义:

type 表示这个 file 对象背后是什么。

ref 是引用计数。

readable 表示是否允许读。

writable 表示是否允许写。

pipe 指向管道对象,仅用于 FD_PIPE。

ip 指向 inode,用于 FD_INODE 和 FD_DEVICE。

off 是普通 inode 文件的当前读写偏移。

major 是设备主设备号,用于设备分发。

为什么需要 struct file?

因为一次打开不仅需要知道“哪个文件”。

还需要知道“这次打开的状态”。

比如:

text
same inode
  |
  +-- open instance A: off = 0
  |
  +-- open instance B: off = 4096
1
2
3
4
5

两个进程可以打开同一个路径。

它们会得到不同 struct file。

这两个 struct file 可以指向同一个 inode。

但它们的 offset 可以不同。

8. 全局 file table ​

xv6 有全局文件表。

它在 kernel/file.c 里。

概念上:

text
ftable
  |
  +-- lock
  +-- file[NFILE]
1
2
3
4

filealloc() 从这里找一个 ref == 0 的槽位。

找到后把 ref 设为 1。

filedup() 增加 ref。

fileclose() 减少 ref。

当 ref 降到 0,才真正释放底层资源。

为什么 file table 是全局的?

因为多个进程可以共享同一个打开实例。

最常见的是 fork()。

父子进程会共享打开文件对象。

另一个例子是 dup()。

同一个进程中两个 fd 可以指向同一个 struct file。

图:

text
ofile[3] ----+
             |
             v
          struct file
             ^
             |
ofile[4] ----+
1
2
3
4
5
6
7

这就是为什么 dup() 后两个 fd 共享 offset。

它们不是两个打开实例。

它们是两个 fd 指向同一个打开实例。

9. inode 是什么 ​

inode 是 index node 的缩写。

可以理解为“文件的编号和元数据对象”。

它不是文件名。

它也不是打开实例。

它描述一个文件本体。

inode 里有:

text
inode
  |
  +-- type
  +-- nlink
  +-- size
  +-- addrs[]
1
2
3
4
5
6

type 表示文件类型。

nlink 表示有多少目录项链接到这个 inode。

size 表示文件大小。

addrs[] 表示文件数据块的位置。

struct inode 是内存中的 inode 缓存对象。

struct dinode 是磁盘上的 inode 格式。

这两个名字只差一个字母,很容易混。

关系:

text
on disk:
  struct dinode

in memory:
  struct inode
1
2
3
4
5

内存 inode 多了 ref、lock、valid 等运行时字段。

磁盘 dinode 只保存持久化元数据。

10. 文件名和 inode 的关系 ​

文件名不在 inode 里。

文件名在目录内容里。

目录本身也是一种文件。

目录文件的内容是许多 struct dirent。

每个目录项包含:

text
dirent
  |
  +-- inum
  +-- name
1
2
3
4

name 是这个目录中的名字。

inum 是对应 inode 编号。

因此路径解析本质是:

text
"/a/b/c"
  |
  v
root inode
  |
  v
look up "a" in root directory
  |
  v
inode of a
  |
  v
look up "b"
  |
  v
inode of b
  |
  v
look up "c"
  |
  v
inode of c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

同一个 inode 可以有多个名字。

这就是硬链接。

link(old, new) 会让 new 目录项指向 old 的 inode。

同时增加 inode 的 nlink。

11. fd、file、inode 的层次 ​

完整层次如下:

text
user process
  |
  | fd = small int
  v
struct proc
  |
  | ofile[fd]
  v
struct file
  |
  | type decides backend
  +--------------------------+
  |                          |
  v                          v
struct inode              struct pipe
  |                          |
  v                          v
disk metadata             memory buffer
  |
  v
data blocks
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

可以把它拆成三类问题:

fd 解决命名问题。

struct file 解决打开实例问题。

inode 解决文件本体问题。

pipe 不需要 inode。

device 需要 inode 作为文件系统中的入口,但真正读写走设备函数。

普通文件需要 inode 和磁盘块。

12. open 的路径 ​

open(path, omode) 的主要逻辑在 sys_open()。

它大致分成几步。

第一步,从用户参数取出路径和打开模式。

第二步,调用 begin_op() 进入文件系统日志事务。

第三步,如果有 O_CREATE,创建 inode。

第四步,如果不是创建,就用 namei() 找到 inode。

第五步,检查目录不能以写模式打开。

第六步,分配 struct file。

第七步,在当前进程 ofile[] 中分配 fd。

第八步,把 file 对象和 inode 连接起来。

第九步,释放 inode 锁,结束事务,返回 fd。

图:

text
sys_open
  |
  v
namei/create -> inode
  |
  v
filealloc -> struct file
  |
  v
fdalloc -> proc.ofile[fd]
  |
  v
return fd to user
1
2
3
4
5
6
7
8
9
10
11
12
13

关键点:

open() 同时创建两种关系。

一种是 file 指向 inode。

另一种是 fd 指向 file。

如果其中一步失败,必须清理已经分配的对象。

所以 sys_open() 的错误处理比表面上复杂。

13. read 的路径 ​

read(fd, buf, n) 的系统调用入口是 sys_read()。

它先通过 argfd() 把 fd 转为 struct file *。

然后调用 fileread()。

fileread() 根据 f->type 分发。

text
sys_read
  |
  v
argfd
  |
  v
fileread
  |
  +-- FD_PIPE   -> piperead
  |
  +-- FD_DEVICE -> devsw[major].read
  |
  +-- FD_INODE  -> readi
1
2
3
4
5
6
7
8
9
10
11
12
13

对于普通文件:

fileread() 会锁住 inode。

然后从 f->off 开始读取。

如果读取成功,就推进 f->off。

最后释放 inode 锁。

所以 offset 属于 struct file。

不是属于 inode。

这解释了两个独立 open 的 offset 不共享。

也解释了 dup 后 offset 共享。

14. write 的路径 ​

write(fd, buf, n) 的入口是 sys_write()。

它同样先 argfd()。

然后调用 filewrite()。

filewrite() 也根据类型分发。

text
sys_write
  |
  v
filewrite
  |
  +-- FD_PIPE   -> pipewrite
  |
  +-- FD_DEVICE -> devsw[major].write
  |
  +-- FD_INODE  -> writei inside log transaction
1
2
3
4
5
6
7
8
9
10

普通文件写入有一个额外细节。

xv6 不会一次把任意大的写请求放进一个事务。

它会把大写入拆成小块。

原因是日志空间有限。

一次写入可能修改:

  • 数据块。
  • bitmap 块。
  • inode 块。
  • 间接块。
  • 非对齐写入涉及的旧块。

如果事务太大,日志放不下。

所以 filewrite() 会按 MAXOPBLOCKS 推导一个最大分块大小。

每个分块单独 begin_op() 和 end_op()。

这说明文件层知道日志限制。

它不是单纯把请求丢给 inode 层。

15. close 的路径 ​

close(fd) 的入口是 sys_close()。

它先拿到 fd 和 file。

然后把 myproc()->ofile[fd] 设为 0。

最后调用 fileclose(f)。

注意顺序。

先从进程 fd 表移除。

再减少 file 引用计数。

fileclose() 做两件事。

先减少 f->ref。

如果 ref 仍然大于 0,直接返回。

如果 ref 变为 0,就根据类型释放底层资源。

图:

text
close(fd)
  |
  v
proc.ofile[fd] = 0
  |
  v
fileclose
  |
  +-- ref still > 0: done
  |
  +-- ref == 0:
        |
        +-- FD_PIPE   -> pipeclose
        +-- FD_INODE  -> iput inside transaction
        +-- FD_DEVICE -> iput inside transaction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

close() 不一定释放 inode。

它只释放当前 fd 对 file 的引用。

只有 file 引用计数归零,才进入底层释放。

只有 inode 的内存引用和磁盘链接都满足条件,inode 才可能真正释放磁盘块。

16. dup 为什么共享 offset ​

dup(oldfd) 创建一个新 fd。

新 fd 指向同一个 struct file。

然后 file 引用计数加一。

不是复制一个新的 file 对象。

图:

text
before dup

fd 3 ---> struct file { off = 100 }

after dup

fd 3 ---+
        |
        v
     struct file { off = 100 }
        ^
        |
fd 4 ---+
1
2
3
4
5
6
7
8
9
10
11
12
13

如果用 fd=3 读了 10 字节,off 变为 110。

再用 fd=4 读,会从 110 开始。

因为两者共享同一个 file 对象。

这个行为符合 Unix 语义。

如果你想要两个独立 offset,就应该调用两次 open()。

17. fork 后文件发生什么 ​

fork() 会复制父进程的打开文件表。

但复制的是指针。

不是复制底层文件内容。

父子进程会共享相同的 struct file。

引用计数会增加。

图:

text
parent ofile[3] ----+
                    |
                    v
                 struct file
                    ^
                    |
child  ofile[3] ----+
1
2
3
4
5
6
7

因此父子进程也会共享 offset。

这解释了为什么 shell 重定向和管道可以跨 fork 工作。

父进程先设置好 fd。

子进程继承 fd。

然后 exec() 替换用户程序代码,但通常保留打开文件表。

于是新程序还能用标准输入输出。

18. exec 后文件发生什么 ​

exec() 替换当前进程的用户地址空间。

但它不默认关闭 fd。

这对 shell 非常重要。

例如:

sh
echo hello > out
1

shell 可以在 fork 后的子进程里把 fd=1 指向文件 out。

然后子进程 exec echo。

echo 不需要知道重定向细节。

它只向 fd=1 写。

内核已经把 fd=1 连接到目标文件。

这就是 fd 抽象的力量。

19. pipe 为什么也是文件 ​

pipe 没有磁盘 inode。

它是一段内核内存缓冲区。

但用户仍然用 read() 和 write() 操作它。

原因是 struct file 的 type 可以是 FD_PIPE。

pipe 的 file 对象里保存 struct pipe *。

fileread() 看到 FD_PIPE 就调用 piperead()。

filewrite() 看到 FD_PIPE 就调用 pipewrite()。

图:

text
fd
  |
  v
struct file { type = FD_PIPE }
  |
  v
struct pipe
  |
  +-- data circular buffer
  +-- nread
  +-- nwrite
  +-- readopen
  +-- writeopen
1
2
3
4
5
6
7
8
9
10
11
12
13

pipe 是字节流。

它没有路径名。

它没有磁盘块。

它有读端和写端。

pipe() 系统调用一次返回两个 fd。

一个可读。

一个可写。

20. pipe 的读写语义 ​

管道读空时会等待。

管道写满时会等待。

读端关闭后,写入会失败。

写端关闭后,读取剩余数据后会返回 0。

这些语义靠 struct pipe 的字段维护。

text
nread
  |
  +-- total bytes consumed

nwrite
  |
  +-- total bytes produced

buffer index
  |
  +-- nread % PIPESIZE
  +-- nwrite % PIPESIZE
1
2
3
4
5
6
7
8
9
10
11
12

这里的 nread 和 nwrite 单调增加。

它们通过取模映射到环形数组。

这个技巧可以区分空和满。

text
empty: nread == nwrite
full : nwrite == nread + PIPESIZE
1
2

21. device 为什么也像文件 ​

设备文件是 Unix 的一个重要思想。

用户程序仍然调用 read() 和 write()。

内核根据 major 设备号分发到驱动函数。

xv6 中设备分发表叫 devsw。

它定义在 kernel/file.h。

概念上:

text
devsw[major]
  |
  +-- read function
  +-- write function
1
2
3
4

console 设备初始化时会注册:

text
devsw[CONSOLE].read  = consoleread
devsw[CONSOLE].write = consolewrite
1
2

当 struct file 的类型是 FD_DEVICE 时:

text
filewrite
  |
  v
devsw[f->major].write(...)
1
2
3
4

设备文件有 inode。

但数据不在 inode 的数据块里。

inode 主要保存类型和设备号。

实际读写由驱动完成。

22. FD_INODE、FD_PIPE、FD_DEVICE ​

struct file 的 type 字段区分后端。

常见值:

text
FD_NONE
FD_PIPE
FD_INODE
FD_DEVICE
1
2
3
4

FD_NONE 表示空闲 file 槽位。

FD_PIPE 表示管道。

FD_INODE 表示普通文件或目录对应的 inode 打开实例。

FD_DEVICE 表示设备文件。

普通文件和设备文件都可能有 ip。

区别在于读写路径。

FD_INODE 调 readi/writei。

FD_DEVICE 调 devsw。

这说明 inode 不是“只能代表磁盘普通文件”。

它也可以代表目录项中的设备节点。

23. inode 引用计数和链接计数 ​

inode 有两个容易混淆的计数。

第一个是内存引用计数 ip->ref。

第二个是磁盘链接计数 ip->nlink。

ref 表示内核里有多少引用正在指向这个内存 inode。

nlink 表示文件系统目录树中有多少名字指向这个 inode。

图:

text
ip->ref
  |
  +-- runtime pointer count
  +-- affected by iget, idup, iput

ip->nlink
  |
  +-- persistent directory link count
  +-- affected by link, unlink, mkdir
1
2
3
4
5
6
7
8
9

一个文件被 unlink() 后,nlink 可能变成 0。

但如果它仍然被某个进程打开,ref 仍然大于 0。

这时文件数据不能立刻释放。

只有最后一个引用也关闭后,才真正释放内容。

这就是 Unix 的“删除打开文件”语义。

24. inode lock 和 file table lock ​

不同层有不同锁。

ftable.lock 保护全局 file table。

它主要保护 ref 和槽位分配。

itable.lock 保护 inode table 的槽位和 ref。

ip->lock 是 sleeplock。

它保护 inode 的内容字段。

例如 type、size、addrs[]。

图:

text
file table metadata
  |
  v
ftable.lock

inode table metadata
  |
  v
itable.lock

one inode's persistent fields
  |
  v
ip->lock
1
2
3
4
5
6
7
8
9
10
11
12
13
14

读源码时不要只写“加锁保证安全”。

要问:

text
这把锁保护哪个对象的哪个不变量?
1

25. fdalloc 和 argfd ​

sysfile.c 里有两个很小但很关键的辅助函数。

第一个是 argfd()。

它把系统调用参数中的 fd 转成 struct file *。

它会检查:

  • fd 是否在合法范围内。
  • myproc()->ofile[fd] 是否非空。
  • 调用者是否需要返回 fd 数字。
  • 调用者是否需要返回 file 指针。

第二个是 fdalloc()。

它从当前进程 ofile[] 里找一个空位。

找到后放入给定 file 指针。

它返回新的 fd。

职责图:

text
argfd:
  fd number -> struct file *

fdalloc:
  struct file * -> new fd number
1
2
3
4
5

这两个函数都只处理当前进程的 fd 表。

它们不分配 inode。

它们不分配磁盘块。

它们也不直接操作设备。

26. fstat 的路径 ​

fstat(fd, st) 用来获取文件元数据。

它不读取文件内容。

路径如下:

text
sys_fstat
  |
  v
argfd
  |
  v
filestat
  |
  v
ilock(ip)
  |
  v
stati(ip, &st)
  |
  v
copyout to user
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

struct stat 中包含:

  • dev。
  • ino。
  • type。
  • nlink。
  • size。

这些信息来自 inode。

所以 pipe 不能正常 fstat。

因为 pipe 没有 inode。

设备文件可以 fstat。

因为设备节点有 inode。

27. unlink 和打开文件 ​

unlink(path) 删除的是目录项。

它不是直接删除打开文件对象。

它也不是直接删除 fd。

路径关系:

text
directory entry
  |
  v
inode
  |
  v
open file object
  |
  v
fd table entry
1
2
3
4
5
6
7
8
9
10

unlink() 主要减少 inode 的 nlink。

如果某个进程还打开着这个 inode,ip->ref 仍然大于 0。

文件内容会保留。

最后一个 close() 触发 iput() 后,才可能释放。

这就是为什么可以创建临时文件:

text
open temporary file
unlink its name
keep fd
use fd
close fd
kernel finally frees storage
1
2
3
4
5
6

这不是魔法。

只是名字、打开实例、文件本体分层后的自然结果。

28. 目录也是 inode ​

目录在 xv6 中也是 inode。

它的 type 是 T_DIR。

目录内容由一串 dirent 组成。

普通 read() 可以读目录吗?

在 xv6 的简化世界里,目录读写限制不像现代 Unix 那么完整。

但在 open() 中,xv6 会阻止以写模式打开目录。

原因是目录结构必须由文件系统代码维护。

如果用户能任意写目录内容,就会破坏路径树。

例如可能写出:

  • 指向不存在 inode 的目录项。
  • 没有 . 的目录。
  • .. 指错父目录。
  • 重复名字。

所以目录也是文件,但不是普通字节文件。

29. 设备节点也是 inode ​

mknod(path, major, minor) 会创建设备节点。

设备节点的 inode 类型是 T_DEVICE。

inode 里保存 major 和 minor。

major 选择设备驱动。

minor 可以表示同类设备中的具体实例。

xv6 中 console 的主设备号是 CONSOLE。

概念图:

text
/console directory entry
  |
  v
inode { type = T_DEVICE, major = CONSOLE }
  |
  v
open -> struct file { type = FD_DEVICE, major = CONSOLE }
  |
  v
devsw[CONSOLE]
1
2
3
4
5
6
7
8
9
10

这解释了为什么设备可以出现在文件系统路径中。

路径负责找到设备节点。

读写负责调用设备驱动。

30. 源码职责总览 ​

把职责压缩成一张表:

text
proc.h
  owns per-process fd table

sysfile.c
  validates syscall args
  maps fd to struct file
  creates and connects objects

file.c
  manages global file table
  dispatches read/write/close by file type

file.h
  defines file, inode, devsw

fs.c
  implements inode, directories, path lookup, readi/writei

pipe.c
  implements pipe backend for FD_PIPE

console.c
  registers console backend for FD_DEVICE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这张表也给出阅读顺序。

如果从 sys_open() 开始读,可以向下追。

如果从 read() 开始读,可以横向看分发。

如果从 unlink() 开始读,可以重点看 inode 引用和链接计数。

31. 常见误解 ​

误解一:fd 是全局文件编号。

不是。

fd 是进程私有表的下标。

误解二:fd 就是 inode。

不是。

fd 指向 file,file 才可能指向 inode。

误解三:每次 dup 都复制 offset。

不是。

dup 后共享同一个 struct file,所以共享 offset。

误解四:每次 open 同一路径都共享 offset。

不是。

两次独立 open 通常得到两个 struct file,offset 独立。

误解五:unlink 会立即让所有 fd 失效。

不是。

unlink 删除名字,打开的 file/inode 引用仍然可以继续使用。

误解六:pipe 是磁盘文件。

不是。

pipe 是内核内存缓冲区,只是通过 file 抽象接入 read/write。

误解七:device 文件的数据存放在 inode 的数据块里。

不是。

设备 inode 只提供路径入口和设备号,数据由驱动产生或消费。

误解八:close 一定释放底层对象。

不是。

close 只减少引用,最后一个引用关闭时才释放底层资源。

32. 调试和阅读方法 ​

读文件描述符相关源码时,可以从系统调用入口开始。

例如读 read():

text
sys_read
  |
  v
argfd
  |
  v
fileread
  |
  v
type dispatch
1
2
3
4
5
6
7
8
9
10

读 open():

text
sys_open
  |
  v
namei/create
  |
  v
filealloc
  |
  v
fdalloc
1
2
3
4
5
6
7
8
9
10

读 close():

text
sys_close
  |
  v
ofile[fd] = 0
  |
  v
fileclose
  |
  v
backend cleanup
1
2
3
4
5
6
7
8
9
10

每追到一个对象,问三个问题:

这个对象属于谁?

这个对象的生命周期如何结束?

这个对象的引用计数在哪里变化?

这样比逐行背代码更稳。

33. 小实验一:观察 dup 共享 offset ​

写一个用户程序。

步骤:

text
open file
write known string
close
open file for reading
dup fd
read 1 byte from fd
read 1 byte from duplicated fd
print two bytes
1
2
3
4
5
6
7
8

如果文件内容是 abc。

第一次读原 fd 得到 a。

第二次读 dup fd 应该得到 b。

这说明两个 fd 共享 offset。

可以在 file.c 的 fileread() 临时打印:

text
fd backend file pointer
old off
bytes read
new off
1
2
3
4

注意打印不要长期保留。

文件读写路径很高频。

34. 小实验二:比较两次 open ​

再写一个程序。

步骤:

text
open same file as fd1
open same file as fd2
read 1 byte from fd1
read 1 byte from fd2
1
2
3
4

如果文件内容是 abc。

两个读都应该得到 a。

原因是两次 open 得到两个不同 struct file。

它们的 offset 都从 0 开始。

这个实验能把 dup 和 open 的差异刻进脑子里。

35. 小实验三:unlink 打开的文件 ​

写一个程序:

text
open("tmp", O_CREATE | O_RDWR)
write("hello")
unlink("tmp")
read/write using fd
close(fd)
1
2
3
4
5

观察现象:

路径名消失。

fd 仍然能访问打开文件。

关闭后文件内容才会被释放。

可以在 fs.c 的 iput() 附近临时打印:

text
iput: inum ref nlink
1

重点观察 nlink == 0 但 ref > 0 的阶段。

36. 小实验四:pipe 的两个 fd ​

写一个程序:

text
int p[2];
pipe(p);
write(p[1], "x", 1);
read(p[0], buf, 1);
1
2
3
4

观察:

p[0] 对应可读 file。

p[1] 对应可写 file。

两个 file 指向同一个 struct pipe。

在 pipealloc() 临时打印两个 struct file * 和一个 struct pipe *。

你会看到:

text
read file != write file
read file->pipe == write file->pipe
1
2

这说明 pipe 是两个打开端共享一个管道对象。

37. 小实验五:设备文件路径 ​

观察 xv6 启动时创建的 console。

在用户空间打开 console。

然后写入文本。

路径大致是:

text
open("console")
  |
  v
inode type T_DEVICE
  |
  v
struct file type FD_DEVICE
  |
  v
devsw[CONSOLE].write
  |
  v
consolewrite
1
2
3
4
5
6
7
8
9
10
11
12
13

可以在 consolewrite() 打印一次提示。

不要在每个字符都打印。

否则输出会混乱。

38. 工程取舍 ​

xv6 的文件抽象很小。

这让源码适合学习。

但它也有明显限制。

例如:

  • 没有 close-on-exec 标志。
  • 没有复杂权限模型。
  • 没有 VFS 层支持多种文件系统。
  • 没有 poll/epoll。
  • 没有 socket。
  • 没有 per-open flags 的完整集合。
  • 没有文件锁。

这些省略不是错误。

它们是教学取舍。

xv6 保留了 Unix 文件抽象的骨架。

同时把细节压缩到读者可以掌握的规模。

理解这套骨架后,再看 Linux 的 struct file、struct dentry、struct inode、VFS 层,会更容易。

39. 和现代 Unix 的对应关系 ​

xv6 的层次能对应到现代系统。

但现代系统更复杂。

大致关系:

text
xv6 fd table
  |
  v
Linux files_struct / fdtable

xv6 struct file
  |
  v
Linux struct file

xv6 inode
  |
  v
Linux struct inode

xv6 directory lookup
  |
  v
Linux dentry + VFS path walk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Linux 增加了 dentry cache。

Linux 支持多个文件系统。

Linux 的权限、挂载、命名空间和设备模型也复杂得多。

但“fd -> open file -> inode/device/pipe”的主线仍然存在。

所以 xv6 是理解 Unix I/O 的好入口。

40. 本篇总结 ​

文件描述符不是文件。

它是当前进程 ofile[] 的下标。

struct file 不是 inode。

它是一次打开的状态。

inode 不是文件名。

它是文件本体的元数据和块地址。

pipe 和 device 能复用 read/write。

原因是 struct file 用 type 做统一分发。

最终心智模型:

text
fd
  |
  v
proc.ofile[]
  |
  v
struct file
  |
  +-- open state: ref, readable, writable, off
  |
  +-- backend:
        |
        +-- inode  -> file system
        +-- pipe   -> kernel memory buffer
        +-- device -> driver table
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

下一篇进入:

text
13-file-system-and-log.md
1

那里会继续向下看 inode 如何映射到磁盘块,以及日志如何保证文件系统更新不被崩溃撕裂。

最后更新于:

Pager
上一篇12. xv6 锁与并发 / xv6 Locks And Concurrency
下一篇14. xv6 文件系统与日志 / xv6 File System And Log

持续记录,持续成长

Copyright © Tidenflow