4. 字符设备驱动核心API变迁
字符设备驱动是Linux驱动开发中最基础、最典型的驱动类型。从4.x到6.x内核,字符设备驱动的核心API经历了显著变化,主要体现在设备号注册、字符设备对象管理以及文件操作接口三个维度。理解这些变迁是完成驱动移植适配的关键前提。
核心要点:字符设备驱动API变迁的核心是“从静态到动态、从手动到自动、从松散到规范”。设备号分配从手动指定转向自动分配,cdev管理从分离操作转向一体化注册,file_operations结构体成员持续精简与类型安全强化。
4.1 设备号注册:register_chrdev_region vs alloc_chrdev_region
设备号是字符设备在内核中的唯一标识,由主设备号(major)和次设备号(minor)组成。在4.x内核中,设备号注册存在两种方式,而6.x内核强烈推荐使用动态分配方式。
| 对比维度 | register_chrdev_region | alloc_chrdev_region |
|---|---|---|
| 分配方式 | 静态分配(手动指定主设备号) | 动态分配(内核自动分配主设备号) |
| 函数原型 | int register_chrdev_region(dev_t from, unsigned count, const char *name) |
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) |
| 主设备号来源 | 调用者通过MKDEV(major, minor)构造 | 内核自动分配,通过dev指针返回 |
| 冲突风险 | 高(主设备号可能已被占用) | 低(内核保证唯一性) |
| 推荐程度(6.x) | 不推荐(仅用于固定主设备号的特殊场景) | 强烈推荐 |
| 移植适配动作 | 需检查主设备号是否冲突,建议改为动态分配 | 通常无需修改,但需注意次设备号起始值 |
移植建议:在6.x内核中,除非有明确的用户空间约定(如某些传统设备固定使用特定主设备号),否则一律使用
alloc_chrdev_region。动态分配的主设备号可通过/proc/devices查看,用户空间通过设备节点路径访问,无需关心主设备号具体值。
以下代码展示了从静态注册到动态注册的移植变化:
/* 4.x内核:静态注册方式(移植前) */
#include <linux/fs.h>
#define MY_MAJOR 240
#define MY_MINOR 0
#define DEVICE_COUNT 1
static dev_t dev_num;
dev_num = MKDEV(MY_MAJOR, MY_MINOR);
ret = register_chrdev_region(dev_num, DEVICE_COUNT, "my_device");
if (ret < 0) {
pr_err("Failed to register chrdev region\n");
return ret;
}
/* 6.x内核:动态注册方式(移植后) */
#include <linux/fs.h>
#define DEVICE_COUNT 1
static dev_t dev_num;
ret = alloc_chrdev_region(&dev_num, 0, DEVICE_COUNT, "my_device");
if (ret < 0) {
pr_err("Failed to alloc chrdev region\n");
return ret;
}
pr_info("Allocated major number: %d\n", MAJOR(dev_num));
注意:使用
alloc_chrdev_region后,主设备号在每次模块加载时可能不同。如果用户空间通过mknod手动创建设备节点,需要动态获取主设备号。推荐配合udev或devtmpfs自动创建设备节点,避免手动管理。
4.2 cdev_init/cdev_add接口变化
字符设备对象(cdev)的初始化和添加操作在4.x到6.x内核中保持了基本接口,但内部实现和推荐用法发生了变化。核心变化在于:6.x内核更推荐使用cdev_add一体化注册,并强化了错误处理要求。
4.2.1 传统方式:cdev_init + cdev_add
在4.x内核中,标准的cdev操作分为两步:
/* 4.x内核:cdev_init + cdev_add 分离操作 */
struct cdev my_cdev;
dev_t dev_num;
/* 第一步:初始化cdev对象 */
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
/* 第二步:添加cdev到内核 */
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
pr_err("Failed to add cdev\n");
return ret;
}
4.2.2 6.x内核的变化与推荐用法
在6.x内核中,虽然cdev_init和cdev_add接口仍然存在,但内核社区推荐以下改进:
- 使用cdev_alloc替代cdev_init:
cdev_alloc动态分配cdev对象并自动初始化,减少遗漏初始化的风险。 - 强化错误检查:
cdev_add返回错误时,必须确保已分配的cdev资源被正确释放。 - 设备号与cdev的关联时机:建议在cdev_add成功后再创建用户空间设备节点,避免设备节点存在但驱动未就绪的竞态条件。
/* 6.x内核:推荐使用cdev_alloc + cdev_add */
struct cdev *my_cdev;
dev_t dev_num;
/* 第一步:动态分配并初始化cdev */
my_cdev = cdev_alloc();
if (!my_cdev) {
pr_err("Failed to alloc cdev\n");
return -ENOMEM;
}
my_cdev->owner = THIS_MODULE;
my_cdev->ops = &my_fops;
/* 第二步:添加cdev到内核 */
ret = cdev_add(my_cdev, dev_num, 1);
if (ret < 0) {
pr_err("Failed to add cdev: %d\n", ret);
kfree(my_cdev); /* 释放动态分配的cdev */
return ret;
}
/* 第三步:cdev_add成功后,再创建设备节点 */
device_create(my_class, NULL, dev_num, NULL, "my_device");
关键差异:在6.x内核中,
cdev_alloc返回的cdev对象使用kfree释放,而非cdev_del。这是因为cdev_alloc使用kzalloc分配内存,而cdev_del仅从内核移除cdev但不释放内存。如果使用cdev_init(栈上或嵌入结构体的cdev),则无需手动释放。
4.3 file_operations结构体成员变更
file_operations结构体是字符设备驱动的核心接口,定义了用户空间通过系统调用访问设备时的回调函数。从4.x到6.x内核,该结构体的成员发生了以下重要变化:
4.3.1 主要变更清单
| 成员名称 | 4.x状态 | 6.x状态 | 变更说明 |
|---|---|---|---|
llseek |
存在 | 存在 | 原型不变,但推荐使用no_llseek或vfs_llseek |
read |
存在 | 存在 | 原型从ssize_t (*read)(struct file *, char __user *, size_t, loff_t *)变为ssize_t (*read)(struct file *, char __user *, size_t, loff_t *),参数不变 |
write |
存在 | 存在 | 原型不变 |
read_iter |
可选 | 推荐 | 6.x内核推荐使用read_iter/write_iter替代传统的read/write,支持iov_iter接口 |
write_iter |
可选 | 推荐 | 同上,支持分散/聚集I/O |
unlocked_ioctl |
存在 | 存在 | 原型不变,但ioctl(BKL版本)已被移除 |
compat_ioctl |
存在 | 存在 | 用于32位应用在64位内核上的ioctl兼容 |
mmap |
存在 | 存在 | 原型不变,但内部实现需适应新的VMA接口 |
open |
存在 | 存在 | 原型不变 |
release |
存在 | 存在 | 原型不变 |
fsync |
存在 | 存在 | 原型不变 |
fasync |
存在 | 存在 | 原型不变 |
flush |
存在 | 存在 | 原型不变 |
poll |
存在 | 存在 | 原型从unsigned int (*poll)(struct file *, struct poll_table_struct *)变为__poll_t (*poll)(struct file *, struct poll_table_struct *),返回值类型变为__poll_t |
owner |
存在 | 移除 | 6.x内核不再使用owner字段,模块引用计数由内核自动管理 |
重要变更:
file_operations中的owner字段在6.x内核中已被移除。如果驱动代码中显式设置了.owner = THIS_MODULE,在6.x内核中编译会报错。移植时需要删除该字段赋值。
4.3.2 poll函数返回值类型变更
poll函数的返回值类型从unsigned int变为__poll_t,这是一个强类型枚举,用于提高类型安全性。移植时需要修改返回值类型和使用的标志宏:
/* 4.x内核:poll函数实现 */
unsigned int my_poll(struct file *filp, struct poll_table_struct *wait)
{
unsigned int mask = 0;
poll_wait(filp, &my_waitq, wait);
if (/* 数据可读 */)
mask |= POLLIN | POLLRDNORM;
if (/* 可写 */)
mask |= POLLOUT | POLLWRNORM;
return mask;
}
/* 6.x内核:poll函数实现(使用__poll_t) */
__poll_t my_poll(struct file *filp, struct poll_table_struct *wait)
{
__poll_t mask = 0;
poll_wait(filp, &my_waitq, wait);
if (/* 数据可读 */)
mask |= EPOLLIN | EPOLLRDNORM;
if (/* 可写 */)
mask |= EPOLLOUT | EPOLLWRNORM;
return mask;
}
移植要点:在6.x内核中,
POLLIN/POLLOUT等宏已被EPOLLIN/EPOLLOUT替代。虽然POLLIN等宏在6.x中仍然定义(作为兼容别名),但推荐使用EPOLL前缀的新宏。同时,file_operations结构体中poll字段的类型声明也相应变化。
4.3.3 read/write vs read_iter/write_iter
6.x内核强烈推荐驱动实现read_iter和write_iter回调,而非传统的read和write。新的迭代器接口支持更高效的I/O操作,特别是对于支持分散/聚集I/O的设备:
/* 6.x内核:使用read_iter/write_iter */
static ssize_t my_read_iter(struct kiocb *iocb, struct iov_iter *to)
{
struct file *filp = iocb->ki_filp;
struct my_device *dev = filp->private_data;
ssize_t ret;
/* 使用iov_iter接口读取数据 */
ret = simple_read_from_iter(to, dev->buffer, dev->data_size);
return ret;
}
static ssize_t my_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
struct file *filp = iocb->ki_filp;
struct my_device *dev = filp->private_data;
ssize_t ret;
/* 使用iov_iter接口写入数据 */
ret = simple_write_to_iter(from, dev->buffer, dev->buffer_size);
return ret;
}
static const struct file_operations my_fops = {
.owner = THIS_MODULE, /* 4.x需要,6.x应删除此行 */
.read_iter = my_read_iter,
.write_iter = my_write_iter,
.unlocked_ioctl = my_ioctl,
.open = my_open,
.release = my_release,
.poll = my_poll,
};
4.4 移植适配清单与常见问题
基于以上分析,以下是字符设备驱动从4.x移植到6.x内核的完整适配清单:
| 序号 | 检查项 | 4.x代码 | 6.x适配动作 |
|---|---|---|---|
| 1 | 设备号注册 | register_chrdev_region |
改为alloc_chrdev_region,或确认主设备号不冲突 |
| 2 | cdev初始化 | cdev_init + 栈上/嵌入cdev |
可选改为cdev_alloc动态分配,注意释放方式 |
| 3 | file_operations.owner | .owner = THIS_MODULE |
删除该字段赋值 |
| 4 | poll返回值 | unsigned int |
改为__poll_t,使用EPOLL前缀宏 |