[Linux 内核驱动学习] IOCTL 分发派遣函数
1. 前言
在 Linux 驱动开发中,字符设备(Character Device)是最基础的模块。标准的 read 和 write 操作主要用于数据的流式传输,但如果我们想对设备进行特定的控制(例如复位设备、配置寄存器、获取设备状态),就需要用到 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>
#define MY_MAGIC 'k'
#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);
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, };
|
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; ioctl(fd, MY_IOCTL_GET_LEN, &length); printf("Current length: %d\n", length);
ioctl(fd, MY_IOCTL_SET_MSG, "New Message from IOCTL");
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_lock 和 mutex_unlock,我们确保了同一时刻只有一个操作能修改 my_buf。
3. 用户空间与内核空间的数据交换
copy_to_user:安全地将内核数据拷贝给用户。
copy_from_user:安全地将用户输入拷贝到内核。
内核不能直接解引用用户态传来的指针,必须使用这两个安全函数,否则会导致系统崩溃。
6. 实验结论
通过运行该驱动,我们可以在 /dev/ 下看到自动生成的设备节点。运行测试程序后,通过 dmesg 命令可以看到内核日志输出了对应的控制指令记录。
这套代码框架不仅适用于简单的 Echo 驱动,也可以作为传感器控制、电机驱动等复杂场景的基础模板。