Linux字符设备驱动开发(一):从零搭建一个可直接运行的驱动框架(附完整代码)

前言

嵌入式Linux开发中,字符设备驱动是最基础、最常见的驱动类型。韦东山老师的课程从这一框架切入,让我们快速建立对Linux驱动模型的认知。本文基于课程思路,整理出一个完整、可编译运行 的字符设备驱动框架,并带你一步步理解 cdevfile_operations、设备号自动分配、自动创建设备节点等核心知识点。

读完这篇文章,你将收获:

  • 一个真正能 insmod 后生成 /dev 节点的驱动模板
  • 对字符设备驱动注册/注销流程的透彻理解
  • 可直接复用的代码和 Makefile

一、字符设备驱动核心概念

在写代码之前,先梳理三个关键的数据结构和概念。

1.1 设备号(dev_t)

Linux 内核通过 主设备号(major)次设备号(minor) 来唯一标识一个设备。

c 复制代码
typedef u32 dev_t;  // 高12位主设备号,低20位次设备号
  • 静态分配:register_chrdev_region(),需手动指定主设备号(可能冲突)
  • 动态分配:alloc_chrdev_region(),由内核分配一个未使用的主设备号(推荐

本驱动采用动态分配,避免手选设备号带来的冲突问题。

1.2 cdev 结构体

c 复制代码
struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};

cdev 是字符设备的抽象,内核用它将设备号和文件操作 file_operations 绑定在一起。

1.3 file_operations 结构体

这是驱动与用户程序的接口 。每一个系统调用(openreadwrite 等)都会对应到这个结构体中的某个函数指针。

c 复制代码
struct file_operations {
    struct module *owner;
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    int (*open) (struct inode *, struct file *);
    int (*release) (struct inode *, struct file *);
    // ...还有很多其他成员
};

我们首先实现 openreleasereadwrite 四个最基本的接口。


二、驱动设计思路

整个驱动的生命周期可以概括为:

  1. 模块加载

    • 动态申请一个可用的主设备号
    • 初始化 cdev,绑定 file_operations
    • 向内核注册 cdev
    • 创建 /sys/class/ 下的类(为了自动创建 /dev/ 节点)
    • 根据类创建设备节点 /dev/chrdev_test
  2. 用户访问

    • 用户程序调用 open("/dev/chrdev_test", ...) → 触发 chrdev_open
    • read / write 触发对应函数(本示例仅打印内核日志)
  3. 模块卸载

    • 严格按照"先创建的后销毁"顺序:
      • 销毁 /dev 节点
      • 销毁 class
      • 删除 cdev
      • 释放设备号

三、代码实现与详细解析

3.1 头文件和全局变量

c 复制代码
#include <linux/module.h>
#include <linux/fs.h>         // file_operations, alloc_chrdev_region...
#include <linux/cdev.h>       // cdev_init, cdev_add...
#include <linux/device.h>     // class_create, device_create
#include <linux/uaccess.h>    // copy_to_user, copy_from_user (备用)
#include <linux/slab.h>       // kmalloc, kfree (备用)

#define DEVICE_NAME "chrdev_test"    // 设备名称,最终出现在 /dev/chrdev_test
#define CLASS_NAME  "chrdev_class"   // 在 /sys/class/ 下创建的同名类

static dev_t dev_num;                // 设备号
static struct cdev my_cdev;          // 字符设备
static struct class *my_class;       // 设备类指针
static struct device *my_device;     // 设备指针
  • dev_num 保存动态分配的设备号,MAJOR(dev_num) 获取主设备号。
  • my_classmy_device 用于自动创建设备节点,这样就不用自己 mknod

3.2 文件操作函数实现

这四个函数目前只是打印日志,不操作实际硬件或数据,目的是验证框架能跑通。

c 复制代码
static int chrdev_open(struct inode *inode, struct file *file)
{
    pr_info("chrdev: device opened\n");
    return 0;
}

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

static ssize_t chrdev_read(struct file *file, char __user *buf,
                           size_t count, loff_t *f_pos)
{
    pr_info("chrdev: read called, count = %zu\n", count);
    return 0;  // 返回0表示文件末尾(EOF)
}

static ssize_t chrdev_write(struct file *file, const char __user *buf,
                            size_t count, loff_t *f_pos)
{
    pr_info("chrdev: write called, count = %zu\n", count);
    return count; // 假装全部写入
}
  • 返回值含义:chrdev_read 返回 0 会让 cat 认为文件已结束;
  • chrdev_write 返回 count 告诉上层所有数据都已"写入"。

3.3 绑定 file_operations 结构体

c 复制代码
static struct file_operations chrdev_fops = {
    .owner   = THIS_MODULE,
    .open    = chrdev_open,
    .release = chrdev_release,
    .read    = chrdev_read,
    .write   = chrdev_write,
};

.owner = THIS_MODULE 用于防止模块在使用中被卸载,是必须的。

3.4 模块初始化函数(重点)

c 复制代码
static int __init chrdev_init(void)
{
    int ret;

    // 1. 动态申请设备号(主设备号可能为0,由内核分配)
    ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        pr_err("chrdev: failed to allocate device number\n");
        return ret;
    }
    pr_info("chrdev: allocated major=%d, minor=%d\n",
            MAJOR(dev_num), MINOR(dev_num));

    // 2. 初始化 cdev 并添加到内核
    cdev_init(&my_cdev, &chrdev_fops);
    my_cdev.owner = THIS_MODULE;
    ret = cdev_add(&my_cdev, dev_num, 1);
    if (ret) {
        pr_err("chrdev: cdev_add failed\n");
        goto err_cdev_add;
    }

    // 3. 创建 class(自动设备节点需要)
    my_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(my_class)) {
        pr_err("chrdev: class_create failed\n");
        ret = PTR_ERR(my_class);
        goto err_class_create;
    }

    // 4. 在 /dev/ 下创建设备节点
    my_device = device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME);
    if (IS_ERR(my_device)) {
        pr_err("chrdev: device_create failed\n");
        ret = PTR_ERR(my_device);
        goto err_device_create;
    }

    pr_info("chrdev: module loaded successfully, /dev/%s created\n", DEVICE_NAME);
    return 0;

// 错误回滚路径(goto链式处理)
err_device_create:
    class_destroy(my_class);
err_class_create:
    cdev_del(&my_cdev);
err_cdev_add:
    unregister_chrdev_region(dev_num, 1);
    return ret;
}

关键解释

  • alloc_chrdev_region:次设备号从 0 开始,连续申请 1 个。成功后 dev_num 会被填上分配的主次设备号。
  • cdev_initfile_operationscdev 绑定。
  • cdev_add 告诉内核该 cdev 代表的设备号范围,只有调用后设备才真正可用。
  • 创建 class 和 device 是借助 udev 机制,加载驱动后会自动在 /dev 下生成设备文件,无需手动 mknod
  • 错误处理使用了经典的 goto 链式回滚,确保中间任何一步出错都会正确清理已申请的资源。

3.5 模块卸载函数

c 复制代码
static void __exit chrdev_exit(void)
{
    // 严格遵守"先创建的后销毁"
    device_destroy(my_class, dev_num);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);

    pr_info("chrdev: module unloaded\n");
}

3.6 完整的驱动代码(可直接复制)

将上面的片段合并到 chrdev_base.c 中(再加上模块入口/出口宏):

c 复制代码
/*
 * chrdev_base.c
 * 一个完整的字符设备驱动框架,加载后自动生成 /dev/chrdev_test
 * 可直接编译运行,适用于 Linux 5.x(4.x 也可)
 * 作者:[你的ID]
 */

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/slab.h>

#define DEVICE_NAME "chrdev_test"
#define CLASS_NAME  "chrdev_class"

static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;
static struct device *my_device;

static int chrdev_open(struct inode *inode, struct file *file)
{
    pr_info("chrdev: device opened\n");
    return 0;
}

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

static ssize_t chrdev_read(struct file *file, char __user *buf,
                           size_t count, loff_t *f_pos)
{
    pr_info("chrdev: read called, count = %zu\n", count);
    return 0;
}

static ssize_t chrdev_write(struct file *file, const char __user *buf,
                            size_t count, loff_t *f_pos)
{
    pr_info("chrdev: write called, count = %zu\n", count);
    return count;
}

static struct file_operations chrdev_fops = {
    .owner   = THIS_MODULE,
    .open    = chrdev_open,
    .release = chrdev_release,
    .read    = chrdev_read,
    .write   = chrdev_write,
};

static int __init chrdev_init(void)
{
    int ret;

    ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        pr_err("chrdev: failed to allocate device number\n");
        return ret;
    }
    pr_info("chrdev: allocated major=%d, minor=%d\n",
            MAJOR(dev_num), MINOR(dev_num));

    cdev_init(&my_cdev, &chrdev_fops);
    my_cdev.owner = THIS_MODULE;
    ret = cdev_add(&my_cdev, dev_num, 1);
    if (ret) {
        pr_err("chrdev: cdev_add failed\n");
        goto err_cdev_add;
    }

    my_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(my_class)) {
        pr_err("chrdev: class_create failed\n");
        ret = PTR_ERR(my_class);
        goto err_class_create;
    }

    my_device = device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME);
    if (IS_ERR(my_device)) {
        pr_err("chrdev: device_create failed\n");
        ret = PTR_ERR(my_device);
        goto err_device_create;
    }

    pr_info("chrdev: module loaded successfully, /dev/%s created\n", DEVICE_NAME);
    return 0;

err_device_create:
    class_destroy(my_class);
err_class_create:
    cdev_del(&my_cdev);
err_cdev_add:
    unregister_chrdev_region(dev_num, 1);
    return ret;
}

static void __exit chrdev_exit(void)
{
    device_destroy(my_class, dev_num);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);
    pr_info("chrdev: module unloaded\n");
}

module_init(chrdev_init);
module_exit(chrdev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A complete character device driver template");
MODULE_VERSION("1.0");

四、编写 Makefile

在内核模块开发中,Makefile 的写法非常固定,只需指定内核源码路径即可。这里提供一份可直接在 x86 Ubuntu树莓派 上使用的 Makefile。

makefile 复制代码
# Makefile for chrdev_base

# 内核源码目录(使用当前运行内核的构建目录)
KERNEL_DIR := /lib/modules/$(shell uname -r)/build

# 当前目录
PWD := $(shell pwd)

# 编译目标
obj-m := chrdev_base.o

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

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

交叉编译说明(编译给 ARM 开发板用):

  • KERNEL_DIR 修改为开发板内核源码的路径(已配置好的)

  • 并设置环境变量,例如:

    bash 复制代码
    export ARCH=arm
    export CROSS_COMPILE=arm-linux-gnueabihf-
    make

五、编译与测试

5.1 编译

chrdev_base.cMakefile 放在同一目录下,执行:

bash 复制代码
make

成功后目录下会生成 chrdev_base.ko

5.2 加载驱动

bash 复制代码
sudo insmod chrdev_base.ko

查看内核输出:

bash 复制代码
dmesg | tail -n 5

预期输出类似:

复制代码
chrdev: allocated major=239, minor=0
chrdev: module loaded successfully, /dev/chrdev_test created

检查设备节点是否生成:

bash 复制代码
ls -l /dev/chrdev_test

预期看到:

复制代码
crw------- 1 root root 239, 0 May 27 10:00 /dev/chrdev_test

其中 239 是你这次得到的主设备号(每次可能不同)。

5.3 测试设备

cat 读取设备,会触发 openreadrelease

bash 复制代码
sudo cat /dev/chrdev_test

再用 dmesg 查看:

复制代码
chrdev: device opened
chrdev: read called, count = 131072
chrdev: device closed

echo 写入:

bash 复制代码
sudo sh -c "echo 'hello' > /dev/chrdev_test"

日志中会增加:

复制代码
chrdev: device opened
chrdev: write called, count = 6
chrdev: device closed

如果普通用户无法访问,可用 sudo chmod 666 /dev/chrdev_test 临时赋权。

5.4 卸载驱动

bash 复制代码
sudo rmmod chrdev_base

dmesg 会显示:

复制代码
chrdev: module unloaded

此时 /dev/chrdev_test自动消失


六、常见问题与解决方法

  1. insmod 报错:Required key not available

    原因:当前系统开启了 Secure Boot,且模块未签名。

    解决:暂时关闭 Secure Boot,或用 mokutil --disable-validation 禁用验证后重启。

  2. insmod 报错:Unknown symbol in module

    原因:可能是内核版本不匹配,或依赖的其他模块未加载。

    解决:确认编译时的内核头版本与运行内核一致(uname -r)。

  3. 加载成功但没有 /dev/chrdev_test

    • 检查 dmesg 是否有 device_create 失败的 log。
    • 确认系统有 udev 且运行正常(ps aux | grep udev)。
  4. 设备节点权限不足

    解决方法:编写 udev 规则,或在测试时直接 chmod


七、总结与下篇预告

本文从零搭建了一个标准的字符设备驱动框架 ,涵盖了设备号申请、cdev 注册、自动节点创建等全部流程,加载后即可在 /dev 下看到设备,并能通过最简的系统调用触发内核打印。

这个框架是后续一切字符驱动开发的基石。下一篇文章,我们将:

  • read / write 中实现内核空间与用户空间的数据拷贝copy_to_user / copy_from_user
  • 演示一个简单的内存缓冲区设备,真正做到"打开、读写、关闭"一个字符设备。

参考与推荐

  • 韦东山《嵌入式Linux应用开发完全手册》
  • Linux 内核文档:Documentation/driver-model/

如果本文对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续输出的动力!有任何疑问也欢迎在评论区留言交流~

相关推荐
蓝莓薄荷1 小时前
Ubuntu修改主机名操作指南
linux·ubuntu
遇印记1 小时前
软考知识点(防火墙)
运维·服务器·网络·学习·安全
都在酒里1 小时前
Linux字符设备驱动开发(二):实现数据交互——内核与用户空间的内存拷贝
linux·驱动开发·交互
qq_452396231 小时前
第五篇:《Docker 容器生命周期管理》
运维·docker·容器
思麟呀1 小时前
C++工业级日志项目(四)日志落地
linux·开发语言·c++·windows
Dymc1 小时前
【Ubuntu系统指令启动】一招解决:Ubuntu 20.04 桌面双击 .desktop 文件不再“用文本编辑器打开”
linux·运维·ubuntu·一键运行
蜀道山老天师1 小时前
Docker 进阶:数据持久化与容器网络互联(数据卷、挂载目录、端口映射、自定义网络)
运维·网络·docker·云原生·容器
qq_白羊座1 小时前
CI/CD 与 DevOps 二
运维·ci/cd·devops
Ms_lan1 小时前
同方工业携手桂花网:广州某养老院智慧健康监测实践案例
运维·服务器·网络