read系统调用剖析--file_operations中readwrite函数与内核系统调用的关系

更新时间:2023-07-27 08:09:07 阅读: 评论:0

read系统调⽤剖析--file_operations中readwrite函数与内核系统
调⽤的关系
在阅读LDD3-第三章-字符设备驱动程序中:
内核空间,scull_read() 通过file_operations结构与scull_fops->read联系
⽤户空间read()-->内核空间sys_read()-->ad-->scull_read();
女士牛仔裤疑问:每次⽤户空间调⽤read()函数,内核会⾃动保存读写的位置指针f_pos,但此指针并没有在⽤户空
间通过read函数传递给内核,哪内核中的f_pos指针从那⾥来,scull_read()中的lofft_t *f_pos参数起什么作⽤?
详细解释见下⽂红字部分
转:
read 系统调⽤剖析
简介:⼤部分程序员可能会有这样的疑问:当在程序中调⽤库函数 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 :保存⽂件的相关信息和各种操作⽂件的函数指针集合
file_operations :操作⽂件的函数接⼝集合
铁西森林公园>更新qq
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 对象和 file_operations 对象。
图2 数据结构关系图:
前提条件:
对于具体的⼀次 read 调⽤,内核中可能遇到的处理情况很多。这⾥举例其中的⼀种情况:
要读取的⽂件已经存在
⽂件经过 page cache
要读的是普通⽂件
磁盘上⽂件系统为 ext2 ⽂件系统,有关 ext2 ⽂件系统的相关内容,参见参考资料
准备:
注:所有清单中代码均来⾃ linux2.6.11 内核原代码
读数据之前,必须先打开⽂件。处理 open 系统调⽤的内核函数为 sys_open 。
所以我们先来看⼀下该函数都作了哪些事。清单1显⽰了 sys_open 的代码(省略了部分内容,以后的程序清单同样⽅式处理)
清单1 sys_open 函数代码
代码解释:
get_unued_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 函数中将会调⽤ file->f_op 中的成员 read 。
fd_install() :以⽂件描述符为索引,关联当前进程描述符和上述的 file 对象,为之后的 read 和 write 等操作作准备。
函数最后返回该⽂件描述符。
图3显⽰了 sys_open 函数返回后, file 对象和当前进程描述符之间的关联关系,以及 file 对象中操作
⽂件的函数指针集合的来源(inode 对象中的成员 i_fop)。
图3 file 对象和当前进程描述符之间的关系
file 对象和当前进程描述符之间的关系
到此为⽌,所有的准备⼯作已经全部结束了,下⾯开始介绍 read 系统调⽤在图1所⽰的各个层次中的处理过程。
虚拟⽂件系统层的处理:
简笔画桥内核函数 sys_read() 是 read 系统调⽤在该层的⼊⼝点,清单2显⽰了该函数的代码。
清单2 sys_read 函数的代码
代码解析:
fget_light() :根据 fd 指定的索引,从当前进程描述符中取出相应的 file 对象(见图3)。
如果没找到指定的 file 对象,则返回错误
如果找到了指定的 file 对象:
调⽤ file_pos_read() 函数取出此次读写⽂件的当前位置。
调⽤ vfs_read() 执⾏⽂件读取操作,⽽这个函数最终调⽤ file->ad() 指向的函数,代码如下:
if (file->f_op->read)
ret = file->f_op->read(file, buf, count, pos);
调⽤ file_pos_write() 更新⽂件的当前读写位置。
调⽤ fput_light() 更新⽂件的引⽤计数。
最后返回读取数据的字节数。
到此,虚拟⽂件系统层所做的处理就完成了,控制权交给了 ext2 ⽂件系统层。
在解析 ext2 ⽂件系统层的操作之前,先让我们看⼀下 file 对象中 read 指针来源。
File 对象中 read 函数指针的来源:
从前⾯对 sys_open 内核函数的分析来看, file->f_op 来⾃于 inode->i_fop 。那么 inode->i_fop 来⾃于哪⾥呢?在初始化 inode 对象时赋予的。见清单3。
清单3 ext2_read_inode() 函数部分代码
从代码中可以看出,如果该 inode 所关联的⽂件是普通⽂件,则将变量 ext2_file_operations 的地址赋予 inode 对象的 i_fop 成员。所以可以知道: inode-
>ad 函数指针所指向的函数为 ext2_file_operations 变量的成员 read 所指向的函数。下⾯来看⼀下 ext2_file_operations 变量的初始化过程,如清单4。清单4 ext2_file_operations 的初始化
刘锜该成员 read 指向函数 generic_file_read 。所以, inode->ad 指向 generic_file_read 函数,进⽽ file->ad 指向 generic_file_read 函数。最终得出结论: generic_file_read 函数才是 ext2 层的真实⼊⼝。
Ext2 ⽂件系统层的处理
图4 read 系统调⽤在 ext2 层中处理时函数调⽤关系
由图 4 可知,该层⼊⼝函数 generic_file_read 调⽤函数 __generic_file_aio_read ,后者判断本次读请求的访问⽅式,如果是直接 io (filp-
>f_flags 被设置了 O_DIRECT 标志,即不经过 cache)的⽅式,则调⽤ generic_file_direct_IO 函数;如果是 page cache 的⽅式,则调⽤
nfc有什么用do_generic_file_read 函数。函数 do_generic_file_read 仅仅是⼀个包装函数,它⼜调⽤ do_generic_mapping_read 函数。
在讲解 do_generic_mapping_read 函数都作了哪些⼯作之前,我们再来看⼀下⽂件在内存中的缓存区域是被怎么组织起来的。
⽂件的 page cache 结构
图5显⽰了⼀个⽂件的 page cache 结构。⽂件被分割为⼀个个以 page ⼤⼩为单元的数据块,这些数据块(页)被组织成⼀个多叉树(称为radix 树)。树中所有叶⼦节点为⼀个个页帧结构(struct page),表⽰了⽤于缓存该⽂件的每⼀个页。在叶⼦层最左端的第⼀个页保存着该⽂件的前4096个字节(如果页的⼤⼩为4096字节),接下来的页保存着⽂件第⼆个4096个字节,依次类推。树中的所有中间节点为组织节点,指⽰某⼀地址上的数据所在的页。此树的层次可以从0层到6层,所⽀持的⽂件
⼤⼩从 0字节到16 T 个字节。树的根节点指针可以从和⽂件相关的 address_space 对象(该对象保存在和⽂件关联的 inode 对象中)中取得(更多关于 page cache 的结构内容请参见参考资料)。
图5 ⽂件的 page cache 结构
现在,我们来看看函数 do_generic_mapping_read 都作了哪些⼯作, do_generic_mapping_read 函数代码较长,本⽂简要介绍下它的主要流程:
根据⽂件当前的读写位置,在 page cache 中找到缓存请求数据的 page
如果该页已经最新,将请求的数据拷贝到⽤户空间
否则, Lock 该页
调⽤ readpage 函数向磁盘发出添页请求(当下层完成该 IO 操作时会解锁该页),代码:
再⼀次 lock 该页,操作成功时,说明数据已经在 page cache 中了,因为只有 IO 操作完成后才可能解锁该页。此处是⼀个同步点,⽤于同步数据从磁盘到内存的过程。
解锁该页
到此为⽌数据已经在 page cache 中了,再将其拷贝到⽤户空间中(之后 read 调⽤可以在⽤户空间返回了)
到此,我们知道:当页上的数据不是最新的时候,该函数调⽤ mapping->a_ops->readpage 所指向的函数(变量 mapping 为 inode 对象中的address_space 对象),那么这个函数到底是什么呢?
Read page 函数的由来
address_space 对象是嵌⼊在 inode 对象之中的,那么不难想象: address_space 对象成员 a_ops 的初始化⼯作将会在初始化 inode 对象时进⾏。如清单3中后半部所显⽰。
可以知道 address_space 对象的成员 a_ops 指向变量 ext2_aops 或者变量 ext2_nobh_aops 。这两个变量的初始化如清单5所⽰。
清单5 变量 ext2_aops 和变量 ext2_nobh_aops 的初始化
从上述代码中可以看出,不论是哪个变量,其中的 readpage 成员都指向函数 ext2_readpage 。所以可以断定:函数
do_generic_mapping_read 最终调⽤ ext2_readpage 函数处理读数据请求。仙卡
到此为⽌, ext2 ⽂件系统层的⼯作结束。
一加一等于几Page cache 层的处理
从上⽂得知:ext2_readpage 函数是该层的⼊⼝点。该函数调⽤ mpage_readpage 函数,清单6显⽰了 mpage_readpage 函数的代码。
清单6 mpage_readpage 函数的代码
int mpage_readpage(struct page *page, get_block_t get_block) { struct bio *bio = NULL; ctor_t last_block_in_bio = 0; bio = do_mpage_readpage(bio, page, 1, &last_block_in_bio, get_block); if (bio) mpage_bio_submit(READ, bio); return 0; }

本文发布于:2023-07-27 08:09:07,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/82/1119669.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:函数   系统   处理   请求   操作
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图