mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1941 字
5 分钟
设备驱动模型
2024-09-18

一、设备驱动模型概述#

Linux 内核需要管理种类繁多的硬件设备——从简单的 LED 到复杂的 NVMe SSD,从串口到网卡。如果没有统一的框架,每种设备都独立实现自己的管理逻辑,内核将变得无法维护。Linux 设备驱动模型(Device Driver Model)正是为了解决这个问题而设计的:它提供了一套统一的内核对象体系,将设备、驱动、总线等概念抽象为面向对象的层次结构,并通过 sysfs 文件系统向用户空间暴露设备信息。

为什么需要设备驱动模型#

在早期内核中,设备管理是分散的——PCI 设备有自己的注册机制,USB 设备有另一套,平台设备又不同。这导致:

  • 代码重复:每种总线类型都实现了类似的设备遍历、匹配、电源管理逻辑
  • 信息不统一:用户空间无法通过统一接口获取设备信息
  • 电源管理困难:没有统一的设备层次关系,无法按序挂起/恢复

2.6 内核引入的设备驱动模型通过统一的 kobject 体系解决了这些问题。

二、kobject:内核对象的基石#

kobject 是 Linux 设备驱动模型的核心数据结构,它提供了引用计数、sysfs 表示、父子关系等通用功能。虽然很少有代码直接操作 kobject,但它是所有设备模型对象的隐含基类。

include/linux/kobject.h
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 指针
事件通知向用户空间发送 ueventkobject_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 过滤操作
};
graph TD subgraph kset K[kset.kobj<br>/sys/devices/] K --> A[kobject: system<br>/sys/devices/system/] K --> B[kobject: pci0000:00<br>/sys/devices/pci0000:00/] K --> C[kobject: platform<br>/sys/devices/platform/] end B --> D[kobject: 0000:00:1f.0<br>/sys/devices/pci0000:00/0000:00:1f.0/] C --> E[kobject: PNP0103:00<br>/sys/devices/platform/PNP0103:00/]

三、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/scheduler
Warning

sysfs 属性文件通常只支持单次读写一个值(不是全文本),且每个属性文件只做一件事。这是 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. 初始化 cdev
struct 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; // 共享内存结束
// ... 更多字段
};

五、设备号管理#

每个字符设备和块设备都有一个设备号,由主设备号和次设备号组成:

include/linux/kdev_t.h
// 设备号 = 主设备号(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 的工作流程#

sequenceDiagram participant Kernel as 内核 participant Netlink as Netlink Socket participant Udevd as udevd participant Rules as udev 规则 participant Dev as /dev/ Kernel->>Netlink: kobject_uevent() 发送 KOBJ_ADD Netlink->>Udevd: 接收 uevent 消息 Udevd->>Rules: 匹配 /etc/udev/rules.d/ 中的规则 Rules->>Udevd: 返回匹配的规则(创建节点、设置权限等) Udevd->>Dev: mknod 创建设备节点 Udevd->>Dev: 设置权限、创建符号链接
  1. 内核发送 uevent:当设备添加/移除时,内核通过 kobject_uevent() 发送 Netlink 消息
  2. udevd 接收事件:udev 守护进程监听 Netlink socket
  3. 匹配规则:根据 /etc/udev/rules.d/ 中的规则决定如何处理
  4. 创建/删除节点:在 /dev/ 下创建设备节点,设置权限和符号链接
# 查看设备的 uevent 属性链
udevadm info -a -n /dev/sda
# 监控实时 uevent
udevadm 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_modulestruct 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);
Note

流式映射必须指定数据传输方向: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 e1000

3. 编写并加载内核模块#

# 编译模块
make
# 加载模块
sudo insmod mydev.ko
# 查看内核日志
dmesg | tail
# 读写设备
echo "Hello from user" > /dev/mydev
cat /dev/mydev
# 卸载模块
sudo rmmod mydev

4. 使用 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

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

设备驱动模型
https://blog.souloss.com/posts/linux-internals/device-driver-model/
作者
Souloss
发布于
2024-09-18
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐