read系统调用剖析.docx
15页read系统调用剖析 read 系统调用剖析 等级:初级 赵健博 ,硕士,中国科学院计算技术研究所 2021 年 3 月 13 日 大部分程序员可能会有这么的疑问:当在程序中调用库函数 read 时,这个请求是经过哪些处理最终抵达磁盘的呢,数据又是怎么被拷贝到用户缓存区的呢?本文介绍了从 read 系统调用发出到结束处理的全过程该过程包含两个部分:用户空间的处理、关键空间的处理用户空间处理部分是系统调用从用户态切到关键态的过程关键空间处理部分则是 read 系统调用在 linux 内核中处理的整个过程Read 系统调用在用户空间中的处理过程 Linux 系统调用(SCI,system call interface)的实现机制实际上是一个多路汇聚和分解的过程,该汇聚点就是 0x80 中止这个入口点(X86 系统结构)也就是说,全部系统调用全部从用户空间中汇聚到 0x80 中止点,同时保留详细的系统调用号当 0x80 中止处理程序运行时,将依据系统调用号对不一样的系统调用分别处理(调用不一样的内核函数处理)系统调用的更多内容,请参见参考资料 Read 系统调用也不例外,当调用发生时,库函数在保留 read 系统调用号和参数后,陷入 0x80 中止。
这时库函数工作结束Read 系统调用在用户空间中的处理也就完成了 Read 系统调用在关键空间中的处理过程 0x80 中止处理程序接管实施后,先检察其系统调用号,然后依据系统调用号查找系统调用表,并从系统调用表中得四处理 read 系统调用的内核函数sys_read,最终传输参数并运行 sys_read 函数至此,内核真正开始处理read 系统调用(sys_read 是 read 系统调用的内核入口) 在讲解 read 系统调用在关键空间中的处理部分中,首先介绍了内核处理磁盘请求的层次模型,然后再按该层次模型从上到下的次序依次介绍磁盘读请求在各层的处理过程 Read 系统调用在关键空间中处理的层次模型 图 1 显示了 read 系统调用在关键空间中所要经历的层次模型从图中看出:对于磁盘的一次读请求,首先经过虚拟文件系统层(vfs layer),其次是详细的文件系统层(比如 ext2),接下来是 cache 层(page cache 层)、通用块层(generic block layer)、IO 调度层(I/O scheduler layer)、块设备驱动层(block device driver layer),最终是物理块设备层(block device layer) 图 1 read 系统调用在关键空间中的处理层次 虚拟文件系统层的作用:屏蔽下层详细文件系统操作的差异,为上层的操作提供一个统一的接口。
正是因为有了这个层次,因此能够把设备抽象成文件,使得操作设备就像操作文件一样简单在详细的文件系统层中,不一样的文件系统(比如 ext2 和 NTFS)详细的操作过程也是不一样的每种文件系统定义了自己的操作集合有关文件系统的更多内容,请参见参考资料引入 cache 层的目标是为了提升 linux 操作系统对磁盘访问的性能Cache 层在内存中缓存了磁盘上的部分数据当数据的请求抵达时,假如在 cache 中存在该数据且是最新的,则直接将数据传输给用户程序,免去了对底层磁盘的操作,提升了性能通用块层的关键工作是:接收上层发出的磁盘请求,并最终发出 IO 请求该层隐藏了底层硬件块设备的特征,为块设备提供了一个通用的抽象视图IO调度层的功效:接收通用块层发出的 IO 请求,缓存请求并试图合并相邻的请求(假如这两个请求的数据在磁盘上是相邻的)并依据设置好的调度算法,回调驱动层提供的请求处理函数,以处理详细的 IO 请求驱动层中的驱动程序对应详细的物理块设备它从上层中取出 IO 请求,并依据该 IO 请求中指定的信息,经过向详细块设备的设备控制器发送命令的方法,来操纵设备传输数据设备层中全部是详细的物理设备。
定义了操作详细设备的规范相关的内核数据结构: Dentry:联络了文件名和文件的 i 节点 inode:文件 i 节点,保留文件标识、权限和内容等信息 file:保留文件的相关信息和多种操作文件的函数指针集合:操作文件的函数接口集合 address_space:描述文件的 page cache 结构和相关信息,并包含有操作 page cache 的函数指针集合address_space_operations:操作 page cache 的函数接口集合 bio:IO 请求的描述数据结构之间的关系: 图 2 示意性地展示了上述各个数据结构(除了 bio)之间的关系能够看出:由 dentry 对象能够找到 inode 对象,从 inode 对象中能够取出 address_space 对象,再由 address_space 对象找到address_space_operations 对象 File 对象能够依据目前进程描述符中提供的信息取得,进而能够找到dentry 对象、address_space 对象和对象 图 2 数据结构关系图: 前提条件: 对于详细的一次 read 调用,内核中可能碰到的处理情况很多。
这里举例其中的一个情况: 要读取的文件已经存在文件经过 page cache 要读的是一般文件磁盘上文件系统为 ext2 文件系统,相关 ext2 文件系统的相关内容,参见参考资料准备: 注:全部清单中代码均来自 linux 2.6.11 内核原代码 读数据之前,必需先打开文件处理 open 系统调用的内核函数为sys_open因此我们先来看一下该函数全部作了哪些事清单 1 显示了 sys_open的代码(省略了部分内容,以后的程序清单一样方法处理) 清单 1 sys_open 函数代码 asmlinkage long sys_open(const char __user* flags,int mode){…fd=get_unused_fd ;if(fd=0){struct (tmp,flags,mode);fd_install(fd,f);}…return fd;…} 代码解释: get_unuesed_fd :取回一个未被使用的文件描述符(每次全部会选择最小的未被使用的文件描述符)filp_open :调用 open_namei 函数取出和该文件相关的 dentry 和 inode(因为前提指明了文件已经存在,因此 dentry 和 inode能够查找到,不用创立),然后调用 dentry_open 函数创立新的 file 对象,并用 dentry 和 inode 中的信息初始化 file 对象(文件目前的读写位置在 file对象中保留)。
注意到 dentry_open 中有一条语句:f-f_op=fops_get(inode-i_fop); 这个赋值语句把和详细文件系统相关的,操作文件的函数指针集合赋给了file 对象的 f _op 变量(这个指针集合是保留在 inode 对象中的),在接下来的sys_read 函数中将会调用中的组员 read fd_install :以文件描述符为索引,关联目前进程描述符和上述的 file对象,为以后的 read 和 write 等操作作准备函数最终返回该文件描述符图3 显示了 sys_open 函数返回后,file 对象和目前进程描述符之间的关联关系,和 file 对象中操作文件的函数指针集合的(inode 对象中的组员i_fop) 图 3 file 对象和目前进程描述符之间的关系 到此为止,全部的准备工作已经全部结束了,下面开始介绍 read 系统调用在图 1 所表示的各个层次中的处理过程 虚拟文件系统层的处理: 内核函数 sys_read 是 read 系统调用在该层的入口点,清单 2 显示了该函数的代码 清单 2 sys_read 函数的代码 asmlinkage ssize_t sys_read(unsigned int fd,char __user*buf,size_t count){struct ;ssize_t ret=-EBADF;int fput_needed;(fd,&fput_needed);if(file){loff_t pos=(file);ret=vfs_read ; ;fput_light ;}return ret;} 代码解析: fget_light :依据 fd 指定的索引,从目前进程描述符中取出对应的file 对象(见图 3)。
假如没找到指定的 file 对象,则返回错误假如找到了指定的 file 对象:调用 函数取出此次读写文件的目前位置调用 vfs_read 实施文件读取操作,而这个函数最终调用 指向的函数,代码以下:if ret= ; 调用 更新文件的目前读写位置调用 fput_light 更新文件的引用计数最终返回读取数据的字节数到此,虚拟文件系统层所做的处理就完成了,控制权交给了 ext2 文件系统层 在解析 ext2 文件系统层的操作之前,先让我们看一下 file 对象中 read 指针 File 对象中 read 函数指针的: 以前面对 sys_open 内核函数的分析来看,来自于 inode-i_fop那么inode-i_fop 来自于哪里呢?在初始化 inode 对象时给予的见清单 3 清单 3 ext2_read_inode 函数部分代码 void ext2_read_inode(struct inode*inode){…if(S_ISREG(inode-i_mode)){inode-i_op=&ext2_;inode-i_fop=&ext2_;if(test_opt(inode-i_sb,NOBH))inode-i_mapping-a_ops=&ext2_nobh_aops;else inode-i_mapping-a_ops=&ext2_aops;}…} 从代码中能够看出,假如该 inode 所关联的文件是一般文件,则将变量ext2_的地址给予 inode 对象的 i_fop 组员。
因此能够知道:inode-i_fop.read函数指针所指向的函数为 ext2_变量的组员 read 所指向的函数下面来看一下ext2_变量的初始化过程,如清单 4 清单 4 ext2_的初始化 struct ext2_{.llseek=generic_,}; 该组员 read 指向函数 generic_因此,inode-i_fop.read 指向 generic_函数,进而指向 generic_函数最终得出结论:generic_函数才是 ext2 层的真实入口 Ext2 文件系统层的处理 图 4 read 系统调用在 ext2 层中处理时函数调用关系 由图 4 可知,该层入口函数 generic_调用函数__generic_,后者判定此次读请求的访问方法,假如是直接 io(filp-f_flags 被设置了 O_DIRECT 标志,即不经过 cache)的方法,则调用 generic_函数;假如是 page cache 的方法,则调用 do_generic_函数函数 do_generic_仅仅是一个包装函数,它又调用do_generic_mapping_read 函数 在讲解 do_generic_mapping_read 函数全部作了哪些工作之前,我们再来看一下文件在内存中的缓存区域是被怎么组织起来的。
文件的 page cache 结构 图 5 显示了一个文。





