ioctl函数讲解
在学习Linux设备驱动和视频开发的时候,对于这个函数又去了解了一下,因为之前觉得似懂非懂,所以这里来个总结吧,下面的都是在v4l2上面来讲解一下
1.ioctl 是什么?
可以把 ioctl
(Input/Output Control)想象成一个万能遥控器。就像平时用遥控器控制电视(换台、调音量、设置画质),ioctl
就是用来控制各种硬件设备的“遥控器”
2.为什么需要它?
- 普通的
read()
和write()
只能做简单的读写(像用键盘打字) - 但硬件设备有很多复杂操作:比如设置摄像头分辨率、调整麦克风音量、让打印机换纸...
- 这些特殊操作,全都可以交给
ioctl
来搞定
3.函数原型
int ioctl(int fd, unsigned long request, ...);
fd
:文件描述符(就是你用open()
打开设备后得到的那个“门票”)request
:你要发送的“指令代码”(比如:调大音量、获取分辨率)...
:附加参数(通常是个结构体指针,用来传递/接收数据)
这里也要解释一下:
误区:认为“结构体里必须包含命令字段”
真相:结构体只是用来传递数据,真正决定操作的是 request
参数。结构体的内容必须与 request
匹配
4.举个栗子 🌰:控制摄像头
假设你有一个摄像头设备 /dev/video0
:
-
打开设备:
int fd = open("/dev/video0", O_RDWR); // 拿到“门票”fd
-
设置视频格式:
struct v4l2_format fmt = {0}; fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = 640; fmt.fmt.pix.height = 480; // 发送指令:告诉摄像头 我要设置格式为640x480 ioctl(fd, VIDIOC_S_FMT, &fmt);
VIDIOC_S_FMT
是预设的指令代码(表示“设置格式”)&fmt
是传递的参数(具体要设置什么格式)
还是很抽象啊
比如说:
a.为什么 request
参数那么奇怪(比如 VIDIOC_S_FMT
)?
- 这些是 Linux 内核预先定义好的“魔法数字”,每个设备驱动都有自己的指令列表
- v4l2 的指令都以
VIDIOC_
开头(Video IO Control),比如:VIDIOC_QUERYCAP
:查询设备能力(“你能做什么?”)VIDIOC_REQBUFS
:申请缓冲区(“给我腾点内存存视频数据!”)
b. 为什么会失败?
- 常见错误:
errno = EINVAL
(无效参数) - 可能原因:设备不支持你的指令、参数结构体填错了
这不得不感叹,搞出这东西的人,真tm天才
好吧,其实还是懵
我有俩个关键点不明白:
-
request
参数决定一切:ioctl
的第二个参数request
是核心指令,它决定了:- 你要执行什么操作(比如“设置分辨率”还是“查询设备信息”)
- 第三个参数应该传什么类型的数据(可能是结构体指针、整数、甚至不需要参数)
-
结构体指针的作用:
- 当你发送一个
request
(比如VIDIOC_S_FMT
),驱动会预期你传递一个特定结构体的指针(比如struct v4l2_format *
)。 - 结构体里的字段必须符合驱动要求,否则会失败(比如填错了字段名,或传了非法值)
- 当你发送一个
举个反例🌰
假设你发送 VIDIOC_S_FMT
(设置格式),但传了一个错误的结构体:
我的理解是:ioctl已经做好了吃草莓的准备了,你硬是要塞个苹果给他吃....他发现味道不对啊,就出现错误了
// 错误!应该传 struct v4l2_format,但传成了 int
int width = 640;
ioctl(fd, VIDIOC_S_FMT, &width); // 会失败,驱动预期的是结构体
- 驱动预期收到
struct v4l2_format *
,但你传了int *
,导致内核无法解析数据,返回EINVAL
错误
到这里,我还是一知半解,不过至少知道了ioctl是遥控器,然后可以给我的设备传输命令了
但是呢,有个问题来了ioctl怎么知道request这个命令的?
好吧,深入一点点内核和驱动的设计逻辑 不过丫的好多,好复杂,做个略懂就可以了
1. request
命令的本质(下面的有些容易懵圈,我自己也只做了解了)
request
是一个整数,但不是随便写的!它的二进制位中编码了关键信息:
// 内核中定义 ioctl 命令的宏(include/uapi/asm-generic/ioctl.h)
#define _IOC(dir, type, nr, size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
- dir(方向): 数据传递方向(
_IOC_NONE
无数据,_IOC_READ
读,_IOC_WRITE
写) - type(类型): 一个字符,标识设备类型(比如 'V' 表示视频设备)
- nr(序号): 命令的编号(比如 1 表示第一个命令)
- size(大小): 第三个参数的数据大小(比如
sizeof(struct my_struct)
)
2. 命令的来源
2.1 驱动开发者定义
每个设备驱动会自己定义支持的 ioctl
命令。例如摄像头驱动(V4L2)定义了大量命令:
// 在 include/uapi/linux/videodev2.h 中
#define VIDIOC_QUERYCAP _IOR('V', 0, struct v4l2_capability) // 查询设备能力
#define VIDIOC_S_FMT _IOW('V', 4, struct v4l2_format) // 设置格式
_IOR
表示这是一个读操作(驱动→用户),第三个参数类型是struct v4l2_capability
_IOW
表示这是一个写操作(用户→驱动),参数类型是struct v4l2_format
2.2 用户空间使用
当你在用户层调用 ioctl(fd, VIDIOC_S_FMT, &fmt)
时:
VIDIOC_S_FMT
已经被预定义为特定整数(包含方向、类型、序号、结构体大小)- 驱动通过解码
request
中的信息,知道你要执行设置格式操作,并期望收到一个struct v4l2_format
结构体
这里我又难受了,什么IOC,IOR,IOW的
来写一下吧,这倒霉孩子
IOC
、IOR
、IOW
是 Linux 内核中用于构造 ioctl
命令的层级式宏,它们的关系可以理解为“基础宏”和“快捷宏”的关系
这里是IOC之类的知识了
这里简单说一下结论吧....就好像_IOC是原材料,_IOR,_IOW就好像是预制菜
关于为什么要写这个....是因为request
命令的来源:由驱动开发者定义,通过宏 _IO
, _IOR
, _IOW
, _IOWR
生成
1.层级关系
_IOC
:最底层的宏,直接构造ioctl
命令的整数。_IO
、_IOR
、_IOW
、_IOWR
:基于_IOC
的快捷宏,简化了常用场景的命令构造。
宏名 | 作用 | 等价于 |
---|---|---|
_IO | 生成一个无数据传输的命令(仅发送指令,不需要参数) | _IOC(_IOC_NONE, type, nr, 0) |
_IOR | 生成一个从内核读取数据的命令(用户→内核→用户) | _IOC(_IOC_READ, type, nr, sizeof(t)) |
_IOW | 生成一个向内核写入数据的命令(用户→内核) | _IOC(_IOC_WRITE, type, nr, sizeof(t)) |
_IOWR | 生成一个双向传输数据的命令(用户↔内核) | _IOC(_IOC_READ|_IOC_WRITE, type, nr, sizeof(t)) |
2. 参数详解
所有宏最终都调用 _IOC
,其参数如下:
_IOC(dir, type, nr, size)
参数 | 作用 |
---|---|
dir | 数据方向(_IOC_NONE 、_IOC_READ 、_IOC_WRITE 或其组合) |
type | 设备类型标识符(一个 ASCII 字符,如 'V' 表示视频设备) |
nr | 命令编号(0~255,区分同一设备的不同操作) |
size | 用户传递的数据大小(例如 sizeof(struct my_data) ) |
request
参数本质上是一个32位整数,它的二进制位被划分为4个关键字段:
有点啰嗦了,但是记得住一些
| 方向 (2 bits) | 类型 (8 bits) | 序号 (8 bits) | 数据大小 (14 bits) |
-
方向 (dir):用2位表示数据传输方向
_IOC_NONE
(0): 无数据传输(纯命令)_IOC_READ
(1): 从驱动读取数据_IOC_WRITE
(2): 向驱动写入数据_IOC_READ|_IOC_WRITE
(3): 双向传输
-
类型 (type):一个ASCII字符(如 'V' 表示视频设备),用于防止不同设备间的命令冲突
-
序号 (nr):命令的编号(0~255),区分同一设备的不同操作
-
数据大小 (size):用户传递的数据结构的大小(最大16KB)
//这里就知道了‘基础宏’和‘快捷宏’了
// 基础宏
#define _IOC(dir,type,nr,size) // 组合四个字段
// 快捷宏
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
到此为止吧,如果有不足的话,请看到这里的佬提醒我,不胜感激