Linux 块设备驱动工作原理详解

Linux 块设备驱动是最复杂,比字符设备复杂的多。

什么是块设备

块设备是用于存储数据的设备,例如 SD 卡、EMMC、Nand Flash、机械硬盘、固态硬盘等。与字符设备相比,块设备有以下几个主要区别:

1. 数据访问单位不同:

- 块设备:以块(通常是512字节或4KB)为单位进行读写。

- 字符设备:以字节为单位进行读写。

2. 随机访问能力:

- 块设备:支持随机访问,可以按块读写。

- 字符设备:顺序访问,不支持随机读写。

3. 缓冲区使用:

- 块设备:使用缓冲区暂存数据,减少对物理设备的直接操作,提高寿命。

- 字符设备:实时传输,不使用缓冲区。

4. I/O算法不同:

- 块设备:根据设备类型采用不同的 I/O 调度算法,如针对机械硬盘的顺序化处理。

- 字符设备:无复杂的 I/O 调度需求。

块设备驱动框架

1. block_device 结构体

Linux 内核使用 `block_device` 表示一个具体的块设备对象,比如一个硬盘或者分区。关键成员包括:

- `bd_disk`:指向通用磁盘结构 `gendisk`。

2. gendisk 结构体

`gendisk` 描述了整个磁盘设备,其主要成员包括:

- `major`:主设备号。

- `first_minor`:第一个次设备号。

- `minors`:次设备号的数量(即分区数)。

- `part_tbl`:分区表。

- `fops`:块设备操作集。

- `queue`:请求队列,所有对该磁盘设备的请求都放在这个队列中。

常用 API

- 申请 gendisk:

```c

struct gendisk alloc_disk(int minors);

```

- 删除 gendisk:

```c

void del_gendisk(struct gendisk gp);

```

- 添加 gendisk 到内核:

```c

void add_disk(struct gendisk disk);

```

- 设置容量:

```c

void set_capacity(struct gendisk disk, sector_t size);

```

- 调整引用计数:

```c

struct kobject get_disk(struct gendisk disk);

void put_disk(struct gendisk disk);

```

3. block_device_operations 结构体

类似于字符设备的 `file_operations`,块设备的操作集为 `block_device_operations`,包含以下常见函数:

- `open`:打开块设备。

- `release`:关闭块设备。

- `rw_page`:读写指定页。

- `ioctl`:I/O控制命令。

- `compat_ioctl`:兼容模式的 I/O 控制命令。

- `getgeo`:获取磁盘几何信息。

4. 块设备 I/O 请求过程

块设备的读写通过 `request_queue` 实现,其中包含了很多 `request` 结构体,每个 `request` 又包含多个 `bio` 结构体,保存了读写的具体信息。

请求队列 (request_queue)

所有的块设备读写请求都被发送到请求队列中,驱动程序需要处理这些请求。

示例代码:使用 RAM 模拟块设备

以下是一个简单的示例代码,展示如何使用板载 RAM 模拟一个块设备:

```c

include

include

include

include

include

include

include

define RAMDISK_SIZE (2 1024 1024) // 2MB

define SECTOR_SIZE 512

static unsigned char ram_buffer;

static struct gendisk my_disk;

static struct request_queue my_queue;

static int major;

// make_request 函数

static blk_qc_t ramdisk_make_request(struct request_queue q, struct bio bio) {

sector_t start_sec = bio->bi_iter.bi_sector;

void dsk_ptr = ram_buffer + (start_sec SECTOR_SIZE);

if (bio_data_dir(bio) == READ)

memcpy(bio_kvec_data(bio), dsk_ptr, bio->bi_iter.bi_size);

else

memcpy(dsk_ptr, bio_kvec_data(bio), bio->bi_iter.bi_size);

return BLK_MQ_RQ_DONE;

}

// getgeo 函数

static int ramdisk_getgeo(struct block_device bdev, struct hd_geometry geo) {

geo->heads = 4;

geo->sectors = 16;

geo->cylinders = (RAMDISK_SIZE >> 9) / (4 16);

return 0;

}

// block_device_operations 结构体

static struct block_device_operations ramdisk_fops = {

.owner = THIS_MODULE,

.getgeo = ramdisk_getgeo,

};

// 初始化函数

static int __init ramdisk_init(void) {

ram_buffer = vmalloc(RAMDISK_SIZE);

if (!ram_buffer) return -ENOMEM;

major = register_blkdev(0, "ramdisk");

if (major main_num = major;

my_disk->first_minor = 0;

my_disk->fops = &ramdisk_fops;

my_disk->queue = my_queue;

strcpy(my_disk->disk_name, "ramdisk");

set_capacity(my_disk, RAMDISK_SIZE / SECTOR_SIZE);

add_disk(my_disk);

printk(KERN_INFO "RAM disk loaded: /dev/ramdisk\n");

return 0;

err_cleanup_q:

blk_cleanup_queue(my_queue);

err_unreg:

unregister_blkdev(major, "ramdisk");

err_vfree:

vfree(ram_buffer);

return -ENOMEM;

}

// 卸载函数

static void __exit ramdisk_exit(void) {

del_gendisk(my_disk);

put_disk(my_disk);

unregister_blkdev(major, "ramdisk");

blk_cleanup_queue(my_queue);

vfree(ram_buffer);

}

module_init(ramdisk_init);

module_exit(ramdisk_exit);

MODULE_LICENSE("GPL");

```

测试步骤

1. 编译并加载模块:

```sh

make

sudo insmod ramdisk.ko

```

2. 查看设备:

```sh

lsblk | grep ramdisk

应该看到 /dev/ramdisk,大小 2M

```

3. 格式化并挂载:

```sh

sudo mkfs.ext4 /dev/ramdisk

sudo mkdir /mnt/ram

sudo mount /dev/ramdisk /mnt/ram

echo "Hello Block!" > /mnt/ram/test.txt

cat /mnt/ram/test.txt

```

4. 卸载并清理:

```sh

sudo umount /mnt/ram

sudo rmmod ramdisk

```

总结

理解块设备驱动的核心在于掌握其结构和流程。从注册块设备号开始,到创建和管理 `gendisk`,再到处理 I/O 请求,这些都是编写高效块设备驱动的关键部分。