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/

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

相关推荐
大树8812 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠12 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质12 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush412 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行52013 小时前
Linux 11 动态监控指令top
linux
Inhand陈工13 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智14 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
不会C语言的男孩14 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
shushangyun_14 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化
古城小栈14 小时前
Unix 与 Linux 异同小叙
linux·服务器·unix