我正在尝试尽可能多地了解系统调用、VFS、设备驱动程序处理以及最终让终端设备执行某些操作之间的相互作用。我想我应该看一个相当简单的例子——创建一个文件——并尝试尽可能详细地理解底层过程。
我创建了一些 C 代码,只是打开一个(不存在的)文件进行写入,编译了它(没有优化),并在运行它时使用 strace 查看了它。特别是,我想关注openat
系统调用,以及为什么以及如何这个调用最终不仅能够创建文件对象/文件描述,而且实际上能够写入磁盘(仅供参考,EXT4 文件系统,SATA HDD) 。
一般来说,排除一些检查和辅助的零碎内容,我对这个过程的理解如下(如果我的理解有偏差,请纠正我!):
- ELF映射到内存
- libc 被映射到内存
fopen
叫做- libc 打开
openat
系统调用被调用,O_CREAT
其中包含标志- 系统调用号放入 RAX 寄存器
- 系统调用参数(例如文件路径等)被放入 RDI 寄存器(以及 RSI、RDX 等,视情况而定)
- Syscall指令发出,CPU转换到ring 0
- 调用MSR_LSTAR寄存器指向的System_call代码
- 寄存器推送到内核堆栈
- 来自 RAX 的函数指针在偏移处调用
sys_call_table
asmlinkage
openat
调用实际系统调用代码的包装器
那时我的理解还不够,但最终我知道:
- open 调用返回一个文件描述符,该描述符对于进程是唯一的,并在内核的文件描述符表中全局维护
- FD映射到文件描述文件对象
- 文件对象由 inode 结构、inode_operations、file_operations 等结构填充。
- 文件操作表应将通用系统调用映射到相应的设备驱动程序以处理相应的调用(例如,当
write
调用系统调用时,会为文件所在的设备调用相应的驱动程序写入调用,例如SCSI 驱动程序) - 此映射基于该文件/设备的主/次编号
- 在此过程中,代码被调用,导致指令被发送到硬盘驱动器的设备驱动器,该指令被发送到磁盘控制器,这导致文件被写入硬盘,尽管这是否是通过中断或 DMA,或其他一些我不确定的 I/O 方法
- 最终,磁盘控制器将一条消息发送回内核,表明已完成,然后内核将控制权返回以使用空间。
我不太擅长跟踪内核源代码,尽管我已经尝试了一些,但感觉还缺少很多东西。我的问题如下:
我在内核源代码中找到了一些返回并销毁 FD 的函数,但找不到实际填充文件的文件对象/文件描述的代码在哪里。
A) 在open
oropenat
系统调用中,当创建新文件时,文件结构是如何填充的?数据从哪里来?具体来说,该文件的 file_operations 和 inode_operations 等是如何填充的?例如,在填充该结构时,内核如何知道该特定文件的文件操作需要是 SCSI 驱动程序的文件操作?
B) 在该过程中的哪个位置(特别是参考源)是否发生了到设备驱动程序的切换?例如,如果ioctl
调用了 或类似的函数,我期望对相应设备调用的指令进行一些引用,以及要传递的数据的一些内存地址,但我找不到发生这种情况的位置。
通过查看内核源代码,我真正能找到的只是分配新 FD 的代码,但没有任何填充文件结构的代码,也没有任何调用相应文件操作以将控制权转移到设备驱动程序的代码。
抱歉,这是一个非常冗长的描述,但我真的想尽可能多地学习,虽然我对 C 有了基本的掌握,但我真的很难理解别人的代码。
希望比我知识渊博的人可以帮助我澄清其中一些事情,因为我似乎遇到了比喻性的砖墙。任何建议将不胜感激。
编辑: 希望以下几点能够澄清我所追求的技术细节。
open
或系统openat
调用采用文件路径和标志(后者还传递一个指向目录的 FD)- 当该
O_CREAT
标志也被传递时,如果文件不存在,则“创建”文件 - 根据文件路径,内核能够识别该文件的设备类型
- 设备类型通常是根据主/次编号来标识的 - 对于已经存在的文件,这些设备类型存储在文件的 inode 结构(作为 member
i_rdev
)和文件的 stat 结构中(作为st_dev
设备类型的成员)文件所在的文件系统,以及st_rdev
文件本身的设备类型)
真的,我的问题是:
当使用任一 open 系统调用创建文件时,还必须创建并填充相应的 inode 和 stat 结构 - open 系统调用如何执行此操作(此时它们必须继续执行的是文件路径和标志)他们是否查看父目录的 inode 或 stat 结构,并从中复制相关的结构成员?
在哪一点(即源代码中的何处)发生这种情况?
据我了解,当调用这些开放系统调用时,它需要知道设备类型,以便 VFS 知道要调用哪些设备驱动程序代码。创建新文件时,设备类型尚未在文件对象结构中设置,会发生什么?叫什么代码?
序列更像是:
用户进程尝试打开新文件 ->open('/tmp/foo', O_CREAT)
打开 -> 查找“/tmp”的结构,获取其设备类型 -> 获取未使用的 FD -> 填充 inode/stat 结构,包括将设备类型设置为父级的设备类型 -> 基于设备类型,将文件操作/索引节点操作映射到设备驱动程序代码 -> 调用设备驱动程序代码进行open
系统调用 -> 向磁盘控制器发送适当的指令以将新文件写入磁盘 -> 整理、检查等 -> 返回 FD到用户调用过程?
答案1
你基本上是对的,但混合了一些不相关的细节(主要是由于 C 的工作方式)。
程序调用open(2)
(系统调用,即对内核的调用)。具体如何完成取决于架构和其他一些细节。
系统调用的内核内部部分被调用(同样,详细信息取决于体系结构、确切的操作系统版本……),并带有参数(此处为请求的路径)和其他信息,例如进程凭据。
从给定的路径,内核知道给定的路径上安装了什么文件系统。它可以从根向下执行正常的遍历,检查调用者是否具有所需的权限、目录是否存在等等。这可能包括从磁盘读取数据。如果一切正常,它将启动对文件系统数据结构的相关更改,以记录新的打开文件、保留空间、在目录中注册它,等等。
对文件系统数据结构(目前在内存中)的更改可能必须写入磁盘(或通过网络发送,或......)。内核会将写操作排队等待完成(如果不需要立即完成)并返回给用户。