4. 字符设备驱动核心

字符设备驱动是Linux内核中最基础、最常用的驱动类型,也是Android HAL层与Kernel交互的主要通道。本章深入剖析字符设备驱动的四个核心要素:file_operations结构体、cdev注册机制、设备号分配策略以及设备节点创建方式。掌握这些内容,是理解HAL层如何通过系统调用最终操作硬件的基础。

4.1 file_operations结构体:驱动与用户空间的接口契约

file_operations结构体是字符设备驱动与用户空间应用程序之间的函数跳转表。用户空间对设备文件执行open()、read()、write()、ioctl()等系统调用时,VFS(虚拟文件系统)会通过该结构体找到驱动中对应的实现函数。

核心要点:驱动开发者必须实现的关键回调函数包括open、release、read、write、unlocked_ioctl。未实现的回调应显式设置为NULL,内核会进行默认处理。
回调函数 原型 触发场景 典型实现
open int (*open)(struct inode *, struct file *) 用户调用open()系统调用 递增使用计数、初始化硬件、分配私有数据
release int (*release)(struct inode *, struct file *) 用户调用close()系统调用 递减使用计数、释放资源、关闭硬件
read ssize_t (*read)(struct file *, char __user *, size_t, loff_t *) 用户调用read()系统调用 从硬件读取数据并拷贝到用户空间
write ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) 用户调用write()系统调用 从用户空间拷贝数据并写入硬件
unlocked_ioctl long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) 用户调用ioctl()系统调用 处理自定义控制命令(如设置波特率、启动采集)
// 典型的 file_operations 结构体定义示例
static int my_driver_open(struct inode *inode, struct file *filp) {
    pr_info("Device opened\n");
    return 0;
}

static int my_driver_release(struct inode *inode, struct file *filp) {
    pr_info("Device released\n");
    return 0;
}

static ssize_t my_driver_read(struct file *filp, char __user *buf,
                               size_t count, loff_t *f_pos) {
    // 从内核缓冲区拷贝数据到用户空间
    if (copy_to_user(buf, kernel_buffer, count))
        return -EFAULT;
    return count;
}

static ssize_t my_driver_write(struct file *filp, const char __user *buf,
                                size_t count, loff_t *f_pos) {
    // 从用户空间拷贝数据到内核缓冲区
    if (copy_from_user(kernel_buffer, buf, count))
        return -EFAULT;
    return count;
}

static long my_driver_ioctl(struct file *filp, unsigned int cmd,
                             unsigned long arg) {
    switch (cmd) {
        case MY_IOCTL_RESET:
            // 执行硬件复位操作
            break;
        default:
            return -EINVAL;
    }
    return 0;
}

static struct file_operations my_fops = {
    .owner          = THIS_MODULE,
    .open           = my_driver_open,
    .release        = my_driver_release,
    .read           = my_driver_read,
    .write          = my_driver_write,
    .unlocked_ioctl = my_driver_ioctl,
};
Android HAL 联动提示:HAL 层中的 open()、close()、read()、write()、ioctl() 函数最终会通过系统调用陷入内核,由 VFS 根据设备文件对应的 file_operations 分发到驱动实现。因此,file_operations 是 HAL 与 Kernel 之间的“函数调用桥梁”。

4.2 cdev注册:将驱动与设备号绑定

cdev(字符设备)结构体是内核用于管理字符设备的核心数据结构。驱动必须初始化一个cdev实例,并将其与file_operations和设备号关联,然后注册到内核中。注册成功后,内核VFS才能根据设备号找到对应的驱动回调。

// cdev 注册的标准流程
#include <linux/cdev.h>

static dev_t dev_num;          // 设备号
static struct cdev my_cdev;    // cdev 结构体

static int __init my_driver_init(void) {
    int ret;

    // 步骤1:分配设备号(此处假设已通过 alloc_chrdev_region 获取)
    // dev_num = MKDEV(major, minor);

    // 步骤2:初始化 cdev 结构体
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;

    // 步骤3:将 cdev 注册到内核
    ret = cdev_add(&my_cdev, dev_num, 1);
    if (ret < 0) {
        pr_err("Failed to add cdev\n");
        return ret;
    }

    pr_info("Character device registered with major %d\n", MAJOR(dev_num));
    return 0;
}

static void __exit my_driver_exit(void) {
    // 步骤4:卸载时删除 cdev
    cdev_del(&my_cdev);
    // 释放设备号
    unregister_chrdev_region(dev_num, 1);
    pr_info("Character device unregistered\n");
}
常见错误:cdev_add() 必须在设备号分配成功之后调用,且 cdev_init() 必须在 cdev_add() 之前完成。如果顺序颠倒,内核可能会访问未初始化的 file_operations 指针,导致内核崩溃(Oops)。

4.3 设备号分配:静态 vs 动态

设备号由主设备号(major)和次设备号(minor)组成,类型为dev_t。主设备号标识设备驱动类型,次设备号标识同一驱动下的不同设备实例。分配方式分为静态分配和动态分配两种。

特性 静态分配 动态分配
API register_chrdev_region() alloc_chrdev_region()
主设备号来源 开发者手动指定(需确保不冲突) 内核自动分配(推荐)
优点 设备节点可预知,适合固定硬件 避免主设备号冲突,适合通用驱动
缺点 主设备号可能被占用,需注册到内核社区 设备节点需动态创建或通过udev/mdev自动生成
使用场景 嵌入式平台中硬件设备固定 Android/Linux通用驱动开发
// 静态分配示例:手动指定主设备号 240
#define MY_MAJOR 240
#define MY_MINOR 0
#define DEVICE_COUNT 1

dev_t dev_num = MKDEV(MY_MAJOR, MY_MINOR);
ret = register_chrdev_region(dev_num, DEVICE_COUNT, "my_driver");
if (ret < 0) {
    pr_err("Failed to register static region\n");
    return ret;
}

// 动态分配示例:由内核自动分配主设备号
ret = alloc_chrdev_region(&dev_num, 0, DEVICE_COUNT, "my_driver");
if (ret < 0) {
    pr_err("Failed to allocate dynamic region\n");
    return ret;
}
pr_info("Allocated major number: %d\n", MAJOR(dev_num));
Android 实践建议:在 Android 内核驱动开发中,强烈推荐使用动态分配。因为 Android 设备种类繁多,静态主设备号容易与其它驱动冲突。动态分配后,通过 /proc/devices 文件可以查看内核分配的主设备号。

4.4 设备节点创建:mknod vs 自动创建

设备节点是用户空间访问驱动的“文件入口”。没有设备节点,用户程序无法通过open()系统调用找到驱动。创建方式分为手动mknod和自动创建(基于devtmpfs或udev/mdev)。

4.4.1 手动创建:mknod命令

在驱动加载后,开发者可以通过mknod命令手动创建设备节点。这种方式适合调试阶段,但生产环境中不推荐。

# 查看内核分配的主设备号
cat /proc/devices | grep my_driver

# 手动创建设备节点(假设主设备号为 240,次设备号为 0)
sudo mknod /dev/my_device c 240 0

# 设置权限
sudo chmod 666 /dev/my_device

# 测试设备
echo "hello" > /dev/my_device
cat /dev/my_device

4.4.2 自动创建:基于class_create与device_create

现代Linux内核(包括Android内核)推荐使用class_create()和device_create() API,配合devtmpfs文件系统,实现设备节点的自动创建和删除。驱动加载时,内核自动在/dev目录下生成节点;驱动卸载时,节点自动消失。

// 自动创建设备节点的标准实现
#include <linux/device.h>

static struct class *my_class;
static struct device *my_device;

static int __init my_driver_init(void) {
    int ret;

    // 1. 分配设备号(动态)
    ret = alloc_chrdev_region(&dev_num, 0, 1, "my_driver");
    if (ret < 0) 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 < 0) {
        unregister_chrdev_region(dev_num, 1);
        return ret;
    }

    // 3. 创建 device class
    my_class = class_create(THIS_MODULE, "my_driver_class");
    if (IS_ERR(my_class)) {
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(my_class);
    }

    // 4. 创建设备节点(自动在 /dev 下生成 my_device)
    my_device = device_create(my_class, NULL, dev_num, NULL, "my_device");
    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("Driver loaded, device /dev/my_device created\n");
    return 0;
}

static void __exit my_driver_exit(void) {
    // 逆序销毁:先销毁设备,再销毁类,最后删除cdev和释放设备号
    device_destroy(my_class, dev_num);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);
    pr_info("Driver unloaded, device node removed\n");
}
自动创建的优势:
  • 无需手动干预:驱动加载/卸载时自动创建/删除节点。
  • 权限自动管理:可通过device_create()的devt参数和udev规则控制访问权限。
  • 与Android兼容:Android的init进程和ueventd守护进程完全支持devtmpfs自动节点创建。

4.5 完整流程SVG架构图:从驱动加载到设备节点生成

下图展示了字符设备驱动从加载到用户空间可访问的完整流程,涵盖了设备号分配、cdev注册、class/device创建以及最终在/dev下生成设备节点的全过程。

字符设备驱动加载与设备节点创建完整流程 module_init() alloc_chrdev_region() cdev_init() cdev_add() class_create() device_create() 内核模块加载 dev_t 设备号 struct cdev VFS 注册表 struct class /dev/my_device 说明:驱动卸载时执行逆序操作 device_destroy() → class_destroy() → cdev_del() → unregister_chrdev_region() 用户空间通过 /dev/my_device 调用 open/read/write/ioctl,VFS 根据 cdev 找到 file_operations 回调

4.6 本章小结与关键检查清单

字符设备驱动的核心流程可以归纳为以下六个步骤,开发者应逐项检查确认:

  1. 定义 file_operations:实现 open/release/read/write/unlocked_ioctl 等回调,并正确初始化结构体。
  2. 分配设备号:优先使用 alloc_chrdev_region() 动态分配,避免主设备号冲突。
  3. 初始化并注册 cdev:调用 cdev_init() 后立即调用 cdev_add(),确保顺序正确。
  4. 创建设备类:使用 class_create() 创建 sysfs 类,用于设备节点管理。
  5. 创建设备节点:使用 device_create() 自动在 /dev 下生成节点,无需手动 mknod。
  6. 实现卸载清理:逆序销毁 device、class、cdev,并释放设备号,防止资源泄漏。
Android 驱动开发特别提醒:在 Android 内核中,device_create() 创建的设备节点默认权限为 0600(仅 root 可访问)。如果 HAL 层以非 root 用户(如 system、radio)运行,需要在 ueventd.rc 或 init.rc 中添加权限规则,例如:/dev/my_device 0666 system system。否则 HAL 层 open() 调用将返回 -EACCES 权限错误。