Linux Kernel --- device driver mode 架构之美

本文详细解析Linux内核中的device_add()函数,探讨设备如何加入设备驱动模型,包括设置设备名字、私有数据,以及与父设备的关系。通过代码分析,揭示内核设计的精妙之处。

platform driver、device 对 device driver mode 封装的差强人意.
kernel 中几乎所有的platform driver 都不会在 /sys 下创建属性文件. 减少了与device 沟通的一种渠道, 内核黑客之所以这样架构是有深刻用意的,

设备驱动模型校验上层侵入的code是及其严格的, "免疫力"足够强大,看下面冰山一角.
在这里插入图片描述
看他们之间的交互通路
在这里插入图片描述
好,先看设备驱动模型暴露出来的第一个API, 先上一张图片,然后详细欣赏
在这里插入图片描述

/*
	看参数,一个原生的struct device, 
	无论你是一个虚拟总线上挂载的设备还是物理总线上挂载的设备, 
	结构体中都必须包含该核心的struct device 结构体. 	
	好,那么device driver mode 拿到了这个device 
	1: 做了哪些事情呢 ?
	2: 又能做什么呢 ? 
	3: 为什么要做这些 ? 作者用意何在 ?
*/
int device_add(struct device *dev)
{
		/*
			该设备的父设备. 通常是该设备相连接的 ControllerBUS, 
			比如 i2c_adapter对应i2c controller,
			ARM上 它挂载在 amba的apb总线上
			那么,拿到该设备的parent 是想:
			1 : 在该parent对应的sys目录下创建目录
			2 : 挂接到该parent维护的klist_children 链表上去
			3 : 父设备的运行时电源管理计数 + 1
			
			好,既然是这样,那么如果该parent 为 NULL,会怎么办 ?
			别急,哈哈,老鼠拉木箱,大的在后头...., 下面就有欣赏
		*/
		struct device *parent;
		/*
			嘿嘿🤭内核对象, 代表sys下的一个目录,真的很重要! 
			这个就不多说了,地球人都知道
			正是由于该对象的存在,sys下才建立起层次关系, device driver mode中的核心所在!
		*/
		struct kobject *kobj;
		
		/*
			它提供了两个回调函数 add_dev 、remove_dev
			提供这两个Callback 有什么作用呢? 没有硬性规定,看你的需求,
			device driver mode 实现的灵活性可见一斑.  下面有个实验来验证这个特性.
			
			再说一下这个class帮助你理解,class是班级的意思,好吧,按照作者的意思 该class下的
			相同功能的设备就应该是班级里的学生了.device_add()函数的最后 会有下面的行为 :
			遍历该 dev->class->p->interfaces 链表,具体该链表挂接的是什么、为什么要在最后来遍历
			它 执行上述的Callback函数,下面再分析,这里仅仅只是声明一个指针. 暂不作深入讨论.
		*/
		struct class_interface *class_intf;
		/*
			返回值,
			返回 0 表示该设备已经加入到 device driver mode中了.
			结果比过程要重要,除非你是去旅游,
			除非能有像弗莱明一样探索过程可以发现青霉素. 否则华而不实
		*/
		int error = -EINVAL;
		/*
			它的实现: 
			return dev->kobj.parent;
			
			在正常的注册过程中该值是没有任何意义,其价值在于注册失败后该怎么做.
			到时再分析吧,这里作者预先铺垫了注册失败后该怎么办.
		*/
		struct kobject *glue_dir = NULL;
		
		/*
			好了, 一个参数 、五个局部变量都定义好了,看下面code的执行逻辑了
			材米油盐都给你准备好了,看你怎么做饭了,
		*/

		/*
			增加设备的引用计数.
			如果传入的参数是 NULL, 那么该函数返回 NULL
			
			在该函数的最后 当该设备已经加入到 device driver mode 中了,还会将该设备的引用计数减1
			调用了这个函数:  put_device(dev);
			
			看整个注册过程是在 该设备的引用计数 > 0 的情况下完成的.
            难道设备的引用计数为 0 就不可以进行注册了吗 ? 好,下面详细说
            
            对称性: 
			这里的对称性我想到了一个: try_to_wake_up() 去唤醒一个 Task的时候
			是在 preempt_disable() 禁用内核抢占环境下执行的,完成后 preempt_enable()又开启抢占
			这种对称性在 kernel中真是太多了....
		*/
		dev = get_device(dev);
		if (!dev)
		    goto done;
	
		/*
			还是在校验传入的参数.
			
			要注册的device 是否设置了设备的私有数据.
			这个私有数据存在的意义之一: 提供该设备的子设备挂接的链表,
		    也就是说: 子设备挂载到了父设备维护的链表上的.
		    struct device_private {
		        struct klist klist_children;
		    }    
		*/
		if (!dev->p) {
		    /*
		        如果你不设置device_private,那么kernel帮你设置一个,
		        也就是说无论如何 注册的每一个设备都必须有自己的私有数据 device_private
		        好,😄,下面画一个图来呈现这个核心的数据结构. bus_type 中也有类似的架构设计
		    */ 
			error = device_private_init(dev); 
			if (error)
				goto done;
		}
		
		/*
	         设置device的名字.
	         注意了,在后面遍历klist链表的过程中,匹配driver的时候,有可能会使用到这里设置的名字
	         platform device driver 可以参见 platform_bus_type 提供的 platform_match()函数.
	         好,如果不提供init_name呢, 也好办,下面就会有设置.
		*/
	    if (dev->init_name) {
		    dev_set_name(dev, "%s", dev->init_name);
		    dev->init_name = NULL;
	    }
	    
		/*
			还是在设置名字,在注册device的时候设置好名字,可以帮组kernel减轻很多工作
			好,即使你注册device的时候没有名字,也行,但是设备所属的总线必须有名字
			不然,下面一个判断就会注册该device失败,
		*/
		if (!dev_name(dev) && dev->bus && dev->bus->dev_name)
			dev_set_name(dev, "%s%u", dev->bus->dev_name, dev->id);
	
		/*
			一个婴儿刚出生加入到人类社会中来,得起个名吧. 即使像朱重八、朱初一之类得,
			感慨那些带有父母姓氏得孩子得名字,也许是女性地位在家庭中的提升的表现吧.
		*/
		if (!dev_name(dev)) {
			error = -EINVAL;
			goto name_error;
		}
		
		/*
			kernel也会给你一个温馨得提示,哈哈😄
			不过这个需要开启 CONFIG_DYNAMIC_DEBUG 配置才行.
			
			好,到这里终于是吧名字给搞定了. 
			看,为了一个name, kernel 竟然用了三个if来判断,TMD啰嗦 !!
			不过既然这样来架构,也说明device 名字得重要性.
		*/
		pr_debug("device: '%s': %s\n", dev_name(dev), __func__);

		/*
			看,上面的code只做了两件事:
			1 : 设置设备的名字
			2 : 设置设备的私有数据结构.

			好, 待把整个函数都分析完了,再从整体上看device driver mode 为什么要这么做.
			有没有更优秀的方法.
		*/
}

在这里插入图片描述

/*
	父设备引用计数 +1
	好了,如果要卸载这个设备,那么父设备的引用计数 就要 -1
	
	device_unregister() {
		👇
		device_del() {
			......
			......
			👇
			put_device(parent);
		}
	}
	生下来在户主户口簿上人数加 1, 死去 -1, 空空的来,如也的走,不带走一片云彩,
	哈哈 😄, 
	
	什么样的设备没有父设备 ? 什么样的设备不挂载到总线上 ? 
	下面就有说明.
*/
parent = get_device(dev->parent);

在这里插入图片描述

### ✅ 回答问题:Linux 内核 6.1 中的 `block` 子系统架构与核心机制详解 --- **简要答案**: 在 Linux 内核 **6.1** 版本中,`block`(块设备)子系统是 I/O 栈的核心组件,负责管理所有面向块的存储设备(如硬盘、SSD、虚拟磁盘等)。它通过统一的接口抽象物理和逻辑设备,支持多队列 I/O 调度(`blk-mq`)、请求排队、分区解析、I/O 合并与延迟控制,并为上层文件系统(ext4, XFS 等)提供可靠的读写服务。 > 🧩 主要位于源码路径:`drivers/block/`, `block/`, `include/linux/blk*.h` --- ## 🏗️ 一、整体架构图(Linux 6.1) ``` +---------------------+ | 文件系统层 | ← ext4, XFS, Btrfs → 发出 bio +---------------------+ ↓ submit_bio() +---------------------+ | Generic Block Layer | ← 处理 BIO(I/O 请求向量) | | | - bio_alloc() | | - merge_bios() | | - partitioning | +---------------------+ ↓ 提交到请求队列 +---------------------+ | blk-mq 框架 | ← 多队列调度(主要入口) | | | - blk_mq_make_request| | - soft queue (hctx) | | - hardware context | +---------------------+ ↓ 派发给驱动 +---------------------+ | 块设备驱动程序 | ← NVMe, virtio-blk, SATA(AHCI), MMC | (Device Driver) | 构造硬件命令并发送 +---------------------+ ↓ +---------------------+ | 物理存储设备 | ← SSD, HDD, eMMC, SD Card +---------------------+ ``` --- ## 🔍 二、关键数据结构(Kernel 6.1) ### 1. `struct bio` —— I/O 请求的基本单位 代表一个“分散-聚集”式的 I/O 请求: ```c struct bio { sector_t bi_iter.bi_sector; // 起始扇区(LBA) unsigned int bi_iter.bi_size; // 字节数 struct bvec_iter bi_iter; // 当前操作位置 struct bio_vec *bi_io_vec; // 指向 bvec 数组 unsigned short bi_vcnt; // 实际使用的 bvec 数量 unsigned short bi_max_vecs; // 最大支持的段数 struct block_device *bi_disk; // 目标设备 struct gendisk *bi_disk; bio_end_io_t *bi_end_io; // 完成回调函数 void *bi_private; // 私有数据(如 fs info) }; ``` > ⚠️ 注意:从内核 5.x 开始,`bio` 不再包含嵌入式 `bvec` 数组,而是动态分配。 #### 示例:分配并初始化一个 `bio` ```c struct bio *bio = bio_alloc(GFP_NOIO, 4); // 支持最多 4 个段 bio_set_dev(bio, bdev); bio->bi_iter.bi_sector = 1024; // LBA=1024 bio_add_page(bio, page, 4096, 0); // 添加一页数据 bio->bi_end_io = my_completion_fn; // 设置完成处理函数 submit_bio(REQ_OP_READ, bio); // 提交读请求 ``` --- ### 2. `struct request_queue` 和 `blk-mq` 在 Linux 6.1 中,旧的单队列模型已被完全弃用,全面采用 **`blk-mq`(Block Multi-Queue)** 架构。 #### 核心结构: ```c struct request_queue { struct blk_mq_ops *mq_ops; // 驱动实现的操作集 struct blk_mq_ctx __percpu *queue_ctx; // 每 CPU 上下文(soft queue) struct blk_mq_hw_ctx **queue_hw_ctx; // 硬件上下文数组 unsigned int nr_hw_queues; // 硬件队列数量 unsigned int queue_depth; // 每个硬件队列深度 struct elevator_queue *elevator; // I/O 调度器(可选) ... }; ``` #### 工作流程: 1. 进程在某个 CPU 上提交 `bio` 2. 映射到该 CPU 对应的 `blk_mq_ctx` 3. 选择一个 `blk_mq_hw_ctx`(可能绑定特定 CPU 或硬件通道) 4. 将请求加入 `hw_ctx` 的队列 5. 触发驱动的 `.queue_rq()` 方法执行派发 --- ### 3. `struct request`(已逐步弱化) 在 `blk-mq` 模型中,`request` 不再是必须存在的对象。许多现代驱动(如 NVMe)直接使用 `struct scsi_cmnd` 或自定义命令结构。 但在一些传统驱动或需要重试/排序时仍会创建: ```c struct request { struct request_queue *q; unsigned int cmd_flags; // REQ_OP_READ / REQ_OP_WRITE struct bio *bio, *biotail; // 链接的 bio 列表 struct list_head queuelist; // 在调度队列中的链表节点 }; ``` --- ### 4. `struct gendisk` —— 通用磁盘描述符 表示一个完整的块设备及其分区信息: ```c struct gendisk { int major; // 主设备号 int first_minor; int minors; // 次设备号范围(1: 只有整盘;>1: 支持分区) char disk_name[DISK_NAME_LEN]; // 名称如 "sda" struct disk_part_tbl __rcu *part_tbl; // 分区表(索引从 0 开始) const struct block_device_operations *fops; // open/release/ioctl 等 struct device part0; // 整个设备对应的 device int flags; }; ``` > 分区设备 `/dev/sda1` 实际对应 `part_tbl->part[1]`,其 `bdev` 会被创建。 --- ## ⚙️ 三、I/O 请求生命周期(以读为例) ```text 1. VFS → ext4_readpage() 2. create BIO for page (sector + len) 3. submit_bio(REQ_OP_READ, bio) 4. generic_make_request() ├→ 查找目标 gendisk 和分区 ├→ 更新统计信息 (part_stat_inc()) └→ 调用 q->make_request_fn() → 默认为 blk_mq_make_request() 5. blk_mq_make_request() ├→ 获取当前 CPU 的 soft queue (ctx) ├→ 分配 hctx(可能基于 CPU 绑定) ├→ 加入 hctx->rq_list └→ 标记需要处理 (__blk_mq_sched_insert_request) 6. 触发运行(软中断或 tasklet) 7. 调用驱动 .queue_rq() 函数(如 nvme_queue_rq) 8. 驱动将命令写入寄存器或共享内存(如 NVMe doorbell) 9. 设备完成操作后触发中断 10. 中断处理函数调用 bio_endio(bio) → 执行 bi_end_io 回调 11. 页面被标记为 unlocked → wake_up_page() ``` --- ## 💡 四、编程示例:在模块中创建虚拟块设备(vbd) 以下是一个可在 Linux 6.1 上运行的简单虚拟块设备模块: ```c #include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/blkdev.h> #include <linux/genhd.h> #include <linux/bio.h> #define VBD_SIZE (16 * 1024 * 1024) // 16MB #define VBD_SECTOR_SIZE 512 #define VBD_NUM_SECTORS (VBD_SIZE / VBD_SECTOR_SIZE) static struct request_queue *vbd_queue; static struct gendisk *vbd_disk; static u8 *vbd_data; // 模拟存储空间 // 请求处理函数(仅用于调试,实际应使用 blk-mq) static blk_status_t vbd_submit_bio(struct bio *bio) { struct bio_vec bvec; struct bvec_iter iter; sector_t sector = bio->bi_iter.bi_sector; if (sector + (bio_bytes(bio) >> 9) > VBD_NUM_SECTORS) { bio_io_error(bio); return BLK_STS_IOERR; } bio_for_each_segment(bvec, bio, iter) { void *daddr = vbd_data + (sector << 9); void *src = kmap_atomic(bvec.bv_page) + bvec.bv_offset; if (bio_data_dir(bio) == READ) memcpy(src, daddr, bvec.bv_len); else memcpy(daddr, src, bvec.bv_len); kunmap_atomic(src); sector += bvec.bv_len >> 9; } bio_endio(bio); // 完成请求 return BLK_STS_OK; } static int __init vbd_init(void) { int ret; // 分配模拟内存 vbd_data = kzalloc(VBD_SIZE, GFP_KERNEL); if (!vbd_data) return -ENOMEM; // 分配请求队列(使用 blk-mq 默认行为) vbd_queue = blk_alloc_queue(NUMA_NO_NODE); if (!vbd_queue) { ret = -ENOMEM; goto out_free; } blk_queue_make_request(vbd_queue, vbd_submit_bio); blk_queue_logical_block_size(vbd_queue, VBD_SECTOR_SIZE); // 分配磁盘结构 vbd_disk = alloc_disk(16); // 支持 16 个分区 if (!vbd_disk) { ret = -ENOMEM; goto out_cleanup_queue; } vbd_disk->major = 0; // 动态分配主设备号 vbd_disk->first_minor = 0; vbd_disk->minors = 16; strcpy(vbd_disk->disk_name, "vbd"); vbd_disk->fops = &vbd_fops; vbd_disk->queue = vbd_queue; set_capacity(vbd_disk, VBD_NUM_SECTORS); add_disk(vbd_disk); printk(KERN_INFO "Virtual block device %s (%d MB) registered\n", vbd_disk->disk_name, VBD_SIZE >> 20); return 0; out_cleanup_queue: blk_cleanup_queue(vbd_queue); out_free: kfree(vbd_data); return ret; } static void __exit vbd_exit(void) { del_gendisk(vbd_disk); put_disk(vbd_disk); blk_cleanup_queue(vbd_queue); kfree(vbd_data); printk(KERN_INFO "vbd module removed\n"); } // 必须定义 file_operations(即使为空) static const struct block_device_operations vbd_fops = { .owner = THIS_MODULE, .report_zones = NULL, }; module_init(vbd_init); module_exit(vbd_exit); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("Simple Virtual Block Device for Linux 6.1"); ``` 📌 编译 Makefile: ```makefile obj-m += vbd.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean install: sudo insmod vbd.ko remove: sudo rmmod vbd ``` 📌 加载后查看设备: ```bash dmesg | tail lsblk | grep vbd ``` 输出示例: ``` vbd 259:0 0 16M 0 disk ``` --- ## 🛠️ 五、常用调试工具与接口 | 工具 | 用途 | |------|------| | `cat /proc/partitions` | 查看当前所有分区 | | `lsblk` | 列出块设备树状结构 | | `iostat -x 1` | 实时监控 I/O 性能 | | `blktrace` | 跟踪块设备请求路径 | | `echo "dump" > /sys/block/sda/trace/act_mask` | 启用跟踪(需配置) | --- ## 📈 六、Linux 6.1 新特性(相比早期版本) | 特性 | 描述 | |------|------| | 更完善的 `bio` 生命周期管理 | 使用 refcount 控制释放 | | 支持 `BIO_FLAG_NOWAIT` | 非阻塞 I/O 提交(配合 io_uring 使用) | | blk-crypto inline support | 为 UFS/EMMC 提供原生加密卸载 | | blk-mq polling mode 改进 | 更低延迟轮询机制 | | REQ_OP_ZONE_APPEND 支持 | 用于 zoned block devices(如 SMR 硬盘) | --- ### 💎 总结 | 组件 | 作用 | |------|------| | `bio` | I/O 请求载体,支持 scatter-gather | | `request_queue` + `blk-mq` | 多队列调度框架,提升并发性能 | | `gendisk` | 块设备抽象,包含分区信息 | | `block_device_operations` | 用户空间操作钩子(open/ioctl) | | `AF_ALG` / `dm-crypt` | 上层应用利用 block+crypto 实现透明加密 | --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值