3. 字符设备驱动基础概念

字符设备驱动是 Linux 内核驱动中最基础、最常用的驱动类型,也是 Android 内核扩展与硬件抽象的核心实现方式。本章将系统讲解字符设备驱动的四个核心概念:设备文件、主设备号与次设备号、file_operations 结构体以及模块加载与卸载。理解这些概念是后续编写 ioctl 实战代码的基石。

学习提示:字符设备以字节流方式读写数据,不支持随机访问(如键盘、鼠标、串口、传感器)。与块设备(如硬盘)和网络设备(如网卡)相比,字符设备的驱动模型最为简洁,适合初学者入门。

3.1 设备文件

在 Linux 系统中,一切皆文件。硬件设备也通过设备文件(又称设备节点)暴露给用户空间。设备文件位于 /dev 目录下,用户程序通过标准的文件操作接口(open、read、write、ioctl、close)与设备通信,无需关心底层硬件细节。

关键理解:设备文件是用户空间与内核驱动之间的桥梁。用户程序操作设备文件时,VFS(虚拟文件系统)根据设备文件记录的设备号,找到对应的内核驱动,并调用驱动注册的操作函数。

设备文件有两种类型:

类型 标识 特点 示例
字符设备文件 c 以字节流方式读写,顺序访问 /dev/ttyS0、/dev/input/event0
块设备文件 b 以块为单位读写,支持随机访问 /dev/sda、/dev/mmcblk0

创建设备文件有两种方式:

  • 手动创建:使用 mknod 命令,例如 mknod /dev/mydevice c 240 0。
  • 自动创建:驱动中使用 class_create 和 device_create 接口,由 udev/mdev 自动在 /dev 下生成设备节点。这是 Android 和现代 Linux 系统的推荐做法。
设备文件与驱动交互架构 用户空间 (User Space) 应用程序 (App / System Service) 系统调用接口 (open / read / write / ioctl / close) VFS (虚拟文件系统) — 根据设备号查找驱动 内核空间 (Kernel Space) 字符设备驱动 (file_operations 实现) 硬件设备 (Hardware) 设备文件 (/dev/xxx)

3.2 主设备号与次设备号

每个设备文件都对应一个设备号,由主设备号(major number)和次设备号(minor number)组成。设备号是内核识别和定位驱动的关键标识。

概念 位数 作用 示例值
主设备号 12 位 (0~4095) 标识设备对应的驱动程序 240(表示某个字符设备驱动)
次设备号 20 位 (0~1048575) 标识同一驱动管理的不同设备实例 0、1、2(表示同一驱动的多个设备)
避坑提醒:主设备号不能重复,否则会导致驱动注册失败。开发调试阶段建议使用 alloc_chrdev_region 动态分配主设备号,避免与已有设备号冲突。生产驱动中,可在 /proc/devices 查看当前系统已占用的主设备号。

设备号在内核中使用 dev_t 类型表示(32 位无符号整数)。内核提供了以下宏来操作设备号:

#include <linux/types.h>
#include <linux/kdev_t.h>

dev_t devno;                     // 设备号变量
int major = MAJOR(devno);        // 从 dev_t 提取主设备号
int minor = MINOR(devno);        // 从 dev_t 提取次设备号
devno = MKDEV(major, minor);     // 由主次设备号合成 dev_t

设备号注册与释放的典型流程:

  • 静态注册:register_chrdev_region(dev_t from, unsigned count, const char *name) — 已知主设备号时使用。
  • 动态分配:alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) — 推荐方式,由内核自动分配未使用的主设备号。
  • 释放:unregister_chrdev_region(dev_t from, unsigned count) — 模块卸载时必须调用。
设备号分配与驱动映射流程 驱动模块加载 alloc_chrdev_region cdev_add 注册设备 device_create 创建设备节点 用户程序 open / ioctl 主设备号动态分配 (避免冲突)

3.3 file_operations 结构体

file_operations 是字符设备驱动的核心数据结构,定义在 <linux/fs.h> 中。它是一组函数指针的集合,将用户空间的系统调用与内核驱动的具体实现绑定在一起。

以下是字符设备驱动最常用的成员:

函数指针 系统调用 功能描述
open open() 打开设备,进行初始化、权限检查、递增引用计数
release close() 关闭设备,释放资源、递减引用计数
read read() 从设备读取数据到用户空间
write write() 从用户空间写入数据到设备
unlocked_ioctl ioctl() 设备控制命令处理(本章实战核心)
llseek lseek() 修改文件读写位置(字符设备通常不支持)
重点说明:在 Linux 2.6.36 之后,ioctl 字段被替换为 unlocked_ioctl(不带大内核锁)。Android 内核中统一使用 unlocked_ioctl,这也是本课程实战部分的核心接口。

典型的 file_operations 定义示例:

#include <linux/fs.h>
#include <linux/uaccess.h>

/* 设备打开函数 */
static int mydev_open(struct inode *inode, struct file *filp) {
    pr_info("mydev: device opened\n");
    return 0;
}

/* 设备关闭函数 */
static int mydev_release(struct inode *inode, struct file *filp) {
    pr_info("mydev: device closed\n");
    return 0;
}

/* ioctl 控制函数(实战核心) */
static long mydev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
    pr_info("mydev: ioctl cmd=0x%x\n", cmd);
    return 0;
}

/* 绑定 file_operations 结构体 */
static const struct file_operations mydev_fops = {
    .owner          = THIS_MODULE,
    .open           = mydev_open,
    .release        = mydev_release,
    .unlocked_ioctl = mydev_ioctl,
    .read           = NULL,   // 暂不支持
    .write          = NULL,   // 暂不支持
};
避坑提醒:file_operations 结构体必须使用 static const 修饰,确保只读且不污染全局命名空间。未实现的函数指针必须显式置为 NULL,内核会自动返回 -ENOTTY 或 -EINVAL。
file_operations 与系统调用绑定关系 open() close() ioctl() read() / write() file_operations .open = mydev_open .release = mydev_release .unlocked_ioctl = mydev_ioctl .read = NULL .write = NULL mydev_open() mydev_release() mydev_ioctl()

3.4 模块加载与卸载

Linux 驱动以内核模块(kernel module)的形式存在,可以动态加载到内核或从内核卸载,无需重新编译整个内核。字符设备驱动的生命周期由模块加载和卸载函数管理。

模块的入口与出口函数:

函数 宏 调用时机 职责
mydev_init module_init(mydev_init) insmod 或系统启动时 分配设备号、初始化 cdev、添加字符设备、创建 class/device
mydev_exit module_exit(mydev_exit) rmmod 或系统关闭时 销毁 device/class、删除 cdev、释放设备号

完整的字符设备驱动模板(加载与卸载部分):

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

#define DEVICE_NAME "mydev"
#define CLASS_NAME  "mydev_class"

static int major;
static struct class *mydev_class = NULL;
static struct device *mydev_device = NULL;
static struct cdev mydev_cdev;

/* 模块加载函数 */
static int __init mydev_init(void) {
    dev_t devno;
    int ret;

    /* 1. 动态分配设备号 */
    ret = alloc_chrdev_region(&devno, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        pr_err("mydev: failed to allocate major number\n");
        return ret;
    }
    major = MAJOR(devno);
    pr_info("mydev: major number = %d\n", major);

    /* 2. 初始化 cdev 并绑定 file_operations */
    cdev_init(&mydev_cdev, &mydev_fops);
    mydev_cdev.owner = THIS_MODULE;

    /* 3. 添加 cdev 到内核 */
    ret = cdev_add(&mydev_cdev, devno, 1);
    if (ret < 0) {
        pr_err("mydev: failed to add cdev\n");
        unregister_chrdev_region(devno, 1);
        return ret;
    }

    /* 4. 创建 device class 和设备节点 */
    mydev_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(mydev_class)) {
        pr_err("mydev: failed to create class\n");
        cdev_del(&mydev_cdev);
        unregister_chrdev_region(devno, 1);
        return PTR_ERR(mydev_class);
    }

    mydev_device = device_create(mydev_class, NULL, devno, NULL, DEVICE_NAME);
    if (IS_ERR(mydev_device)) {
        pr_err("mydev: failed to create device\n");
        class_destroy(mydev_class);
        cdev_del(&mydev_cdev);
        unregister_chrdev_region(devno, 1);
        return PTR_ERR(mydev_device);
    }

    pr_info("mydev: driver loaded successfully\n");
    return 0;
}

/* 模块卸载函数 */
static void __exit mydev_exit(void) {
    dev_t devno = MKDEV(major, 0);

    device_destroy(mydev_class, devno);   // 销毁设备节点
    class_destroy(mydev_class);           // 销毁 class
    cdev_del(&mydev_cdev);               // 删除 cdev
    unregister_chrdev_region(devno, 1);   // 释放设备号

    pr_info("mydev: driver unloaded\n");
}

module_init(mydev_init);
module_exit(mydev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Android Kernel Driver Course");
MODULE_DESCRIPTION("A simple character device driver example");
最佳实践:模块加载函数使用 __init 标记,卸载函数使用 __exit 标记。这些标记告诉内核:加载完成后可以释放 init 函数占用的内存(如果内核配置了 CONFIG_MODULE_FORCE_UNLOAD)。所有错误路径都必须确保已分配的资源被正确释放,否则会导致内核泄漏。
模块加载与卸载生命周期 加载流程 (insmod / module_init) alloc_chrdev_region cdev_init + cdev_add class_create + device_create 卸载流程 (rmmod / module_exit) device_destroy class_destroy cdev_del + unregister_chrdev_region 关键原则:加载与卸载必须完全对称 每个 xxx_create 对应一个 xxx_destroy,顺序相反 insmod mydev.ko rmmod mydev
本章小结:字符设备驱动的四个基础概念环环相扣——设备文件是用户空间的访问入口,主/次设备号是内核定位驱动的钥匙,file_operations 是驱动功能的函数表,模块加载/卸载是驱动生命周期的管理框架。掌握这些概念后,下一章将进入实战环节,编写第一个完整的字符设备驱动并实现 ioctl 命令。