【Linux内核】设备驱动之块设备介绍
目录
一、Linux块设备驱动概述
二、块设备驱动的基本结构
1. block_device_operations结构体
2. gendisk结构体
2.1 gendisk结构体的主要字段
2.2 驱动初始化与注册
2.3 应用场景
3. request_queue结构体
3.1 请求队列的初始化
3.2 请求处理
3.3 管理待处理的I/O请求队列
3.4 实现I/O调度算法
3.5 处理请求的合并和排序优化
3.6 提供请求完成通知机制
3.7 应用场景
三、块设备驱动的初始化
四、请求处理函数
五、块设备驱动的读写操作
六、块设备驱动的错误处理
七、块设备驱动的卸载
八、块设备驱动的性能优化
九、块设备驱动的调试
十、块设备驱动的实例
十一、总结
一、Linux块设备驱动概述
Linux块设备驱动是操作系统与硬件设备之间的桥梁,负责管理块设备的读写操作。块设备通常指硬盘、SSD等以固定大小的块为单位进行数据存储的设备。块设备驱动的核心任务是将文件系统的请求转换为硬件设备的操作指令,并处理设备的中断和错误。
二、块设备驱动的基本结构
Linux块设备驱动的核心结构包括block_device_operations
、gendisk
和request_queue
。block_device_operations
定义了驱动与设备交互的操作函数,如打开、关闭、读写等。gendisk
结构体用于描述一个磁盘设备,包含设备的分区信息、容量等。request_queue
是块设备请求队列,用于管理待处理的I/O请求。
1. block_device_operations结构体
该结构体定义了驱动与块设备交互的基本操作函数,是驱动程序的接口层。它包含了一系列函数指针,用于处理设备的各种操作请求。主要函数包括:
open
:当设备被打开时调用,用于初始化设备或增加引用计数release
:当设备被关闭时调用,进行资源释放ioctl
:处理设备的控制命令getgeo
:获取设备的几何信息(如柱面、磁头、扇区等)read
和write
:处理设备的读写操作(在现代驱动中通常由request_queue处理)
struct block_device_operations {int (*open)(struct block_device *, fmode_t);void (*release)(struct gendisk *, fmode_t);int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);int (*compat_ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);unsigned int (*check_events) (struct gendisk *disk, unsigned int clearing);int (*revalidate_disk)(struct gendisk *);int (*getgeo)(struct block_device *, struct hd_geometry *);struct module *owner;
};
2. gendisk结构体
gendisk
结构体是Linux内核中用于描述块设备(如硬盘、SSD等)的核心数据结构,它包含了设备的所有关键信息,使得内核能够管理和操作该设备。在驱动开发中,gendisk
结构体的正确初始化和注册是确保设备能够被系统识别和使用的关键步骤。
2.1 gendisk
结构体的主要字段
major和first_minor:
-
major
:设备的主设备号,用于标识设备的类型。例如,SCSI磁盘的主设备号通常为8。 -
first_minor
:设备的起始次设备号,用于标识同一类型设备中的不同实例。例如,/dev/sda
的次设备号为0,/dev/sdb
的次设备号为16。
fops:
-
指向
block_device_operations
结构体的指针,该结构体定义了块设备的操作函数集,如打开、关闭、读写等。这些函数由设备驱动实现,供内核调用。
queue:
-
指向
request_queue
结构体的指针,该结构体管理设备的I/O请求队列。内核将块设备的读写请求放入队列中,由驱动处理。
disk_name:
-
设备的名称,通常是一个字符串,如
sda
、sdb
等。该名称用于在/dev
目录下创建设备节点。
capacity:
-
设备的存储容量,以扇区为单位。一个扇区通常为512字节。例如,一个1TB的硬盘的容量为2,147,483,648个扇区。
part_tbl:
-
分区表信息,用于描述设备的分区情况。每个分区在
gendisk
结构体中都有一个对应的hd_struct
结构体,记录分区的起始扇区、大小等信息。
2.2 驱动初始化与注册
在驱动初始化时,通常需要执行以下步骤:
-
分配
gendisk
结构体:- 使用
alloc_disk()
函数分配一个gendisk
结构体。该函数会为设备分配内存,并初始化部分字段。
struct gendisk *disk = alloc_disk(MINOR_COUNT);
- 使用
-
设置
gendisk
字段disk->major = SCSI_DISK_MAJOR; disk->first_minor = 0; disk->fops = &my_block_ops; disk->queue = my_request_queue; snprintf(disk->disk_name, DISK_NAME_LEN, "sda"); set_capacity(disk, 2147483648); // 1TB
-
注册
gendisk
:- 使用
add_disk()
函数将gendisk
结构体注册到系统中。注册后,设备将在/dev
目录下创建相应的设备节点,并可以被用户空间访问。
add_disk(disk);
- 使用
2.3 应用场景
例如,当系统检测到一个新的SCSI硬盘时,SCSI驱动会创建一个gendisk
实例来描述该设备。驱动会设置设备的主设备号为8,次设备号根据设备顺序分配,如sda
的次设备号为0,sdb
的次设备号为16。驱动还会初始化设备的操作函数集和I/O请求队列,并设置设备的容量和分区表信息。最后,驱动调用add_disk()
函数将设备注册到系统中,使得用户可以通过/dev/sda
等设备节点访问该硬盘。
3. request_queue结构体
request_queue
是Linux内核中块设备驱动用于管理I/O请求的核心数据结构,它在块设备驱动中扮演着至关重要的角色。以下是request_queue
的主要功能及其实现细节:
3.1 请求队列的初始化
块设备驱动通常通过以下两个函数之一来初始化请求队列:
-
blk_init_queue()
:这是传统的请求队列初始化函数,适用于单队列(Single Queue)模型。它需要指定一个请求处理函数(request_fn),该函数会在有I/O请求到达时被调用。 -
blk_mq_init_queue()
:这是多队列(Multi-Queue)模型的初始化函数,适用于现代高性能存储设备(如NVMe SSD)。它支持并行处理多个I/O请求,显著提高了I/O性能。
在初始化过程中,驱动会注册一个请求处理函数。例如:
request_queue_t *q = blk_init_queue(my_request_fn, &my_lock);
其中,my_request_fn
是驱动定义的请求处理函数,my_lock
是用于保护队列的自旋锁。
3.2 请求处理
当有I/O请求到达时,内核会调用驱动注册的请求处理函数来执行实际的读写操作。例如:
- 写请求:驱动需要将数据从内存缓冲区写入到物理设备中。具体步骤包括:
- 从请求队列中获取一个写请求。
- 将请求中的数据从内核缓冲区复制到设备的DMA区域。
- 向设备发送写命令。
- 等待设备完成写操作。
- 调用
blk_end_request()
通知内核请求已完成。
- 读请求:驱动需要从物理设备中读取数据并存储到内存缓冲区中。具体步骤与写请求类似,但方向相反。
3.3 管理待处理的I/O请求队列
request_queue
负责维护一个待处理的I/O请求队列。当应用程序发起I/O操作时,内核会将请求插入到队列中,等待驱动处理。队列的管理包括:
- 请求的入队和出队操作。
- 请求的优先级管理。
- 请求的超时处理。
3.4 实现I/O调度算法
request_queue
支持多种I/O调度算法,以优化I/O性能。常见的调度算法包括:
- CFQ(Completely Fair Queuing):为每个进程分配公平的I/O带宽,适用于多任务环境。
- Deadline:确保每个请求在截止时间内得到处理,适用于实时性要求较高的场景。
- Noop:简单的FIFO队列,不进行任何调度优化,适用于高性能存储设备(如SSD)。
驱动可以通过以下方式设置调度算法:
elevator_change(q, "noop");
3.5 处理请求的合并和排序优化
request_queue
会对请求进行合并和排序,以减少I/O操作的开销。例如:
- 请求合并:将相邻的读写请求合并为一个更大的请求,减少设备寻址次数。
- 请求排序:根据设备的特性(如旋转磁盘的磁头位置)对请求进行排序,减少寻道时间。
3.6 提供请求完成通知机制
当驱动完成一个I/O请求时,需要通过blk_end_request()
或blk_mq_end_request()
函数通知内核。内核会根据请求的状态(成功或失败)更新相关数据结构,并唤醒等待该请求完成的进程。
3.7 应用场景
- 传统硬盘驱动:使用
blk_init_queue()
和CFQ调度算法,优化旋转磁盘的I/O性能。 - SSD驱动:使用
blk_mq_init_queue()
和Noop调度算法,充分发挥SSD的并行处理能力。 - 网络存储设备:使用Deadline调度算法,确保实时性要求较高的请求能够及时处理。
通过request_queue
,块设备驱动能够高效地管理I/O请求,优化设备性能,并满足不同应用场景的需求。
三、块设备驱动的初始化
块设备驱动的初始化过程包括注册设备、分配gendisk
结构体、初始化请求队列等。驱动首先需要调用register_blkdev
函数注册设备,然后使用alloc_disk
分配gendisk
结构体,并通过blk_init_queue
初始化请求队列。
static int __init my_block_driver_init(void) {int ret;ret = register_blkdev(MY_BLOCK_MAJOR, "my_block_device");if (ret < 0) {printk(KERN_ERR "Failed to register block device\n");return ret;}my_disk = alloc_disk(1);if (!my_disk) {printk(KERN_ERR "Failed to allocate disk\n");unregister_blkdev(MY_BLOCK_MAJOR, "my_block_device");return -ENOMEM;}my_queue = blk_init_queue(my_request_fn, &my_lock);if (!my_queue) {printk(KERN_ERR "Failed to initialize queue\n");put_disk(my_disk);unregister_blkdev(MY_BLOCK_MAJOR, "my_block_device");return -ENOMEM;}my_disk->major = MY_BLOCK_MAJOR;my_disk->first_minor = 0;my_disk->fops = &my_block_ops;my_disk->queue = my_queue;sprintf(my_disk->disk_name, "my_block_device");set_capacity(my_disk, MY_DISK_SIZE);add_disk(my_disk);return 0;
}
四、请求处理函数
请求处理函数是块设备驱动的核心,负责处理来自文件系统的I/O请求。驱动需要从请求队列中获取请求,并根据请求的类型(读或写)执行相应的操作。处理完成后,驱动需要调用blk_end_request
函数通知内核请求已完成。
static void my_request_fn(struct request_queue *q) {struct request *req;while ((req = blk_fetch_request(q)) != NULL) {if (req->cmd_type != REQ_TYPE_FS) {blk_end_request_all(req, -EIO);continue;}if (rq_data_dir(req) == READ) {my_read(req);} else {my_write(req);}blk_end_request(req, 0, blk_rq_bytes(req));}
}
五、块设备驱动的读写操作
块设备驱动的读写操作通常涉及DMA(直接内存访问)或PIO(程序控制I/O)方式,这两种方式在性能、实现复杂度和适用场景上各有特点。
DMA方式通过硬件直接访问内存,无需CPU的干预,因此效率较高,特别适合大数据量的传输场景。在DMA方式下,驱动程序需要处理DMA缓冲区的分配和映射。具体来说,驱动程序首先需要调用dma_alloc_coherent()
或dma_map_single()
等函数来分配和映射DMA缓冲区,确保设备能够直接访问这些内存区域。然后,驱动程序通过设置DMA控制器的寄存器来启动传输,传输完成后,设备会通过中断通知CPU,驱动程序再处理后续的清理工作,如释放DMA缓冲区。DMA方式的优势在于减少了CPU的负担,提高了系统的整体性能,但实现起来较为复杂,且需要硬件支持。
PIO方式则通过CPU直接读写设备寄存器来完成数据传输,适用于小数据量的操作。在PIO方式下,驱动程序通过inb()
、outb()
等函数直接与设备的I/O端口进行交互,逐字节或逐字地读取或写入数据。这种方式实现简单,不需要额外的硬件支持,但由于CPU需要频繁参与数据传输,效率较低,特别是在大数据量传输时,CPU的负载会显著增加。
在实际应用中,选择DMA还是PIO方式取决于具体的需求和硬件条件。例如,在高速存储设备(如SSD)中,通常使用DMA方式来提高数据传输效率;而在一些简单的嵌入式设备中,PIO方式可能更为合适,因为其实现简单且硬件成本较低。
static void my_read(struct request *req) {struct bio *bio;struct bio_vec bvec;struct bvec_iter iter;char *buffer;bio = req->bio;bio_for_each_segment(bvec, bio, iter) {buffer = kmap(bvec.bv_page) + bvec.bv_offset;my_hardware_read(buffer, bvec.bv_len);kunmap(bvec.bv_page);}
}static void my_write(struct request *req) {struct bio *bio;struct bio_vec bvec;struct bvec_iter iter;char *buffer;bio = req->bio;bio_for_each_segment(bvec, bio, iter) {buffer = kmap(bvec.bv_page) + bvec.bv_offset;my_hardware_write(buffer, bvec.bv_len);kunmap(bvec.bv_page);}
}
六、块设备驱动的错误处理
块设备驱动需要处理各种硬件错误,如读写失败、设备未响应等。驱动可以通过检查硬件状态寄存器或使用超时机制来检测错误,并根据错误类型采取相应的恢复措施,如重试操作或报告错误。
static int my_check_errors(struct gendisk *disk, unsigned int clearing) {if (my_hardware_error()) {printk(KERN_ERR "Hardware error detected\n");return -EIO;}return 0;
}
七、块设备驱动的卸载
块设备驱动的卸载过程包括释放gendisk
结构体、销毁请求队列和注销设备。驱动需要调用del_gendisk
释放gendisk
,使用blk_cleanup_queue
销毁请求队列,并通过unregister_blkdev
注销设备。
static void __exit my_block_driver_exit(void) {del_gendisk(my_disk);put_disk(my_disk);blk_cleanup_queue(my_queue);unregister_blkdev(MY_BLOCK_MAJOR, "my_block_device");
}
八、块设备驱动的性能优化
块设备驱动的性能优化可以从多个方面入手,如使用多队列机制、优化请求处理逻辑、减少锁竞争等。多队列机制可以将请求分发到多个队列中,提高并发处理能力。优化请求处理逻辑可以减少不必要的操作,提高处理效率。减少锁竞争可以通过使用无锁数据结构或细粒度锁来降低锁的开销。
static int my_init_multiqueue(void) {int i;for (i = 0; i < MY_NUM_QUEUES; i++) {my_queues[i] = blk_mq_init_queue(&my_mq_ops);if (!my_queues[i]) {return -ENOMEM;}}return 0;
}
九、块设备驱动的调试
块设备驱动的调试可以通过打印日志、使用调试工具和分析内核崩溃信息等方式进行。驱动可以使用printk
函数打印调试信息,帮助定位问题。调试工具如gdb
和ftrace
可以用于分析驱动代码的执行流程和性能瓶颈。内核崩溃信息可以通过dmesg
命令查看,帮助分析驱动崩溃的原因。
static void my_debug_print(const char *message) {printk(KERN_DEBUG "my_block_driver: %s\n", message);
}
十、块设备驱动的实例
以下是一个简单的块设备驱动实例,模拟了一个虚拟的块设备,支持基本的读写操作。该驱动通过my_hardware_read
和my_hardware_write
函数模拟硬件操作,并使用my_request_fn
处理请求。
#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/spinlock.h>#define MY_BLOCK_MAJOR 240
#define MY_DISK_SIZE 1024 * 1024 * 1024static struct gendisk *my_disk;
static struct request_queue *my_queue;
static spinlock_t my_lock;static void my_hardware_read(char *buffer, unsigned int len) {memset(buffer, 0, len);
}static void my_hardware_write(char *buffer, unsigned int len) {// Simulate write operation
}static void my_request_fn(struct request_queue *q) {struct request *req;while ((req = blk_fetch_request(q)) != NULL) {if (req->cmd_type != REQ_TYPE_FS) {blk_end_request_all(req, -EIO);continue;}if (rq_data_dir(req) == READ) {my_read(req);} else {my_write(req);}blk_end_request(req, 0, blk_rq_bytes(req));}
}static int __init my_block_driver_init(void) {int ret;ret = register_blkdev(MY_BLOCK_MAJOR, "my_block_device");if (ret < 0) {printk(KERN_ERR "Failed to register block device\n");return ret;}my_disk = alloc_disk(1);if (!my_disk) {printk(KERN_ERR "Failed to allocate disk\n");unregister_blkdev(MY_BLOCK_MAJOR, "my_block_device");return -ENOMEM;}spin_lock_init(&my_lock);my_queue = blk_init_queue(my_request_fn, &my_lock);if (!my_queue) {printk(KERN_ERR "Failed to initialize queue\n");put_disk(my_disk);unregister_blkdev(MY_BLOCK_MAJOR, "my_block_device");return -ENOMEM;}my_disk->major = MY_BLOCK_MAJOR;my_disk->first_minor = 0;my_disk->fops = &my_block_ops;my_disk->queue = my_queue;sprintf(my_disk->disk_name, "my_block_device");set_capacity(my_disk, MY_DISK_SIZE);add_disk(my_disk);return 0;
}static void __exit my_block_driver_exit(void) {del_gendisk(my_disk);put_disk(my_disk);blk_cleanup_queue(my_queue);unregister_blkdev(MY_BLOCK_MAJOR, "my_block_device");
}module_init(my_block_driver_init);
module_exit(my_block_driver_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple block device driver");
十一、总结
Linux块设备驱动是操作系统与硬件设备之间的关键组件,负责管理块设备的读写操作。本文详细介绍了块设备驱动的基本结构、初始化过程、请求处理函数、读写操作、错误处理、卸载过程、性能优化、调试方法和实例。通过深入理解这些技术要点,开发者可以更好地设计和实现高效的块设备驱动,满足不同应用场景的需求。