3. 字符设备驱动基础(一):主设备号与次设备号、设备文件、file_operations 结构体、简单的字符设备驱动实现

字符设备是嵌入式 Linux 驱动开发中最基础、最常用的设备类型。本章将深入剖析字符设备驱动的核心概念:主设备号与次设备号、设备文件以及file_operations 结构体,并通过一个完整的示例,带你实现第一个简单的字符设备驱动。

学习目标:理解设备号的分配与管理机制;掌握设备文件与内核驱动的关联方式;熟练填充 file_operations 结构体;能够独立编写并测试一个基础的字符设备驱动。

3.1 主设备号与次设备号

在 Linux 内核中,每个设备都由一个 设备号 唯一标识。设备号是一个 dev_t 类型的 32 位无符号整数,其中高 12 位表示主设备号(major number),低 20 位表示次设备号(minor number)。

  • 主设备号:用于标识设备对应的驱动程序。不同的设备驱动拥有不同的主设备号。内核通过主设备号找到正确的驱动。
  • 次设备号:由驱动程序内部使用,用于区分同一种驱动控制下的不同物理设备或逻辑单元。例如,一个串口驱动可以管理多个串口,每个串口使用相同的驱动(主设备号相同),但次设备号不同。
设备号结构说明
组成部分 位数 范围 说明
主设备号 12 位 0 ~ 4095 标识驱动程序
次设备号 20 位 0 ~ 1048575 标识具体设备实例
内核宏操作:使用 MAJOR(dev_t dev) 从设备号中提取主设备号,使用 MINOR(dev_t dev) 提取次设备号,使用 MKDEV(int major, int minor) 将主次设备号组合成 dev_t 类型。
设备号结构示意图 设备号 (dev_t) 结构 主设备号 (12 bits) 标识驱动程序 次设备号 (20 bits) 标识具体设备 MAJOR(dev) → 主设备号;MINOR(dev) → 次设备号

3.2 设备文件

在 Linux 用户空间,设备通过 设备文件(又称设备节点)来访问。设备文件位于 /dev 目录下,是用户程序与内核驱动交互的桥梁。用户程序通过标准的文件操作(如 open、read、write)对设备文件进行操作,内核将这些操作转发给对应的驱动程序。

设备文件有两种类型:

  • 字符设备文件:以 c 开头,对应字符设备驱动,数据以字节流形式访问。
  • 块设备文件:以 b 开头,对应块设备驱动,数据以块为单位访问。

创建设备文件可以使用 mknod 命令:

mknod /dev/mydevice c 240 0

其中 c 表示字符设备,240 是主设备号,0 是次设备号。

注意事项:手动使用 mknod 创建设备文件需要 root 权限。在实际产品中,通常使用 udev 或 devtmpfs 自动管理设备节点,驱动加载时自动在 /dev 下生成对应的设备文件。
设备文件交互示意图 用户空间与内核空间通过设备文件交互 用户空间 应用程序 /dev/mydevice 设备文件(节点) 内核空间 字符设备驱动 用户程序通过文件操作接口访问设备文件,内核将请求路由到驱动

3.3 file_operations 结构体

file_operations 结构体是字符设备驱动的核心,它定义了驱动能够响应的所有文件操作函数指针。当用户程序对设备文件执行系统调用时,内核通过该结构体找到对应的驱动函数并执行。

常用的成员函数包括:

成员函数 系统调用 功能描述
open open() 打开设备,进行初始化操作
release close() 关闭设备,释放资源
read read() 从设备读取数据
write write() 向设备写入数据
unlocked_ioctl ioctl() 设备控制命令(无大内核锁)
llseek lseek() 修改文件读写位置
关键点:驱动开发者不需要实现所有函数,未实现的函数指针应设置为 NULL,内核会使用默认行为。例如,llseek 为 NULL 时,默认不允许定位。
file_operations 结构体与系统调用映射 file_operations 结构体与系统调用映射 系统调用 open() read() write() close() ioctl() lseek() file_operations .open = my_open .read = my_read .write = my_write .release = my_release .unlocked_ioctl = my_ioctl .llseek = my_llseek 驱动实现 my_open() my_read() my_write() my_release() my_ioctl() my_llseek()

3.4 简单的字符设备驱动实现

下面我们通过一个完整的示例,实现一个简单的字符设备驱动。该驱动在内存中维护一个缓冲区,支持 open、read、write 和 release 操作。

3.4.1 驱动代码框架

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

#define DEVICE_NAME "my_char_dev"
#define BUFFER_SIZE 1024

static int major;
static char *kernel_buffer;

/* 打开设备 */
static int my_open(struct inode *inode, struct file *file) {
    printk(KERN_INFO "Device opened\n");
    return 0;
}

/* 释放设备 */
static int my_release(struct inode *inode, struct file *file) {
    printk(KERN_INFO "Device closed\n");
    return 0;
}

/* 从设备读取数据 */
static ssize_t my_read(struct file *file, char __user *user_buf,
                       size_t count, loff_t *pos) {
    size_t available = BUFFER_SIZE - *pos;
    size_t to_read = count < available ? count : available;

    if (to_read == 0)
        return 0;

    if (copy_to_user(user_buf, kernel_buffer + *pos, to_read)) {
        return -EFAULT;
    }

    *pos += to_read;
    return to_read;
}

/* 向设备写入数据 */
static ssize_t my_write(struct file *file, const char __user *user_buf,
                        size_t count, loff_t *pos) {
    size_t available = BUFFER_SIZE - *pos;
    size_t to_write = count < available ? count : available;

    if (to_write == 0)
        return -ENOSPC;

    if (copy_from_user(kernel_buffer + *pos, user_buf, to_write)) {
        return -EFAULT;
    }

    *pos += to_write;
    return to_write;
}

/* file_operations 结构体定义 */
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) {
    major = register_chrdev(0, DEVICE_NAME, &my_fops);
    if (major < 0) {
        printk(KERN_ALERT "Failed to register device\n");
        return major;
    }

    kernel_buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);
    if (!kernel_buffer) {
        unregister_chrdev(major, DEVICE_NAME);
        return -ENOMEM;
    }
    memset(kernel_buffer, 0, BUFFER_SIZE);

    printk(KERN_INFO "Module loaded, major number: %d\n", major);
    return 0;
}

/* 模块退出 */
static void __exit my_exit(void) {
    kfree(kernel_buffer);
    unregister_chrdev(major, DEVICE_NAME);
    printk(KERN_INFO "Module unloaded\n");
}

module_init(my_init);
module_exit(my_exit);

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

3.4.2 驱动加载与测试步骤

  1. 编译驱动:使用内核源码树中的 Makefile 编译生成 .ko 文件。
  2. 加载模块:sudo insmod my_char_dev.ko,查看内核日志确认主设备号。
  3. 创建设备文件:sudo mknod /dev/my_char_dev c <major> 0。
  4. 测试读写:
    • 写入:echo "Hello, Driver!" > /dev/my_char_dev
    • 读取:cat /dev/my_char_dev
  5. 卸载模块:sudo rmmod my_char_dev,并删除设备文件。
调试技巧:使用 dmesg 查看内核打印信息,确认驱动是否正常加载以及 open/read/write 操作是否被调用。
字符设备驱动加载与测试流程 字符设备驱动加载与测试流程 编译驱动 make 加载模块 insmod 创建设备文件 mknod 测试读写 echo / cat 卸载模块 rmmod 使用 dmesg 查看内核日志,确认驱动加载和操作是否正常 测试完成后记得卸载模块并删除设备文件

3.5 本章小结

  • 主设备号标识驱动,次设备号标识设备实例。
  • 设备文件是用户空间访问内核驱动的入口,位于 /dev 目录。
  • file_operations 结构体定义了驱动支持的所有文件操作,是驱动与 VFS 的接口。
  • 通过 register_chrdev 注册驱动,通过 mknod 创建设备文件,即可完成一个基础字符设备驱动的加载与测试。