ESC
输入关键词搜索文章标题和内容

嵌入式Linux字符设备驱动开发:从内核模块到用户接口

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

嵌入式Linux字符设备驱动开发:从内核模块到用户接口

字符设备是Linux驱动开发的基础入口。本文从核心数据结构出发,拆解cdev注册、file_operations回调、设备号分配、自动节点创建的完整流程,给出兼容Linux 6.x的可运行代码,帮你避开API版本迁移的坑。

一、原理简析

字符设备(Character Device)是Linux三大设备类型之一,与块设备、网络设备并列。它的核心特征是按字节流顺序访问,不支持随机寻址,数据通常不经页缓存直接在驱动与用户空间之间传递。串口、键盘、LED、传感器等都是典型的字符设备。

理解字符设备驱动,关键是掌握三个核心数据结构及其关系:

  • dev_t:设备号,32位无符号整数,高12位为主设备号(标识驱动),低20位为次设备号(标识设备实例)。通过MKDEV(major, minor)构造,MAJOR(dev)和MINOR(dev)分解。
  • struct cdev:内核内部表示字符设备的核心结构体,内嵌kobject、owner指针、file_operations指针、设备号和次设备计数。
  • struct file_operations:驱动与VFS之间的桥梁,定义了open、read、write、release、unlocked_ioctl等回调函数,用户空间的系统调用最终分派到这里。

整个调用链路:用户空间open()/read()/write() → 系统调用 → VFS层 → file_operations回调 → 驱动程序 → 硬件设备。

二、开发流程

字符设备驱动开发就是"注册-实现-销毁",具体分七步:

第一步:分配设备号。调用alloc_chrdev_region()让内核动态分配一个未使用的主设备号,或用register_chrdev_region()静态指定。推荐动态分配,避免冲突。

第二步:初始化cdev。调用cdev_init()将cdev结构体与file_operations绑定,并设置cdev.owner = THIS_MODULE。

第三步:添加cdev到内核。调用cdev_add()将cdev注册到内核,此后内核就知道该设备号对应的操作函数集。

第四步:创建设备类。调用class_create()创建一个设备类,为后续自动创建设备节点做准备。

第五步:创建设备节点。调用device_create()在/dev下自动创建设备文件,udev/mdev会根据类和设备信息生成节点。

第六步:实现file_operations回调。编写open、release、read、write、unlocked_ioctl等函数,完成具体的设备操作逻辑。

第七步:模块卸载时逆序清理。依次调用device_destroy()、class_destroy()、cdev_del()、unregister_chrdev_region()释放所有资源。

三、代码实现

完整驱动代码

// mychardev.c - 字符设备驱动示例(兼容 Linux 5.x/6.x)
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/slab.h>

#define DEVICE_NAME  "mychardev"
#define BUF_SIZE     4096
#define CLASS_NAME   "mychardev_class"

/* ioctl 命令定义 */
#define MYCHARDEV_MAGIC 'k'
#define MYCHARDEV_RESET    _IO(MYCHARDEV_MAGIC, 0)
#define MYCHARDEV_GET_SIZE _IOR(MYCHARDEV_MAGIC, 1, int)
#define MYCHARDEV_SET_SIZE _IOW(MYCHARDEV_MAGIC, 2, int)

/* 设备私有数据结构 */
struct mychar_dev {
    struct cdev    cdev;
    char          *buf;
    size_t         buf_len;
    struct mutex   lock;
};

static struct mychar_dev mydev;
static dev_t devt;
static struct class *myclass;
static struct device *mydevice;

static int mydev_open(struct inode *inode, struct file *filp)
{
    struct mychar_dev *dev;

    dev = container_of(inode->i_cdev, struct mychar_dev, cdev);
    filp->private_data = dev;

    pr_info("mychardev: device opened\n");
    return 0;
}

static int mydev_release(struct inode *inode, struct file *filp)
{
    pr_info("mychardev: device closed\n");
    return 0;
}

static ssize_t mydev_read(struct file *filp, char __user *buf,
                          size_t count, loff_t *ppos)
{
    struct mychar_dev *dev = filp->private_data;
    ssize_t bytes_read;

    mutex_lock(&dev->lock);

    if (*ppos >= dev->buf_len) {
        mutex_unlock(&dev->lock);
        return 0;  /* EOF */
    }

    if (count > dev->buf_len - *ppos)
        count = dev->buf_len - *ppos;

    if (copy_to_user(buf, dev->buf + *ppos, count)) {
        mutex_unlock(&dev->lock);
        return -EFAULT;
    }

    *ppos += count;
    bytes_read = count;

    mutex_unlock(&dev->lock);
    pr_info("mychardev: read %zu bytes\n", count);
    return bytes_read;
}

static ssize_t mydev_write(struct file *filp, const char __user *buf,
                           size_t count, loff_t *ppos)
{
    struct mychar_dev *dev = filp->private_data;
    ssize_t bytes_written;

    mutex_lock(&dev->lock);

    if (*ppos + count > BUF_SIZE)
        count = BUF_SIZE - *ppos;

    if (count == 0) {
        mutex_unlock(&dev->lock);
        return -ENOSPC;  /* 缓冲区已满 */
    }

    if (copy_from_user(dev->buf + *ppos, buf, count)) {
        mutex_unlock(&dev->lock);
        return -EFAULT;
    }

    *ppos += count;
    if (*ppos > dev->buf_len)
        dev->buf_len = *ppos;

    bytes_written = count;

    mutex_unlock(&dev->lock);
    pr_info("mychardev: wrote %zu bytes\n", count);
    return bytes_written;
}

static long mydev_ioctl(struct file *filp, unsigned int cmd,
                        unsigned long arg)
{
    struct mychar_dev *dev = filp->private_data;
    int ret = 0;
    int val;

    switch (cmd) {
    case MYCHARDEV_RESET:
        mutex_lock(&dev->lock);
        memset(dev->buf, 0, BUF_SIZE);
        dev->buf_len = 0;
        *filp->f_pos = 0;
        mutex_unlock(&dev->lock);
        pr_info("mychardev: buffer reset\n");
        break;

    case MYCHARDEV_GET_SIZE:
        mutex_lock(&dev->lock);
        val = (int)dev->buf_len;
        mutex_unlock(&dev->lock);
        if (copy_to_user((int __user *)arg, &val, sizeof(val)))
            return -EFAULT;
        break;

    case MYCHARDEV_SET_SIZE:
        if (copy_from_user(&val, (int __user *)arg, sizeof(val)))
            return -EFAULT;
        mutex_lock(&dev->lock);
        if (val < 0 || val > BUF_SIZE) {
            mutex_unlock(&dev->lock);
            return -EINVAL;
        }
        dev->buf_len = val;
        mutex_unlock(&dev->lock);
        break;

    default:
        return -ENOTTY;
    }

    return ret;
}

static const struct file_operations mydev_fops = {
    .owner          = THIS_MODULE,
    .open           = mydev_open,
    .release        = mydev_release,
    .read           = mydev_read,
    .write          = mydev_write,
    .unlocked_ioctl = mydev_ioctl,
};

static int __init mydev_init(void)
{
    int ret;

    /* 1. 分配设备号 */
    ret = alloc_chrdev_region(&devt, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        pr_err("mychardev: failed to alloc device number\n");
        return ret;
    }
    pr_info("mychardev: major=%d, minor=%d\n",
            MAJOR(devt), MINOR(devt));

    /* 2. 初始化cdev */
    cdev_init(&mydev.cdev, &mydev_fops);
    mydev.cdev.owner = THIS_MODULE;

    /* 3. 分配缓冲区 */
    mydev.buf = kzalloc(BUF_SIZE, GFP_KERNEL);
    if (!mydev.buf) {
        ret = -ENOMEM;
        goto err_unregister;
    }
    mydev.buf_len = 0;
    mutex_init(&mydev.lock);

    /* 4. 添加cdev到内核 */
    ret = cdev_add(&mydev.cdev, devt, 1);
    if (ret < 0) {
        pr_err("mychardev: cdev_add failed\n");
        goto err_free_buf;
    }

    /* 5. 创建设备类(兼容 Linux 6.4+) */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)
    myclass = class_create(CLASS_NAME);
#else
    myclass = class_create(THIS_MODULE, CLASS_NAME);
#endif
    if (IS_ERR(myclass)) {
        pr_err("mychardev: class_create failed\n");
        ret = PTR_ERR(myclass);
        goto err_cdev_del;
    }

    /* 6. 创建设备节点 */
    mydevice = device_create(myclass, NULL, devt, NULL, DEVICE_NAME);
    if (IS_ERR(mydevice)) {
        pr_err("mychardev: device_create failed\n");
        ret = PTR_ERR(mydevice);
        goto err_class_destroy;
    }

    pr_info("mychardev: module loaded successfully\n");
    return 0;

err_class_destroy:
    class_destroy(myclass);
err_cdev_del:
    cdev_del(&mydev.cdev);
err_free_buf:
    kfree(mydev.buf);
err_unregister:
    unregister_chrdev_region(devt, 1);
    return ret;
}

static void __exit mydev_exit(void)
{
    device_destroy(myclass, devt);
    class_destroy(myclass);
    cdev_del(&mydev.cdev);
    kfree(mydev.buf);
    mutex_destroy(&mydev.lock);
    unregister_chrdev_region(devt, 1);
    pr_info("mychardev: module unloaded\n");
}

module_init(mydev_init);
module_exit(mydev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Developer");
MODULE_DESCRIPTION("Character Device Driver Example");

Makefile

# Makefile for mychardev driver
obj-m += mychardev.o

KDIR ?= /lib/modules/$(shell uname -r)/build

all:
    make -C $(KDIR) M=$(PWD) modules

clean:
    make -C $(KDIR) M=$(PWD) clean

用户空间测试程序

// test_mychardev.c - 用户空间测试程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define DEVICE_PATH "/dev/mychardev"

#define MYCHARDEV_MAGIC 'k'
#define MYCHARDEV_RESET    _IO(MYCHARDEV_MAGIC, 0)
#define MYCHARDEV_GET_SIZE _IOR(MYCHARDEV_MAGIC, 1, int)
#define MYCHARDEV_SET_SIZE _IOW(MYCHARDEV_MAGIC, 2, int)

int main(void)
{
    int fd;
    char write_buf[] = "Hello, char device!";
    char read_buf[64] = {0};
    int size;

    fd = open(DEVICE_PATH, O_RDWR);
    if (fd < 0) {
        perror("open failed");
        return EXIT_FAILURE;
    }

    /* 写入测试 */
    if (write(fd, write_buf, strlen(write_buf)) < 0) {
        perror("write failed");
        close(fd);
        return EXIT_FAILURE;
    }
    printf("Written: %s\n", write_buf);

    /* ioctl: 获取缓冲区大小 */
    if (ioctl(fd, MYCHARDEV_GET_SIZE, &size) < 0) {
        perror("ioctl GET_SIZE failed");
        close(fd);
        return EXIT_FAILURE;
    }
    printf("Buffer size: %d\n", size);

    /* 回到文件起始位置读取 */
    lseek(fd, 0, SEEK_SET);
    if (read(fd, read_buf, sizeof(read_buf) - 1) < 0) {
        perror("read failed");
        close(fd);
        return EXIT_FAILURE;
    }
    printf("Read: %s\n", read_buf);

    /* ioctl: 重置缓冲区 */
    if (ioctl(fd, MYCHARDEV_RESET) < 0) {
        perror("ioctl RESET failed");
        close(fd);
        return EXIT_FAILURE;
    }
    printf("Buffer reset done\n");

    close(fd);
    return EXIT_SUCCESS;
}

编译测试程序:

来自 linuxros.cn · linuxROS
gcc -o test_mychardev test_mychardev.c

加载驱动并测试:

# 编译驱动
make

# 加载模块
sudo insmod mychardev.ko

# 查看分配的主设备号
dmesg | tail -5

# 运行测试
sudo ./test_mychardev

# 卸载模块
sudo rmmod mychardev

四、对比表格

新旧API对比

维度 旧版API 新版API
设备号注册 register_chrdev() alloc_chrdev_region() + cdev_add()
设备号注销 unregister_chrdev() unregister_chrdev_region() + cdev_del()
cdev管理 内部隐式管理 显式cdev_init()/cdev_add()/cdev_del()
次设备号范围 固定0~255 灵活指定,支持0~1048575
多设备实例 需要多次调用 一次注册多个次设备号
设备类创建 class_create(THIS_MODULE, name) class_create(name)(6.4+)
推荐程度 仅兼容旧内核 新开发必须使用

ioctl命令宏对比

宏 方向 参数 典型场景
_IO(type, nr) 无数据传输 魔数+序号 重置设备、启动/停止
_IOR(type, nr, datatype) 内核→用户 +数据类型 读取设备状态
_IOW(type, nr, datatype) 用户→内核 +数据类型 设置设备参数
_IOWR(type, nr, datatype) 双向 +数据类型 交换配置数据

注意:_IOR/_IOW/_IOWR的第三个参数是数据类型的名称(如int、struct my_config),不是sizeof()的结果。宏内部会自动计算大小。

五、核心流程图

字符设备驱动注册流程

flowchart TB A(["开始:模块加载"]) --> B["alloc_chrdev_region()<br/>分配设备号"] B --> C{"分配成功?"} C -->|"否"| D(["返回错误码"]) C -->|"是"| E["cdev_init()<br/>初始化cdev结构体"] E --> F["设置 cdev.owner<br/>= THIS_MODULE"] F --> G["cdev_add()<br/>将cdev添加到内核"] G --> H{"添加成功?"} H -->|"否"| I["unregister_chrdev_region()"] I --> D H -->|"是"| J["class_create()<br/>创建设备类"] J --> K["device_create()<br/>创建设备节点"] K --> L{"创建成功?"} L -->|"否"| M["逆序清理已注册资源"] M --> D L -->|"是"| N(["注册完成<br/>/dev/下生成设备节点"]) style A fill:#E8F5E9 style N fill:#E8F5E9 style D fill:#FFEBEE style C fill:#FFF8E1 style H fill:#FFF8E1 style L fill:#FFF8E1 style B fill:#E3F2FD style E fill:#E3F2FD style F fill:#E3F2FD style G fill:#E3F2FD style J fill:#E3F2FD style K fill:#E3F2FD style I fill:#FFEBEE style M fill:#FFEBEE

用户空间系统调用到驱动的分派过程

flowchart TB A(["用户程序调用<br/>open/read/write/ioctl"]) --> B["系统调用入口"] B --> C["VFS层"] C --> D["根据inode->i_cdev<br/>找到cdev结构体"] D --> E["通过cdev->ops<br/>找到file_operations"] E --> F{"操作类型?"} F -->|"open"| G[".open() 回调"] F -->|"read"| H[".read() 回调<br/>copy_to_user()"] F -->|"write"| I[".write() 回调<br/>copy_from_user()"] F -->|"ioctl"| J[".unlocked_ioctl() 回调"] G --> K(["返回用户空间"]) H --> K I --> K J --> K style A fill:#E8F5E9 style K fill:#E8F5E9 style F fill:#FFF8E1 style B fill:#E3F2FD style C fill:#E3F2FD style D fill:#E3F2FD style E fill:#E3F2FD style G fill:#F3E5F5 style H fill:#F3E5F5 style I fill:#F3E5F5 style J fill:#F3E5F5

模块卸载清理流程

flowchart TB A(["开始:模块卸载"]) --> B["device_destroy()<br/>销毁设备节点"] B --> C["class_destroy()<br/>销毁设备类"] C --> D["cdev_del()<br/>从内核移除cdev"] D --> E["kfree()<br/>释放驱动缓冲区"] E --> F["mutex_destroy()<br/>销毁互斥锁"] F --> G["unregister_chrdev_region()<br/>释放设备号"] G --> H(["卸载完成"]) style A fill:#E8F5E9 style H fill:#E8F5E9 style B fill:#E3F2FD style C fill:#E3F2FD style D fill:#E3F2FD style E fill:#E3F2FD style F fill:#E3F2FD style G fill:#E3F2FD

六、常见问题解决

Q1:insmod后/dev下没有设备节点?

检查内核是否支持udev/mdev。嵌入式系统需确保CONFIG_DEVTMPFS和CONFIG_DEVTMPFS_MOUNT已开启,或在启动脚本中手动挂载:mount -t devtmpfs devtmpfs /dev。同时确认device_create()未返回错误指针。

Q2:class_create编译报错"too many arguments"?

Linux 6.4起class_create()移除了THIS_MODULE参数,只接受类名字符串。这是Greg Kroah-Hartman在2023年3月提交的改动,原因是owner参数从未被实际使用。解决方案是用LINUX_VERSION_CODE宏做条件编译,如本文代码所示。

Q3:copy_to_user/copy_from_user返回非零值?

返回值是未能拷贝的字节数,不是已拷贝的字节数。返回0表示全部成功,返回非零应返回-EFAULT给用户空间。常见原因:用户空间指针非法、内核缓冲区越界。

Q4:ioctl命令返回-ENOTTY?

-ENOTTY表示"不合适的ioctl操作",驱动收到无法识别的命令时应返回此值。检查用户空间的命令宏定义是否与驱动一致,特别是魔数字符和序号。

Q5:rmmod提示"Device or resource busy"?

设备文件仍被用户进程打开。cdev.owner = THIS_MODULE确保设备打开期间模块引用计数不为零,防止正在使用时被卸载。先关闭所有打开的文件描述符,再rmmod。

七、总结

字符设备驱动开发的核心是理解"设备号-cdev-file_operations"三者关系:设备号是寻址标识,cdev是内核管理实体,file_operations是功能实现入口。新版API将注册过程拆分为独立的步骤,虽然代码量增加,但带来了更灵活的次设备号管理和更清晰的资源生命周期控制。实际开发中,注意Linux 6.4对class_create()的签名变更,用条件编译保证跨版本兼容。下期讲中断处理与等待队列,实现带阻塞I/O的字符设备驱动。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页