2. Linux内核驱动基础

零拷贝与DMA延迟优化的前提,是先把驱动“挂”进内核并让内核正确识别设备。本节从内核模块的最小骨架出发,串起字符设备、块设备、设备模型与sysfs,最后落到加载与调试手段,为后续分析DMA路径建立可观测的入口。

一、内核模块结构:从入口到出口

Linux驱动通常以可加载内核模块(LKM)形式存在。模块没有main函数,而是通过module_init()与module_exit()注册入口和出口。入口负责申请资源、注册设备、建立数据结构;出口必须按相反顺序释放,否则卸载时会出现资源泄漏或内核oops。

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>

static dev_t devno;
static struct cdev my_cdev;

static int __init mydrv_init(void)
{
    alloc_chrdev_region(&devno, 0, 1, "mydrv");
    cdev_init(&my_cdev, &my_fops);
    cdev_add(&my_cdev, devno, 1);
    pr_info("mydrv: init, major=%d minor=%d\n",
            MAJOR(devno), MINOR(devno));
    return 0;
}

static void __exit mydrv_exit(void)
{
    cdev_del(&my_cdev);
    unregister_chrdev_region(devno, 1);
    pr_info("mydrv: exit\n");
}

module_init(mydrv_init);
module_exit(mydrv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("kernel-course");
MODULE_DESCRIPTION("zero-copy demo driver");

模块元信息(LICENSE、AUTHOR、DESCRIPTION、别名)不仅影响合规性,也影响modinfo与自动加载。缺少GPL声明会导致部分内核符号无法使用,这是初学者最常见的编译通过但加载失败的原因之一。

二、字符设备与块设备驱动:两种I/O模型

字符设备以字节流方式访问,典型如串口、GPIO、自定义采集卡;块设备以固定大小块为单位,经过块层调度与页缓存,典型如磁盘、NVMe、RAM disk。二者在注册接口、数据路径和延迟特征上差异明显,直接影响零拷贝方案的选择。

对比项字符设备块设备
访问单位字节流扇区/块(通常512B或4K)
核心结构file_operations + cdevblock_device_operations + gendisk
数据路径read/write 直接拷贝到用户缓冲经块层、请求队列、bio
缓存机制通常无页缓存页缓存 + 回写
典型延迟来源copy_to_user、中断上下文调度、合并、DMA映射
零拷贝切入点mmap、DMA-BUF、io_uringO_DIRECT、bio、DMA scatter-gather

字符设备通过file_operations暴露open/read/write/ioctl/mmap等回调。若驱动支持mmap,用户态可直接映射设备物理页,避免copy_to_user,这正是零拷贝的常见起点。块设备则通过gendisk与请求队列接入通用块层,DMA延迟往往隐藏在请求合并与映射阶段。

三、设备模型与sysfs:让内核“看得见”设备

Linux设备模型用kobject、kset、device、driver、bus描述设备与驱动的层次关系,并在sysfs中导出为目录与属性文件。sysfs不是普通文件系统,而是内核对象的内存视图,读写属性会直接调用驱动的show/store回调。

下图展示一个字符设备驱动从模块加载到sysfs暴露属性的完整关系:模块入口注册cdev,设备模型建立device与driver绑定,sysfs生成属性节点,用户态通过文件接口与属性接口访问同一驱动。

字符设备驱动加载与sysfs暴露关系图 用户态 open/read/ioctl/mmap VFS file_operations 字符设备驱动 cdev + my_fops read/write/mmap 设备模型 device / driver / bus kobject / kset 引用计数与生命周期 sysfs /sys/class/mydrv/ attr show/store 模块加载 insmod / modprobe 调试接口 dmesg / ftrace /proc/devices 访问 分发 注册 绑定 导出 触发 观测

sysfs属性是调试利器。例如在驱动中定义DEVICE_ATTR_RW(status),即可在/sys/class/mydrv/mydrv0/status读写状态。对于DMA延迟分析,可以把DMA描述符地址、完成计数、错误码暴露为属性,用户态用cat即可采样,无需重新编译内核。

四、驱动加载与调试方法

加载与卸载的基本命令如下,注意modprobe会处理依赖,insmod只加载单个模块:

# 编译模块
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

# 加载与查看
sudo insmod mydrv.ko
lsmod | grep mydrv
cat /proc/devices | grep mydrv
dmesg | tail -20

# 创建设备节点
sudo mknod /dev/mydrv c $(awk '$2=="mydrv"{print $1}' /proc/devices) 0

# 卸载
sudo rmmod mydrv

调试手段按侵入性从低到高排列:dmesg与pr_info看日志;/proc/devices、/sys/class确认注册结果;ftrace跟踪函数调用与延迟;perf采样热点;kprobe动态插桩;必要时用crash或kdump分析oops。对于DMA延迟,优先用ftrace的irq与dma事件,再结合驱动内时间戳,避免一开始就上重型工具。

建议在驱动入口打印devno与cdev_add返回值,在出口打印释放顺序。很多“设备打不开”问题,根源是cdev_add失败后仍继续注册,或卸载时先删cdev再释放中断导致竞态。
常见误区:把copy_to_user当作唯一延迟来源。实际上在块设备与DMA场景中,映射、同步、缓存一致性维护往往占更大比例。调试时应先用ftrace确认时间花在拷贝、映射还是中断处理,再决定优化方向。

五、复盘:为后续零拷贝与DMA分析铺路

本节建立的四个支点——模块生命周期、字符/块设备差异、设备模型与sysfs、加载调试链路——是后续所有优化的观测基础。只有先让驱动可加载、可枚举、可观测,才能进一步讨论mmap、DMA-BUF、O_DIRECT与scatter-gather映射带来的延迟变化。建议在动手优化前,先确认lsmod、/proc/devices、/sys/class、dmesg四条信息链完整,再进入下一阶段的零拷贝改造。