1. PCB(task_struct,进程控制块)
操作系统每创建一个程序(进程),就要在内存里开辟一块空间记录这个进程的全部信息,这块信息就叫 PCB。 里面存着:进程 ID、进程状态、内存信息、打开了哪些文件等等一大堆东西。
其中有一个成员,是专门管文件的:files,它指向文件描述符表。
简单理解: PCB = 这个进程的「个人档案袋」,档案袋里面有一张清单,就是文件描述符表。
2. 文件描述符表
它本质是一个数组,属于这个进程私有,别的进程看不到这个表。 数组的每一条记录包含两样东西:
- 下标:文件描述符 fd,就是一个普通整数(0、1、2、3、4......)
- 数组的值:一个指针,指向内核里的
struct file对象。
举例子 默认启动进程,自动打开 3 个文件: fd=0 标准输入(键盘) fd=1 标准输出(屏幕打印) fd=2 标准错误(报错信息)
重点: fd 只是数组的编号,本身不是文件,只是一个快捷号码。 不同进程可以都用 fd=3,互相之间完全不冲突,因为每个进程有自己独立的这张表。
3. struct file(文件打开实例)
当你调用open()打开一个文件,内核就会在内存新建一个 struct file。
关键点:每 open 一次,就新建一个 struct file。
它记录的是「这次打开文件之后的状态」,里面重要信息:
- f_pos:当前读到文件的哪个位置(读写偏移量)
- f_flags:你是以只读、只写还是追加方式打开的
- f_inode:指针,指向这个文件对应的 inode
- f_op:文件操作函数(read、write、close 等)
- f_count:引用计数,记录现在有多少个 fd 在引用这个 struct file
生活化理解: inode 是一本书本身; struct file 就是你翻开这本书看书,会记住你读到第几页(f_pos)。 你两次打开同一本书,相当于翻开两份独立的阅读记录,各自读到的页码互不影响。
重要场景: fork 创建子进程的时候,子进程会复制父进程的文件描述符表。父子进程 fd 指向同一个 struct file。 此时大家共用同一个 f_pos,你写一点,另一个读的时候就从新位置读。管道进程通信就是靠这个特性实现的。
close (fd) 做的事情:把 struct file 的引用计数减 1。当计数变成 0,内核就释放这个 struct file。
4. struct inode(文件本体)
inode 代表文件本身,存在内核内存里(从磁盘加载上来)。 磁盘上的一个文件,永远只对应一个 inode,不管多少个进程打开它。
inode 保存文件的固定属性,不会记录读到哪里:
- 文件大小、权限、所属用户
- 文件数据在磁盘上存放在哪些块
- 文件类型(普通文件、目录、管道、socket、设备文件)
一句话区分: inode:文件本身的静态信息(不变) struct file:一次打开文件产生的动态阅读记录(会变,比如读文件会改变 f_pos)
整体串联一遍流程
- 创建进程,内核分配 PCB(进程档案)
- PCB 里面有指针,指向属于这个进程的文件描述符表(数组)
- 调用 open 打开文件:内核找到磁盘上文件,加载它的 inode 到内存
- 内核新建 struct file,让它的 f_inode 指向这个 inode
- 在当前进程的 fd 表里找一个空闲下标,填入指向这个 struct file 的指针,返回这个下标给用户,就是 fd
- 之后你调用 read (fd): 根据 fd 去 fd 表拿到 struct file → 借助 struct file 里的 inode 找到磁盘数据,同时更新 struct file 里面的读写位置 f_pos
进程间通信(管道)怎么利用这套结构
管道本质不是磁盘上的文件,是内核内存里的特殊 inode。
- pipe 系统调用,内核创建管道,生成特殊 inode,创建 struct file
- 得到两个 fd(读端 fd、写端 fd),在当前进程 fd 表中,两个 fd 指向同一个 struct file
- fork 之后,子进程复制 fd 表,父子进程都持有这两个 fd
- 父子进程共用同一个 struct file,共享缓冲区和读写偏移,一个写,另一个就可以读到数据