Linux内核驱动ioctl命令设计与控制通路 - 第1节
1.1 内核模块基础
Linux 内核模块是可以在运行时动态加载到内核或从内核卸载的代码片段。它允许在不重新编译整个内核的情况下扩展内核功能,是字符设备驱动、网络协议栈扩展、文件系统等内核功能的主要载体。
1.1.1 模块的入口与出口
每个内核模块必须包含两个关键函数:初始化函数和清理函数。它们通过宏 module_init() 和 module_exit() 注册。
#include <linux/module.h>
#include <linux/kernel.h>
static int __init my_driver_init(void)
{
printk(KERN_INFO "My driver loaded\n");
return 0;
}
static void __exit my_driver_exit(void)
{
printk(KERN_INFO "My driver unloaded\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Expert");
MODULE_DESCRIPTION("A sample kernel module");
1.1.2 模块编译与加载
内核模块的编译依赖内核源码树中的 Kbuild 系统。典型的 Makefile 如下:
obj-m := my_driver.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
加载与卸载命令:
- insmod my_driver.ko — 插入模块,调用
module_init - rmmod my_driver — 移除模块,调用
module_exit - lsmod — 列出当前已加载的模块
- modinfo my_driver.ko — 查看模块信息(许可证、作者、描述等)
提示: 模块必须使用 GPL 兼容许可证,否则内核会标记为“污染”(tainted),部分内核 API 可能不可用。
1.2 字符设备驱动框架
字符设备是 Linux 中最基本的设备类型之一,以字节流方式读写数据。典型的字符设备包括串口、GPIO、I2C 设备、音频设备等。字符设备驱动的核心是注册一个 cdev 结构体到内核,并关联设备号。
1.2.1 设备号管理
每个字符设备由主设备号(major number)和次设备号(minor number)唯一标识。主设备号对应驱动,次设备号对应同一驱动下的不同设备实例。
| 概念 | 说明 | 示例 |
|---|---|---|
| 主设备号 | 标识设备驱动类型 | 89(I2C 设备) |
| 次设备号 | 标识同一驱动下的不同设备 | 0, 1, 2 … |
| 静态分配 | 使用 register_chrdev_region() 注册已知设备号 |
用于固定硬件 |
| 动态分配 | 使用 alloc_chrdev_region() 由内核自动分配 |
推荐用于通用驱动 |
1.2.2 cdev 注册流程
字符设备驱动的标准注册步骤:
- 调用
alloc_chrdev_region()或register_chrdev_region()获取设备号 - 初始化
cdev结构体并关联file_operations - 调用
cdev_add()将设备添加到内核 - 创建设备节点(手动
mknod或通过 udev 自动创建)
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
static dev_t dev_num;
static struct cdev my_cdev;
static int __init my_char_init(void)
{
// 1. 动态分配设备号
alloc_chrdev_region(&dev_num, 0, 1, "my_char_dev");
// 2. 初始化 cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 3. 添加 cdev 到内核
cdev_add(&my_cdev, dev_num, 1);
printk(KERN_INFO "Char device registered, major=%d\n", MAJOR(dev_num));
return 0;
}
static void __exit my_char_exit(void)
{
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "Char device unregistered\n");
}
关键点:
cdev_add() 之后设备立即对用户空间可见。如果驱动初始化后续步骤失败,必须先调用 cdev_del() 再释放设备号,避免留下孤儿设备。
1.3 file_operations 结构体
file_operations 是字符设备驱动的核心接口,它定义了用户空间通过系统调用操作设备文件时,内核调用的驱动函数指针表。每个设备文件对应一个 file 结构体,而 file 结构体中的 f_op 指针指向该设备的 file_operations 实例。
1.3.1 主要成员函数
| 函数指针 | 系统调用 | 典型用途 |
|---|---|---|
open |
open() |
初始化设备、递增使用计数、分配私有数据 |
release |
close() |
清理资源、递减使用计数 |
read |
read() |
从设备拷贝数据到用户空间 |
write |
write() |
从用户空间拷贝数据到设备 |
unlocked_ioctl |
ioctl() |
设备控制命令(无大内核锁) |
compat_ioctl |
ioctl() (32位兼容) |
64位内核处理32位应用ioctl |
llseek |
lseek() |
修改文件读写位置 |
mmap |
mmap() |
将设备内存映射到用户空间 |
1.3.2 典型实现示例
static int my_open(struct inode *inode, struct file *filp)
{
// 从 inode 获取私有数据
struct my_device *dev = container_of(inode->i_cdev, struct my_device, cdev);
filp->private_data = dev;
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;
// 使用 copy_to_user() 安全拷贝到用户空间
if (copy_to_user(buf, dev->buffer, len))
return -EFAULT;
return len;
}
static ssize_t my_write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{
struct my_device *dev = filp->private_data;
// 使用 copy_from_user() 安全从用户空间拷贝
if (copy_from_user(dev->buffer, buf, len))
return -EFAULT;
return len;
}
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct my_device *dev = filp->private_data;
// ioctl 命令处理(后续章节详细展开)
return 0;
}
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 = no_llseek,
};
注意: 在
read 和 write 中必须使用 copy_to_user() / copy_from_user() 访问用户空间缓冲区,绝不能直接解引用用户指针。这既是为了安全(防止用户传入非法地址),也是为了兼容不同架构的内存模型。
1.4 整体框架关系图
下图展示了用户空间、VFS 层、字符设备驱动核心组件之间的调用关系:
1.5 本章小结
- 内核模块 是驱动的基本载体,通过
module_init/module_exit管理生命周期,使用insmod/rmmod动态加载卸载。 - 字符设备驱动框架 以
cdev为核心,通过设备号注册到内核,用户空间通过设备文件访问。 - file_operations 是驱动与 VFS 之间的契约,定义了所有文件操作的回调函数,其中
unlocked_ioctl是后续章节重点研究的控制命令入口。
预备知识: 在进入第 2 章 ioctl 命令设计之前,请确保你已掌握本章的内核模块编写、字符设备注册、以及 file_operations 各回调函数的实现方法。这些是驱动控制通路的基础设施。