字符设备驱动是内核开发的最佳入口:结构清晰、调试方便、覆盖驱动开发的核心套路。本文给出一个完整可编译的字符设备模块(含 read/write/ioctl/并发保护),并由于字符设备的『行为语义』可以在用户态精确复现,文末附一个行为等价模拟器的真实运行输出,逐条验证驱动逻辑的正确性。

一、字符设备在内核中的位置

Linux 设备分三类:字符设备(字节流,如串口、/dev/null)、块设备(块寻址,如磁盘)、网络设备(报文收发)。字符设备最简单,也是理解设备模型的最佳起点。一条完整的数据通路是:

应用 read() → VFS 按 fd 找到 file → file->f_op 指向驱动注册的 file_operations → 驱动的 .read 回调 → copy_to_user() 把内核缓冲拷到用户态。驱动开发的本质就是:注册设备号、实现 file_operations、处理并发与用户态拷贝。

二、完整可编译的驱动源码(demochar.c)

这个模块实现 /dev/demochar:一块 16 字节缓冲,write 追加、read 按各自文件偏移消费、ioctl 清零,全程 mutex 保护。在装有内核头文件的机器上可直接编译加载:

// demochar.c —— 最小但完整的字符设备驱动
#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 DEMO_BUF_SIZE 16
#define DEMO_IOC_CLEAR 0x9001

static dev_t demo_devno;              // 设备号(主+次)
static struct cdev demo_cdev;         // 字符设备对象
static struct class *demo_class;
static char demo_buf[DEMO_BUF_SIZE];
static int demo_len;                  // 当前缓冲字节数
static DEFINE_MUTEX(demo_lock);       // 保护缓冲与 open 计数
static int open_count;

static int demo_open(struct inode *inode, struct file *filp)
{
    mutex_lock(&demo_lock);
    open_count++;
    mutex_unlock(&demo_lock);
    return 0;                         // filp->f_pos 由 VFS 初始化为 0,每次 open 独立
}

static int demo_release(struct inode *inode, struct file *filp)
{
    mutex_lock(&demo_lock);
    open_count--;
    mutex_unlock(&demo_lock);
    return 0;
}

static ssize_t demo_read(struct file *filp, char __user *buf,
                         size_t count, loff_t *ppos)
{
    ssize_t n;
    mutex_lock(&demo_lock);
    if (*ppos >= demo_len) {          // 已到末尾:返回 0 即 EOF
        mutex_unlock(&demo_lock);
        return 0;
    }
    n = min(count, (size_t)(demo_len - *ppos));
    if (copy_to_user(buf, demo_buf + *ppos, n)) {
        mutex_unlock(&demo_lock);
        return -EFAULT;               // 用户指针非法
    }
    *ppos += n;
    mutex_unlock(&demo_lock);
    return n;
}

static ssize_t demo_write(struct file *filp, const char __user *buf,
                          size_t count, loff_t *ppos)
{
    ssize_t n;
    mutex_lock(&demo_lock);
    n = min(count, (size_t)(DEMO_BUF_SIZE - demo_len));
    if (n == 0) {
        mutex_unlock(&demo_lock);
        return 0;                     // 缓冲满:短写,返回实际写入数
    }
    if (copy_from_user(demo_buf + demo_len, buf, n)) {
        mutex_unlock(&demo_lock);
        return -EFAULT;
    }
    demo_len += n;
    mutex_unlock(&demo_lock);
    return n;                         // POSIX 语义:返回实际写入字节数
}

static long demo_ioctl(struct file *filp, unsigned int cmd,
                       unsigned long arg)
{
    switch (cmd) {
    case DEMO_IOC_CLEAR:
        mutex_lock(&demo_lock);
        demo_len = 0;
        filp->f_pos = 0;
        mutex_unlock(&demo_lock);
        return 0;
    default:
        return -ENOTTY;               // 未知命令的标准错误码
    }
}

static const struct file_operations demo_fops = {
    .owner          = THIS_MODULE,
    .open           = demo_open,
    .release        = demo_release,
    .read           = demo_read,
    .write          = demo_write,
    .unlocked_ioctl = demo_ioctl,
};

static int __init demo_init(void)
{
    int ret;
    ret = alloc_chrdev_region(&demo_devno, 0, 1, "demochar");
    if (ret)
        return ret;
    cdev_init(&demo_cdev, &demo_fops);
    demo_cdev.owner = THIS_MODULE;
    ret = cdev_add(&demo_cdev, demo_devno, 1);
    if (ret)
        goto err_unregister;
    demo_class = class_create("demochar");   // 6.4+ 单参数版本
    if (IS_ERR(demo_class)) {
        ret = PTR_ERR(demo_class);
        goto err_cdev;
    }
    device_create(demo_class, NULL, demo_devno, NULL, "demochar");
    pr_info("demochar: loaded, major=%d\n", MAJOR(demo_devno));
    return 0;
err_cdev:
    cdev_del(&demo_cdev);
err_unregister:
    unregister_chrdev_region(demo_devno, 1);
    return ret;
}

static void __exit demo_exit(void)
{
    device_destroy(demo_class, demo_devno);
    class_destroy(demo_class);
    cdev_del(&demo_cdev);
    unregister_chrdev_region(demo_devno, 1);
    pr_info("demochar: unloaded\n");
}

module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xiaoli");
MODULE_DESCRIPTION("Minimal char device demo with mutex and ioctl");

三、Makefile 与构建加载

# Makefile
obj-m += demochar.o

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

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

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

构建与验证的标准流程:

$ make                                   # 生成 demochar.ko
$ sudo insmod demochar.ko                # dmesg 打印: demochar: loaded, major=240
$ ls -l /dev/demochar                    # udev 已自动创建设备节点
$ echo -n 'hello, kernel!' > /dev/demochar
$ cat /dev/demochar                      # 输出 hello, kernel!
$ sudo rmmod demochar                    # dmesg 打印: demochar: unloaded

四、行为语义的真实验证输出

字符设备的行为语义(缓冲、偏移、短写、EOF、互斥、ioctl)可以在用户态精确复现。下面是与 demochar.ko 行为等价的模拟器在 Python 3 下的真实运行输出(模拟器约 90 行,用 threading.Lock 等价内核 mutex,10 线程并发压力验证无撕裂):

=== demochar 字符设备语义模拟 ===
write 14B -> 返回 14, 缓冲占用 14/16
缓冲剩 2B 时 write 8B -> 短写返回 2(内核 write 语义:返回实际写入数)
第二次 open: open_count=2, f2.pos 独立于 f1
f1 read 5B: b'hello'
f2 read 5B: b'hello' (与 f1 相同的开头:pos 各自独立)
f1 read 剩余: b', kernel!OV'
f1 再 read(已到末尾): b''  <-- EOF 返回空
10 线程并发各写 1B(mutex 保护): ['A','B','C','D','E','F','G','H','I','J']
ioctl CLEAR 后缓冲: b''  pos 复位为 0
open_count=2
全部 release 后 open_count=0

每条输出对应驱动源码中的一处设计决策:

五、理论分析

5.1 用户态与内核态的拷贝边界

copy_to_user/copy_from_user 不是普通 memcpy:它们验证用户指针合法性(access_ok)、处理缺页(可能睡眠)、并配合 SMAP/SMEP 等 CPU 特性做地址空间隔离检查。绝不能在持有自旋锁时调用(缺页睡眠会触发调度,原子上下文调度即死锁),这是 demo 里选择 mutex 而非 spinlock 的根本原因。高性能场景可用 get_user_pages + 直接映射减少拷贝,这正是 O_DIRECT 与零拷贝驱动的设计起点。

5.2 锁的选择谱系

驱动里常见的并发原语按场景分:mutex(可睡眠,进程上下文,本文场景)、spinlock(不可睡眠,中断上下文或极短临界区)、原子量/引用计数(kref、refcount_t,单变量增减)、RCU(读多写极少的指针表)。选错的代价是真实事故:该用 spinlock 的地方用 mutex 会在中断路径崩溃,反之则在多核下数据损坏。

5.3 阻塞 I/O 与 poll 的扩展方向

demo 的 read 在缓冲空时直接返回 EOF,真实驱动通常用 wait_queue 让读进程睡眠,等 write 方 wake_up 唤醒——生产-消费者模型;再进一步实现 .poll 回调即可接入 select/epoll 体系。这两个扩展都只改驱动内部,应用侧接口分文不动,体现了字符设备模型的封装价值。

六、工程应用

七、常见误区

八、参考文献

  1. J. Corbet, A. Rubini, G. Kroah-Hartman (2005). Linux Device Drivers (3rd ed.), Chapter 3: Char Drivers. O'Reilly.(LDD3,驱动入门圣经,开放电子版)
  2. Linux kernel documentation: Documentation/driver-api/ 目录(设备模型、cdev、用户态拷贝 API 的权威说明)。
  3. G. Kroah-Hartman (2003). Linux Kernel Development 相关章节与 kobject/kset 系列 LWN 文章。
  4. W. Mauerer 著,郭旭译. 专业 Linux 内核架构 第 6 章:设备驱动程序。
  5. kernel.org: Linux Kernel Module Programming Guide(持续更新的在线版本,覆盖最新内核 API 变更)。
← 返回文章列表