在现代计算机系统中,块设备驱动程序是操作系统内核中一个重要的组成部分,它用于管理硬盘、闪存等存储设备。Linux内核是一个开源、自由的操作系统内核,驱动程序源代码公开可用,可以帮助开发人员更好地了解Linux内核块设备驱动的工作原理。
一、块设备驱动程序的基础概念
1、块设备驱动程序的作用
块设备驱动程序是一种负责管理块设备(如硬盘、NVMe快闪存储器等)的软件组件,它负责实现块设备的读写操作、磁盘缓存的管理等。块设备驱动程序使得操作系统内核和各种应用程序都可以通过标准的接口访问块设备。
2、Linux内核块设备驱动程序中的主要数据结构
在Linux内核中,块设备驱动程序主要包含以下数据结构:
(1)bio
I/O操作描述符(I/O descriptor,简称bio)是Linux内核中块设备驱动程序中最基本的数据结构。它描述了一个块设备操作的所有细节和参数,包括读写操作的块数、读写的数据指针、物理地址、缓冲区大小、操作类型等。bio数据结构的重要性在于它是向块设备发送I/O操作的载体。
(2)request_queue
request_queue是块设备驱动程序中的另一个重要的数据结构,它管理着一组bio数据结构。request_queue中可以管理多个bio请求,并且可以高效地组织和处理这些请求。当一个新的I/O请求到来时,request_queue会将其和之前未完成的请求进行合并,以提高I/O操作的效率。
(3)gendisk设备结构
gendisk设备结构是Linux内核中块设备驱动程序的子结构之一,它是块设备驱动程序和块设备层之间的接口数据结构。每个块设备(如硬盘、固态硬盘等)都对应着一个gendisk设备结构,以便于被块设备驱动程序和块设备层管理和访问。gendisk设备结构中包括了块设备的主要属性,例如块大小、可访问的扇区数、分区信息、分块信息等。
3、Linux内核块设备驱动程序的主要工作流程
Linux内核中块设备驱动程序的主要工作流程如下:
(1)初始化块设备驱动程序
在Linux内核中,块设备驱动程序的初始化通常是在模块加载时完成的(即init函数中完成初始化)。块设备驱动程序的初始化包括注册块设备驱动程序、创建gendisk设备结构以及建立request_queue等。
(2)接受并处理I/O请求
块设备驱动程序通常是被块设备层调用的,以提供块设备的读写服务,块设备层会将I/O请求通过request_queue发送给块设备驱动程序,块设备驱动程序会在这里接收到并处理I/O请求。
(3)处理I/O请求
块设备驱动程序主要实现I/O请求的处理,其处理流程通常包括以下几个步骤:
- 将待处理的I/O请求从request_queue中取出。
- 将请求转化为通用的bio数据结构。
- 将bio数据结构添加到硬件设备的操作队列中。
- 等待硬件设备完成I/O请求。
- 在I/O请求完成后,将bio数据结构从硬件设备的操作队列中移除,并修改I/O请求的状态。
(4)释放块设备驱动程序资源
在Linux内核中,块设备驱动程序资源的释放实际上是由模块卸载时完成的(即exit函数中完成资源释放)。在资源释放时,块设备驱动程序需要注销注册设备、删除gendisk设备结构以及销毁request_queue等。
二、块设备驱动程序源代码分析
1、块设备驱动程序的编写
Linux内核中块设备驱动程序涉及到很多I/O操作,因此需要仔细编写。下面分别简要介绍块设备驱动程序的读、写和I/O请求处理函数的编写方法。
(1)块设备驱动程序的读函数编写
块设备驱动程序的读函数(read函数)通常是异步的,即它不会等待传输完成。当执行一个读请求时,驱动程序中的read函数会创建一个读取请求,并将其添加到request_queue队列中等待处理。一旦请求被添加到request_queue中,驱动程序就会返回给调用者一个代表读请求正在处理的值。
(2)块设备驱动程序的写函数编写
块设备驱动程序的写函数(write函数)与读函数类似,也是异步的,与读函数不同的是它需要等待写操作完成。当执行一个写请求时,驱动程序中的write函数会创建一个写请求,并将其添加到request_queue队列中等待处理。一旦请求被添加到request_queue中,驱动程序就会等待写操作完成后将控制权限返回给调用者。
(3)块设备驱动程序的I/O请求处理函数编写
块设备驱动程序中最重要的函数是I/O请求处理函数,它被用来接收和处理所有接收到的I/O请求。当新的I/O请求到来时,块设备层会将请求通过request_queue发送给块设备驱动程序中的I/O请求处理函数进行处理。
I/O请求处理函数主要包括以下几个步骤:
- 判断请求类型,并将其应用到相应的数据结构中。
- 将请求转化为通用的bio数据结构。
- 将bio数据结构添加到硬件设备的操作队列中。
- 等待硬件设备完成I/O请求。
- 在I/O请求完成后,将bio数据结构从硬件设备的操作队列中移除,并修改I/O请求的状态。
2、块设备驱动程序源代码
下面为读者介绍Linux内核块设备驱动程序的一个例子(内核版本为4.19.0),该程序负责管理SATA磁盘设备的读写操作。
(1)块设备驱动程序的头文件
#include
#include
#include
#include
(2)块设备驱动程序的声明
static int dev_major = 0;
static int volumes_count = 3;
static int block_size = 512;
static int sector_size = 512;
static int mydrv_open(struct block_device *bdev, fmode_t mode);
static void mydrv_release(struct gendisk *gd, fmode_t mode);
static int mydrv_ioctl(struct block_device *bdev, fmode_t mode,
unsigned int cmd, unsigned long arg);
static int mydrv_getgeo(struct block_device *bdev, struct hd_geometry *geo);
static struct block_device_operations mydrv_ops = {
.owner = THIS_MODULE,
.open = mydrv_open,
.release = mydrv_release,
.ioctl = mydrv_ioctl,
.getgeo = mydrv_getgeo,
};
static struct request_queue *mydrv_queue = NULL;
static void mydrv_request(struct request_queue *q);
(3)块设备驱动程序的模块加载及卸载函数
static int __init mydrv_init(void)
{
int ret = -1;
struct gendisk *disk = NULL;
dev_major = register_blkdev(dev_major, "mydrv");
if (dev_major <= 0) {
pr_err("mydrv: block device registration failed\n");
goto err_reg;
}
mydrv_queue = blk_alloc_queue(GFP_KERNEL);
if (!mydrv_queue) {
pr_err("mydrv: request queue creation failed\n");
goto err_queue;
}
blk_queue_make_request(mydrv_queue, mydrv_request);
if (!init_volumes(&mydrv_queue, &disk)) {
pr_err("mydrv: volume initialization failed\n");
goto err_vols;
}
if (!add_disk(disk)) { pr_err("mydrv: disk registration failed\n");
goto err_disk;
}
disk->fops = &mydrv_ops;
return 0;
err_disk:
if (disk) {
del_gendisk(disk);
put_disk(disk);
}
err_vols:
blk_cleanup_queue(mydrv_queue);
err_queue:
unregister_blkdev(dev_major, "mydrv");
err_reg:
return ret;
}
static void __exit mydrv_exit(void)
{
unregister_blkdev(dev_major, "mydrv");
blk_cleanup_queue(mydrv_queue);
cleanup_volumes();
}
(4)块设备驱动程序的操作函数
及请求处理函数。
```c
static int mydrv_open(struct block_device *bdev, fmode_t mode)
{
return 0;
}
static void mydrv_release(struct gendisk *gd, fmode_t mode)
{
}
static int mydrv_ioctl(struct block_device *bdev, fmode_t mode,
unsigned int cmd, unsigned long arg)
{
return -ENOTTY;
}
static int mydrv_getgeo(struct block_device *bdev, struct hd_geometry *geo)
{
struct mydrv_volume *vol = NULL;
int ret = -1;
vol = bdev->bd_disk->private_data;
if (!vol) {
pr_err("mydrv: invalid volume information\n");
goto out;
}
geo->heads = vol->heads;
geo->sectors = vol->sectors;
geo->cylinders = vol->cylinders;
ret = 0;
out:
return ret;
}
static void mydrv_request(struct request_queue *q)
{
struct request *req = NULL;
struct bio *bio = NULL;
struct mydrv_volume *vol = NULL;
while ((req = blk_fetch_request(q)) != NULL) {
if (req->cmd_type != REQ_TYPE_FS) {
pr_err("mydrv: wrong request type\n");
__blk_end_request_all(req, -EIO);
continue;
}
__rq_for_each_bio(bio, req) {
vol = bio->bi_bdev->bd_disk->private_data;
if (!vol) {
pr_err("mydrv: invalid volume information\n");
__blk_end_request_all(req, -EIO);
continue;
}
switch (bio_rw(bio)) {
case READ:
mydrv_read(vol, bio);
break;
case WRITE:
mydrv_write(vol, bio);
break;
default:
pr_err("mydrv: wrong I/O operation\n");
__blk_end_request_all(req, -EIO);
break;
}
}
__blk_end_request_all(req, 0);
}
}
三、总结
块设备驱动程序是Linux内核中非常重要的组件之一,它负责处理块设备的读写操作,为操作系统内核和各种应用程序提供标准接口。在块设备驱动程序的编写过程中,需要仔细处理读、写和I/O请求处理函数的实现,以实现块设备的最佳操作效率。
希望大家通过阅读本文,了解和掌握块设备驱动程序的工作原理,进一步提高对Linux内核的理解和认知。