在 Linux 字符设备驱动开发中,open、read、write 等基础系统调用能够满足设备的基本数据读写需求。然而,当应用程序需要对设备进行更复杂的控制时------比如设置串口的波特率、配置 A/D 转换的采样精度、控制 LED 的闪烁频率等------仅仅依靠 read/write 就显得力不从心了。
ioctl(Input/Output Control)正是为解决这类问题而生的系统调用。它充当了用户空间应用程序与设备驱动程序之间的"特殊指令通道",允许应用程序向驱动程序发送控制命令和配置参数。可以这样理解:字符设备驱动像一个工具箱,基础功能(打开/关闭设备、读写数据)是标配工具,而 ioctl 就像这个工具箱的扩展插槽,当需要特殊操作时,就可以通过添加新的 ioctl 指令来实现额外功能。

一个典型的例子是串口驱动:串口数据的收发通过 read/write 操作,而串口的波特率、校验位、停止位则通过 ioctl 进行设置。
1.ioctl函数介绍
用户空间ioctl函数原型为:
cpp
int ioctl(int fd, int cmd, ...) ;
fd为文件描述符,cmd为命令码,......为参数;
ioctl 函数调用时,第二个参数 cmd 是一个 32 位的命令码,它并不是一个随意填写的整数,而是经过了精心的编码。Linux 将 32 位的 cmd 划分为四个位段:
| 位段 | 位范围 | 宽度 | 含义 |
|---|---|---|---|
| 方向(direction) | 31-30 | 2 bits | 数据传输方向 |
| 数据大小(size) | 29-16 | 14 bits | 传输的数据大小 |
| 设备类型(type/幻数) | 15-8 | 8 bits | 驱动标识符 |
| 命令编号(number) | 7-0 | 8 bits | 具体命令序号 |
我们可以通过位操作来构造cmd命令码,打包比较麻烦,所以内核提供了四个宏来构造命令码,简化了命令的定义过程:
| 宏 | 含义 | 适用场景 |
|---|---|---|
| _IO(type, nr) | 无数据方向的命令 | 不传输任何参数的命令 |
| _IOR(type, nr, data_type) | 从驱动读取数据 | 获取设备状态、参数 |
| _IOW(type, nr, data_type) | 向驱动写入数据 | 设置设备参数、配置 |
| _IOWR(type, nr, data_type) | 双向数据传输 | 需要同时读写数据的命令 |
命令宏的使用格式如下:
cpp
// 定义幻数(通常用一个ASCII字符标识你的驱动)
#define MY_DEVICE_TYPE 'M'
// 定义各种命令
#define MY_CMD_RESET _IO(MY_DEVICE_TYPE, 0) // 无参数命令
#define MY_CMD_GET_STATUS _IOR(MY_DEVICE_TYPE, 1, int) // 读取状态
#define MY_CMD_SET_CONFIG _IOW(MY_DEVICE_TYPE, 2, struct my_config) // 设置配置
#define MY_CMD_EXCHANGE _IOWR(MY_DEVICE_TYPE, 3, struct my_data) // 双向传输
2.驱动中ioctl的实现
在新版 Linux 内核(2.6.36 以后)中,file_operations 结构体中原有的 ioctl 函数指针已被移除,取而代之的是 unlocked_ioctl 和 compat_ioctl 。对于大多数字符设备驱动,只需要现 unlocked_ioctl 即可。
内核中 unlocked_ioctl 的函数原型为:
cpp
long (*unlocked_ioctl)(struct file *filp, unsigned int cmd, unsigned long arg);
驱动ioctl的实现:
cpp
#define CMD_TEST0 _IO('L',0)
#define CMD_TEST1 _IO('L',1)
#define CMD_TEST2 _IOW('A',0,int)
#define CMD_TEST3 _IOR('A',1,int)
long cdev_test_ioctl (struct file *filp, unsigned int cmd, unsigned long argv)
{
int val = 0;
int ret;
switch(cmd)
{
case CMD_TEST0:
printk("this is cmd0\n");
break;
case CMD_TEST2:
val = (int)argv;
printk("cmd2's argv:%d",val);
break;
case CMD_TEST3:
val = 99;
ret = copy_to_user((int*)argv,&val,sizeof(val));
if(ret > 0)
{
printk("copy to user failed");
return -1;
}
default:
break;
}
return 0;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
.release = cdev_test_release,
.llseek = cdev_test_llseek,
.unlocked_ioctl = cdev_test_ioctl,
};
测试程序:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#define CMD_TEST0 _IO('L',0)
#define CMD_TEST1 _IO('L',1)
#define CMD_TEST2 _IOW('A',0,int)
#define CMD_TEST3 _IOR('A',1,int)
int main(int argc,char* argv)
{
int fd = open("/dev/cdev_test_device",O_RDWR);
if(fd < 0)
{
printf("open failed\n");
perror("open failed");
return -1;
}
int ret;
// ret = ioctl(fd,CMD_TEST0);
// if(ret < 0)
// {
// printf("ioctl failed");
// return -1;
// }
int val;
ret = ioctl(fd,CMD_TEST3,&val);
if(ret < 0)
{
printf("ioctl failed");
return -1;
}
printf("cmd3 val is %d",val);
return 0;
}
注意,用户空间和内核空间都要声明:
cpp
#define CMD_TEST0 _IO('L',0)
#define CMD_TEST1 _IO('L',1)
#define CMD_TEST2 _IOW('A',0,int)
#define CMD_TEST3 _IOR('A',1,int)
一般情况下可以写到一个头文件中。
3.ioctl传参相关
-
如果用户程序调用 ioctl(fd, CMD, 100),那么驱动中 arg 的值就是
100。 -
如果用户程序调用 ioctl(fd, CMD, &data),那么驱动中 arg 的值就是 变量 data 在用户空间的虚拟地址。
这种设计带来两种完全不同的数据传递方式,需要开发者根据命令码的定义来区分对待。
| 对比维度 | 传值(传递整数值) | 传地址(传递指针) |
|---|---|---|
| 用户调用示例 | ioctl(fd, CMD, 100) | ioctl(fd, CMD, &my_struct) |
驱动中 arg 的含义 |
用户想要传递的数值本身 | 用户空间缓冲区的虚拟地址 |
| 驱动中访问方式 | 直接使用 arg | 必须通过 copy_from_user / copy_to_user |
| 适用数据类型 | int,char等标量值 | 结构体、数组、字符串等复杂数据 |
温馨提示
- **类型转换:**在内核里,arg 是 unsigned long 类型。如果你传的是负数或者指针,记得进行适当的强制类型转换。
- **安全风险:**如果 arg 代表的是地址,千万不要在内核里直接解引用它(例如 int v = *(int *)arg;)。这会导致内核崩溃或安全漏洞,务必使用 copy_from_user。
小结
ioctl 是 Linux 设备驱动开发中不可或缺的核心接口。理解它的命令码编码规则、四个命令宏的用途、内核中 unlocked_ioctl 的实现方式以及用户空间的调用方法,是掌握字符设备驱动开发的关键一步。在实际项目中,合理地使用 ioctl 可以让驱动更加灵活、功能更加丰富。