文件描述符
在之前介绍的概念中,inode是用来表示一个文件的,用于描述文件的存储信息,文件的权限等。但这个inode结构是操作系统为自己的文件系统准备的数据结构,仅供其内部使用,与用户的关系不大,接下来要介绍的文件描述符才是用户能够使用的结构。
在linux系统中,读写文件的本质是先通过文件的inode找到文件数据块的扇区地址,随后对这些扇区的数据进行读写,从而实现了文件的读写。
对用户进程来说,一个进程可以多次的打开同一个文件,一个文件也可以被多个进行同时打开。对于这个被多次打开的文件,每打开一次,都需要一个结构来记录该文件目前的状态。比如说A进行第一次打开1.txt的时候,读取到了第10行的位置,第二次打开1.txt的时候,读取到了20行的位置。这里的10行,20行就是打开的文件状态中的文件偏移量,它记录的是相对于文件首地址的一个偏移。即使一个文件被同时多次打开,各自操作的偏移量也互不影响。
也就是说,会有一个文件结构来描述文件打开后,文件读写的偏移量等信息。一个文件对应一个inode,一个文件可以被多次打开,即一个inode对应多个文件结构。
文件描述符的基本结构如下
接下来通过linux的open函数来了解一下文件描述符
open的函数原型如下1
int open(const char *pathname, inf flags);
成功调用该函数之后,它会返回文件pathname的文件描述符,该返回值是一个int的数值,肯定不是代表真正的文件描述符结构。它是作为进程pcb中文件描述符数组的下标索引。通过这个下标在进程pcb中找到某个数据。大家可能会认为这个数据就是真正的文件描述符了。其实不然,在进程pcb的文件描述符数组中,记录的任然不是真正的文件描述符结构,它是一个指针数组,数组中的数据指向文件表中某个文件结构,该文件结构就是真正记录被打开文件的信息所在。
该过程比较复杂,将其分解为下面四步
- 调用open函数,得到一个文件描述符
- 将第一步得到的文件描述符作为进程pcb中文件描述符数组的下标,取得该下标对应的数据
- 将第二步得到的数据,相当于是一个指针,从这个指针中取到对应的文件结构
- 从该文件结构中获取到记录的文件信息
通过一副图来描述一下这个过程
看到这里获取会有一个疑问,为什么pcb中不直接记录文件的描述符信息,而是指向了其他的区域。
因为记录文件信息的描述符较大,每打开一次文件,就需要记录一次。打开的文件多了之后,进程pcb的结构就会变的很大,而pcb占用的内存通常就是几个页框,linux中的pcb也只是2页框的大小,所以这些信息不会被放入pcb中。
看完了上面的理论之后,接下来就来具体看一下文件描述符的代码实现
1 | struct task_struct |
这是在进程pcb中记录的文件描述符数组,为了简化实现,这里并不是一个指针数组。里面记录的数据就是对应文件表的下标值。通过该下标去文件表中找到对应的文件结构。
文件描述符的初始化
1 | void init_thread(task_struct* pthread, char* name, int prio) |
文件描述符的前三个文件结构将作为标准输入,标准输出,标准错误预留出来。置为-1表示该文件描述符可被分配。
文件描述符结构和文件表1
2
3
4
5
6
7
8
9struct file
{
uint32_t fd_pos; // 记录当前文件操作的偏移地址,以0为起始,最大为文件大小-1
uint32_t fd_flag;// 文件打开的标志
struct inode *fd_inode;
};
// 文件表
struct file file_table[MAX_FILE_OPEN];
文件描述符只存储了打开一个需要记录的最基础的信息。文件表的本质就是一个文件描述符结构的数组。
创建文件
1 | /* 创建文件,若成功则返回文件描述符,否则返回-1 */ |
该函数的功能是在目录parent_dir下以创建模式flag去创建普通文件,如果函数执行成功的话返回文件描述符。
创建的过程如下:
- 首先为文件创建inode,该过程需要向inode的管理单元inode_bitmap申请inode号,并更新inode_bitmap
- 确定文件存储的扇区地址,这个需要在block_bitmap中申请可用的块,并更新block_bitmap
- 新增的文件必然位于某个目录中,所以该目录的目录项数量要加1,并且要将新增的目录项写入目录对应的扇区中,如果原有的扇区已满,需要申请新扇区来存储目录项
- 将上面过程中被改变的数据写入硬盘中。
系统调用open
open函数的功能相当强大,通过它的打开标志,不仅可以打开一个文件,同样可以创建一个文件。所以这里不打算单独实现文件的创建。
文件创建的标志1
2
3
4
5
6
7enum oflags
{
O_RDONLY, // 只读
O_WRONLY, // 只写
O_RDWR, // 读写
O_CREAT = 4 // 创建
};
1 | /* 打开或创建文件成功后,返回文件描述符,否则返回-1 */ |
文件的创建过程中主要是对路径的解析,这里暂时还不支持相对路径,这里必须使用绝对路径来进行文件的创建。在路径没有问题且该文件不存在的前提下,就会调用之前的file_create函数创建文件。
接下来在模拟器上运行一下,看看创建文件的具体表现
上面的图片是主分区格式化时数据的区域,我用红框标记了数据的起始扇区位置,该位置应该与根目录所在的位置相同。
将数据起始扇区的位置*512后得到数据区的地址,在该地址处查看512字节也就是一扇区的数据内容,结果如下
在根目录下,目前有三个目录项 . ..和创建的文件file1,每个目录项包含三部分的内容,16字节的filename,4字节的inode号,4字节的文件类型。共24字节的内容
用红框标记的是 . 这个目录项的数据,2E代表它的文件名,它表示当前目录也就是根目录,其inode号为0,文件类型为2,代表一个目录
黄线标记的是 .. 它代表上一层目录,因为目前在根目录下,所以它还是表示根目录。
绿线标记的就是刚刚创建的文件,文件名为file1。inode号为1,文件类型是1,表示这是一个普通文件。
上面通过open函数创建了一个文件,接下来就完成其打开文件的功能。打开文件的功能主要是通过下面这个函数实现的。
1 | /* 打开编号为inode_no的inode对应的文件,若成功则返回文件描述符,否则返回-1 */ |
可以看到,打开一个文件的本质就是在进程的pcb中安装了该文件对应的文件描述符。
open函数的功能实现完了之后就可以将其添加到系统调用中,以供用户的使用。
1 | void syscall_init(void) |
系统调用close
1 | /* 关闭文件描述符fd指向的文件,成功返回0,否则返回-1 */ |
添加系统调用
1 | /* 初始化系统调用 */ |