4. 字符设备驱动核心概念:主设备号与次设备号、设备文件、file_operations 结构体

字符设备驱动是 Linux 驱动开发中最基础、最典型的驱动类型。理解其核心概念——设备号、设备文件与 file_operations 结构体——是编写任何字符设备驱动的第一步。这三者共同构成了用户空间与内核空间交互的桥梁。

4.1 主设备号与次设备号:设备的“身份证”

Linux 内核使用设备号来唯一标识一个设备。设备号是一个 dev_t 类型的 32 位无符号整数,其中高 12 位表示主设备号,低 20 位表示次设备号。

  • 主设备号 (Major Number):标识设备对应的驱动程序。内核通过主设备号找到正确的驱动。
  • 次设备号 (Minor Number):由驱动程序内部使用,用于区分同一种驱动控制下的不同物理设备或逻辑单元。
类比理解: 主设备号好比“公司总机号码”,次设备号好比“分机号码”。拨打总机(主设备号)找到公司,再拨分机(次设备号)找到具体的人(设备实例)。

4.1.1 设备号分配方式

分配方式 函数/宏 适用场景 特点
静态分配 register_chrdev_region() 已知主设备号(如实验、特定硬件) 需手动指定,可能冲突
动态分配 alloc_chrdev_region() 通用驱动、开源项目 内核自动分配,推荐使用

动态分配是现代驱动开发的首选方式,它避免了主设备号冲突的问题。以下是动态分配设备号的典型代码片段:

#include <linux/fs.h>
#include <linux/kdev_t.h>

dev_t dev_num;
int major, minor;
int ret;

/* 动态分配一个设备号,名称用于 /proc/devices 显示 */
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_char_device");
if (ret < 0) {
    printk(KERN_ERR "Failed to allocate device number\n");
    return ret;
}

major = MAJOR(dev_num);  /* 提取主设备号 */
minor = MINOR(dev_num);  /* 提取次设备号 */
printk(KERN_INFO "Allocated major=%d, minor=%d\n", major, minor);

4.1.2 设备号管理工具

用户空间可以通过以下命令查看系统中已分配的设备号:

  • cat /proc/devices:显示已注册的主设备号与驱动名称。
  • ls -l /dev/:查看设备文件的设备号(第5、6列)。
关键点: 设备号是内核资源,驱动卸载时必须使用 unregister_chrdev_region(dev_num, count) 释放,否则会导致资源泄漏。
设备号 (dev_t) 结构 32 位 dev_t 主设备号 (12位) 次设备号 (20位) 标识驱动程序 标识具体设备实例 MAJOR() / MINOR() 宏提取

4.2 设备文件:用户空间的“操作手柄”

在 Linux 中,一切皆文件。设备文件(又称设备节点)是用户空间程序访问内核驱动的入口。它位于 /dev 目录下,通过 mknod 命令或 udev 设备管理器自动创建。

4.2.1 设备文件类型

类型 文件类型标识 说明
字符设备文件 c 以字节流方式访问,无缓冲区,如串口、键盘
块设备文件 b 以块为单位访问,有缓冲区,如硬盘、U盘

使用 ls -l /dev/ 查看设备文件时,输出格式如下:

crw-rw---- 1 root root 10, 235 Mar 15 10:30 /dev/my_device
^          ^     ^    ^    ^     ^
|          |     |    |    |     +-- 文件名
|          |     |    |    +-- 次设备号 (235)
|          |     |    +-- 主设备号 (10)
|          |     +-- 所属组
|          +-- 所有者
+-- 文件类型 'c' 表示字符设备

4.2.2 创建设备文件的方式

  • 手动创建: mknod /dev/my_device c 240 0(主设备号240,次设备号0)。
  • 自动创建: 使用 udev 或 devtmpfs,驱动注册后自动在 /dev 下生成节点。
注意: 手动创建时,主设备号必须与驱动注册的设备号一致,否则操作设备文件时会返回“No such device”错误。
用户空间通过设备文件访问内核驱动 用户空间 (User Space) 应用程序 (App) — open/read/write/close 系统调用层 (VFS) /dev/my_device 内核空间 (Kernel Space) — 字符设备驱动

4.3 file_operations 结构体:驱动的“功能菜单”

file_operations 是字符设备驱动的核心数据结构。它定义了一个函数指针集合,将用户空间的系统调用(如 open、read、write)映射到驱动中具体的实现函数。

4.3.1 核心成员函数

函数指针 对应系统调用 功能描述
open open() 打开设备,进行初始化
release close() 关闭设备,释放资源
read read() 从设备读取数据
write write() 向设备写入数据
llseek lseek() 修改文件读写位置
unlocked_ioctl ioctl() 设备控制命令(无大内核锁)

4.3.2 典型定义与初始化

以下是一个典型的 file_operations 结构体定义:

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

/* 驱动函数声明 */
static int my_open(struct inode *inode, struct file *filp);
static int my_release(struct inode *inode, struct file *filp);
static ssize_t my_read(struct file *filp, char __user *buf, size_t len, loff_t *off);
static ssize_t my_write(struct file *filp, const char __user *buf, size_t len, loff_t *off);
static loff_t my_llseek(struct file *filp, loff_t offset, int whence);
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);

/* file_operations 结构体实例化 */
static struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
    .llseek  = my_llseek,
    .unlocked_ioctl = my_ioctl,
};
关键点: .owner = THIS_MODULE 是必需的,它防止模块在使用中被卸载。如果某个函数指针不需要实现,可以设置为 NULL,内核将使用默认行为。

4.3.3 函数实现示例:open 与 read

static int my_open(struct inode *inode, struct file *filp)
{
    /* 从 inode 中获取次设备号,用于区分不同设备实例 */
    unsigned int minor = iminor(inode);
    printk(KERN_INFO "Device opened, minor=%d\n", minor);

    /* 将私有数据关联到 file 结构体 */
    filp->private_data = &my_device_data[minor];
    return 0;
}

static ssize_t my_read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
    struct my_device *dev = filp->private_data;
    size_t available = dev->data_len - *off;
    size_t to_read = min(len, available);

    if (to_read == 0)
        return 0;  /* EOF */

    /* 将内核数据拷贝到用户空间 */
    if (copy_to_user(buf, dev->buffer + *off, to_read))
        return -EFAULT;

    *off += to_read;
    return to_read;
}
安全提示: 在 read 和 write 中,必须使用 copy_to_user() 和 copy_from_user() 进行数据拷贝,不能直接使用 memcpy。直接访问用户空间指针会导致内核崩溃或安全漏洞。
file_operations 结构体:系统调用到驱动函数的映射 用户调用 open() 用户调用 read() 用户调用 write() 用户调用 ioctl() file_operations .open = my_open .read = my_read .write = my_write .unlocked_ioctl = my_ioctl ... 其他成员 my_open() 实现 my_read() 实现 my_write() 实现 my_ioctl() 实现

4.4 三者的协作流程

当用户空间程序操作一个字符设备时,内核按照以下流程完成调用:

  1. 打开设备: 用户调用 open("/dev/my_device", O_RDWR)。
  2. VFS 查找: 内核虚拟文件系统 (VFS) 根据设备文件中的主设备号,找到对应的驱动。
  3. 调用 open: 内核调用驱动中 file_operations 的 open 函数。
  4. 数据操作: 用户调用 read()/write(),VFS 再次通过 file_operations 映射到驱动函数。
  5. 关闭设备: 用户调用 close(),内核调用 release 函数释放资源。
总结: 主设备号定位驱动,次设备号定位设备实例,设备文件提供用户空间入口,file_operations 提供操作函数映射。掌握这四者的关系,就掌握了字符设备驱动的核心骨架。