ioctl 的原型定义在 <sys/ioctl.h> 中:

(图片来源网络,侵删)
#include <sys/ioctl.h> int ioctl(int fd, unsigned long request, ...);
ioctl 函数接受可变数量的参数,但核心是前三个,我们逐一分析。
参数详解
int fd (文件描述符)
- 类型:
int - 含义: 文件描述符,这是
ioctl操作的目标,它必须是一个打开的文件、设备或套接字的描述符。 - 如何获取:
- 对于设备文件,通常使用
open()系统调用打开,打开串口/dev/ttyS0:int fd = open("/dev/ttyS0", O_RDWR); if (fd < 0) { // 错误处理 } - 对于文件或套接字,同样是在打开或创建后获得其文件描述符。
- 对于设备文件,通常使用
- 关键点:
ioctl的操作是针对这个fd所代表的特定资源的,驱动程序通过这个fd内部关联到对应的设备实例,从而知道要对哪个设备执行操作。
unsigned long request (请求码)
-
类型:
unsigned long -
含义: 请求码,这是
ioctl的灵魂,它是一个唯一的数字,告诉驱动程序你想要执行哪种具体的操作,设置波特率、获取MAC地址、清空缓冲区等。 -
重要性: 请求码必须由驱动程序和用户空间程序预先约定好,如果用户空间程序使用了驱动程序不认识的请求码,行为是不可预测的,通常会导致系统调用返回错误
-1并设置errno为EINVAL(Invalid argument)。
(图片来源网络,侵删) -
请求码的构造 (魔数): 为了避免不同驱动的请求码冲突,Linux 内核采用了一套约定俗成的规则来构造请求码,它通常是一个32位的数字,被划分为几个部分:
| 8 bits (类型) | 8 bits (序号) | 2 bits (数据方向) | 8 bits (大小) |- 类型 (Type / "Magic Number"): 8位,一个唯一的数字,用于标识特定的驱动,开发者需要向内核申请一个唯一的类型号,以防止与其他驱动的请求码冲突。
'T'(0x54) 常用于tty设备。 - 序号 (Number / Nr): 8位,驱动程序内部不同命令的编号,从0开始,每个命令一个序号。
- 数据方向 (Direction / Dir): 2位,指示数据传输方向,这对于内核正确处理参数至关重要。
_IOC_NONE(0): 无数据传输,只是触发一个操作。_IOC_READ(2): 从内核(驱动)读取数据到用户空间。_IOC_WRITE(1): 从用户空间写入数据到内核(驱动)。_IOC_READ | _IOC_WRITE(3): 双向传输数据。
- 大小 (Size / Size): 14位 (在32位系统中是8位,但通常使用14位来对齐),指定与该请求关联的参数数据的大小(以字节为单位),内核会用这个值来验证用户空间缓冲区的大小,防止内存越界访问。
- 类型 (Type / "Magic Number"): 8位,一个唯一的数字,用于标识特定的驱动,开发者需要向内核申请一个唯一的类型号,以防止与其他驱动的请求码冲突。
-
宏定义: 内核提供了几个宏来方便地构造和解析请求码:
_IO(type, nr): 创建一个无数据传输的请求码。_IOR(type, nr, datatype): 创建一个从内核读取数据的请求码。datatype是参数在用户空间的数据类型,内核用它来计算大小。_IOW(type, nr, datatype): 创建一个向内核写入数据的请求码。_IOWR(type, nr, datatype): 创建一个双向数据传输的请求码。
示例: 假设我们为驱动
'A'(0x41) 定义一个命令GET_DEV_INFO(序号1),它返回一个struct dev_info类型的数据。// 在驱动程序中定义 struct dev_info { int version; char name[32]; }; #define MYIOC_TYPE 'A' #define GET_DEV_INFO _IOR(MYIOC_TYPE, 1, struct dev_info)在用户空间,我们直接使用
GET_DEV_INFO这个宏作为request参数。
(可变参数 - 通常是 void *arg)
- 类型: (可变参数),但实际使用时,它几乎总是被转换为
void *(指向任意类型的指针)。 - 含义: 参数指针,它指向了用户空间的一个缓冲区,用于向
ioctl传递数据或从ioctl获取数据。 - 如何使用:
- 向驱动写入数据: 用户空间程序分配一个缓冲区,填入数据,然后将该缓冲区的地址作为
arg传给ioctl,驱动程序从该地址读取数据。 - 从驱动读取数据: 用户空间程序分配一个缓冲区(可以先清零),然后将该缓冲区的地址作为
arg传给ioctl,驱动程序向该地址写入数据。 - 无参数: 对于不需要数据传输的
request,这个参数可以传NULL或0。
- 向驱动写入数据: 用户空间程序分配一个缓冲区,填入数据,然后将该缓冲区的地址作为
- 关键点:
- 指针是用户空间的地址:
ioctl内部会处理这个从用户空间到内核空间的地址转换,驱动程序绝不能直接解引用这个指针,而必须使用内核提供的专用函数,如copy_from_user()和copy_to_user(),以确保安全和正确性。 - 大小匹配: 请求码中的
size字段会与arg指向的缓冲区大小进行比较,如果大小不匹配,ioctl会失败并返回EINVAL。
- 指针是用户空间的地址:
完整示例
假设我们有一个虚拟的字符设备 /dev/mychardev,它的驱动程序支持以下操作:
DEV_RESET('M', 0): 无参数,重置设备。DEV_SET_LED('M', 1): 写入一个int值来控制LED (0=关, 1=开)。DEV_GET_STATUS('M', 2): 读取一个int值获取设备状态。
驱动程序中的宏定义 (概念性)
// 在驱动代码中 #define MYDEV_IOC_MAGIC 'M' #define DEV_RESET _IO(MYDEV_IOC_MAGIC, 0) #define DEV_SET_LED _IOW(MYDEV_IOC_MAGIC, 1, int) #define DEV_GET_STATUS _IOR(MYDEV_IOC_MAGIC, 2, int)
用户空间程序示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <string.h>
// 使用与驱动程序中相同的宏定义
#define MYDEV_IOC_MAGIC 'M'
#define DEV_RESET _IO(MYDEV_IOC_MAGIC, 0)
#define DEV_SET_LED _IOW(MYDEV_IOC_MAGIC, 1, int)
#define DEV_GET_STATUS _IOR(MYDEV_IOC_MAGIC, 2, int)
void print_usage(const char *prog_name) {
printf("Usage: %s <reset|set_led <0|1>|get_status>\n", prog_name);
}
int main(int argc, char *argv[]) {
if (argc < 2) {
print_usage(argv[0]);
return 1;
}
int fd = open("/dev/mychardev", O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
if (strcmp(argv[1], "reset") == 0) {
// 调用无参数的ioctl
if (ioctl(fd, DEV_RESET) < 0) {
perror("ioctl DEV_RESET");
} else {
printf("Device reset successfully.\n");
}
} else if (strcmp(argv[1], "set_led") == 0) {
if (argc < 3) {
print_usage(argv[0]);
close(fd);
return 1;
}
int led_state = atoi(argv[2]);
// 调用写入int参数的ioctl
if (ioctl(fd, DEV_SET_LED, &led_state) < 0) {
perror("ioctl DEV_SET_LED");
} else {
printf("LED set to %d.\n", led_state);
}
} else if (strcmp(argv[1], "get_status") == 0) {
int status = 0;
// 调用读取int参数的ioctl,传入一个缓冲区地址
if (ioctl(fd, DEV_GET_STATUS, &status) < 0) {
perror("ioctl DEV_GET_STATUS");
} else {
printf("Device status: %d\n", status);
}
} else {
print_usage(argv[0]);
}
close(fd);
return 0;
}
替代方案与注意事项
ioctl 的缺点
由于其设计的复杂性(请求码魔数、参数处理不安全等),ioctl 被认为是一种“反模式”,在现代 Linux 驱动开发中,有更好的替代方案:
sysfs: 通过/sys文件系统暴露设备的属性,用户空间程序通过标准的文件读写操作(open,read,write,scanf,printf等)来配置或查询设备,这是最推荐的方式,因为它简单、直观、符合 Unix 文件哲学。configfs: 类似于sysfs,但提供了一种更动态的、为复杂配置设计的机制。- Netlink 套接字: 专门用于内核与用户空间进行复杂的、异步的网络相关通信(如路由、防火墙规则)。
/dev上的普通文件: 对于简单的设备,直接将其映射到用户空间内存(如mmap),或者通过读写文件描述符来传递命令和数据,比ioctl更清晰。
| 参数 | 类型 | 描述 |
|---|---|---|
fd |
int |
文件描述符,指向要操作的设备或文件。 |
request |
unsigned long |
请求码,唯一标识要执行的操作,由类型、序号、数据方向和大小组成,是驱动和用户空间的契约。 |
arg |
void * |
指向用户空间缓冲区的指针,用于传递数据,必须使用 copy_from_user/copy_to_user 在驱动中安全访问。 |
尽管 ioctl 仍然广泛存在于许多遗留系统和内核子系统中(如终端、存储、网络设备),但对于新项目,应优先考虑使用 sysfs 等更现代、更安全的接口。
