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 轮询。

Linux 驱动结构与加载机制总览 用户态 open/read/write socket/send/recv mount + 文件读写 insmod / modprobe 内核公共层 VFS / 字符设备框架 cdev 块层 blk-mq + I/O 调度 网络协议栈 字符设备驱动 file_operations cdev_add / alloc_chrdev_region copy_to_user / copy_from_user /dev/xxx 节点 块设备驱动 gendisk / block_device_ops request_queue / blk-mq bio 合并与调度 /dev/sdX 或 /dev/nvmeX 网络设备驱动 net_device / netdev_ops ndo_start_xmit NAPI poll / 中断合并 eth0 / wlan0 内核模块机制 module_init / module_exit 符号导出 EXPORT_SYMBOL 引用计数与依赖 .ko 加载 / 卸载 硬件层 寄存器 / MMIO / DMA 中断 IRQ / MSI-X PCIe / USB / 平台总线 性能关键路径:拷贝次数、锁粒度、中断频率、队列深度

从图中可以看出,四类驱动共享“用户态 → 内核公共层 → 驱动 → 硬件”的主干,但公共层不同:字符设备走 VFS 与 cdev,块设备走块层与请求队列,网络设备走协议栈与 netdev。性能优化时,先判断瓶颈落在哪一段,再决定是减少拷贝、调整队列深度,还是改用 NAPI 降低中断开销。

三、方法流程:从模块加载到设备可用

以字符设备为例,一个可工作的驱动通常遵循固定流程:申请设备号 → 初始化 cdev 并绑定 file_operations → 创建设备节点 → 实现读写回调 → 卸载时逆序释放。块设备则需额外注册 gendisk 与请求队列,网络设备需注册 net_device 并实现收发回调。

  1. 申请设备号:alloc_chrdev_region() 动态分配,或 register_chrdev_region() 指定主次设备号。
  2. 初始化并添加 cdev:cdev_init() 绑定 file_operations,cdev_add() 注册到内核。
  3. 创建节点:手动 mknod,或通过 class_create() + device_create() 由 udev 自动生成。
  4. 实现回调:open/release 管理资源,read/write 完成用户态与内核态数据交换。
  5. 卸载清理:依次 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() 批量传输,并将中断上半部与下半部拆分,减少关中断时间。

性能视角下,驱动加载成功不等于性能达标。init 阶段应避免耗时操作(如大块内存分配、固件同步加载),否则会拖长启动时间;运行阶段应关注拷贝次数、锁竞争与中断频率。

五、常见误区与对比清单

对比项字符设备块设备网络设备
访问方式字节流,/dev 节点块/扇区,可随机访问数据包,socket 接口
核心结构cdev + file_operationsgendisk + request_queuenet_device + netdev_ops
数据单位字节bio / requestsk_buff
典型性能点拷贝次数、阻塞唤醒队列深度、调度合并NAPI、中断合并、零拷贝
加载方式insmod/modprobeinsmod/modprobeinsmod/modprobe
常见误区:把 printk 当作调试常态。高频日志会显著增加 I/O 与锁开销,性能测试时应使用 pr_debug 或动态调试 dyndbg,避免在热路径中输出日志。

掌握模块加载机制与四类驱动的结构差异,是后续分析驱动性能问题的前提。只有明确数据从用户态到硬件经过哪些层、每层做了什么,才能准确判断优化应落在拷贝、锁、队列还是中断上。