代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。
文件描述符即file descriptor,但凡叫“描述符”的数据结构都用于描述一个对象,文件描述符所描述的对象是文件的操作。 为了搞清楚文件描述符的意义,咱们先看下它与inode的区别和联系。
几乎所有的操作系统都允许一个进程同时、多次、打开同一个文件,同样该文件也可以被多个不同的进程同时打开。为实现文件任意位置的读写,执行读写操作时可以指定偏移量作为该文件内的起始地址,此偏移量相当于文件内的指针。文件系统需要把任意时刻的偏移量记录下来。Linux提供了称为文件结构的数据结构(也称为file结构),专门用于记录与文件操作相关的信息,每次打开一个文件就会产生一个文件结构,多次打开该文件就为该文件生成多个文件结构,各自文件操作的偏移量分别记录在不同的文件结构中。
文件结构的逻辑表示如图:

在Linux中,我们读写函数文件时都是通过操作文件描述符来完成的。 拿open函数来说,其原型为int open(const char *pathname, int flags),pathname是待打开的文件路径及文件名, flag是打开标识,调用它之后,系统会返回文件pathname的文件描述符。返回值类型为int,而该数字就是我们所说的文件描述符,它是PCB中文件描述符数组元素的下标,用来表示位置,它是位于进程PCB中的文件描述符数组的元素的下标,而文件描述符数组元素中的信息又指向文件表中的某个文件结构。
在Linux中每个进程都有单独的、完全相同的一套文件描述符,因此它们与其他进程的文件描述符互不干涉,这些文件描述符被组织成文件描述符数组统一管理。文件结构的数量必须是有限的,这就是进程可打开的最大文件数有限的原因(在Linux中可用ulimit命令来修改),文件描述符数组中的前3个都是标准的文件描述符,如文件描述符0表示标准输入,1表示标准输出,2表示标准错误。
Linux通过文件描述符查找文件数据块的过程:
- 某进程把文件描述符作为参数提交给文件系统时,文件系统用此文件描述符在该进程的PCB中的文件描述符数组中索引对应的元素。
- 从该元素中获取对应的文件结构的下标,用该下标在文件表中索引相应的文件结构。
- 从该文件结构中获取文件的inode,最终找到了文件的数据块。
- 若该inode在inode队列中不存在,文件系统会从硬盘上将该inode加载到inode队列中,并使文件结构中的fd_inode指向它。
这涉及到以下三个数据结构,它们都是位于内存中的:
- PCB 中的文件描述符数组。
- 存储所有文件结构的文件表。
- inode队列,也就是inode 缓存。
如下图:

文件描述符创建过程:
- 在全局的inode队列中新建一inode (这肯定是在空位置处新建),然后返回该inode地址。
- 在全局的文件表中的找一空位,在该位置填充文件结构,使其fd_inode指向上一步中返回的inode地址,然后返回本文件结构在文件表中的下标值。
- 在PCB中的文件描述符数组中找一空位,使该位置的值指向上一步中返回的文件结构下标,并返回本文件描述符在文件描述符数组中的下标值。
接下来就是实现文件描述符了
先修改PCB的结构
修改/thread/thread.h
#define MAX_FILES_OPEN_PER_PROC 8
/* 进程或线程的pcb,程序控制块 */
struct task_struct {
uint32_t* self_kstack; // 各内核线程都用自己的内核栈
pid_t pid;
enum task_status status;
char name[16];
uint8_t priority;
uint8_t ticks; // 每次在处理器上执行的时间嘀嗒数
/* 此任务自上cpu运行后至今占用了多少cpu嘀嗒数,
* 也就是此任务执行了多久*/
uint32_t elapsed_ticks;
int32_t fd_table[MAX_FILES_OPEN_PER_PROC]; // 文件描述符数组
/* general_tag的作用是用于线程在一般的队列中的结点 */
struct list_elem general_tag;
/* all_list_tag的作用是用于线程队列thread_all_list中的结点 */
struct list_elem all_list_tag;
uint32_t* pgdir; // 进程自己页表的虚拟地址
struct virtual_addr userprog_vaddr; // 用户进程的虚拟地址
struct mem_block_desc u_block_desc[DESC_CNT]; // 用户进程内存块描述符
uint32_t stack_magic; // 用这串数字做栈的边界标记,用于检测栈的溢出
};fd_table是任务的文件描述符数组,其类型是int32_t,即每个成员都是int32_t整数,其长度是宏MAX_FILES_OPEN_PER_PROC,此宏的值是8,也就是每个任务可以打开的文件数是8。
这个数组需要被初始化,在thread_init函数中实现
修改修改/thread/thread.h
/* 初始化线程环境 */
void thread_init(void) {
put_str("thread_init start\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
lock_init(&pid_lock);
/* 将当前main函数创建为线程 */
make_main_thread();
/* 创建idle线程 */
idle_thread = thread_start("idle", 10, idle, NULL);
put_str("thread_init done\n");
}有三个标准的文件描述符,0是标准输入,1是标准输出,2是标准错误,因此将fd_table[0~2]分别置为0、1、2,也就是预留出了这3个文件描述符。接着将fd_table中其余的文件描述符初始化为-1,在这里-1表示该文件描述符可分配,为空位。将来会通过此值来找可分配的文件描述符。
参考
郑钢著操作系统真象还原
田宇著一个64位操作系统的设计与实现
丁渊著ORANGE’S:一个操作系统的实现


