2. 字符设备驱动基础:file_operations结构体、主次设备号、设备节点创建

字符设备驱动是Linux驱动开发中最基础、最常用的驱动类型。本章将系统讲解字符设备驱动的核心概念:file_operations结构体、主次设备号以及设备节点的创建方法,并通过一个完整的“Hello驱动”示例串联所有知识点。

学习目标:掌握字符设备驱动的注册流程,理解用户空间与内核空间的交互桥梁,能够独立编写并加载一个简单的字符设备驱动。

2.1 字符设备驱动架构概览

字符设备(Character Device)以字节流的形式进行数据传输,典型的字符设备包括串口、键盘、GPIO等。用户空间程序通过文件操作接口(open、read、write、ioctl等)与字符设备交互,内核中的字符设备驱动则负责实现这些操作的具体逻辑。

字符设备驱动架构 用户空间 (User Space) 应用程序 (App) → open() / read() / write() / ioctl() 系统调用层 (VFS) 内核空间 (Kernel Space) 字符设备驱动 → file_operations 回调函数 硬件设备 (GPIO / UART / ...) 设备节点 (/dev/xxx)

2.2 file_operations 结构体

file_operations 是字符设备驱动的核心数据结构,定义在 <linux/fs.h> 中。它包含一系列函数指针,每个指针对应一个系统调用在内核中的实现。驱动开发者通过填充该结构体,将用户空间的文件操作与内核中的具体实现绑定。

2.2.1 关键成员

成员名称 函数原型 说明
owner struct module *owner 通常设置为 THIS_MODULE,防止模块被卸载时仍有打开的文件
open int (*open)(struct inode *, struct file *) 设备打开时调用,用于初始化硬件或分配资源
release int (*release)(struct inode *, struct file *) 设备关闭时调用,用于释放资源
read ssize_t (*read)(struct file *, char __user *, size_t, loff_t *) 从设备读取数据,数据通过 copy_to_user 传递给用户空间
write ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) 向设备写入数据,数据通过 copy_from_user 从用户空间获取
unlocked_ioctl long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) 设备控制命令处理,用于非阻塞I/O控制(现代内核推荐)
llseek loff_t (*llseek)(struct file *, loff_t, int) 修改文件读写位置
关键提示:在Linux 2.6.35之后的内核中,ioctl 已被 unlocked_ioctl 取代。驱动中应使用 unlocked_ioctl,并注意加锁保护共享数据。

2.2.2 file_operations 结构体定义示例

// hello_driver.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "hello_device"
#define BUF_SIZE 256

static char device_buffer[BUF_SIZE] = {0};
static int major_num;

// open 回调
static int hello_open(struct inode *inodep, struct file *filep) {
    pr_info("Hello device opened\n");
    return 0;
}

// release 回调
static int hello_release(struct inode *inodep, struct file *filep) {
    pr_info("Hello device closed\n");
    return 0;
}

// read 回调
static ssize_t hello_read(struct file *filep, char __user *buf,
                          size_t len, loff_t *offset) {
    size_t available = strlen(device_buffer);
    size_t to_copy = min(len, available);

    if (*offset >= available)
        return 0;

    if (copy_to_user(buf, device_buffer + *offset, to_copy))
        return -EFAULT;

    *offset += to_copy;
    return to_copy;
}

// write 回调
static ssize_t hello_write(struct file *filep, const char __user *buf,
                           size_t len, loff_t *offset) {
    size_t to_copy = min(len, (size_t)(BUF_SIZE - 1));

    if (copy_from_user(device_buffer, buf, to_copy))
        return -EFAULT;

    device_buffer[to_copy] = '\0';
    pr_info("Received %zu bytes from user: %s\n", to_copy, device_buffer);
    return to_copy;
}

// 填充 file_operations 结构体
static struct file_operations hello_fops = {
    .owner   = THIS_MODULE,
    .open    = hello_open,
    .release = hello_release,
    .read    = hello_read,
    .write   = hello_write,
};

2.3 主次设备号

设备号是内核标识设备的唯一编号,由两部分组成:主设备号(Major Number)和次设备号(Minor Number)。主设备号标识设备对应的驱动程序,次设备号标识由同一驱动管理的具体设备实例。

设备号结构 (dev_t) 主设备号 (Major) 12 bit (0~4095) 次设备号 (Minor) 20 bit (0~1048575) dev_t = (major << 20) | minor MAJOR(dev_t) / MINOR(dev_t) 宏提取

2.3.1 设备号分配方式

方式 函数 适用场景 优点 缺点
静态分配 register_chrdev_region() 已知主设备号(如实验、固定设备) 设备号固定,易于管理 可能与其他驱动冲突
动态分配 alloc_chrdev_region() 大多数实际项目 自动分配未使用的主设备号,避免冲突 设备号不固定,需通过 /proc/devices 查看
避坑指南:不要使用 register_chrdev() 旧接口(无参数版本),该接口已过时且会占用整个主设备号下的所有次设备号范围(0~255),导致资源浪费。推荐使用 alloc_chrdev_region() + cdev_init() + cdev_add() 组合。

2.4 设备节点创建

设备节点是用户空间访问设备的入口,位于 /dev 目录下。设备节点的创建有两种方式:手动创建和自动创建。

2.4.1 手动创建设备节点

使用 mknod 命令手动创建设备节点:

# 查看已分配的设备号
cat /proc/devices | grep hello

# 手动创建设备节点(需要root权限)
sudo mknod /dev/hello_device c 240 0
# 参数说明:c 表示字符设备,240 是主设备号,0 是次设备号

# 设置权限
sudo chmod 666 /dev/hello_device

2.4.2 自动创建设备节点

现代Linux系统使用 udev 或 devtmpfs 自动管理设备节点。驱动中通过 class_create 和 device_create 接口,在加载时自动创建节点,卸载时自动删除。

// 在模块初始化函数中添加自动创建设备节点代码
static struct class *hello_class;
static struct device *hello_device;

static int __init hello_init(void) {
    // 1. 动态分配设备号
    alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    major_num = MAJOR(dev_num);
    pr_info("Allocated major number: %d\n", major_num);

    // 2. 初始化并添加cdev
    cdev_init(&hello_cdev, &hello_fops);
    cdev_add(&hello_cdev, dev_num, 1);

    // 3. 创建类
    hello_class = class_create(THIS_MODULE, DEVICE_NAME);
    if (IS_ERR(hello_class)) {
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(hello_class);
    }

    // 4. 创建设备节点(自动出现在 /dev/hello_device)
    hello_device = device_create(hello_class, NULL, dev_num,
                                 NULL, DEVICE_NAME);
    if (IS_ERR(hello_device)) {
        class_destroy(hello_class);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(hello_device);
    }

    pr_info("Hello driver loaded successfully\n");
    return 0;
}

static void __exit hello_exit(void) {
    // 反向销毁:设备 -> 类 -> cdev -> 设备号
    device_destroy(hello_class, dev_num);
    class_destroy(hello_class);
    cdev_del(&hello_cdev);
    unregister_chrdev_region(dev_num, 1);
    pr_info("Hello driver unloaded\n");
}

module_init(hello_init);
module_exit(hello_exit);
自动创建设备节点流程 alloc_chrdev_region 分配设备号 cdev_init + cdev_add 注册字符设备 class_create 创建设备类 device_create 创建设备节点 结果:/dev/hello_device 自动创建 udev 或 devtmpfs 监听内核事件自动生成节点 卸载顺序(反向):device_destroy → class_destroy → cdev_del → unregister_chrdev_region 设备节点自动删除,设备号释放

2.5 完整的 Hello 驱动示例

下面是一个完整的字符设备驱动示例,整合了上述所有知识点:动态分配设备号、填充 file_operations、自动创建设备节点。

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

#define DEVICE_NAME "hello_device"
#define CLASS_NAME  "hello_class"
#define BUF_SIZE    256

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver example");

static int major_num;
static struct cdev hello_cdev;
static struct class *hello_class;
static struct device *hello_device;
static dev_t dev_num;
static char device_buffer[BUF_SIZE] = "Hello from kernel!\n";

static int hello_open(struct inode *inodep, struct file *filep) {
    pr_info("Hello device opened\n");
    return 0;
}

static int hello_release(struct inode *inodep, struct file *filep) {
    pr_info("Hello device closed\n");
    return 0;
}

static ssize_t hello_read(struct file *filep, char __user *buf,
                          size_t len, loff_t *offset) {
    size_t available = strlen(device_buffer);
    size_t to_copy = min(len, available);

    if (*offset >= available)
        return 0;

    if (copy_to_user(buf, device_buffer + *offset, to_copy))
        return -EFAULT;

    *offset += to_copy;
    return to_copy;
}

static ssize_t hello_write(struct file *filep, const char __user *buf,
                           size_t len, loff_t *offset) {
    size_t to_copy = min(len, (size_t)(BUF_SIZE - 1));

    if (copy_from_user(device_buffer, buf, to_copy))
        return -EFAULT;

    device_buffer[to_copy] = '\0';
    pr_info("Received %zu bytes: %s\n", to_copy, device_buffer);
    return to_copy;
}

static struct file_operations hello_fops = {
    .owner   = THIS_MODULE,
    .open    = hello_open,
    .release = hello_release,
    .read    = hello_read,
    .write   = hello_write,
};

static int __init hello_init(void) {
    // 1. 动态分配设备号
    alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    major_num = MAJOR(dev_num);
    pr_info("Allocated major number: %d\n", major_num);

    // 2. 初始化并添加cdev
    cdev_init(&hello_cdev, &hello_fops);
    hello_cdev.owner = THIS_MODULE;
    if (cdev_add(&hello_cdev, dev_num, 1)) {
        pr_err("Failed to add cdev\n");
        unregister_chrdev_region(dev_num, 1);
        return -1;
    }

    // 3. 创建类
    hello_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(hello_class)) {
        pr_err("Failed to create class\n");
        cdev_del(&hello_cdev);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(hello_class);
    }

    // 4. 创建设备节点
    hello_device = device_create(hello_class, NULL, dev_num,
                                 NULL, DEVICE_NAME);
    if (IS_ERR(hello_device)) {
        pr_err("Failed to create device\n");
        class_destroy(hello_class);
        cdev_del(&hello_cdev);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(hello_device);
    }

    pr_info("Hello driver loaded. Device /dev/%s created.\n", DEVICE_NAME);
    return 0;
}

static void __exit hello_exit(void) {
    device_destroy(hello_class, dev_num);
    class_destroy(hello_class);
    cdev_del(&hello_cdev);
    unregister_chrdev_region(dev_num, 1);
    pr_info("Hello driver unloaded\n");
}

module_init(hello_init);
module_exit(hello_exit);

2.5.1 编译与测试

使用 Makefile 编译驱动,然后加载并测试:

# Makefile
obj-m += hello_driver_complete.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
# 编译
make

# 加载驱动
sudo insmod hello_driver_complete.ko

# 查看设备号
cat /proc/devices | grep hello

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

# 测试读操作
cat /dev/hello_device

# 测试写操作
echo "Hello from user!" > /dev/hello_device

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

# 卸载驱动
sudo rmmod hello_driver_complete
测试结果预期:执行 cat /dev/hello_device 应输出 "Hello from kernel!";执行 echo "Hello from user!" > /dev/hello_device 后,通过 dmesg 可看到内核打印的接收信息。

2.6 本章小结

  • file_operations 是字符设备驱动的核心,定义了用户空间文件操作与内核回调函数的映射关系。
  • 主设备号标识驱动,次设备号标识设备实例,推荐使用动态分配方式避免冲突。
  • 设备节点是用户空间访问设备的入口,手动创建使用 mknod,自动创建依赖 class_create + device_create。
  • 完整的字符设备驱动流程:分配设备号 → 初始化cdev → 添加cdev → 创建类 → 创建设备节点。
  • 卸载时需按反向顺序销毁资源,避免内存泄漏或设备节点残留。
课后练习:修改上述Hello驱动,增加 unlocked_ioctl 回调,实现一个简单的命令:IOCTL_CLEAR_BUFFER 清空设备缓冲区。提示:使用 _IO 宏定义命令号。