首页IT科技linux创建块设备文件(linux块设备读写流程详解)

linux创建块设备文件(linux块设备读写流程详解)

时间2025-05-03 01:59:05分类IT科技浏览4942
导读:在学习块设备原理的时候,我最关系块设备的数据流程,从应用程序调用Read或者Write开始,数据在内核中到底是如何流通、处理的呢?然后又如何抵达具体的物理设备的呢?下面对一个带Cache功能的块设备数据流程进行分析。一起去看看吧!...

在学习块设备原理的时候            ,我最关系块设备的数据流程                    ,从应用程序调用Read或者Write开始      ,数据在内核中到底是如何流通              、处理的呢?然后又如何抵达具体的物理设备的呢?下面对一个带Cache功能的块设备数据流程进行分析            。一起去看看吧!

1                  、 用户态程序通过open()打开指定的块设备         ,通过systemcall机制陷入内核                    ,执行blkdev_open()函数          ,该函数注册到文件系统方法(file_operations)中的open上                    。在blkdev_open函数中调用bd_acquire()函数      ,bd_acquire函数完成文件系统inode到块设备bdev的转换                   ,具体的转换方法通过hash查找实现      。得到具体块设备的bdev之后              ,调用do_open()函数完成设备打开的操作         。在do_open函数中会调用到块设备驱动注册的open方法   ,具体调用如下:gendisk->fops->open(bdev->bd_inode, file)                    。

2      、 用户程序通过read           、write函数对设备进行读写                  ,文件系统会调用相应的方法                 ,通常会调用如下两个函数:generic_file_read和blkdev_file_write          。在读写过程中采用了多种策略,首先分析读过程      。

3                   、 用户态调用了read函数               ,内核执行generic_file_read                    ,如果不是direct io方式   ,那么直接调用do_generic_file_read->do_generic_mapping_read()函数            ,在do_generic_mapping_read(函数位于filemap.c)函数中                    ,首先查找数据是否命中Cache      ,如果命中         ,那么直接将数据返回给用户态;否则通过address_space->a_ops->readpage函数发起一个真实的读请求                   。在readpage函数中                    ,构造一个buffer_head          ,设置bh回调函数end_buffer_async_read      ,然后调用submit_bh发起请求              。在submit_bh函数中                   ,根据buffer_head构造bio              ,设置bio的回调函数end_bio_bh_io_sync   ,最后通过submit_bio将bio请求发送给指定的快设备   。

4         、 如果用户态调用了一个write函数                  ,内核执行blkdev_file_write函数                 ,如果不是direct io操作方式,那么执行buffered write操作过程               ,直接调用generic_file_buffered_write函数                  。Buffered write操作方法会将数据直接写入Cache                    ,并进行Cache的替换操作   ,在替换操作过程中需要对实际的快设备进行操作            ,address_space->a_ops提供了块设备操作的方法                 。当数据被写入到Cache之后                    ,write函数就可以返回了      ,后继异步写入的任务绝大部分交给了pdflush daemon(有一部分在替换的时候做了)

5       、 数据流操作到这一步         ,我们已经很清楚用户的数据是如何到内核了。与用户最接近的方法是file_operations                    ,每种设备类型都定义了这一方法(由于Linux将所有设备都看成是文件          ,所以为每类设备都定义了文件操作方法      ,例如                   ,字符设备的操作方法为def_chr_fops              ,块设备为def_blk_fops   ,网络设备为bad_sock_fops)               。每种设备类型底层操作方法是不一样的                  ,但是通过file_operations方法将设备类型的差异化屏蔽了                 ,这就是Linux能够将所有设备都理解为文件的缘由                    。到这里,又提出一个问题:既然这样               ,那设备的差异化又该如何体现呢?在文件系统层定义了文件系统访问设备的方法                    ,该方法就是address_space_operations   ,文件系统通过该方法可以访问具体的设备   。对于字符设备而言            ,没有实现address_space_operations方法                    ,也没有必要      ,因为字符设备的接口与文件系统的接口是一样的         ,在字符设备open操作的过程中                    ,将inode所指向的file_operations替换成cdev所指向的file_operations就可以了            。这样用户层读写字符设备可以直接调用cdev中file_operations方法了                    。

6                    、 截至到步骤(4)          ,读操作在没有命中Cache的情况下通过address_space_operations方法中的readpage函数发起块设备读请求;写操作在替换Cache或者Pdflush唤醒时发起块设备请求      。发起块设备请求的过程都一样      ,首先根据需求构建bio结构                   ,bio结构中包含了读写地址            、长度   、目的设备                    、回调函数等信息         。构造完bio之后              ,通过简单的submit_bio函数将请求转发给具体的块设备                    。从这里可以看出   ,块设备接口很简单                  ,接口方法为submit_bio(更底层函数为generic_make_request)                 ,数据结构为struct bio          。

7               、 submit_bio函数通过generic_make_request转发bio,generic_make_request是一个循环               ,其通过每个块设备下注册的q->make_request_fn函数与块设备进行交互      。如果访问的块设备是一个有queue的设备                    ,那么会将系统的__make_request函数注册到q->make_request_fn中;否则块设备会注册一个私有的方法                   。在私有的方法中   ,由于不存在queue队列            ,所以不会处理具体的请求                    ,而是通过修改bio中的方法实现bio的转发      ,在私有make_request方法中         ,往往会返回1                    ,告诉generic_make_request继续转发比bio              。Generic_make_request的执行上下文可能有两种          ,一种是用户上下文      ,另一种为pdflush所在的内核线程上下文   。

8、 通过generic_make_request的不断转发                   ,最后请求一定会到一个存在queue队列的块设备上              ,假设最终的那个块设备是某个scsi disk(/dev/sda)                  。generic_make_request将请求转发给sda时   ,调用__make_request                  ,该函数是Linux提供的块设备请求处理函数                 。在该函数中实现了极其重要的操作                 ,通常所说的IO Schedule就在该函数中实现。在该函数中试图将转发过来的bio merge到一个已经存在的request中,如果可以合并               ,那么将新的bio请求挂载到一个已经存在request中               。如果不能合并                    ,那么分配一个新的request   ,然后将bio添加到其中                    。这一切搞定之后            ,说明通过generic_make_request转发的bio已经抵达了内核的一个站点——request                    ,找到了一个临时归宿   。此时      ,还没有真正启动物理设备的操作            。在__make_request退出之前         ,会判断一个bio中的sync标记                    ,如果该标记有效          ,说明请求的bio是一个是实时性很强的操作      ,不能在内核中停留                   ,因此调用了__generic_unplug_device函数              ,该函数将触发下一阶段的操作;如果该标记无效的话   ,那么该请求就需要在queue队列中停留一段时间                  ,等到queue队列触发闹钟响了之后                 ,再触发下一阶段的操作                    。__make_request函数返回0,告诉generic_make_request无需再转发bio了               ,bio转发结束      。

9                 、 到目前为止                    ,文件系统(pdflush或者address_space_operations)发下来的bio已经merge到request queue中   ,如果为sync bio            ,那么直接调用__generic_unplug_device                    ,否则需要在unplug timer的软中断上下文中执行q->unplug_fn         。后继request的处理方法应该和具体的物理设备相关      ,但是在标准的块设备上如何体现不同物理设备的差异性呢?这种差异性就体现在queue队列的方法上         ,不同的物理设备                    ,queue队列的方法是不一样的                    。举例中的sda是一个scsi设备          ,在scsi middle level将scsi_request_fn函数注册到了queue队列的request_fn方法上          。在q->unplug_fn(具体方法为:generic_unplug_device)函数中会调用request队列的具体处理函数q->request_fn      。Ok      ,到这一步实际上已经将块设备层与scsi总线驱动层联系在了一起                   ,他们的接口方法为request_fn(具体函数为scsi_request_fn)                   。

10                  、明白了第(9)点之后              ,接下来的过程实际上和具体的scsi总线操作相关了              。在scsi_request_fn函数中会扫描request队列   ,通过elv_next_request函数从队列中获取一个request   。在elv_next_request函数中通过scsi总线层注册的q->prep_rq_fn(scsi层注册为scsi_prep_fn)函数将具体的request转换成scsi驱动所能认识的scsi command                  。获取一个request之后                  ,scsi_request_fn函数直接调用scsi_dispatch_cmd函数将scsi command发送给一个具体的scsi host                 。到这一步                 ,有一个问题:scsi command具体转发给那个scsi host呢?秘密就在于q->queuedata中,在为sda设备分配queue队列时               ,已经指定了sda块设备与底层的scsi设备(scsi device)之间的关系                    ,他们的关系是通过request queue维护的。

11   、 在scsi_dispatch_cmd函数中   ,通过scsi host的接口方法queuecommand将scsi command发送给scsi host               。通常scsi host的queuecommand方法会将接收到的scsi command挂到自己维护的队列中            ,然后再启动DMA过程将scsi command中的数据发送给具体的磁盘                    。DMA完毕之后                    ,DMA控制器中断CPU      ,告诉CPU DMA过程结束         ,并且在中断上下文中设置DMA结束的中断下半部   。DMA中断服务程序返回之后触发软中断                    ,执行SCSI中断下半部            。

12              、在SCSi中断下半部中          ,调用scsi command结束的回调函数      ,这个函数往往为scsi_done                   ,在scsi_done函数调用blk_complete_request函数结束请求request              ,每个请求维护了一个bio链   ,所以在结束请求过程中回调每个请求中的bio回调函数                  ,结束具体的bio                    。Bio又有文件系统的buffer head生成                 ,所以在结束bio时,回调buffer_head的回调处理函数bio->bi_end_io(注册为end_bio_bh_io_sync)      。自此               ,由中断引发的一系列回调过程结束                    ,总结一下回调过程如下:scsi_done->end_request->end_bio->end_bufferhead         。

13                  、回调结束之后   ,文件系统引发的读写操作过程结束                    。

声明:本站所有文章            ,如无特殊说明或标注                    ,均为本站原创发布          。任何个人或组织      ,在未征得本站同意时         ,禁止复制      、盗用           、采集                   、发布本站内容到任何网站         、书籍等各类媒体平台      。如若本站内容侵犯了原著者的合法权益                    ,可联系我们进行处理                   。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
win怎么给文件夹加密(win11文件夹怎么加密?win11系统自带文件加密的方法步骤) windows11激活密码忘了(亲测 Windows11密钥/激活码分享 附激活工具)