2. 字符设备驱动框架

字符设备是Linux驱动模型中最基础、最常用的设备类型,其核心在于通过VFS(虚拟文件系统)将内核中的设备操作与用户空间的open()、read()、write()等系统调用桥接起来。本章深入剖析字符设备驱动的四大支柱:cdev结构体、设备号管理、file_operations操作集以及设备节点的创建机制。

核心要点:字符设备驱动本质上是向VFS注册一组操作函数,并分配唯一的设备标识(设备号),最终通过设备节点暴露给用户空间。

2.1 cdev结构体:内核中的设备抽象

cdev(character device)是内核用于描述字符设备的核心数据结构,定义在<linux/cdev.h>中。每个字符设备在内核中对应一个cdev实例,它连接了设备号与具体的操作函数集。

成员 类型 说明
kobj struct kobject 内核对象基类,用于设备生命周期管理
owner struct module * 指向所属模块,通常设为THIS_MODULE
ops const struct file_operations * 指向设备操作函数集
list struct list_head 用于将cdev链接到系统链表
dev dev_t 设备号(主设备号+次设备号)
count unsigned int 关联的次设备号数量

cdev的初始化与注册流程:

  1. 使用cdev_alloc()或静态定义struct cdev变量
  2. 调用cdev_init()关联file_operations结构体
  3. 设置cdev.owner = THIS_MODULE
  4. 调用cdev_add()将设备注册到内核
// cdev 初始化与注册示例
struct cdev my_cdev;
dev_t dev_num;

// 1. 初始化cdev,关联操作函数集
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;

// 2. 注册cdev到内核
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
    printk(KERN_ERR "Failed to add cdev\n");
    return ret;
}
最佳实践:建议在cdev_add()成功后再创建设备节点,避免用户空间在设备未完全就绪时访问。
cdev 结构体与内核核心组件关系 struct cdev 设备抽象核心 file_operations open/read/write/ioctl dev_t 设备号 主设备号 + 次设备号 VFS 虚拟文件系统 系统调用分发层 cdev 通过 ops 指针关联 file_operations,通过 dev 字段绑定设备号,最终由 VFS 统一调度

2.2 设备号注册与注销

设备号是内核识别设备的唯一标识,由主设备号(major number)和次设备号(minor number)组成,类型为dev_t(32位无符号整数,高12位为主设备号,低20位为次设备号)。

2.2.1 设备号分配策略

分配方式 函数 适用场景 优点
静态分配 register_chrdev_region() 已知主设备号(如实验/教学) 设备号固定,易于调试
动态分配 alloc_chrdev_region() 生产环境/未知主设备号 避免设备号冲突,推荐使用
// 动态分配设备号(推荐)
dev_t dev_num;
int major;
int ret;

ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0) {
    printk(KERN_ERR "Failed to allocate device number\n");
    return ret;
}
major = MAJOR(dev_num);
printk("Allocated major number: %d\n", major);

// 静态分配设备号
ret = register_chrdev_region(MKDEV(240, 0), 1, "my_device");
if (ret < 0) {
    printk(KERN_ERR "Failed to register device number\n");
    return ret;
}

2.2.2 设备号注销

在模块卸载时,必须释放已分配的设备号,否则后续模块无法复用该设备号。

// 设备号注销
unregister_chrdev_region(dev_num, 1);
常见错误:忘记注销设备号会导致insmod新模块时返回-EBUSY错误。务必在模块退出路径中调用unregister_chrdev_region()。
设备号分配与注销生命周期 模块加载 已知主设备号? register_chrdev_region 静态分配 alloc_chrdev_region 动态分配 cdev_add 注册设备 模块卸载时:cdev_del → unregister_chrdev_region

2.3 file_operations结构体:驱动与VFS的契约

file_operations是驱动开发者与VFS之间的核心契约。该结构体定义了所有用户空间可以通过文件描述符调用的操作函数指针。当用户执行open()、read()等系统调用时,VFS根据inode找到对应的cdev,进而调用file_operations中注册的函数。

2.3.1 关键成员函数

函数指针 系统调用对应 典型实现职责
open open() 初始化设备、分配私有数据、递增使用计数
release close() 释放资源、递减使用计数
read read() 从设备拷贝数据到用户空间
write write() 从用户空间拷贝数据到设备
unlocked_ioctl ioctl() 设备控制命令处理(无大内核锁)
llseek lseek() 修改文件读写位置
// file_operations 结构体定义示例
struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
    .unlocked_ioctl = my_ioctl,
    .llseek  = my_llseek,
};
关键设计原则:所有在file_operations中注册的函数都在进程上下文中执行,可以睡眠。中断处理函数中不能调用的操作(如copy_to_user)在这里是安全的。

2.3.2 核心数据传递:copy_to_user / copy_from_user

由于内核空间与用户空间地址隔离,驱动不能直接解引用用户空间指针。必须使用内核提供的安全拷贝函数:

// read 实现示例
ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    char kernel_buf[128];
    size_t bytes_to_copy;

    // 准备内核数据
    snprintf(kernel_buf, sizeof(kernel_buf), "Device data at offset %lld\n", *f_pos);
    bytes_to_copy = min(count, strlen(kernel_buf));

    // 安全拷贝到用户空间
    if (copy_to_user(buf, kernel_buf, bytes_to_copy))
        return -EFAULT;

    *f_pos += bytes_to_copy;
    return bytes_to_copy;
}

// write 实现示例
ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
    char kernel_buf[128];
    size_t bytes_to_copy = min(count, sizeof(kernel_buf) - 1);

    // 从用户空间安全拷贝
    if (copy_from_user(kernel_buf, buf, bytes_to_copy))
        return -EFAULT;

    kernel_buf[bytes_to_copy] = '\0';
    printk("Received from user: %s\n", kernel_buf);

    return bytes_to_copy;
}
file_operations 调用链路:用户空间 → VFS → 驱动 用户空间 read(fd, buf, count) sys_read 系统调用 VFS 通用处理层 file_operations.read 驱动 my_read 函数 硬件设备 寄存器/内存操作

2.4 设备节点创建

设备节点是用户空间访问设备的文件接口,位于/dev目录下。创建设备节点有两种方式:手动创建(mknod)和自动创建(udev/mdev)。现代Linux系统强烈推荐使用自动创建机制。

2.4.1 手动创建(调试用)

# 手动创建设备节点
# mknod /dev/my_device c major minor
# 示例:主设备号240,次设备号0
mknod /dev/my_device c 240 0
chmod 666 /dev/my_device

2.4.2 自动创建(生产环境)

内核通过device_create()函数在sysfs中创建设备信息,用户空间的udev(或嵌入式中的mdev)监听到事件后自动在/dev下生成节点。

// 自动创建设备节点所需步骤
// 1. 创建类(通常在模块初始化时)
static struct class *my_class;
my_class = class_create(THIS_MODULE, "my_device_class");
if (IS_ERR(my_class)) {
    return PTR_ERR(my_class);
}

// 2. 在cdev_add成功后创建设备
struct device *dev;
dev = device_create(my_class, NULL, dev_num, NULL, "my_device");
if (IS_ERR(dev)) {
    class_destroy(my_class);
    return PTR_ERR(dev);
}

// 3. 模块卸载时销毁
device_destroy(my_class, dev_num);
class_destroy(my_class);
自动创建的优势:设备节点生命周期与设备绑定,设备移除时节点自动删除;无需手动管理主次设备号;支持设备热插拔。

2.4.3 完整驱动框架模板

// 字符设备驱动完整框架
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "my_device"
#define CLASS_NAME  "my_class"

static int major;
static struct class *my_class;
static struct cdev my_cdev;
static dev_t dev_num;

// 操作函数实现
static int my_open(struct inode *inode, struct file *filp) {
    printk("Device opened\n");
    return 0;
}

static int my_release(struct inode *inode, struct file *filp) {
    printk("Device closed\n");
    return 0;
}

static ssize_t my_read(struct file *filp, char __user *buf,
                       size_t count, loff_t *f_pos) {
    char data[] = "Hello from kernel!\n";
    size_t len = strlen(data);
    if (*f_pos >= len) return 0;
    if (count > len - *f_pos) count = len - *f_pos;
    if (copy_to_user(buf, data + *f_pos, count)) return -EFAULT;
    *f_pos += count;
    return count;
}

static ssize_t my_write(struct file *filp, const char __user *buf,
                        size_t count, loff_t *f_pos) {
    char kbuf[256];
    size_t size = min(count, sizeof(kbuf) - 1);
    if (copy_from_user(kbuf, buf, size)) return -EFAULT;
    kbuf[size] = '\0';
    printk("Received: %s\n", kbuf);
    return size;
}

static struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
};

static int __init my_init(void) {
    int ret;

    // 1. 分配设备号
    ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    if (ret < 0) return ret;
    major = MAJOR(dev_num);

    // 2. 初始化并注册cdev
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;
    ret = cdev_add(&my_cdev, dev_num, 1);
    if (ret < 0) goto err_cdev;

    // 3. 创建类
    my_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(my_class)) {
        ret = PTR_ERR(my_class);
        goto err_class;
    }

    // 4. 创建设备节点
    if (IS_ERR(device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME))) {
        ret = PTR_ERR(device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME));
        goto err_device;
    }

    printk("Module loaded, major=%d\n", major);
    return 0;

err_device:
    class_destroy(my_class);
err_class:
    cdev_del(&my_cdev);
err_cdev:
    unregister_chrdev_region(dev_num, 1);
    return ret;
}

static void __exit my_exit(void) {
    device_destroy(my_class, dev_num);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);
    printk("Module unloaded\n");
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Expert");
MODULE_DESCRIPTION("Character device driver framework example");
字符设备驱动完整框架:初始化与退出流程 模块初始化 (module_init) ① alloc_chrdev_region 分配设备号 ② cdev_init + cdev_add 注册设备 ③ class_create 创建设备类 ④ device_create 自动创建设备节点 模块卸载 (module_exit) ① device_destroy 销毁设备节点 ② class_destroy 销毁设备类 ③ cdev_del 注销字符设备 ④ unregister_chrdev_region 释放设备号 ← 上一章 📖 返回目录 下一章 →