[Linux 内核驱动学习] IOCTL 分发派遣函数

1. 前言

在 Linux 驱动开发中,字符设备(Character Device)是最基础的模块。标准的 readwrite 操作主要用于数据的流式传输,但如果我们想对设备进行特定的控制(例如复位设备、配置寄存器、获取设备状态),就需要用到 ioctl(Input/Output Control)接口。

本文将演示如何实现一个规范的字符设备驱动,重点讲解如何实现 ioctl 分发派遣函数,以及如何利用**互斥锁(Mutex)**保证内核数据的安全。


2. 知识准备:什么是 IOCTL?

ioctl 是一个系统调用,允许应用程序向内核驱动发送控制命令。

  • 命令码(Command Code):并不是简单的 1, 2, 3,而是通过位域宏(_IO, _IOR, _IOW)生成的唯一编码。
  • 分发派遣:在驱动中,通过 switch-case 结构解析命令码,执行对应的逻辑。

3. 项目结构与代码实现

3.1 协议头文件:mychardev.h

为了让应用层和内核层“说同样的语言”,我们需要定义统一的 IOCTL 命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef MYCHARDEV_H
#define MYCHARDEV_H
#include <linux/ioctl.h>

// 1. 定义幻数(Magic Number),用于唯一标识驱动
#define MY_MAGIC 'k'

// 2. 使用内核宏定义命令码
// _IO: 无数据传输; _IOR: 从内核读; _IOW: 向内核写
#define MY_IOCTL_RESET _IO(MY_MAGIC, 1) // 重置缓冲区
#define MY_IOCTL_GET_LEN _IOR(MY_MAGIC, 2, int) // 获取当前数据长度
#define MY_IOCTL_SET_MSG _IOW(MY_MAGIC, 3, char*) // 通过命令直接设置消息内容

#endif

3.2 驱动层实现:mychardev.c

驱动程序实现了动态设备号分配、自动创建设备节点以及互斥锁保护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/mutex.h>
#include "mychardev.h"

#define BUF_SIZE 1024
#define DEVICE_NAME "mychardev"

static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;
static char my_buf[BUF_SIZE];
static size_t my_len = 0;
static DEFINE_MUTEX(my_lock); // 定义互斥锁,防止并发竞争

// --- IOCTL 分发派遣函数 ---
static long mychardev_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
int len_val;

switch (cmd) {
case MY_IOCTL_RESET:
mutex_lock(&my_lock);
memset(my_buf, 0, BUF_SIZE);
my_len = 0;
mutex_unlock(&my_lock);
pr_info("mychardev: ioctl reset executed\n");
break;

case MY_IOCTL_GET_LEN:
len_val = (int)my_len;
if (copy_to_user((int __user *)arg, &len_val, sizeof(int)))
return -EFAULT;
break;

case MY_IOCTL_SET_MSG:
mutex_lock(&my_lock);
if (copy_from_user(my_buf, (char __user *)arg, BUF_SIZE)) {
mutex_unlock(&my_lock);
return -EFAULT;
}
my_buf[BUF_SIZE-1] = '\0'; // 安全截断
my_len = strlen(my_buf);
mutex_unlock(&my_lock);
pr_info("mychardev: ioctl set msg done\n");
break;

default:
return -ENOTTY; // 命令不支持时的标准返回值
}
return 0;
}

// 绑定文件操作接口
static const struct file_operations mychardev_fops = {
.owner = THIS_MODULE,
.read = mychardev_read, // 基础读实现
.write = mychardev_write, // 基础写实现
.unlocked_ioctl = mychardev_ioctl, // 绑定 ioctl 接口
};

// ... 省略常规的 init/exit 和基础读写函数实现 ...

4. 应用层测试:test_app.c

在应用层,我们通过标准的 ioctl() 函数与驱动交互。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include "mychardev.h"

int main() {
int fd = open("/dev/mychardev", O_RDWR);
int length = 0;

// 测试 1: 获取当前长度
ioctl(fd, MY_IOCTL_GET_LEN, &length);
printf("Current length: %d\n", length);

// 测试 2: 通过 IOCTL 设置消息
ioctl(fd, MY_IOCTL_SET_MSG, "New Message from IOCTL");

// 测试 3: 重置设备
ioctl(fd, MY_IOCTL_RESET);

close(fd);
return 0;
}

5. 核心要点解析

1. 为什么使用 unlocked_ioctl

早期的 Linux 内核使用 ioctl 并由内核自动加锁(大内核锁 BKL),效率低下。现代内核使用 unlocked_ioctl,驱动程序需要自己负责并发安全。

2. 并发保护:Mutex 的必要性

在内核中,可能有多个进程同时访问同一个驱动。

  • 如果进程 A 正在执行 write 写入数据,而进程 B 此时调用 MY_IOCTL_RESET 清空数据,就会发生竞态(Race Condition)。
  • 通过 mutex_lockmutex_unlock,我们确保了同一时刻只有一个操作能修改 my_buf

3. 用户空间与内核空间的数据交换

  • copy_to_user:安全地将内核数据拷贝给用户。
  • copy_from_user:安全地将用户输入拷贝到内核。
    内核不能直接解引用用户态传来的指针,必须使用这两个安全函数,否则会导致系统崩溃。

6. 实验结论

通过运行该驱动,我们可以在 /dev/ 下看到自动生成的设备节点。运行测试程序后,通过 dmesg 命令可以看到内核日志输出了对应的控制指令记录。

这套代码框架不仅适用于简单的 Echo 驱动,也可以作为传感器控制、电机驱动等复杂场景的基础模板。