2. Linux驱动开发基础
Linux 驱动性能优化的第一步,不是急着改代码,而是先看清“驱动在内核里长什么样、怎么被加载、数据从用户态到硬件经过哪些层”。本节围绕内核模块机制与四类典型驱动(字符、块、网络,以及贯穿其中的模块加载框架)建立结构认知,为后续定位 CPU 占用、锁竞争、内存拷贝与中断延迟打下基础。
一、核心原理:内核模块是驱动的“可插拔容器”
Linux 内核是单内核架构,但通过 可加载内核模块(LKM) 获得动态扩展能力。驱动通常以 .ko 形式存在,加载时由内核完成符号解析、内存分配与初始化回调,卸载时释放资源。模块与内核共享地址空间,因此驱动中的死循环、越界访问、长时间关中断都会直接拖垮整机性能。
一个模块的生命周期由四个关键入口构成:module_init() 注册初始化函数,module_exit() 注册清理函数,MODULE_LICENSE() 声明许可证,MODULE_AUTHOR/DESCRIPTION 提供元信息。加载时内核调用 init,失败则回滚;卸载时调用 exit,若模块被引用计数占用则拒绝卸载。
#include <linux/module.h>
#include <linux/init.h>
static int __init demo_init(void)
{
pr_info("demo: init\n");
return 0; /* 非0表示加载失败 */
}
static void __exit demo_exit(void)
{
pr_info("demo: exit\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("driver performance demo");
加载机制上,insmod 只加载单个模块,modprobe 会依据 /lib/modules/$(uname -r)/modules.dep 自动解析依赖;lsmod 查看已加载模块与引用计数,rmmod 卸载,modinfo 读取元信息。性能排障时,dmesg 中的 init/exit 日志是判断驱动是否真正生效的第一手证据。
二、四类驱动的基本结构与数据路径
不同驱动面向的设备模型不同,其结构差异直接决定了性能瓶颈的位置。字符设备以字节流为单位,强调 file_operations 回调;块设备以扇区/块为单位,强调请求队列与 I/O 调度;网络设备不以文件节点暴露,而是通过 net_device 与协议栈交互,强调收发队列与 NAPI 轮询。
从图中可以看出,四类驱动共享“用户态 → 内核公共层 → 驱动 → 硬件”的主干,但公共层不同:字符设备走 VFS 与 cdev,块设备走块层与请求队列,网络设备走协议栈与 netdev。性能优化时,先判断瓶颈落在哪一段,再决定是减少拷贝、调整队列深度,还是改用 NAPI 降低中断开销。
三、方法流程:从模块加载到设备可用
以字符设备为例,一个可工作的驱动通常遵循固定流程:申请设备号 → 初始化 cdev 并绑定 file_operations → 创建设备节点 → 实现读写回调 → 卸载时逆序释放。块设备则需额外注册 gendisk 与请求队列,网络设备需注册 net_device 并实现收发回调。
- 申请设备号:
alloc_chrdev_region()动态分配,或register_chrdev_region()指定主次设备号。 - 初始化并添加 cdev:
cdev_init()绑定file_operations,cdev_add()注册到内核。 - 创建节点:手动
mknod,或通过class_create()+device_create()由 udev 自动生成。 - 实现回调:open/release 管理资源,read/write 完成用户态与内核态数据交换。
- 卸载清理:依次
cdev_del()、unregister_chrdev_region()、销毁 class 与 device。
# 常用操作命令
insmod demo.ko # 加载模块
lsmod | grep demo # 查看引用计数
dmesg | tail -20 # 查看 init 日志
cat /proc/devices # 查看已注册设备号
mknod /dev/demo c 240 0 # 手动创建设备节点
rmmod demo # 卸载模块
四、案例与场景:一次“加载成功但性能异常”的复盘
某采集卡字符设备驱动加载后功能正常,但高帧率下 CPU 占用飙升。排查发现:read 回调中每次仅拷贝 64 字节,且未使用等待队列,用户态采用轮询方式反复调用 read,导致大量系统调用与上下文切换。优化方向包括:增大单次拷贝粒度、引入 wait_event_interruptible() 阻塞唤醒、使用 copy_to_user() 批量传输,并将中断上半部与下半部拆分,减少关中断时间。
五、常见误区与对比清单
| 对比项 | 字符设备 | 块设备 | 网络设备 |
|---|---|---|---|
| 访问方式 | 字节流,/dev 节点 | 块/扇区,可随机访问 | 数据包,socket 接口 |
| 核心结构 | cdev + file_operations | gendisk + request_queue | net_device + netdev_ops |
| 数据单位 | 字节 | bio / request | sk_buff |
| 典型性能点 | 拷贝次数、阻塞唤醒 | 队列深度、调度合并 | NAPI、中断合并、零拷贝 |
| 加载方式 | insmod/modprobe | insmod/modprobe | insmod/modprobe |
printk 当作调试常态。高频日志会显著增加 I/O 与锁开销,性能测试时应使用 pr_debug 或动态调试 dyndbg,避免在热路径中输出日志。掌握模块加载机制与四类驱动的结构差异,是后续分析驱动性能问题的前提。只有明确数据从用户态到硬件经过哪些层、每层做了什么,才能准确判断优化应落在拷贝、锁、队列还是中断上。