Linux 字符设备驱动开发:从内核模块到设备节点的全链路实践

Linux 字符设备驱动开发:从内核模块到设备节点的全链路实践

一、字符设备驱动的工程定位:为什么它是 Linux 驱动开发的入门必修

Linux 设备驱动分为三类:字符设备、块设备、网络设备。字符设备是最基础的驱动类型——它以字节流的方式提供数据访问,典型的例子包括串口、LED、按键、传感器。

字符设备驱动是 Linux 驱动开发的入门必修课,原因有三。第一,概念完整:字符设备驱动涵盖了内核模块、设备注册、文件操作、中断处理、并发控制等核心概念,是理解 Linux 驱动模型的基础。第二,实现简洁:一个完整的字符设备驱动约 200-300 行代码,可以在一个工作日内完成。第三,调试友好:字符设备通过 /dev 节点访问,可以用 catecho 等标准工具测试,无需专门的测试程序。

二、字符设备驱动的核心架构

flowchart TD
    A[用户空间] -->|open/read/write/close| B[系统调用接口]
    B --> C[VFS 虚拟文件系统]
    C --> D[字符设备框架]
    D --> E[file_operations 结构体]
    E --> F[驱动实现的操作函数]

    F --> G[硬件操作]
    G --> H[设备寄存器]

    subgraph 内核空间
        C
        D
        E
        F
        G
    end

    subgraph 驱动注册流程
        I[alloc_chrdev_region] --> J[分配设备号]
        J --> K[cdev_init] --> L[初始化 cdev]
        L --> M[cdev_add] --> N[添加字符设备]
        N --> O[device_create] --> P[创建 /dev 节点]
    end

设备号:每个字符设备有一个唯一的主设备号和次设备号。主设备号标识驱动程序,次设备号标识同一驱动下的不同设备实例。

cdev 结构体:内核中字符设备的抽象,包含设备号和 file_operations 指针。

file_operations:驱动程序提供给内核的函数指针表,定义了 open、read、write、release 等操作的实现。

设备节点:用户空间通过 /dev/xxx 节点访问字符设备,节点的主设备号与驱动注册的主设备号匹配。

三、字符设备驱动实现

3.1 完整的字符设备驱动

// chardev.c
// Linux 字符设备驱动示例:虚拟传感器设备

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

#define DEVICE_NAME "vsensor"
#define BUF_SIZE 256

// 设备私有数据结构
struct vsensor_dev {
    struct cdev cdev;           // 字符设备结构
    struct mutex lock;          // 互斥锁,保护并发访问
    char data_buf[BUF_SIZE];    // 数据缓冲区
    int data_len;               // 当前数据长度
    dev_t dev_num;              // 设备号
};

static struct vsensor_dev *vsensor;
static dev_t dev_num;
static struct class *vsensor_class;

// open 操作:初始化设备状态
static int vsensor_open(struct inode *inode, struct file *filp)
{
    struct vsensor_dev *dev;

    // 通过 inode 获取设备私有数据
    dev = container_of(inode->i_cdev, struct vsensor_dev, cdev);
    filp->private_data = dev;

    // 初始化缓冲区(模拟传感器数据)
    mutex_lock(&dev->lock);
    snprintf(dev->data_buf, BUF_SIZE,
             "temperature: %.1f\nhumidity: %.1f\n",
             25.0 + (prandom_u32() % 100) / 10.0,
             60.0 + (prandom_u32() % 100) / 10.0);
    dev->data_len = strlen(dev->data_buf);
    mutex_unlock(&dev->lock);

    pr_info("vsensor: 设备已打开\n");
    return 0;
}

// read 操作:将传感器数据拷贝到用户空间
static ssize_t vsensor_read(struct file *filp, char __user *buf,
                            size_t count, loff_t *f_pos)
{
    struct vsensor_dev *dev = filp->private_data;
    int bytes_to_copy;
    int ret;

    mutex_lock(&dev->lock);

    // 如果已经读完,返回 0 表示 EOF
    if (*f_pos >= dev->data_len) {
        mutex_unlock(&dev->lock);
        return 0;
    }

    // 计算本次可读取的字节数
    bytes_to_copy = min(count, (size_t)(dev->data_len - *f_pos));

    // 将内核数据拷贝到用户空间
    // 必须使用 copy_to_user,不能直接 memcpy
    // 因为用户空间地址可能无效或被换出
    ret = copy_to_user(buf, dev->data_buf + *f_pos, bytes_to_copy);
    if (ret) {
        mutex_unlock(&dev->lock);
        return -EFAULT;  // 拷贝失败,返回错误
    }

    *f_pos += bytes_to_copy;
    mutex_unlock(&dev->lock);

    pr_info("vsensor: 读取 %d 字节\n", bytes_to_copy);
    return bytes_to_copy;
}

// write 操作:接收用户写入的配置命令
static ssize_t vsensor_write(struct file *filp, const char __user *buf,
                             size_t count, loff_t *f_pos)
{
    struct vsensor_dev *dev = filp->private_data;
    int bytes_to_copy;
    int ret;
    char kbuf[BUF_SIZE];

    // 限制写入长度,防止缓冲区溢出
    bytes_to_copy = min(count, (size_t)(BUF_SIZE - 1));

    // 从用户空间拷贝数据到内核
    ret = copy_from_user(kbuf, buf, bytes_to_copy);
    if (ret) {
        return -EFAULT;
    }
    kbuf[bytes_to_copy] = '\0';

    mutex_lock(&dev->lock);

    // 处理配置命令(简化实现)
    if (strncmp(kbuf, "reset", 5) == 0) {
        // 重置传感器数据
        snprintf(dev->data_buf, BUF_SIZE,
                 "temperature: %.1f\nhumidity: %.1f\n",
                 25.0 + (prandom_u32() % 100) / 10.0,
                 60.0 + (prandom_u32() % 100) / 10.0);
        dev->data_len = strlen(dev->data_buf);
        pr_info("vsensor: 传感器数据已重置\n");
    }

    mutex_unlock(&dev->lock);

    return bytes_to_copy;
}

// release 操作:清理设备状态
static int vsensor_release(struct inode *inode, struct file *filp)
{
    pr_info("vsensor: 设备已关闭\n");
    return 0;
}

// 文件操作函数表
static const struct file_operations vsensor_fops = {
    .owner   = THIS_MODULE,
    .open    = vsensor_open,
    .read    = vsensor_read,
    .write   = vsensor_write,
    .release = vsensor_release,
};

// 模块初始化
static int __init vsensor_init(void)
{
    int ret;

    // 第一步:分配设备号
    ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        pr_err("vsensor: 分配设备号失败\n");
        return ret;
    }
    pr_info("vsensor: 主设备号 = %d, 次设备号 = %d\n",
            MAJOR(dev_num), MINOR(dev_num));

    // 第二步:分配设备结构体
    vsensor = kzalloc(sizeof(*vsensor), GFP_KERNEL);
    if (!vsensor) {
        ret = -ENOMEM;
        goto fail_alloc;
    }
    vsensor->dev_num = dev_num;
    mutex_init(&vsensor->lock);

    // 第三步:初始化并添加 cdev
    cdev_init(&vsensor->cdev, &vsensor_fops);
    vsensor->cdev.owner = THIS_MODULE;
    ret = cdev_add(&vsensor->cdev, dev_num, 1);
    if (ret) {
        pr_err("vsensor: cdev_add 失败\n");
        goto fail_cdev;
    }

    // 第四步:创建设备类和设备节点
    vsensor_class = class_create(THIS_MODULE, DEVICE_NAME);
    if (IS_ERR(vsensor_class)) {
        ret = PTR_ERR(vsensor_class);
        goto fail_class;
    }

    // device_create 会在 /dev 下自动创建设备节点
    // 无需手动 mknod
    if (IS_ERR(device_create(vsensor_class, NULL, dev_num, NULL, DEVICE_NAME))) {
        ret = -EINVAL;
        goto fail_device;
    }

    pr_info("vsensor: 驱动加载成功\n");
    return 0;

fail_device:
    class_destroy(vsensor_class);
fail_class:
    cdev_del(&vsensor->cdev);
fail_cdev:
    kfree(vsensor);
fail_alloc:
    unregister_chrdev_region(dev_num, 1);
    return ret;
}

// 模块卸载
static void __exit vsensor_exit(void)
{
    // 按注册的逆序清理资源
    device_destroy(vsensor_class, dev_num);
    class_destroy(vsensor_class);
    cdev_del(&vsensor->cdev);
    kfree(vsensor);
    unregister_chrdev_region(dev_num, 1);
    pr_info("vsensor: 驱动已卸载\n");
}

module_init(vsensor_init);
module_exit(vsensor_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Developer");
MODULE_DESCRIPTION("Virtual Sensor Character Device Driver");

3.2 编译与测试

# Makefile
obj-m += chardev.o

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

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

clean:
	make -C $(KDIR) M=$(PWD) clean
# 编译驱动
make

# 加载驱动模块
sudo insmod chardev.ko

# 查看设备节点
ls -l /dev/vsensor

# 读取传感器数据
cat /dev/vsensor

# 写入配置命令
echo "reset" > /dev/vsensor

# 查看内核日志
dmesg | tail -20

# 卸载驱动
sudo rmmod chardev

四、架构权衡与适用边界

互斥锁 vs 自旋锁的选择。字符设备的 read/write 操作可能睡眠(如 copy_to_user),必须使用互斥锁(mutex)而非自旋锁(spinlock)。自旋锁在中断上下文中使用,持有期间不允许睡眠。

缓冲区大小的权衡。内核空间的内存有限,缓冲区不宜过大。对于传感器数据,256 字节通常足够。对于需要传输大量数据的场景(如视频帧),应使用 mmap 或 DMA 而非 read/write。

设备节点权限管理。默认创建的设备节点权限为 0600(仅 root 可访问)。生产环境中需要通过 udev 规则设置合适的权限,允许特定用户组访问。

适用边界:字符设备驱动适用于低速、按字节流访问的设备(传感器、LED、按键)。对于需要块级访问的存储设备,应使用块设备驱动。对于网络通信,应使用网络设备驱动。对于高性能数据传输,应考虑 DMA 或 mmap 方案。

五、总结

Linux 字符设备驱动的开发流程包含四个核心步骤:分配设备号(alloc_chrdev_region)、初始化 cdev(cdev_init + cdev_add)、创建设备类和节点(class_create + device_create)、实现 file_operations。关键注意事项:用户空间数据拷贝必须使用 copy_to_user/copy_from_user,并发访问必须用 mutex 保护(read/write 可能睡眠),设备节点通过 device_create 自动创建无需手动 mknod。字符设备适用于低速字节流设备,高性能场景应考虑 DMA 或 mmap。

评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值