一、设备驱动模型概述
Linux 内核需要管理种类繁多的硬件设备——从简单的 LED 到复杂的 NVMe SSD,从串口到网卡。如果没有统一的框架,每种设备都独立实现自己的管理逻辑,内核将变得无法维护。Linux 设备驱动模型(Device Driver Model)正是为了解决这个问题而设计的:它提供了一套统一的内核对象体系,将设备、驱动、总线等概念抽象为面向对象的层次结构,并通过 sysfs 文件系统向用户空间暴露设备信息。
为什么需要设备驱动模型
在早期内核中,设备管理是分散的——PCI 设备有自己的注册机制,USB 设备有另一套,平台设备又不同。这导致:
- 代码重复:每种总线类型都实现了类似的设备遍历、匹配、电源管理逻辑
- 信息不统一:用户空间无法通过统一接口获取设备信息
- 电源管理困难:没有统一的设备层次关系,无法按序挂起/恢复
2.6 内核引入的设备驱动模型通过统一的 kobject 体系解决了这些问题。
二、kobject:内核对象的基石
kobject 是 Linux 设备驱动模型的核心数据结构,它提供了引用计数、sysfs 表示、父子关系等通用功能。虽然很少有代码直接操作 kobject,但它是所有设备模型对象的隐含基类。
struct kobject { const char *name; // 对象名称,在 sysfs 中显示 struct list_head entry; // 链入父 kset 的链表节点 struct kobject *parent; // 父对象,构建层次关系 struct kset *kset; // 所属的 kset struct kobj_type *ktype; // 操作方法表 struct kernfs_node *sd; // sysfs 目录项 struct kref kref; // 引用计数 unsigned int state_initialized:1; unsigned int state_in_sysfs:1; unsigned int state_add_uevent_sent:1; unsigned int state_remove_uevent_sent:1;};kobject 的核心功能
| 功能 | 说明 | 关键函数 |
|---|---|---|
| 引用计数 | 跟踪对象使用情况,计数归零时释放 | kobject_get()/kobject_put() |
| sysfs 表示 | 每个 kobject 在 sysfs 中对应一个目录 | kobject_add()/kobject_del() |
| 父子关系 | 构建对象的层次树 | parent 指针 |
| 事件通知 | 向用户空间发送 uevent | kobject_uevent() |
ktype:对象的”虚函数表”
kobj_type 定义了 kobject 的行为,类似于面向对象编程中的虚函数表:
struct kobj_type { void (*release)(struct kobject *kobj); // 引用计数归零时调用 const struct sysfs_ops *sysfs_ops; // sysfs 属性读写操作 const struct attribute_group **default_groups; // 默认属性组 const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);};release 回调是最重要的——当引用计数归零时,内核调用它来释放包含该 kobject 的上层结构体。忘记实现 release 会导致内存泄漏,且内核会打印警告。
kset:对象的容器
kset 是一组相关 kobject 的集合,它同时也是一个 kobject(拥有自己的目录),并且维护一个链表:
struct kset { struct list_head list; // 链入的所有 kobject spinlock_t list_lock; // 保护链表的自旋锁 struct kobject kobj; // kset 自身也是一个 kobject const struct kset_uevent_ops *uevent_ops; // uevent 过滤操作};三、sysfs:设备模型的用户空间窗口
sysfs 是一个基于内存的虚拟文件系统,挂载在 /sys,它将内核设备模型的层次结构直接映射为目录树。每个 kobject 在 sysfs 中对应一个目录,kobject 的属性(attribute)对应目录中的文件。
sysfs 的目录结构
/sys/├── block/ # 块设备(符号链接到 /sys/devices/)├── bus/ # 总线类型,每种总线一个子目录├── class/ # 设备类,按功能分类├── dev/ # 设备节点视图│ ├── block/ # 块设备 major:minor → 设备路径│ └── char/ # 字符设备 major:minor → 设备路径├── devices/ # 所有设备的全局视图,按总线拓扑组织├── firmware/ # 固件信息├── fs/ # 文件系统参数├── kernel/ # 内核全局参数├── module/ # 已加载内核模块└── power/ # 电源管理sysfs 属性文件
属性文件是 sysfs 与用户空间交互的主要方式。读取属性文件获取设备状态,写入属性文件修改设备配置:
# 读取 CPU 频率cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq
# 修改内核参数echo 1 > /sys/devices/system/cpu/cpu0/online
# 查看块设备调度器cat /sys/block/sda/queue/schedulersysfs 属性文件通常只支持单次读写一个值(不是全文本),且每个属性文件只做一件事。这是 sysfs 的设计原则。
四、设备分类:字符、块与网络
Linux 将设备分为三大类,每类有不同的注册和使用方式:
字符设备
字符设备以字节流方式访问,不支持随机寻址。大多数设备属于这一类:
- 串口(
/dev/ttyS0) - 终端(
/dev/tty) - 输入设备(
/dev/input/event0) - 音频设备(
/dev/snd/pcmC0D0p)
// 字符设备的核心结构struct cdev { struct kobject kobj; // 内嵌的 kobject struct module *owner; // 所属模块 const struct file_operations *ops; // 文件操作函数表 struct list_head list; // 链入 cdev_map 的节点 dev_t dev; // 设备号 unsigned int count; // 连续次设备号数量};注册字符设备的关键步骤:
// 1. 分配设备号dev_t dev;alloc_chrdev_region(&dev, 0, 1, "mydev");
// 2. 初始化 cdevstruct cdev my_cdev;cdev_init(&my_cdev, &my_fops);my_cdev.owner = THIS_MODULE;
// 3. 添加到系统cdev_add(&my_cdev, dev, 1);
// 4. 创建设备节点(通常由 udev 自动完成)// 或者手动:mknod /dev/mydev c <major> <minor>块设备
块设备以固定大小的块(通常 512 字节或 4KB)为单位随机访问,支持缓冲和缓存:
- 硬盘(
/dev/sda) - SSD(
/dev/nvme0n1) - USB 存储设备(
/dev/sdb)
块设备通过 gendisk 结构表示,通过 block_device_operations 提供操作接口。详见第 10 章:块设备与 I/O 栈。
网络设备
网络设备不通过文件描述符访问,而是通过套接字(socket)API。它由 net_device 结构表示:
// 网络设备的关键字段struct net_device { char name[IFNAMSIZ]; // 接口名(eth0, wlan0) unsigned long state; // 设备状态 const struct net_device_ops *netdev_ops; // 操作函数表 unsigned int irq; // 中断号 unsigned long mem_start; // 共享内存起始 unsigned long mem_end; // 共享内存结束 // ... 更多字段};五、设备号管理
每个字符设备和块设备都有一个设备号,由主设备号和次设备号组成:
// 设备号 = 主设备号(12位) + 次设备号(20位)#define MAJOR(dev) ((dev) >> 20)#define MINOR(dev) ((dev) & 0xfffff)#define MKDEV(ma, mi) ((ma) << 20 | (mi))| 组成 | 位数 | 含义 |
|---|---|---|
| 主设备号 | 12 位 | 标识设备驱动程序 |
| 次设备号 | 20 位 | 标识同一驱动下的具体设备 |
# 查看系统中已注册的字符设备cat /proc/devices | head -20
# 查看设备节点的主次设备号ls -la /dev/sda /dev/tty1# brw-rw---- 1 root disk 8, 0 ... /dev/sda (块设备,主8次0)# crw--w---- 1 root tty 4, 1 ... /dev/tty1 (字符设备,主4次1)六、udev 与 uevent:用户空间设备管理
在早期 Linux 中,/dev 目录下的设备节点需要手动创建(mknod),或者由 devfs 在内核空间创建。现代 Linux 使用 udev——一个用户空间的设备管理守护进程。
udev 的工作流程
- 内核发送 uevent:当设备添加/移除时,内核通过
kobject_uevent()发送 Netlink 消息 - udevd 接收事件:udev 守护进程监听 Netlink socket
- 匹配规则:根据
/etc/udev/rules.d/中的规则决定如何处理 - 创建/删除节点:在
/dev/下创建设备节点,设置权限和符号链接
# 查看设备的 uevent 属性链udevadm info -a -n /dev/sda
# 监控实时 ueventudevadm monitor
# 查看设备数据库udevadm info -q all -n /dev/sda七、设备树(Device Tree)
在 x86 平台上,BIOS/UEFI 通过 ACPI 向内核描述硬件拓扑。但在 ARM、RISC-V 等嵌入式平台上,使用**设备树(Device Tree)**来描述硬件。
设备树是一种数据结构(通常为 .dts 源文件编译为 .dtb 二进制),描述了:
- CPU 数量和类型
- 总线拓扑和地址映射
- 中断控制器配置
- 外设寄存器地址和中断号
- 时钟和电源域
// 设备树源码示例(.dts 片段)uart@fe201000 { compatible = "brcm,bcm2835-uart"; reg = <0xfe201000 0x200>; interrupts = <2 25>; clock-frequency = <48000000>; status = "okay";};内核启动时解析设备树,为每个设备节点创建 platform_device,然后与已注册的 platform_driver 进行匹配(通过 compatible 属性)。
八、内核模块机制
内核模块(Kernel Module)是 Linux 实现宏内核灵活性的关键机制——它允许在运行时动态加载和卸载代码,而不需要重新编译内核或重启系统。
模块的基本结构
// hello.c — 最简单的内核模块#include <linux/module.h>#include <linux/init.h>
// 模块元信息MODULE_LICENSE("GPL"); // 必须声明许可证MODULE_AUTHOR("Your Name");MODULE_DESCRIPTION("Hello World Module");MODULE_VERSION("1.0");
// 模块参数(可通过 insmod 传入)static int count = 1;module_param(count, int, 0644);MODULE_PARM_DESC(count, "Number of greetings");
// 初始化函数static int __init hello_init(void){ printk(KERN_INFO "Hello, kernel! (count=%d)\n", count); return 0; // 返回 0 表示成功}
// 退出函数static void __exit hello_exit(void){ printk(KERN_INFO "Goodbye, kernel!\n");}
// 注册初始化和退出函数module_init(hello_init);module_exit(hello_exit);模块的 ELF 结构
.ko 文件是特殊的 ELF 文件,包含额外的段:
| 段名 | 用途 |
|---|---|
.modinfo | 模块元信息(许可证、作者、参数描述等) |
__versions | 模块使用的内核符号版本校验信息 |
.gnu.linkonce.this_module | struct module 实例 |
.exit.text | 退出函数代码(卸载时调用) |
# 查看模块信息modinfo hello.ko# filename: hello.ko# license: GPL# author: Your Name# description: Hello World Module# parm: count:int Number of greetings
# 查看模块依赖modprobe --show-depends e1000模块加载与卸载
# 加载模块insmod hello.ko # 直接加载insmod hello.ko count=5 # 传递参数modprobe hello # 自动处理依赖关系(推荐)
# 查看已加载模块lsmod # 等同于 cat /proc/modules
# 卸载模块rmmod hello # 直接卸载modprobe -r hello # 自动卸载依赖(推荐)
# 查看内核日志dmesg | tail -5# [ 123.456] Hello, kernel! (count=5)符号导出与依赖
模块可以通过 EXPORT_SYMBOL 导出符号,供其他模块使用:
// 在模块 A 中导出符号int my_function(int arg) { return arg * 2; }EXPORT_SYMBOL(my_function); // 导出,任何模块可用EXPORT_SYMBOL_GPL(my_function); // 仅 GPL 模块可用// 在模块 B 中使用导出的符号extern int my_function(int arg); // 声明外部符号内核通过 Module.symvers 文件记录符号的 CRC 校验值,确保模块与内核的 ABI 兼容。
九、编写字符设备驱动实战
下面是一个完整的字符设备驱动示例,展示了设备驱动模型的核心概念:
// mydev.c — 完整的字符设备驱动#include <linux/module.h>#include <linux/fs.h>#include <linux/cdev.h>#include <linux/device.h>#include <linux/uaccess.h>
#define DEVICE_NAME "mydev"#define BUF_SIZE 1024
static dev_t dev_num;static struct cdev my_cdev;static struct class *my_class;static struct device *my_device;static char kernel_buf[BUF_SIZE];
// 打开设备static int mydev_open(struct inode *inode, struct file *filp){ pr_info("mydev: device opened\n"); return 0;}
// 读取数据(内核 → 用户空间)static ssize_t mydev_read(struct file *filp, char __user *buf, size_t count, loff_t *offset){ int bytes = min(count, (size_t)BUF_SIZE); if (copy_to_user(buf, kernel_buf, bytes)) return -EFAULT; pr_info("mydev: read %d bytes\n", bytes); return bytes;}
// 写入数据(用户空间 → 内核)static ssize_t mydev_write(struct file *filp, const char __user *buf, size_t count, loff_t *offset){ int bytes = min(count, (size_t)BUF_SIZE); if (copy_from_user(kernel_buf, buf, bytes)) return -EFAULT; pr_info("mydev: wrote %d bytes\n", bytes); return bytes;}
// 关闭设备static int mydev_release(struct inode *inode, struct file *filp){ pr_info("mydev: device closed\n"); return 0;}
// 文件操作函数表static const struct file_operations my_fops = { .owner = THIS_MODULE, .open = mydev_open, .read = mydev_read, .write = mydev_write, .release = mydev_release,};
// 模块初始化static int __init mydev_init(void){ int ret;
// 1. 动态分配设备号 ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME); if (ret < 0) { pr_err("mydev: failed to alloc chrdev region\n"); return ret; }
// 2. 初始化并添加 cdev cdev_init(&my_cdev, &my_fops); my_cdev.owner = THIS_MODULE; ret = cdev_add(&my_cdev, dev_num, 1); if (ret) { unregister_chrdev_region(dev_num, 1); return ret; }
// 3. 创建设备类和设备节点(udev 自动创建 /dev/mydev) my_class = class_create(DEVICE_NAME); if (IS_ERR(my_class)) { cdev_del(&my_cdev); unregister_chrdev_region(dev_num, 1); return PTR_ERR(my_class); }
my_device = device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME); if (IS_ERR(my_device)) { class_destroy(my_class); cdev_del(&my_cdev); unregister_chrdev_region(dev_num, 1); return PTR_ERR(my_device); }
pr_info("mydev: registered with major=%d, minor=%d\n", MAJOR(dev_num), MINOR(dev_num)); return 0;}
// 模块退出(必须逆序释放资源)static void __exit mydev_exit(void){ device_destroy(my_class, dev_num); class_destroy(my_class); cdev_del(&my_cdev); unregister_chrdev_region(dev_num, 1); pr_info("mydev: unregistered\n");}
module_init(mydev_init);module_exit(mydev_exit);
MODULE_LICENSE("GPL");MODULE_DESCRIPTION("A simple character device driver");Makefile
obj-m += mydev.o
KDIR := /lib/modules/$(shell uname -r)/build
all: make -C $(KDIR) M=$(PWD) modules
clean: make -C $(KDIR) M=$(PWD) clean十、DMA 与流式映射
DMA(Direct Memory Access)允许设备直接读写内存,无需 CPU 介入。这是高性能 I/O 的基础。
DMA 映射类型
| 类型 | 说明 | 适用场景 |
|---|---|---|
| 一致性映射 | CPU 和设备始终看到一致的数据 | 长期存在的共享缓冲区 |
| 流式映射 | 需要显式同步操作 | 一次性的 I/O 传输 |
// 一致性 DMA 映射void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag);void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_handle);
// 流式 DMA 映射dma_addr_t dma_map_single(struct device *dev, void *cpu_addr, size_t size, enum dma_data_direction direction);void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction);流式映射必须指定数据传输方向:DMA_TO_DEVICE(CPU→设备)、DMA_FROM_DEVICE(设备→CPU)、DMA_BIDIRECTIONAL(双向)。错误的方向可能导致数据不一致。
十一、动手实践
1. 浏览设备模型层次
# 查看 /sys/devices 下的设备拓扑ls /sys/devices/
# 查看特定设备的 sysfs 属性链udevadm info -a -n /dev/sda
# 查看总线类型ls /sys/bus/
# 查看设备类ls /sys/class/2. 查看已加载内核模块
# 列出所有已加载模块lsmod
# 查看模块详细信息modinfo ext4
# 查看模块依赖关系modprobe --show-depends e10003. 编写并加载内核模块
# 编译模块make
# 加载模块sudo insmod mydev.ko
# 查看内核日志dmesg | tail
# 读写设备echo "Hello from user" > /dev/mydevcat /dev/mydev
# 卸载模块sudo rmmod mydev4. 使用 dynamic_debug 动态调试
# 启用模块的所有调试输出echo 'module mydev +p' > /sys/kernel/debug/dynamic_debug/control
# 启用特定文件的调试输出echo 'file mydev.c +p' > /sys/kernel/debug/dynamic_debug/control
# 查看当前启用的调试规则cat /sys/kernel/debug/dynamic_debug/control | grep mydev参考资料
- 《Linux 设备驱动程序》(LDD3)第 2、14 章
- Kernel Documentation — Driver Model
- Kernel Documentation — Kernel Module
- man 8 udevadm、man 8 modprobe
- Linux Device Tree Documentation
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






