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,
};
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));
/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下生成设备节点的全过程。
4.6 本章小结与关键检查清单
字符设备驱动的核心流程可以归纳为以下六个步骤,开发者应逐项检查确认:
- 定义 file_operations:实现 open/release/read/write/unlocked_ioctl 等回调,并正确初始化结构体。
- 分配设备号:优先使用
alloc_chrdev_region()动态分配,避免主设备号冲突。 - 初始化并注册 cdev:调用
cdev_init()后立即调用cdev_add(),确保顺序正确。 - 创建设备类:使用
class_create()创建 sysfs 类,用于设备节点管理。 - 创建设备节点:使用
device_create()自动在/dev下生成节点,无需手动 mknod。 - 实现卸载清理:逆序销毁 device、class、cdev,并释放设备号,防止资源泄漏。
device_create() 创建的设备节点默认权限为 0600(仅 root 可访问)。如果 HAL 层以非 root 用户(如 system、radio)运行,需要在 ueventd.rc 或 init.rc 中添加权限规则,例如:/dev/my_device 0666 system system。否则 HAL 层 open() 调用将返回 -EACCES 权限错误。