3. 内核模块框架
在Linux驱动开发中,内核模块框架是驱动程序的骨架。本章将深入剖析如何通过module_init/module_exit实现模块的加载与卸载,如何注册file_operations结构体,以及如何将ioctl函数指针绑定到该结构体中。这三者构成了字符设备驱动与用户空间交互的基石。
核心要点:内核模块的生命周期管理(init/exit)、设备文件操作接口(file_operations)的注册、以及ioctl命令分发机制的建立。
3.1 模块入口与出口:module_init / module_exit
每个内核模块都必须定义入口函数和出口函数,分别对应模块加载和卸载时的回调。Linux内核通过module_init()和module_exit()宏来声明这两个函数。
| 宏/函数 | 触发时机 | 典型职责 |
|---|---|---|
module_init() |
insmod 或内核自动加载时 |
分配资源、注册设备、初始化锁、创建/proc或/sys节点 |
module_exit() |
rmmod 或内核卸载时 |
释放资源、注销设备、销毁锁、清理节点 |
以下是一个标准的内核模块入口/出口框架代码:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "my_ioctl_dev"
#define CLASS_NAME "my_ioctl_class"
static int major_num;
static struct class *my_class = NULL;
static struct device *my_device = NULL;
// 模块加载函数
static int __init my_driver_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;
}
major_num = MAJOR(dev_num);
pr_info("Allocated major number %d\n", major_num);
// 2. 创建设备类(用于udev自动创建设备节点)
my_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(my_class)) {
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(my_class);
}
// 3. 创建设备节点
my_device = device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME);
if (IS_ERR(my_device)) {
class_destroy(my_class);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(my_device);
}
pr_info("Driver loaded successfully\n");
return 0;
}
// 模块卸载函数
static void __exit my_driver_exit(void)
{
device_destroy(my_class, dev_num);
class_destroy(my_class);
unregister_chrdev_region(dev_num, 1);
pr_info("Driver unloaded\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Expert");
MODULE_DESCRIPTION("Ioctl data copy and security check demo");
最佳实践:使用
__init和__exit标记可以优化内存——__init函数在初始化完成后会被内核释放,__exit函数在非模块化编译时会被丢弃。
3.2 file_operations 结构体注册
file_operations是字符设备驱动的核心接口,它定义了用户空间通过文件描述符操作设备时内核调用的回调函数集合。驱动开发者需要填充该结构体,并通过cdev_init()或register_chrdev()将其注册到内核。
| 成员 | 函数原型 | 说明 |
|---|---|---|
.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 *) |
从设备读取数据到用户空间 |
.write |
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) |
从用户空间写入数据到设备 |
.unlocked_ioctl |
long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) |
无大内核锁(BKL)的ioctl接口(现代内核推荐) |
.compat_ioctl |
long (*compat_ioctl)(struct file *, unsigned int, unsigned long) |
32位用户空间程序在64位内核上的ioctl兼容接口 |
注册file_operations的标准流程:
- 定义结构体实例:填充
.open、.release、.unlocked_ioctl等成员。 - 初始化cdev:调用
cdev_init(&my_cdev, &my_fops)将结构体与字符设备关联。 - 添加设备到系统:调用
cdev_add(&my_cdev, dev_num, 1)将设备注册到VFS层。
// 定义 file_operations 结构体实例
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.unlocked_ioctl = my_ioctl,
.compat_ioctl = my_compat_ioctl, // 64位内核兼容32位应用
};
// 在 module_init 中注册
static struct cdev my_cdev;
static int __init my_driver_init(void)
{
// ... 分配设备号等步骤 ...
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret) {
pr_err("Failed to add cdev\n");
// 清理已分配的资源
return ret;
}
// ... 创建设备节点 ...
return 0;
}
避坑指南:在
cdev_add()之后,设备文件可能立即被用户空间打开。因此,所有必要的初始化(如互斥锁、缓冲区分配)必须在cdev_add()之前完成,否则可能导致竞态条件。
3.3 ioctl 函数指针绑定
在file_operations结构体中,.unlocked_ioctl成员是驱动实现自定义控制命令的核心入口。用户空间通过ioctl(fd, cmd, arg)系统调用触发该回调,驱动开发者需要在回调函数中解析命令并执行相应操作。
ioctl函数指针绑定的关键步骤:
- 选择正确的ioctl变体:现代内核(2.6.35+)推荐使用
.unlocked_ioctl,它不持有大内核锁(BKL),驱动需自行保证并发安全。 - 实现命令分发:在回调函数中使用
switch语句根据cmd参数分发到不同的处理逻辑。 - 数据拷贝:使用
copy_from_user()/copy_to_user()在用户空间和内核空间之间传输数据。 - 安全校验:验证命令号、参数指针、数据长度,防止恶意用户空间程序破坏内核。
// ioctl 命令定义(使用 _IOW/_IOR/_IOWR 宏)
#define MY_IOCTL_SET_DATA _IOW('k', 1, struct my_data)
#define MY_IOCTL_GET_DATA _IOR('k', 2, struct my_data)
#define MY_IOCTL_RESET _IO('k', 3)
// ioctl 回调函数实现
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct my_data data;
int ret = 0;
// 安全检查:验证命令类型和方向
if (_IOC_TYPE(cmd) != 'k') {
pr_err("Invalid ioctl command type\n");
return -ENOTTY;
}
// 安全检查:验证命令大小(防止缓冲区溢出)
if (_IOC_SIZE(cmd) > sizeof(struct my_data)) {
pr_err("ioctl command size exceeds buffer\n");
return -EINVAL;
}
switch (cmd) {
case MY_IOCTL_SET_DATA:
// 从用户空间拷贝数据到内核
if (copy_from_user(&data, (void __user *)arg, sizeof(data))) {
pr_err("copy_from_user failed\n");
return -EFAULT;
}
// 处理数据...
pr_info("SET_DATA received: %d\n", data.value);
break;
case MY_IOCTL_GET_DATA:
// 准备要返回的数据
data.value = 42;
// 将数据拷贝回用户空间
if (copy_to_user((void __user *)arg, &data, sizeof(data))) {
pr_err("copy_to_user failed\n");
return -EFAULT;
}
break;
case MY_IOCTL_RESET:
// 执行复位操作(无数据拷贝)
pr_info("Device reset\n");
break;
default:
pr_err("Unknown ioctl command: 0x%x\n", cmd);
return -ENOTTY;
}
return ret;
}
// 兼容32位应用(64位内核)
static long my_compat_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
// 对于简单结构体,通常可以直接调用主ioctl
return my_ioctl(filp, cmd, (unsigned long)compat_ptr(arg));
}
安全提示:始终使用
_IOC_TYPE、_IOC_NR、_IOC_SIZE宏来验证命令的合法性。不要信任用户空间传入的arg指针——必须使用copy_from_user/copy_to_user进行安全拷贝,禁止直接解引用用户空间指针。
3.4 完整模块框架整合
将上述三个部分整合在一起,形成一个完整的字符设备驱动框架。以下表格总结了各组件在模块中的角色:
| 组件 | 位置 | 关键函数/宏 | 职责 |
|---|---|---|---|
| 模块入口 | module_init() |
my_driver_init() |
分配设备号、初始化cdev、注册设备类、创建节点 |
| 模块出口 | module_exit() |
my_driver_exit() |
销毁节点、注销设备类、释放设备号 |
| 操作接口 | file_operations |
my_fops |
定义open/release/ioctl等回调 |
| ioctl处理 | .unlocked_ioctl |
my_ioctl() |
命令分发、数据拷贝、安全校验 |
驱动加载后,用户空间可以通过/dev/my_ioctl_dev设备文件与驱动交互。整个框架的调用链为:
- insmod →
module_init()→ 注册设备 → 创建/dev节点 - open("/dev/my_ioctl_dev") → VFS →
my_fops.open() - ioctl(fd, cmd, arg) → VFS →
my_fops.unlocked_ioctl()→ 安全校验 → 数据拷贝 → 命令处理 - close(fd) → VFS →
my_fops.release() - rmmod →
module_exit()→ 注销设备 → 清理资源
关键总结:
module_init/module_exit管理模块生命周期,file_operations提供设备操作接口,ioctl函数指针绑定实现自定义控制命令。三者缺一不可,共同构成字符设备驱动的标准骨架。