read 系统调用剖析.docx
- 文档编号:5043371
- 上传时间:2022-12-12
- 格式:DOCX
- 页数:8
- 大小:22.52KB
read 系统调用剖析.docx
《read 系统调用剖析.docx》由会员分享,可在线阅读,更多相关《read 系统调用剖析.docx(8页珍藏版)》请在冰豆网上搜索。
read系统调用剖析
read系统调用剖析
级别:
初级
赵健博(zhaojianbo@),硕士,中国科学院计算技术研究所
2008年3月13日
大部分程序员可能会有这样的疑问:
当在程序中调用库函数read时,这个请求是经过哪些处理最终到达磁盘的呢,数据又是怎么被拷贝到用户缓存区的呢?
本文介绍了从read系统调用发出到结束处理的全过程。
该过程包括两个部分:
用户空间的处理、核心空间的处理。
用户空间处理部分是系统调用从用户态切到核心态的过程。
核心空间处理部分则是read系统调用在linux内核中处理的整个过程。
Read系统调用在用户空间中的处理过程
Linux系统调用(SCI,systemcallinterface)的实现机制实际上是一个多路汇聚以及分解的过程,该汇聚点就是0x80中断这个入口点(X86系统结构)。
也就是说,所有系统调用都从用户空间中汇聚到0x80中断点,同时保存具体的系统调用号。
当0x80中断处理程序运行时,将根据系统调用号对不同的系统调用分别处理(调用不同的内核函数处理)。
系统调用的更多内容,请参见参考资料。
Read系统调用也不例外,当调用发生时,库函数在保存read系统调用号以及参数后,陷入0x80中断。
这时库函数工作结束。
Read系统调用在用户空间中的处理也就完成了。
Read系统调用在核心空间中的处理过程
0x80中断处理程序接管执行后,先检察其系统调用号,然后根据系统调用号查找系统调用表,并从系统调用表中得到处理read系统调用的内核函数sys_read,最后传递参数并运行sys_read函数。
至此,内核真正开始处理read系统调用(sys_read是read系统调用的内核入口)。
在讲解read系统调用在核心空间中的处理部分中,首先介绍了内核处理磁盘请求的层次模型,然后再按该层次模型从上到下的顺序依次介绍磁盘读请求在各层的处理过程。
Read系统调用在核心空间中处理的层次模型
图1显示了read系统调用在核心空间中所要经历的层次模型。
从图中看出:
对于磁盘的一次读请求,首先经过虚拟文件系统层(vfslayer),其次是具体的文件系统层(例如ext2),接下来是cache层(pagecache层)、通用块层(genericblocklayer)、IO调度层(I/Oschedulerlayer)、块设备驱动层(blockdevicedriverlayer),最后是物理块设备层(blockdevicelayer)
图1read系统调用在核心空间中的处理层次
虚拟文件系统层的作用:
屏蔽下层具体文件系统操作的差异,为上层的操作提供一个统一的接口。
正是因为有了这个层次,所以可以把设备抽象成文件,使得操作设备就像操作文件一样简单。
在具体的文件系统层中,不同的文件系统(例如ext2和NTFS)具体的操作过程也是不同的。
每种文件系统定义了自己的操作集合。
关于文件系统的更多内容,请参见参考资料。
引入cache层的目的是为了提高linux操作系统对磁盘访问的性能。
Cache层在内存中缓存了磁盘上的部分数据。
当数据的请求到达时,如果在cache中存在该数据且是最新的,则直接将数据传递给用户程序,免除了对底层磁盘的操作,提高了性能。
通用块层的主要工作是:
接收上层发出的磁盘请求,并最终发出IO请求。
该层隐藏了底层硬件块设备的特性,为块设备提供了一个通用的抽象视图。
IO调度层的功能:
接收通用块层发出的IO请求,缓存请求并试图合并相邻的请求(如果这两个请求的数据在磁盘上是相邻的)。
并根据设置好的调度算法,回调驱动层提供的请求处理函数,以处理具体的IO请求。
驱动层中的驱动程序对应具体的物理块设备。
它从上层中取出IO请求,并根据该IO请求中指定的信息,通过向具体块设备的设备控制器发送命令的方式,来操纵设备传输数据。
设备层中都是具体的物理设备。
定义了操作具体设备的规范。
相关的内核数据结构:
Dentry:
联系了文件名和文件的i节点inode:
文件i节点,保存文件标识、权限和内容等信息file:
保存文件的相关信息和各种操作文件的函数指针集合file_operations:
操作文件的函数接口集合address_space:
描述文件的pagecache结构以及相关信息,并包含有操作pagecache的函数指针集合address_space_operations:
操作pagecache的函数接口集合bio:
IO请求的描述数据结构之间的关系:
图2示意性地展示了上述各个数据结构(除了bio)之间的关系。
可以看出:
由dentry对象可以找到inode对象,从inode对象中可以取出address_space对象,再由address_space对象找到address_space_operations对象。
File对象可以根据当前进程描述符中提供的信息取得,进而可以找到dentry对象、address_space对象和file_operations对象。
图2数据结构关系图:
前提条件:
对于具体的一次read调用,内核中可能遇到的处理情况很多。
这里举例其中的一种情况:
要读取的文件已经存在文件经过pagecache要读的是普通文件磁盘上文件系统为ext2文件系统,有关ext2文件系统的相关内容,参见参考资料准备:
注:
所有清单中代码均来自linux2.6.11内核原代码
读数据之前,必须先打开文件。
处理open系统调用的内核函数为sys_open。
所以我们先来看一下该函数都作了哪些事。
清单1显示了sys_open的代码(省略了部分内容,以后的程序清单同样方式处理)
清单1sys_open函数代码
asmlinkagelongsys_open(constchar__user*filename,intflags,intmode){…fd=get_unused_fd();if(fd=0){structfile*f=filp_open(tmp,flags,mode);fd_install(fd,f);}…returnfd;…}
代码解释:
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函数中将会调用file-f_op中的成员read。
fd_install():
以文件描述符为索引,关联当前进程描述符和上述的file对象,为之后的read和write等操作作准备。
函数最后返回该文件描述符。
图3显示了sys_open函数返回后,file对象和当前进程描述符之间的关联关系,以及file对象中操作文件的函数指针集合的来源(inode对象中的成员i_fop)。
图3file对象和当前进程描述符之间的关系
到此为止,所有的准备工作已经全部结束了,下面开始介绍read系统调用在图1所示的各个层次中的处理过程。
虚拟文件系统层的处理:
内核函数sys_read()是read系统调用在该层的入口点,清单2显示了该函数的代码。
清单2sys_read函数的代码
asmlinkagessize_tsys_read(unsignedintfd,char__user*buf,size_tcount){structfile*file;ssize_tret=-EBADF;intfput_needed;file=fget_light(fd,&fput_needed);if(file){loff_tpos=file_pos_read(file);ret=vfs_read(file,buf,count,&pos);file_pos_write(file,pos);fput_light(file,fput_needed);}returnret;}
代码解析:
fget_light():
根据fd指定的索引,从当前进程描述符中取出相应的file对象(见图3)。
如果没找到指定的file对象,则返回错误如果找到了指定的file对象:
调用file_pos_read()函数取出此次读写文件的当前位置。
调用vfs_read()执行文件读取操作,而这个函数最终调用file-f_op.read()指向的函数,代码如下:
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。
清单3ext2_read_inode()函数部分代码
voidext2_read_inode(structinode*inode){…if(S_ISREG(inode-i_mode)){inode-i_op=&ext2_file_inode_operations;inode-i_fop=&ext2_file_operations;if(test_opt(inode-i_sb,NOBH))inode-i_mapping-a_ops=&ext2_nobh_aops;elseinode-i_mapping-a_ops=&ext2_aops;}…}
从代码中可以看出,如果该inode所关联的文件是普通文件,则将变量ext2_file_operations的地址赋予inode对象的i_fop成员。
所以可以知道:
inode-i_fop.read函数指针所指向的函数为ext2_file_operations变量的成员read所指向的函数。
下面来看一下ext2_file_operations变量的初始化过程,如清单4。
清单4ext2_file_operations的初始化
structfile_operationsext2_file_operations={.llseek=generic_file_llseek,.read=generic_file_read,.write=generic_file_write,.aio_read=generic_file_aio_read,.aio_write=generic_file_aio_write,.ioctl=ext2_ioctl,.mmap=generic_file_mmap,.open=generic_file_open,.release=ext2_release_file,.fsync=ext2_sync_file,.readv=generic_file_readv,.writev=generic_file_writev,.sendfile=generic_file_sendfile,};
该成员read指向函数generic_file_read。
所以,inode-i_fop.read指向generic_file_read函数,进而file-f_op.read指向generic_file_read函数。
最终得出结论:
generic_file_read函数才是ext2层的真实入口。
Ext2文件系统层的处理
图4read系统调用在ext2层中处理时函数调用关系
由图4可知,该层入口函数generic_file_read调用函数__generic_file_aio_read,后者判断本次读请求的访问方式,如果是直接io(filp-f_flags被设置了O_DIRECT标志,即不经过cache)的方式,则调用generic_file_direct_IO函数;如果是pagecache的方式,则调用do_generic_file_read函数。
函数do_generic_file_read仅仅是一个包装函数,它又调用do_generic_mapping_read函数。
在讲解do_generic_mapping_read函数都作了哪些工作之前,我们再来看一下文件在内存中的缓存区域是被怎么组织起来的。
文件的pagecache结构
图5显示了一个文件的pagecache结构。
文件被分割为一个个以page大小为单元的数据块,这些数据块(页)被组织成一个多叉树(称为radix树)。
树中所有叶子节点为一个个页帧结构(structpage),表示了用于缓存该文件的每一个页。
在叶子层最左端的第一个页保存着该文件的前4096个字节(如果页的大小为4096字节),接下来的页保存着文件第二个4096个字节,依次类推。
树中的所有中间节点为组织节点,指示某一地址上的数据所在的页。
此树的层次可以从0层到6层,所支持的文件大小从0字节到16T个字节。
树的根节点指针可以从和文件相关的address_space对象(该对象保存在和文件关联的inode对象中)中取得(更多关于pagecache的结构内容请参见参考资料)。
图5文件的pagecache结构
现在,我们来看看函数do_generic_mapping_read都作了哪些工作,do_generic_mapping_read函数代码较长,本文简要介绍下它的主要流程:
根据文件当前的读写位置,在pagecache中找到缓存请求数据的page如果该页已经最新,将请求的数据拷贝到用户空间否则,Lock该页调用readpage函数向磁盘发出添页请求(当下层完成该IO操作时会解锁该页),代码:
error=mapping-a_ops-readpage(filp,page);
再一次lock该页,操作成功时,说明数据已经在pagecache中了,因为只有IO操作完成后才可能解锁该页。
此处是一个同步点,用于同步数据从磁盘到内存的过程。
解锁该页到此为止数据已经在pagecache中了,再将其拷贝到用户空间中(之后read调用可以在用户空间返回了)到此,我们知道:
当页上的数据不是最新的时候,该函数调用mapping-a_ops-readpage所指向的函数(变量mapping为inode对象中的address_space对象),那么这个函数到底是什么呢?
Readpage函数的由来
address_space对象是嵌入在inode对象之中的,那么不难想象:
address_space对象成员a_ops的初始化工作将会在初始化inode对象时进行。
如清单3中后半部所显示。
if(test_opt(inode-i_sb,NOBH))inode-i_mapping-a_ops=&ext2_nobh_aops;elseinode-i_mapping-a_ops=&ext2_aops;
可以知道address_space对象的成员a_ops指向变量ext2_aops或者变量ext2_nobh_aops。
这两个变量的初始化如清单5所示。
清单5变量ext2_aops和变量ext2_nobh_aops的初始化
structaddress_space_operationsext2_aops={.readpage=ext2_readpage,.readpages=ext2_readpages,.writepage=ext2_writepage,.sync_page=block_sync_page,.prepare_write=ext2_prepare_write,.commit_write=generic_commit_write,.bmap=ext2_bmap,.direct_IO=ext2_direct_IO,.writepages=ext2_writepages,};structaddress_space_operationsext2_nobh_aops={.readpage=ext2_readpage,.readpages=ext2_readpages,.writepage=ext2_writepage,.sync_page=block_sync_page,.prepare_write=ext2_nobh_prepare_write,.commit_write=nobh_commit_write,.bmap=ext2_bmap,.direct_IO=ext2_direct_IO,.writepages=ext2_writepages,};
从上述代码中可以看出,不论是哪个变量,其中的readpage成员都指向函数ext2_readpage。
所以可以断定:
函数do_generic_mapping_read最终调用ext2_readpage函数处理读数据请求。
到此为止,ext2文件系统层的工作结束。
Pagecache层的处理
从上文得知:
ext2_readpage函数是该层的入口点。
该函数调用mpage_readpage函数,清单6显示了mpage_readpage函数的代码。
清单6mpage_readpage函数的代码
intmpage_readpage(structpage*page,get_block_tget_block){structbio*bio=NULL;sector_tlast_block_in_bio=0;bio=do_mpage_readpage(bio,page,1,&last_block_in_bio,get_block);if(bio)mpage_bio_submit(READ,bio);return0;}
该函数首先调用函数do_mpage_readpage函数创建了一个bio请求,该请求指明了要读取的数据块所在磁盘的位置、数据块的数量以及拷贝该数据的目标位置--缓存区中page的信息。
然后调用mpage_bio_submit函数处理请求。
mpage_bio_submit函数则调用submit_bio函数处理该请求,后者最终将请求传递给函数generic_make_request,并由generic_make_request函数将请求提交给通用块层处理。
到此为止,pagecache层的处理结束。
通用块层的处理
generic_make_request函数是该层的入口点,该层只有这一个函数处理请求。
清单7显示了函数的部分代码
清单7generic_make_request函数部分代码
voidgeneric_make_request(structbio*bio){…do{charb[BDEVNAME_SIZE];q=bdev_get_queue(bio-bi_bdev);…block_wait_queue_running(q);/**Ifthisdevicehaspartitions,remapblockn*ofpartitionptoblockn+start(p)ofthedisk.*/blk_partition_remap(bio);ret=q-make_request_fn(q,bio);}while(ret);}
主要操作:
根据bio中保存的块设备号取得请求队列q检测当前IO调度器是否可用,如果可用,则继续;否则等待调度器可用调用q-make_request_fn所指向的函数将该请求(bio)加入到请求队列中到此为止,通用块层的操作结束。
IO调度层的处理
对make_request_fn函数的调用可以认为是IO调度层的入口,该函数用于向请求队列中添加请求。
该函数是在创建请求队列时指定的,代码如下(blk_init_queue函数中):
q-request_fn=rfn;blk_queue_make_request(q,__make_request);
函数blk_queue_make_request将函数__make_request的地址赋予了请求队列q的make_request_fn成员,那么,__make_request函数才是IO调度层的真实入口。
__make_request函数的主要工作为:
检测请求队列是否为空,若是,延缓驱动程序处理当前请求(其目的是想积累更多的请求,这样就有机会对相邻的请求进行合并,从而提高处理的性能),并跳到3,否则跳到2试图将当前请求同请求队列中现有的请求合并,如果合并成功,则函数返回,否则跳到3该请求是一个新请求,创建新的请求描述符,并初始化相应的域,并将该请求描述符加入到请求队列中,函数返回将请求放入到请求队列中后,何时被处理就由IO调度器的调度算法决定了(有关IO调度器的算法内容请参见参考资料)。
一旦该请求能够被处理,便调用请求队列中成员request_fn所指向的函数处理。
这个成员的初始化也是在创建请求队列时设置的:
q-request_fn=rfn;blk_queue_make_request(q,__make_request);
第一行是将请求处理函数rfn指针赋给了请求队列的request_fn成员。
而rfn则是在创建请求队列时通过参数传入的。
对请求处理函数request_fn的调用意味着IO调度层的处理结束了。
块设备驱动层的处理
request_fn函数是块设备驱动层的入口。
它是在驱动程序创建请求队列时由驱动程序传递给IO调度层的。
IO调度层通过回调request_fn函数的方式,把请求交给了驱动程序。
而驱动程序从该函数的参数中获得上层发出的IO请求,并根据请求中指定的信息操作设备控制器(这一请求的发出需要依据物理设备指定的规范进行)。
到此为止,块设备驱动层的操作结束。
块设备层的处理
接受来自驱动层的请求,完成实际的数据拷贝工作等等。
同时规定了一系列规范,驱动程序必须按照这个规范操作硬件。
后续工作
当设备完成了IO请求之后,通过中断的方式通知cpu,而中断处理程序又会调用request_fn函数进行处理。
当驱动再次处理该请求时,会根据本次数据传输的结果通知上层函数本次IO操作是否成功,如果成功,上层函数解锁IO操作所涉及的页面(在do_generic_mapping_read函数中加的锁)。
该页被解锁后,do_generic_mapping_read()函数就可以再次成功获得该锁(数据的同步点),并继续执行程序了。
之后,函数sys_read可以返回了。
最终read系统调用也可以返回了。
至此,read系统调用从发出到结束的整个处理过程就全部结束了。
总结
本文介绍了linux系统调用read的处理全过程。
该过程分为两个部分:
用户空间的处理和核心空间的处理。
在用户空间中通过0x80中断的方式将控制权交给内核处理,内核接管后,经过6个层次的处理最后将请求交给磁盘,由磁盘完成最终的数据拷贝操作。
在这个过程中,调用了一系列的内核函数。
如图6
图6read系统调用在内核中所经历的函数调用层次
参考资料
查看文章"使用Linux系统调用的内核命令",了解系统调用的基本原理以及如何实现自己的系统调用的方法。
查
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- read 系统调用剖析 系统 调用 剖析