块设备驱动编写总结

块设备与字符设备的区别
 
1、 从字面上理解,块设备和字符设备最大的区别在于读写数据的基本单元不同。块设备读写数据的基本单元为块,例如磁盘通常为一个sector,而字符设备的基本单元为字节。所以Linux中块设备驱动往往为磁盘设备的驱动,但是由于磁盘设备的IO性能与CPU相比很差,因此,块设备的数据流往往会引入文件系统的Cache机制。
 
2、 从实现角度来看,Linux为块设备和字符设备提供了两套机制。字符设备实现的比较简单,内核例程和用户态API一一对应,用户层的Read函数直接对应了内核中的Read例程,这种映射关系由字符设备的file_operations维护。块设备接口相对于字符设备复杂,read、write API没有直接到块设备层,而是直接到文件系统层,然后再由文件系统层发起读写请求。
 
块设备读写流程 

在学习块设备原理的时候,我最关系块设备的数据流程,从应用程序调用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。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/2646dcab5471dabc40ab840bc6c79d39.html