Linux内核学习11——编写块设备驱动程序(上)

Linux内核学习11——编写块设备驱动程序(上)

块设备可以随机存储。字符设备,比如键盘,只能按照输入顺序存取,不可随机,打乱输入的字节流。

磁盘的最小读写单元是一个扇区

Linux内核学习11——编写块设备驱动程序(上)

文件系统层,包括常见的文件系统,以及虚拟文件系统层VFS,字符设备可以直接用应用程序打开。块设备不会在应用程序直接打开,而是要通过文件系统访问块设备。

通用块层:为了通用的块设备,建立了一个统一的模型,这一层的主要工作是接收文件系统层发出的磁盘请求,并最终向磁盘设备发出io请求,所以它隐藏了很多底层硬件块设备的细节,为块设备提供了一个抽象的模型。

IO调度层:接收通用块层发出的IO请求,缓存这些请求,然后根据IO调度算法来合并相邻的请求,然后调用驱动层提供的处理函数,发送这些io请求到硬件设备。这里io调度层有多种调度算法,比如电梯调度算法,CFQ调度算法,因为传统机械硬盘,他的物理结构特性,内核是不会简单的按照请求的顺序来提交给块设备的,io调度器会根据场景进行优化,来做一些合并或排序这样一些预操作。

块设备驱动:这一层就是实际的块设备驱动,比如磁盘驱动,光驱驱动,ssd驱动。一般来说,块设备驱动包含块设备的注册,打开,关闭以及具体的IO处理

Linux内核学习11——编写块设备驱动程序(上)

从上面的介绍,我们知道了linux内核的块设备子系统的架构,也了解了块设备驱动为了解决上层应用程序访问块设备,和底层块设备硬件之间的衔接问题,它建立了一个通用的块层,要深入了解块设备驱动,我们还要了解linux内核为块设备驱动设计了哪些主要的数据结构。要去理解为什么要抽象和设计这些数据结构。

block_device:是虚拟文件系统和块设备子系统的一个粘合剂,用户程序一般是不会直接操作块设备的,通常是通过文件系统的接口来访问块设备

Linux内核学习11——编写块设备驱动程序(上)

linux内核把磁盘类设备,关于磁盘通用的那部分提取出来,使用一个gendisk这样一个数据结构来描述,所以它实际上也是一种抽象,这个gendisk数据结构,可以表示一个已经分区的磁盘,也可以表示一个未分区的磁盘,我们看一下gendisk重要的成员。

第一个是major,major表示这个块设备的主设备号,通常用来指定当前设备对应的哪个设备驱动。

first,minor,这两个是用来表示从设备号的一个范围,

minors 表示当前gendisk对象所能包含的最大从设备个数

disk_name表示当前块设备对象名称

disk_part_tbl表示gendisk对象的分区表信息

fops表示块设备的一组文件操作集合

queue表示当前gendisk对象所代表的块设备的io请求队列

Linux内核学习11——编写块设备驱动程序(上)

对于字符设备驱动,linux内核里面有一整套的操作方法,它包含在file_operations数据结构里。

对于块设备驱动,linux内核也为它准备了一套类似的操作方法集,相对于字符设备的操作方法集,块设备的操作方法集明显要弱化了很多

这里主要的操作方法有:open打开块设备,release关闭块设备,ioctl来实现块设备的标准命令,media_changed检查驱动器的介质是否改变,getgeo获取磁盘器的一些信息,大家会发现块设备的操作方法集里面已经没有了read write这个方法了,并不是说块设备不需要read/write,正是因为块设备的一些特性,已经把块设备的读写方法使用了特别的读写请求队列来实现了,这个差异也是块设备和字符设备的一个重要的特点。

块设备一般与系统进行交互的数据量比较大,而字符设备一般来说交互的数据量不大,因此对于块设备子系统,针对这些不同的需求和特性linux内核进行了单独的优化,

Linux内核学习11——编写块设备驱动程序(上)

对于来自通用块层的请求,在提交到块设备驱动的时候,我们需要构造一个请求,就是request,也要把这些request插入到块设备请求队列中,因此,文件系统有新的请求的时候就会把新的请求加入到请求队列中,这里通常会通过submit_io这个请求队列把来自文件系统的请求添加到请求队列中。只要请求队列不为空,队列中对应的这些块设备驱动程序就会从请求队列里面获取请求,然后把这些对应的请求发送到具体的块设备驱动里面进行处理

Linux内核学习11——编写块设备驱动程序(上)

块设备子系统最重要的一个功能就是把文件系统层发来的数据请求进行处理,而这个处理过程中最核心的一个数据对象就是bio,bio有点类似网络的协议包,它在文件系统和块设备子系统中来回的游动,把要读的数据和要写的数据来回的搬移。通常来讲,bio代表来自文件系统的一个请求,每个bio都代表不同的访问上下文,可能来自不同的文件系统或应用程序,所以bio对象包含了块设备处理一个请求它所需要的所有的信息

Linux内核学习11——编写块设备驱动程序(上)

比如,文件系统里面有一个test文件,我们在用户空间里面使用open函数进行打开, 在程序中就会返回一个文件句柄fd,在内核空间里面,这个被打开的文件怎么和存储它的块设备关联起来?

当在用户空间打开一个文件的时候,在内核空间就会有一个file的数据结构与它对应,这个file的数据结构有一个f_mapping字段,它是address_space数据结构,这个数据结构是用来管理文件映射到内存页面的。这个数据结构里面有一个host成员,

host成员是inode数据结构,inode数据结构就是索引节点,用来存放文件和目录的基本信息,每个文件或目录都会有一个inode,这个inode数据结构里面有一个i_bdef成员。

i_bdef就是我们今天讲的block_device数据结构,在block_device数据结构里面,有一个叫做bd_disk成员,它指向了gendisk数据结构,在gendisk数据结构里面有一个queue的成员,它指向request_queue数据结构。

来自文件系统的一个个请求,会被添加到request_queue队列里面,这就是我们说的请求队列,实际上这个块设备驱动就会从这个请求队列里面读取request,然后进行处理,把处理发送到硬件设备中。

Linux内核学习11——编写块设备驱动程序(上)

我们了解了块设备中主要数据结构的设计理念,下面介绍块设备驱动中最常用的API接口函数。

块设备注册函数register_blkdev函数,这个函数有两个参数,major参数是块设备使用的主设备号,第二个参数那么是设备名,如果第一个参数major=0,内核会自动分配一个新的主设备号,这个函数成功的话就会返回一个主设备号。如果返回负数表示失败,

注销函数unregister_blkdev函数,它也有两个参数,传递的参数必须与register_blkdev一致

Linux内核学习11——编写块设备驱动程序(上)

初始化请求队列blk_init_queue,这个函数为当前设备分配一个request请求队列,第一个参数rfn表示请求处理函数,它是一个函数指针,第二个参数lock是请求队列要使用的一个锁,通常我们的设备驱动里面需要初始化这个锁,

注销请求队列blk_cleanup_queue,

Linux内核学习11——编写块设备驱动程序(上)

alloc_disk,动态分配一个gendisk数据结构,第一个参数minors表示这个磁盘使用的次设备号的数量,即磁盘分区的数量。

add_disk,该函数会像linux内核注册一个磁盘设备,这里和字符设备的cdev_add函数非常类似,都是向系统注册一个设备,这个函数的参数是alloc_disk函数分配的gendisk对象

del_gendisk,当磁盘设备不再使用时,我们用该函数释放gendisk对象。

Linux内核学习11——编写块设备驱动程序(上)

当需要和块设备进行数据传输,或者对块设备发送iO请求的时候,内核需要发送请求对象bio到请求队列里面,此时就可以调用submit_bio这个接口函数来完成。该函数有两个参数,第一个参数rw表示发送请求是读还是写。第二个参数bio对象。

如果驱动程序需要单独调用submit_bio这个api的时候,驱动程序需要去负责填充bio,

Linux内核学习11——编写块设备驱动程序(上)

读写ramdis

Linux内核学习11——编写块设备驱动程序(上)

上一篇:复盘《The Last of Us Part 2》为什么失败


下一篇:可以在判断返回值是否是需要的情况下进行retry: