首页 理论教育深入剖析Linux内核与设备驱动:块设备核心数据结构及操作

深入剖析Linux内核与设备驱动:块设备核心数据结构及操作

【摘要】:图5-21块设备内部框架2核心管理实体从整体的层次上看,块设备需要有针对文件系统和VFS的接口实体,另外还需要管理生命周期的设备模型相关实体以及驱动管理实体。图5-22块设备层各实体及系统关系框图从图5-23可见,文件系统中的设备层次由block_device来实现,驱动的管理实体通过gendisk来进行管理,hd_struct进行物理的分区管理,同时为设备模型的接口。图5-24块设备驱动管理实体从图5-24可见,其中主要包括设备的控制操作接口

1ᤫ整体框架

要了解块设备首先要从系统层面了解块设备所在的位置和上下层的关系,如图5-20所示。

978-7-111-49426-3-Chapter05-88.jpg

图5-20 块设备在系统中层次关系

从图5-20可见,块设备层既可以作为裸设备来供应用层使用,也可以作为物理文件系统的承载设备与文件系统进行关联。为了提升整个系统的性能,在块设备层之上增加了buffer层来缓冲数据,buffer是缓存在内存中,所以物理上会将多个buffer组织在page中,整体形成pagecache。这种架构满足了之前涉及的需求,并兼顾灵活性与整体的性能。

块设备层的内部设计同样要考虑性能,从整体上块设备的内部框架如图5-21所示。从图5-21可见,块设备的框架层次还是很灵活的,可以为了满足某种需求,设计虚拟块设备实现如负载均衡等高级的功能;为了整体性能和吞吐量,增加了IO scheduler层来优化物理设备的读写性能;也可以跳过IO scheduler层直接进行块设备操作。总体上讲,这种内部框架可以把分层的驱动综合起来实现复杂的功能。

978-7-111-49426-3-Chapter05-89.jpg

图5-21 块设备内部框架

2ᤫ核心管理实体

从整体的层次上看,块设备需要有针对文件系统和VFS的接口实体,另外还需要管理生命周期的设备模型相关实体以及驱动管理实体。除此之外块设备的数据是以块为单位,自然在操作上要有相关的单位操作管理实体以及管理方法,这些就形成了驱动。

针对以上的分析,来看看Linux内核的具体实现,块设备主要管理实体及系统关系如图5-22所示。

图5-22中主要涉及的管理实体是block_device、hd_struct和gendisk。与操作相关的是re-quest和bio结构,另外图中还涉及与buffer/page cache的关系。buffer/page cache是将文件系统等非内存空间的数据存放在内存页面中,所以在系统结构上通过内存管理的page以及地址转换管理address_space(可以根据不同的内容重载为不同的管理操作)实现这部分的管理,块设备的数据单元块在内存中通过buffer_head进行管理,一个物理页中会存放多个块,而物理页的管理实体page会通过链表对buffer head进行管理,进而管理其中存放的块。而page中的private和标识共同表示其中管理的是buffer_head链表。buffer_head和page是通过attach_page_buffers来进行关联该函数会通过set_page_private来设置page的private属性,这样通过page,buffer_head以及address_space就实现了buffer/page cache的管理。涉及块设备层和上层之间的缓冲操作也可以通过该框架实现,只要实现不同的address_space操作即可。

物理的块设备(如硬盘)通常可以进行分区,而不同分区在用户看来是不同的设备。硬盘本身同样是设备,这样实际就有设备层次,需要在文件系统层面能够了解这种设备层次,是设备管理的需求,否则用户无法了解某个分区及物理磁盘的关系。这就要求块设备对应的文件系统相关的管理实体能够表现这种层次关系。同一个物理磁盘,相关的操作没有差别,与之上的文件系统没有关系,所以驱动管理实体并不需要每个分区有一个,为了实现数据的批处理操作相应的操作数据需要以链表的形式存在。再者在块设备层与文件系统的接口管理实体中直接表示硬件的分区信息是不合适的,所以也要一个实体进行物理分区的管理,分区本身就是实际的逻辑设备,所以也会将设备模型相关的管理归入其中。以上的管理都是通过管理实体block_device、hd_struct和gendisk来实现的。它们之间各种关系的细节如图5-23所示。

978-7-111-49426-3-Chapter05-90.jpg

图5-22 块设备层各实体及系统关系框图

从图5-23可见,文件系统中的设备层次由block_device来实现,驱动的管理实体通过gendisk来进行管理,hd_struct进行物理的分区管理,同时为设备模型的接口。这样各种结构的功能就明确了,接下来看看细节。

978-7-111-49426-3-Chapter05-91.jpg

图5-23 块设备内部管理实体关系框图

先看看block_device的主要信息:

978-7-111-49426-3-Chapter05-92.jpg

978-7-111-49426-3-Chapter05-93.jpg

从其中的属性可见与VFS以及具体的文件系统都会有关联,这点不难理解。设备本身可以作为文件提供给用户,另外块设备还是具体的文件系统的承载,这两点都要体现,就都需要关联彼此了。

接下来看看hd_struct的主要信息:

978-7-111-49426-3-Chapter05-94.jpg

其中不仅包含了物理分区的信息还与设备模型有关联,毕竟分区就是对应着物理的设备。相应的设备模型的管理主要是在设置__dev上,在add_partition中的设置如下:

978-7-111-49426-3-Chapter05-95.jpg

可见相关操作时初始化device实体并分配设备号,其中device关联特定的part_type类型,生命周期结束时可以释放相关的空间。整个磁盘又是如何与设备模型相关联呢?这就需要看看gendisk:

978-7-111-49426-3-Chapter05-96.jpg

可见其中嵌入了structhd_struct part0;这样既有了整体disk的物理信息又可以与设备模型相关联。在alloc_disk中会调用alloc_disk_node,其中包含如下代码:

978-7-111-49426-3-Chapter05-97.jpg

这里设置的设备模型相关类型为disk_type,可以对disk进行生命周期管理,在必要的时候可以释放其中分区的信息。

3ᤫ驱动管理及块设备驱动流程

块设备层的驱动管理工作同样是由kobj_map来实现的,kobj_map中的内容之前在字符设备中已经进行了介绍,下面看看块设备层是如何使用它完成驱动管理的。

驱动管理要涉及设备号的范围及管理实体,块设备层将其封装为blk_register_region,细节如下:(www.chuimin.cn)

978-7-111-49426-3-Chapter05-98.jpg

下面来看看具体的管理实体是什么?相关在add_disk中的实现如下:

978-7-111-49426-3-Chapter05-99.jpg

978-7-111-49426-3-Chapter05-100.jpg

从代码分析可见,驱动管理实体是gendisk,在注册驱动管理实体的同时还向sys文件系统注册一些属性,以便运行时可以进行相关设置。

块设备驱动管理的子设备号由具体的块设备进行设置,因为这和其能力相关,块设备层提供了alloc_disk接口进行相关操作。

驱动管理实体gendisk的细节如图5-24所示。

978-7-111-49426-3-Chapter05-101.jpg

图5-24 块设备驱动管理实体

从图5-24可见,其中主要包括设备的控制操作接口block_device_operations,以及与设备数据流相关的数据队列及操作。block_device_operations中主要是对设备打开、关闭,以及换盘等控制操作的接口。这样设计的原因是:块设备是文件系统的物理基础,而物理文件系统不会调用VFS中的文件接口(这样做使得层次混乱),所以相关的控制操作通常会在块设备层中进行封装,比如blkdev_get就会进行设备open的操作,这样可以为上层各种需求提供统一的接口。

块设备数据流请求的具体关系以一个例子进行说明,如图5-25所示。

图5-25中的一次操作的请求是读取连续16KB的数据,由于Linux内核中将sect定义为512B,所以是32个sect。相应的请求拆成两个bio进行操作,每个bio为8KB的操作,对应到内存的两个page,相应分配bio_vec的数组为2会关联到两个物理页面的管理实体。实际的操作就是在页面与bio之间进行,通常这些块设备DMA的操作通过散列式DMA的方式实现,以提高整体的性能。

978-7-111-49426-3-Chapter05-102.jpg

图5-25 块设备数据请求例子

针对块设备的数据操作流程,块设备层统一提供了操作单元bio,相应的起始操作接口是submit_bio,从submit_bio到请求队列的流程如图5-26所示。

978-7-111-49426-3-Chapter05-103.jpg

图5-26 submit_bio到请求队列流程

从图5-26可见,在真正处理请求之前是可以对请求进行合并等操作的,这样保证最终执行的请求尽可能的集中,以减少如机械硬盘等有机械件的块设备的机械操作。

通常情况下具体块设备相应的请求处理流程如图5-27所示。

978-7-111-49426-3-Chapter05-104.jpg

图5-27 一般块设备请求处理流程

从图5-27可见,上层下发的请求是完全按照顺序进行处理的,其中请求队列中的re-quest_fn通常是唤醒具体块设备的驱动进程进行实际的请求处理,这样将上层与驱动分开在不同的执行实体执行,可以通过对数据进行批处理操作,从而提高整体的性能。

从系统的角度,IO性能除了吞吐量外,还要考虑不同应用IO请求的响应延时。如果只是按照顺序进行块设备的请求处理并不能很好地满足各种响应延时的需求,所以需要在请求执行过程中加入调度的机制,就是IO scheduler,来提升整体的性能。由于这些都是对实际操作请求的调整,所以都被包含在request_queue中,不同的IO scheduler方式由struct eleva-tor_type来定义,主要的操作接口包含在struct elevator_ops中。request_queue如果使用IO Scheduler则逻辑上有两套queue,一个是IO scheduler使用的queue;另一个是物理驱动操作的queue。具体的流程如图5-28所示。

从图5-28可见,真正对请求的操作顺序是会依据之前的IO操作重新调整顺序的,作为内核块设备层可以实现一个反馈系统,从而尽力满足所有用户的性能需求,提高整体的IO性能。当然不会有对所有情况都满足的调度方法,所以Linux内核提供了很多种IO scheduler方法,如CFQ、Deadline等以适应不同的需求。系统可以通过elevator_switch来进行不同调度算法之间的切换。由于IO scheduler是与算法紧密相关的这里就不进行详细的分析。

978-7-111-49426-3-Chapter05-105.jpg

图5-28 带io scheduler的块设备请求处理流程

4ᤫ块设备驱动初始化

针对块设备驱动,块设备层提供了很多接口包括如何操作请求队列(start,stop,延时操作等),还提供了默认生成request的接口__make_request,但是只有一个接口request_fn需要由驱动提供,前面提到了该接口是具体唤醒驱动请求处理的函数,也是和具体驱动关系最紧密的部分,相关的初始化就是要将驱动特定的request_fn接口加入request_queue中,Linux内核通过blk_init_queue生成包含驱动特定接口的request_queue来实现该功能。具体驱动的初始化流程如图5-29所示。

从图5-29可见,对于具体的块设备驱动中最重要的两部分就是request_fn和中断。re-quest_fn负责下发请求并唤醒处理罗辑;中断则负责处理之后的流程唤醒。两者完整的结合实现整体数据操作。

978-7-111-49426-3-Chapter05-106.jpg

图5-29 具体驱动的初始化流程

在块设备框架层,具体的块设备驱动初始化一般按照以下流程实现:首先需要通过al-loc_disk来分配驱动管理实体gendisk;然后将blk_init_queue生成的设备自身request_queue加入到gendisk中;最后通过add_disk来将驱动管理实体加入系统进行管理。这样系统就可以通过驱动进行操作了。