2. 字符设备驱动框架:file_operations结构体、主次设备号、cdev结构体、设备节点的创建
字符设备是Linux驱动开发中最基础、最常用的设备类型,其核心框架围绕file_operations结构体、设备号、cdev结构体以及设备节点四个要素展开。面试中,面试官通常要求你从零搭建一个完整的字符设备驱动骨架,并解释每个要素的作用与关联。
面试高频提示: 字符设备驱动的“四步法”是必考内容——分配设备号、初始化cdev、注册cdev、创建设备节点。任何一步缺失或顺序错误都会导致驱动无法正常工作。
2.1 file_operations结构体:驱动与VFS的契约
file_operations 是字符设备驱动的核心数据结构,它定义了用户空间通过系统调用(open、read、write、ioctl等)访问设备时,内核VFS层调用的驱动回调函数集合。每个字符设备驱动必须实现该结构体中的相关成员。
| 成员 | 函数原型 | 说明 |
|---|---|---|
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) |
设备控制命令处理(现代内核使用,替代ioctl) |
llseek |
loff_t (*llseek)(struct file *, loff_t, int) |
修改文件读写位置 |
// 典型的file_operations定义示例
static 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,
};
避坑指南: 面试中常问“为什么
read/write回调中不能直接使用用户空间指针?”——因为用户空间指针可能无效或导致内核崩溃,必须通过copy_to_user/copy_from_user进行安全拷贝,并检查返回值。
2.2 主次设备号:设备的身份证
设备号是内核标识字符设备的唯一编号,由主设备号和次设备号组成(32位dev_t类型,高12位为主设备号,低20位为次设备号)。主设备号对应驱动类型,次设备号对应同类型下的不同设备实例。
| 概念 | 位数 | 作用 | 示例 |
|---|---|---|---|
| 主设备号 (major) | 12 bit (0~4095) | 标识设备驱动类型,内核通过主设备号找到对应的cdev | 1: 内存设备, 4: tty设备 |
| 次设备号 (minor) | 20 bit (0~1048575) | 标识同类型下的具体设备实例 | /dev/tty0 (4,0), /dev/tty1 (4,1) |
// 设备号操作宏
#include <linux/types.h> // dev_t
#include <linux/kdev_t.h> // MAJOR/MINOR/MKDEV
dev_t dev_num = MKDEV(major, minor); // 合成设备号
int major = MAJOR(dev_num); // 提取主设备号
int minor = MINOR(dev_num); // 提取次设备号
面试要点: 设备号分配有两种方式——静态分配(手动指定主设备号,使用
register_chrdev_region)和动态分配(内核自动分配,使用alloc_chrdev_region)。面试中推荐回答“优先使用动态分配,避免主设备号冲突”。
2.3 cdev结构体:字符设备的内核表示
cdev 是内核中用于描述字符设备的核心结构体,它建立了设备号与file_operations之间的关联。每个注册的字符设备在内核中都有一个对应的cdev实例。
// cdev结构体定义(简化)
struct cdev {
struct kobject kobj; // 内核对象,用于设备模型
struct module *owner; // 所属模块,通常设为 THIS_MODULE
const struct file_operations *ops; // 操作函数集
struct list_head list; // 链表节点
dev_t dev; // 设备号
unsigned int count; // 次设备号数量
};
cdev的初始化与注册流程是驱动加载的核心步骤:
- 分配cdev:使用
cdev_alloc()或静态定义 - 初始化cdev:调用
cdev_init(&cdev, &fops)绑定file_operations - 设置owner:
cdev.owner = THIS_MODULE - 注册cdev:调用
cdev_add(&cdev, dev_num, count)将设备加入内核
// 标准cdev注册示例
static struct cdev my_cdev;
static int __init my_driver_init(void)
{
dev_t dev_num;
int ret;
// 1. 分配设备号(动态)
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
// 2. 初始化cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 3. 注册cdev
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
unregister_chrdev_region(dev_num, 1);
pr_err("Failed to add cdev\n");
return ret;
}
pr_info("My driver loaded, major=%d minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
return 0;
}
static void __exit my_driver_exit(void)
{
dev_t dev_num = my_cdev.dev;
// 卸载顺序与加载相反
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
pr_info("My driver unloaded\n");
}
关键注意:
cdev_add 之后设备立即可用,因此必须在cdev_add成功之后才暴露设备节点。卸载时先cdev_del再释放设备号,顺序不可颠倒。
2.4 设备节点的创建:用户空间的访问入口
设备节点(device node)是用户空间访问字符设备的文件接口,位于/dev目录下。设备节点记录了设备类型(字符/块)和设备号,VFS通过设备节点中的设备号找到对应的cdev驱动。
| 创建方式 | 命令/接口 | 适用场景 |
|---|---|---|
| 手动创建 | mknod /dev/mydev c major minor |
嵌入式系统、调试阶段 |
| udev自动创建 | 基于uevent机制,用户空间udev监听内核事件 | 桌面Linux、服务器 |
| mdev自动创建 | BusyBox中的轻量级udev替代 | 嵌入式Linux(Buildroot/Yocto) |
| devtmpfs | 内核自动创建,无需用户空间工具 | 现代Linux内核(推荐方式) |
// 手动创建设备节点示例
# mknod /dev/mydevice c 240 0
# ls -l /dev/mydevice
crw------- 1 root root 240, 0 Jan 1 00:00 /dev/mydevice
// 使用class_create自动创建设备节点(驱动中实现)
static struct class *my_class;
static int __init my_driver_init(void)
{
// ... 分配设备号、注册cdev ...
// 创建设备类
my_class = class_create(THIS_MODULE, "my_class");
if (IS_ERR(my_class)) {
// 错误处理
}
// 创建设备节点(自动在/dev下生成)
device_create(my_class, NULL, dev_num, NULL, "mydevice%d", 0);
return 0;
}
static void __exit my_driver_exit(void)
{
// 卸载时销毁设备节点和类
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
}
面试加分点: 现代驱动开发推荐使用
class_create + device_create组合,内核会自动在/dev下创建设备节点,无需手动mknod。面试时提及“devtmpfs + class_create”方案,表明你了解最新的内核机制。
2.5 完整字符设备驱动框架总结
将上述四个要素整合,一个完整的字符设备驱动框架包含以下核心步骤:
| 步骤 | 操作 | 关键函数/宏 | 注意事项 |
|---|---|---|---|
| 1 | 定义file_operations | struct file_operations fops = {...} |
必须设置.owner = THIS_MODULE |
| 2 | 分配设备号 | alloc_chrdev_region() 或 register_chrdev_region() |
优先动态分配 |
| 3 | 初始化cdev | cdev_init() |
绑定file_operations |
| 4 | 注册cdev | cdev_add() |
成功后设备立即可用 |
| 5 | 创建设备节点 | class_create() + device_create() |
或使用mknod/udev |
| 6 | 实现回调函数 | open/read/write/ioctl/release | 注意用户空间数据拷贝 |
面试核心问题清单:
- file_operations中的每个回调在什么场景下被调用?
- 主次设备号的作用是什么?如何避免主设备号冲突?
- cdev_add之后设备就可用了吗?为什么?
- 设备节点中存储了什么信息?VFS如何通过设备节点找到驱动?
- 驱动卸载时为什么必须先cdev_del再unregister_chrdev_region?
掌握字符设备驱动框架是Linux驱动开发的基石。面试中,面试官通常会让你在白板上画出cdev、file_operations、设备号、设备节点之间的关系图,并手写一个最小化的字符设备驱动骨架。熟练理解并能够清晰阐述上述四个要素的协作关系,是顺利通过面试的关键。