tags : Linux源码
这篇文章主要是对学习虚拟文件系统和驱动架构的过程中遇到的一些比较关键的问题的讨论,文中的很大部分都是自己的理解、对现有书籍的补充,在看书的过程中结合本博客可以更快更好的理解内核。涉及的书籍有《深入理解Linux内核》、《深入Linux内核架构》、《Linux内核情景分析》,内核版本为2.6.24
。
由于之前没有 linux 相关系统的使用经验对设备文件这个概念蒙了好长时间,后来渐渐的从代码和书籍中看出点端倪。设备文件和普通的文件差别也不大,只不过设备文件可以代表着整个设备;数据结构方面区分普通文件和设备文件的地方在于inode
中的umode_t i_mode
字段,该字段用来表示一个文件类型和访问权限,通过该字段可以看出来一个inode
是表示一个普通的文件还是一个设备文件。前两篇文章也提及过设备文件存在是设计使然,并不是说非要用一个文件表示一个设备,只不过一个设备同样具有和普通文件类似的属性(读写权限,层级关系),所以用一个文件表示一个设备并将这样的文件命名为设备文件,明白了内核设计者这一意图看书的时候才不至于晕掉。单从读写设备文件和普通文件来说,设备文件反而比普通的文件实现起来更简单 : 前面介绍过普通的文件的读写,一个普通的文件的文件相对块号 A 具体映射到磁盘的哪一块上对于不同的文件系统来说是不同的;但是对于设备文件其文件相对块号 1 就是映射到对应设备的磁盘相对块号 1 上面的,这是比普通文件简单的多的,这也就是为什么《Linux内核情景分析》中说设备文件的文件系统层“很薄”。下图为cat sda1
直接读取设备文件的情况,可以看到很多都是乱码
但是一个设备文件存在的意义并不是用来读写的而是用来表示一个设备的,譬如前文提到过的挂载、分区操作的参数就是一个设备文件,还有一些IO控制,这才是其真正意义。
设备号和设备文件时息息相关的,想要比较好的理解设备号和设备文件存在的意义,需要理解以下四个方面 :
6.2.5
节)。inode
时都会调用init_special_inode
函数void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
inode->i_mode = mode;
if (S_ISCHR(mode)) { //字符设备文件
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
} else if (S_ISBLK(mode)) {//块设备文件
inode->i_fop = &def_blk_fops;
inode->i_rdev = rdev;
}
}
其中的def_chr_fops
和def_blk_fops
(在《架构》上面都有介绍不再赘述)会根据设备号
建立inode
和具体设备与特定函数指针
的关系。也就是说如果自己实现一个文件系统注册到内核中去,但是在打开文件的过程中创建inode的时候不去调用init_special_inode
,那么创建在此文件系统中的设备文件(以及其他特殊文件)将不存在实际的意义。而通常使用的/dev
目录则是挂载的udev
文件系统(内存文件系统)
这也就是说需要内核
、驱动程序
、具体文件系统
之间的配合才能将设备号的作用发挥到极致。
在很好几本书上面看到过这个图片,展示的是如何用一个bio
实例描述一次读写操作
bio
结构中有读写请求的开始逻辑磁盘号
、需要读的块的数据
和一组用bio_vec
描述的缓冲区,也就是说一个bio
实例就代表着“以读/写的方式使缓冲区里面的数据和磁盘中的数据一致”。当时一直想不明白的地方在于“为什么需要用一组bio_vec来描述缓冲区呢”?。由于新式的磁盘控制器支持所谓的分散-聚合*(scatter-gather)*DMA传送方式 : 此种方式中在需要读写的磁盘块是连续的情况系,磁盘可以与一些非连续的物理内存区传送数据,块设备驱动需要向磁盘控制器发送 (a) 要传输的其实磁盘扇区号和总的要传输的扇区数;(b) 内存的描述符链表,其中的链表的每一项包含一个地址和一个长度。再去看bio
结构体的构成,不就是为了和DMA
控制器数据交互而生么。
直观上感觉bio
结构体和request
结构体语义上面有些重复 : bio
描述一次读写请求,request
也描述一次读写请求。这篇文章中提到
bio 代表一个IO 请求
request 是bio 提交给IO调度器产生的数据,一个request 中放着顺序排列的bio。
所说的顺序排列应该就是指的逻辑磁盘块号连续,可以一个bio
描述的不已经是逻辑磁盘块号连续的一次请求了么,那request
的存在意义到底是什么呢?
这个问题可以这样理解,举例来说,进程A
发起一次读请求,需要读的扇区号是 10 ,这个读请求抽象为bioA
,也就是用bioA
来记录必要的状态来表示这一次读请求;进程B
发起一次读请求,需要读的扇区号是 11 ,这个读请求抽象为bioB
.通用块层在处理(排序)这两个bio
的时候发现它们两个需要读的磁盘块物理上是挨着的,所以为了加速读取将这两个bio
合并到一个request
之中.具体的连接关系如下:
这也就说明了为什么不能把两个bioA
、bioB
合并成一个bioA&B
,而是通过链表的方式连接在request
结构体中 : 因为即使连续的两个bio
可能是来自不同进程的读写请求。
可是为什么来自不同进程的bio
就不能合并成一个bio
呢?这和内存和磁盘完成数据交换之后如何通知发起进程的方式是息息相关的。在同步读写的方式下,进程发起读写请求之后数据交换完成之前进程会变为阻塞状态
。阻塞的进程总归要放在一个地方保存起来,以便数据交换完成之后中断处理程序将阻塞的进程恢复正常让其继续执行。而阻塞的进程存放的地方是需要和bio
建立联系的,就是说找到一个bio
就能找到因为这次读写请求阻塞的进程。可想而知这是非常符合逻辑的,数据交换完成之后中断处理程序很容易找到bio
(参见《Linux内核情景分析》 1149−1152 )也就很容易找到为之阻塞的进程了。所以bioA
即使和bioB
逻辑磁盘号相邻也不应该合并,因为它们还肩负着帮助中断处理程序找到阻塞进程的重任呢。
这个时候就可以更正一下上面的说法了
bio
描述的是特定进程的逻辑磁盘号连续的一次读写请求。
request
描述的是整个操作系统逻辑磁盘号连续的一次读写请求。
顺便说一下,对于进程A
的一次读写请求可能会需要多个bio
,只有当所有的bio
都完成操作之后才会恢复进程的执行。
《深入Linux内核架构》是学习内核过程中看的遍数最多的书了,其从架构的角度讲解了内核的关键位置。而《深入理解Linux内核》更多是手册性质、《Linux内核情景分析》则更多的是对代码的讲解,如果没有一个系统的认识很容易在看书的过程中被绕晕,所以总的来说《架构》这本书真心不错的。但是《架构》的第六章的让新手很容易犯迷糊 : 6.4 字符设备的操作
其实是对表示字符设备的设备文件的操作,同理6.5 块设备操作
是对表示块设备的设备文件的操作,而且其中6.5.4~6.5.8
都应该算是虚拟文件层的东西,需要结合着文件的读写操作来看。
第六章前几节出现的genhd
应该是gendisk
,估计是打错了。