3. 字符设备驱动框架:file_operations 结构体、主次设备号、设备节点创建
字符设备是 Linux 驱动开发中最基础的设备类型,其核心在于通过 file_operations 结构体将用户空间的系统调用与内核驱动函数绑定。本章将深入剖析字符设备驱动的三大支柱:操作接口、设备编号与节点创建。
3.1 字符设备驱动核心模型
字符设备驱动的本质是“文件抽象”——用户通过 open/read/write/close 等文件操作与硬件交互。内核使用 struct cdev 表示字符设备,并通过 file_operations 提供标准接口。
3.2 file_operations 结构体详解
file_operations 是字符设备驱动的“函数跳转表”,定义在 <linux/fs.h> 中。每个成员对应一个系统调用,驱动开发者只需实现需要的接口。
3.2.1 核心成员与函数原型
| 成员名称 | 系统调用触发 | 函数原型 | 典型用途 |
|---|---|---|---|
.open |
open() |
int (*open)(struct inode *, struct file *) |
设备初始化、权限检查、分配私有数据 |
.release |
close() |
int (*release)(struct inode *, struct file *) |
资源释放、关闭设备 |
.read |
read() |
ssize_t (*read)(struct file *, char __user *, size_t, loff_t *) |
从设备读取数据到用户空间 |
.write |
write() |
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) |
从用户空间写入数据到设备 |
.unlocked_ioctl |
ioctl() |
long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) |
设备控制命令(无大内核锁) |
.mmap |
mmap() |
int (*mmap)(struct file *, struct vm_area_struct *) |
设备内存映射到用户空间 |
.poll |
poll()/select() |
__poll_t (*poll)(struct file *, struct poll_table_struct *) |
设备可读/可写状态查询 |
3.2.2 标准初始化模板
// 定义驱动函数
static int my_open(struct inode *inode, struct file *filp) {
pr_info("Device opened\n");
return 0;
}
static int my_release(struct inode *inode, struct file *filp) {
pr_info("Device closed\n");
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos) {
// 实现数据读取逻辑
return 0;
}
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos) {
// 实现数据写入逻辑
return count;
}
// 填充 file_operations 结构体
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
关键提示:
.owner 必须设置为 THIS_MODULE,防止模块正在使用时被卸载。未实现的接口应设为 NULL,内核会返回 -EINVAL。
3.3 主次设备号管理
设备号是内核识别设备的唯一标识,由 主设备号 (major) 和 次设备号 (minor) 组成,类型为 dev_t(32位无符号整数)。
3.3.1 设备号结构
3.3.2 设备号操作宏
| 宏/函数 | 功能 | 示例 |
|---|---|---|
MKDEV(major, minor) |
将主次设备号合并为 dev_t |
dev_t dev = MKDEV(240, 0); |
MAJOR(dev) |
从 dev_t 提取主设备号 |
int major = MAJOR(dev); |
MINOR(dev) |
从 dev_t 提取次设备号 |
int minor = MINOR(dev); |
3.3.3 设备号分配策略
- 静态分配: 使用
register_chrdev_region(dev_t first, unsigned int count, const char *name),需提前知道主设备号(常用于已知设备号或教学场景)。 - 动态分配: 使用
alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name),由内核自动分配主设备号(推荐生产环境使用)。
// 动态分配设备号示例
dev_t dev_num;
int ret;
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
pr_info("Allocated major=%d minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
避坑指南: 动态分配时不要假设主设备号固定。用户空间通过
/proc/devices 或 udev 获取设备号,切勿硬编码主设备号到应用程序中。
3.4 设备节点创建
设备节点是用户空间访问驱动的“文件入口”,位于 /dev 目录下。创建方式分为手动和自动两种。
3.4.1 手动创建(mknod)
适用于开发调试阶段,使用 mknod 命令:
# mknod /dev/mydevice c 240 0
# 格式:mknod 路径 类型(c/b) 主设备号 次设备号
# c 表示字符设备,b 表示块设备
3.4.2 自动创建(udev/mdev + class)
生产环境推荐使用设备模型自动创建节点。驱动中只需创建 device class 和 device,用户空间的 udev(桌面)或 mdev(嵌入式 busybox)会自动在 /dev 下生成节点。
static struct class *my_class;
static struct device *my_device;
// 在模块初始化函数中
my_class = class_create(THIS_MODULE, "my_class");
if (IS_ERR(my_class)) {
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(my_class);
}
my_device = device_create(my_class, NULL, dev_num,
NULL, "mydevice");
if (IS_ERR(my_device)) {
class_destroy(my_class);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(my_device);
}
// 在模块退出函数中
device_destroy(my_class, dev_num);
class_destroy(my_class);
unregister_chrdev_region(dev_num, 1);
3.4.3 设备节点创建流程对比
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| mknod 手动 | 开发调试、嵌入式无 udev | 简单直接,不依赖用户空间工具 | 需手动管理,设备号变化时需重新创建 |
| udev 自动 | 桌面 Linux、服务器 | 自动创建/删除,支持热插拔 | 依赖 udev 服务,系统启动稍慢 |
| mdev 自动 | 嵌入式 busybox 环境 | 轻量级,适合资源受限设备 | 功能较 udev 弱,需手动触发扫描 |
3.5 完整字符设备注册流程
3.6 综合代码示例:最小字符设备驱动
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "demo_device"
#define CLASS_NAME "demo_class"
static dev_t dev_num;
static struct cdev demo_cdev;
static struct class *demo_class;
static struct device *demo_device;
static int demo_open(struct inode *inode, struct file *filp) {
pr_info("demo: device opened\n");
return 0;
}
static int demo_release(struct inode *inode, struct file *filp) {
pr_info("demo: device closed\n");
return 0;
}
static ssize_t demo_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos) {
char kernel_buf[] = "Hello from kernel!\n";
size_t len = strlen(kernel_buf);
if (*f_pos >= len)
return 0;
if (count > len - *f_pos)
count = len - *f_pos;
if (copy_to_user(buf, kernel_buf + *f_pos, count))
return -EFAULT;
*f_pos += count;
return count;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = demo_open,
.release = demo_release,
.read = demo_read,
};
static int __init demo_init(void) {
int ret;
// 1. 动态分配设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
pr_info("Allocated major=%d minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
// 2. 初始化并添加 cdev
cdev_init(&demo_cdev, &fops);
demo_cdev.owner = THIS_MODULE;
ret = cdev_add(&demo_cdev, dev_num, 1);
if (ret < 0) {
unregister_chrdev_region(dev_num, 1);
pr_err("Failed to add cdev\n");
return ret;
}
// 3. 创建设备类
demo_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(demo_class)) {
cdev_del(&demo_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(demo_class);
}
// 4. 创建设备节点
demo_device = device_create(demo_class, NULL, dev_num,
NULL, DEVICE_NAME);
if (IS_ERR(demo_device)) {
class_destroy(demo_class);
cdev_del(&demo_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(demo_device);
}
pr_info("demo: driver loaded successfully\n");
return 0;
}
static void __exit demo_exit(void) {
device_destroy(demo_class, dev_num);
class_destroy(demo_class);
cdev_del(&demo_cdev);
unregister_chrdev_region(dev_num, 1);
pr_info("demo: driver unloaded\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Expert");
MODULE_DESCRIPTION("Minimal character device driver demo");
测试步骤:
- 编译驱动:
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules - 加载驱动:
sudo insmod demo.ko - 查看设备节点:
ls -l /dev/demo_device - 读取数据:
cat /dev/demo_device - 卸载驱动:
sudo rmmod demo
3.7 本章小结
- file_operations 是字符设备驱动的核心接口,每个成员对应一个用户空间系统调用。
- 设备号 由主设备号(12位)和次设备号(20位)组成,推荐使用
alloc_chrdev_region动态分配。 - 设备节点 可通过
mknod手动创建,或通过class_create + device_create配合 udev/mdev 自动创建。 - 驱动注册与注销必须严格对称,顺序为:alloc → cdev_init/add → class_create → device_create,注销时反向操作。