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 注册流程

字符设备驱动的标准注册步骤:

  1. 调用 alloc_chrdev_region() 或 register_chrdev_region() 获取设备号
  2. 初始化 cdev 结构体并关联 file_operations
  3. 调用 cdev_add() 将设备添加到内核
  4. 创建设备节点(手动 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 层、字符设备驱动核心组件之间的调用关系:

字符设备驱动整体框架 用户空间 (User Space) 应用程序调用 open() / read() / write() / ioctl() / close() 虚拟文件系统 (VFS) 根据 inode 找到 file_operations 指针,分发系统调用 file_operations 结构体 .open = my_open, .read = my_read, .write = my_write, .unlocked_ioctl = my_ioctl .release = my_release, .llseek = no_llseek, .owner = THIS_MODULE 字符设备驱动实现 cdev 注册 / 设备号管理 / 硬件操作 / 数据拷贝 / ioctl 命令处理 ioctl 是控制通路入口 后续章节重点展开

1.5 本章小结

  • 内核模块 是驱动的基本载体,通过 module_init / module_exit 管理生命周期,使用 insmod / rmmod 动态加载卸载。
  • 字符设备驱动框架 以 cdev 为核心,通过设备号注册到内核,用户空间通过设备文件访问。
  • file_operations 是驱动与 VFS 之间的契约,定义了所有文件操作的回调函数,其中 unlocked_ioctl 是后续章节重点研究的控制命令入口。
预备知识: 在进入第 2 章 ioctl 命令设计之前,请确保你已掌握本章的内核模块编写、字符设备注册、以及 file_operations 各回调函数的实现方法。这些是驱动控制通路的基础设施。