这两天把之前写的博文回顾了一遍,所以才迟迟没有写新的博文。这篇博文主要关注文件系统相关的操作(如打开、关闭文件等)。关于文件系统基础知识,请先阅读以下两篇博文:
Linux 内核学习笔记:进程 1 的创建及执行(第 4 部分)
Linux 内核学习笔记:进程 2 的创建及执行(第 1 部分)
安装文件系统
在此之前,我们已经安装好了根文件系统。安装其他文件系统就是在根文件系统的基础上,把逻辑设备(如硬盘一个分区)上的文件系统安装到根文件系统上,使操作系统具备以文件的形式与该逻辑设备进行数据交互的能力。具体来说,就是将该逻辑设备上的文件系统的根 i 节点,关联到根文件系统的某个 i 节点上。
实现文件系统安装的命令是 mount
。如 “mount /dev/hd1 /mnt” 表示将设备 /dev/hd1 上的文件系统挂载在 /mn 目录文件下。shell 进程接到该命令后,会创建一个新进程,新进程调用 mount 命令,并最终映射到 sys_mount 系统调用函数。安装文件系统的工作就由 sys_mount 函数完成。
sys_mount
函数定义如下:
1 | // fs/super.c ---------------------------------- |
打开和读取文件
打开和读取文件实例来自《Linux 内核设计的艺术》:
1 | // hello.txt |
打开文件
打开文件是拟人化的表述,在操作系统中就是确定进程操作哪个文件。打开文件过程涉及到 3 个重要数据结构:
- 进程的
*filp[20]
:内核通过 struct file *filp[20] 掌控一个进程可以打开的文件,即可以打开多个不同的文件,也可以同一个文件多次打开,每打开一个文件(不论是否是同一个文件),就要在 *file[20] 中占用一项(如 hello.txt 文件被一个进程打开两次,就要在 *filp[20] 中占用两项)记录指针,所以,一个进程可以同时打开的文件次数不能超过 20 次。 - 内核的
file_table[64]
:内核中的 file_table[64] 是管理所有进程打开文件的数据结构,不但记录了不同的进程打开不同的文件,也记录了不同的进程打开同一文件,甚至纪录了同一进程多次打开同一个文件。与 *filp[20] 类似,只要打开一次文件,就要在 file_table[64] 中记录。 - 内核的
inode_table[32]
:文件的 i 节点是记载文件属性的最关键的数据结构。在操作系统中 i 节点和文件是一一对应的,找到 i 节点就能够唯一确定文件。内核通过 inode_table[32] 掌控正在使用的文件 i 节点,每个被使用的文件 i 节点都要记录在其中。
打开文件的本质就是要建立 *filp[20、file_table[64]、inode_table[32] 三者之间的关系:
图片来源:《Linux 内核设计的艺术》
**打开文件过程
**:
- 第 1 步:将用户进程 task_struct 中的 *filp[20] 与内核中的 file_table[64] 相挂接
- 第 2 步:以用户给定的路径名(/mnt/user/user1/user2/hello.txt)为线索,找到 hello.txt 文件的 i 节点(登记到 inode_table[32])
- 第 3 步:将 hello.txt 的 i 节点在 file_table[64] 进行登记
详细过程请参考之前博文 Linux 内核学习笔记:进程 2 的创建及执行(第 1 部分)。
读取文件
读文件就是从进程“已打开的文件”中读取数据,读文件由 read 函数完成。read 函数最终映射到 sys_read 函数去执行。
sys_read 函数定义如下:
1 | // fs/read_write.c ----------------------------- |
verify_area 函数
verify_area 函数定义如下:
1 | // kernel/fork.c ------------------------------- |
这里需要特别强调的是 un_wp_page 函数,它真正体现了“写时复制”的思想
。
file_read 函数
file_read 函数定义如下:
1 | // fs/file_dev.c ------------------------------- |
重要重要!在前头 verify_area,我们已经确定了“进程空间线性地址” buf 有对应的物理页面。但在 file_read 函数貌似没有将 buf 转化为真正的物理地址就往 buf 传送数据了:put_fs_byte(*(p++),buf++)
。如果真是这种情况,那肯定要出问题。要解决这里的问题,看下边程序解释:
1 | // kernel/system_calls.s ----------------------- |
另外,我们重点看下 bmap
函数:
1 | // fs/inode.c ---------------------------------- |
要看懂 bmap
函数,只需理解 i 节点结构体中的 i_zone[9]
数组。关于该数组详细解释请参考之前博文 Linux 内核学习笔记:进程 1 的创建及执行(第 4 部分)。
新建和写入文件
新建文件和写入文件实例来自《Linux 内核设计的艺术》:
1 | void main() |
新建文件
新建文件的函数是 creat 函数。creat 函数最终映射到 sys_creat 函数执行。sys_creat 函数定义如下:
1 | // fs/open.c ----------------------------------- |
查找文件
open_namei 函数定义如下:
1 | // fs/namei.c ---------------------------------- |
新建 i 节点
new_inode 函数定义如下:
1 | // fs/bitmap.c --------------------------------- |
添加目录项
add_entry 函数定义如下:
1 | // fs/namei.c ---------------------------------- |
其中,create_block
函数定义如下:
1 | // fs/inode.c ---------------------------------- |
写入文件
《Linux 内核设计的艺术》写文件有一个简要阐述:
操作系统对写文件操作的规定是:
进程空间的数据先要写入缓冲块中,然后操作系统在适当的条件下,将缓冲区中的数据同步到外设上。
而且,操作系统只能以数据块(1KB)为单位,将缓冲区中的缓冲块的数据同步到外设上。这就需要在同步之前,缓冲块与外设上要写入的逻辑块一对一绑定,确定外设上的写入位置,以此保证用户空间写入缓冲块的数据,能够准确地同步到指定逻辑块中。
调用 sys_write
write 函数最终映射到 sys_write 函数去执行。sys_write 函数定义如下:
1 | // fs/read_write.c ----------------------------- |
调用 file_write
file_write 函数定义如下:
1 | // fs/file_dev.c ------------------------------- |
数据同步
数据从缓冲区同步到外设有两种方法。一种是 update 进程定期同步;另一种是因缓冲区使用达到极限,操作系统强行同步。这两种同步方法最终都会调用 sys_sync 函数。
第 2 种方法比较隐晦,它是由 getblk
函数完成的:
1 | // fs/buffer.c --------------------------------- |
sys_sync
函数定义如下:
1 | // fs/buffer.c --------------------------------- |
修改文件
修改文件的本质就是可以在文件的任意位置插入、删除数据,且不影响文件已有数据。
修改文件需要用到 sys_read、sys_write、sys_lseek 3 个函数。
下边先介绍 sys_lseek 函数:
1 | // fs/read_write.c ---------------------------- |
为了说明修改文件的过程,《Linux 内核设计的艺术》举了一个实例:
1 |
|
从上边程序可以看出,修改文件其实就是读文件、写文件的组合。
关闭文件
关闭文件是由 close 函数完成的。close 函数最终映射到 sys_close 函数去执行。sys_close 函数定义如下:
1 | // fs/open.c ---------------------------------- |
删除文件
删除文件的函数是 unlink。unlink 函数最终映射到 sys_unlink 函数执行。
清空目录项
sys_unlink 函数定义如下:
1 | // fs/namei.c ---------------------------------- |
释放逻辑块
释放逻辑块调用的函数是 truncate:
1 | // fs/truncate.c ------------------------------- |
释放 i 节点
释放 i 节点调用的函数是 free_inode:
1 | // fs/bitmap.c --------------------------------- |