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自动创建设备节点,避免手动管理。
设备号注册流程对比:静态 vs 动态 register_chrdev_region(静态) 步骤1:手动选择主设备号(如240) 步骤2:MKDEV()构造dev_t 步骤3:register_chrdev_region() 风险:主设备号可能冲突 alloc_chrdev_region(动态) 步骤1:调用alloc_chrdev_region() 步骤2:内核自动分配主设备号 步骤3:通过dev指针获取设备号 优势:内核保证主设备号唯一

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),则无需手动释放。
cdev生命周期管理:4.x vs 6.x 推荐流程 4.x 传统流程 cdev_init(&cdev, &fops) cdev_add(&cdev, dev, count) cdev_del(&cdev) // 移除 6.x 推荐流程 cdev = cdev_alloc() cdev->ops = &fops; cdev->owner = THIS_MODULE cdev_del(cdev); kfree(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,
};
file_operations 结构体成员变迁(4.x → 6.x) 4.x file_operations .owner = THIS_MODULE .llseek = my_llseek .read = my_read .write = my_write .unlocked_ioctl = my_ioctl .poll = my_poll (unsigned int) .open = my_open .release = my_release 6.x file_operations .owner 已移除 ❌ .llseek = my_llseek .read_iter = my_read_iter ✅ 推荐 .write_iter = my_write_iter ✅ 推荐 .unlocked_ioctl = my_ioctl .poll = my_poll (__poll_t) 🔄 .open = my_open .release = my_release

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前缀宏