文章目录
1.什么是ioctl?
ioctl
(输入/输出控制)是一种系统调用,用于设备驱动程序与用户空间应用程序之间进行设备特定的输入/输出操作。它通常用于对设备执行标准文件操作(如读取、写入、打开和关闭)之外的特殊操作。ioctl
提供了一种通用的机制,通过文件描述符与设备进行交互,支持各种类型的设备操作和控制。
ioctl
的工作原理如下:
-
定义命令和参数:设备驱动程序和用户空间应用程序之间的交互是通过命令和参数来实现的。命令通常是一个宏,表示特定的操作,而参数则是传递给驱动程序的数据。
-
调用
ioctl
系统调用:用户空间应用程序通过文件描述符调用ioctl
系统调用,传递命令和参数。例如:int ioctl(int fd, unsigned long request, ...);
这里,
fd
是文件描述符,request
是设备特定的操作命令,后面的省略号表示可选的参数。 -
设备驱动处理:驱动程序在内核空间接收到
ioctl
请求后,根据传入的命令和参数执行相应的操作。驱动程序通常会在其文件操作结构中实现一个ioctl
处理函数,用于处理不同的命令。
示例
假设有一个字符设备,需要通过ioctl
来设置设备的某个参数:
-
定义命令:
#define MY_IOCTL_SET_PARAM _IOW('M', 1, int)
-
用户空间调用:
int fd = open("/dev/my_device", O_RDWR); int param = 42; ioctl(fd, MY_IOCTL_SET_PARAM, ¶m); close(fd);
-
设备驱动实现:
static long my_device_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { switch (cmd) { case MY_IOCTL_SET_PARAM: int param; if (copy_from_user(¶m, (int __user *)arg, sizeof(param))) return -EFAULT; // 设置设备参数 break; default: return -EINVAL; } return 0; }
2.ioctl 与 write,read 有什么区别?
ioctl
与 write
和 read
的区别在于用途和灵活性。虽然 write
和 read
是用于基本的输入/输出操作的常见系统调用,但 ioctl
提供了更灵活的方式来处理设备特定的控制操作。
复杂的设备控制
write
和 read
主要用于传输数据,但设备通常需要进行更复杂的控制操作,例如:
- 改变设备的配置参数。
- 执行特定的硬件操作,如调整音量、设置网络接口参数、获取设备状态等。
例如,假设你有一个音频设备,你可能需要调整音量、均衡器设置等,这些操作并不适合通过 write
或 read
来实现,而更适合通过 ioctl
进行:
define AUDIO_SET_VOLUME _IOW('A', 1, int)
int fd = open("/dev/audio", O_RDWR);
int volume = 10;
ioctl(fd, AUDIO_SET_VOLUME, &volume);
close(fd);
多种数据类型和操作
ioctl
可以处理不同类型的数据和操作,而 write
和 read
仅限于简单的数据传输。ioctl
可以支持多种数据类型和复杂的控制逻辑,例如:
- 传递结构体。
- 执行读写组合操作。
struct device_status {
int power;
int signal_strength;
};
#define DEVICE_GET_STATUS _IOR('D', 1, struct device_status)
struct device_status status;
ioctl(fd, DEVICE_GET_STATUS, &status);
简化应用层代码
如果所有控制操作都通过 write
和 read
实现,应用层代码可能需要处理大量的特定协议逻辑,这增加了代码复杂性。ioctl
使得驱动程序可以集中处理这些复杂性,简化应用层代码。
区分数据和控制
write
和 read
本质上是用于数据传输的,而 ioctl
专门用于控制操作。将数据传输和控制操作分开,可以使代码更加清晰和模块化。
3.ioctl命令的构成
设备类型 | 代表这一类设备,一般用一个字母或一个8bit 数字来表示 |
---|---|
序列号 | 代表是这个设备的第几个命令 |
方向 | 表示命令的方向,只读(10) 只写(01) 写读(11) 无数据(00) |
数据大小 | 用户数据的大小,传递是数据类型,比如传递四个字节,可以写int |
ioctl
命令的构成一般包括一个由四个部分组成的宏定义,这四个部分分别指定了命令的类型、命令的编号、数据的方向以及数据的大小。这种构成方式在 Linux 中非常常见。具体来说,一个 ioctl
命令通常使用如下的宏定义来构建:
_IO(type, nr) // 无参数的命令
_IOR(type, nr, size) // 从设备读取数据
_IOW(type, nr, size) // 向设备写入数据
_IOWR(type, nr, size) // 读写数据
宏定义的组成部分
-
type:一个字符,用于表示设备类型。通常使用一个唯一的字符来标识设备类别,例如 ‘T’ 表示终端设备,‘S’ 表示串行设备等。
-
nr:命令编号。一个整数,用于区分同一类型设备的不同命令。
-
size:数据的大小。通常是数据结构的大小,用于指定传递给命令的数据大小。
具体的宏定义
这些宏的具体定义在系统头文件 <linux/ioctl.h>
中。以下是每个宏的详细说明:
_IO(type, nr)
定义一个无参数的 ioctl
命令:
#define MY_IOCTL_NO_PARAM _IO('M', 1)
_IOR(type, nr, size)
定义一个从设备读取数据的 ioctl
命令:
#define MY_IOCTL_READ _IOR('M', 2, int)
_IOW(type, nr, size)
定义一个向设备写入数据的 ioctl
命令:
#define MY_IOCTL_WRITE _IOW('M', 3, int)
_IOWR(type, nr, size)
定义一个读写数据的 ioctl
命令:
#define MY_IOCTL_READ_WRITE _IOWR('M', 4, struct my_struct)
举个栗子
假设有一个设备需要以下 ioctl
命令:
- 设置参数(无参数)。
- 获取状态(读取一个整数)。
- 设置配置(写入一个结构体)。
- 获取和设置配置(读写一个结构体)。
可以定义如下的 ioctl
命令:
#define MY_IOCTL_SET_PARAM _IO('M', 1)
#define MY_IOCTL_GET_STATUS _IOR('M', 2, int)
#define MY_IOCTL_SET_CONFIG _IOW('M', 3, struct my_config)
#define MY_IOCTL_GET_SET_CONFIG _IOWR('M', 4, struct my_config)
struct my_config {
int setting1;
int setting2;
};
4.ioctl命令的分解
OCTL(输入输出控制)命令由 32 位整数表示,它包含了方向、类型、编号和大小等信息。我们将通过分解一个具体的 IOCTL 命令来理解其构成和解码方法。
宏定义
在 Linux 内核中,IOCTL 宏通常定义在 <linux/ioctl.h>
中。这里是一些关键宏的定义
#define _IOC_DIR(nr) (((nr) >> _IOC_DIRSHIFT) & ((1 << _IOC_DIRBITS) - 1))
#define _IOC_TYPE(nr) (((nr) >> _IOC_TYPESHIFT) & ((1 << _IOC_TYPEBITS) - 1))
#define _IOC_NR(nr) (((nr) >> _IOC_NRSHIFT) & ((1 << _IOC_NRBITS) - 1))
#define _IOC_SIZE(nr) (((nr) >> _IOC_SIZESHIFT) & ((1 << _IOC_SIZEBITS) - 1))
举个栗子
假设我们有以下 IOCTL 命令定义:
#define DEVICE_CMD3 _IOW('M', 2, int)
我们将分解这个命令,并解码其各个部分。
首先,看看 _IOW
宏的定义:
#define _IOW(type, nr, size) _IOC(_IOC_WRITE, (type), (nr), sizeof(size))
将参数传入 _IOC
宏:
#define DEVICE_CMD3 _IOC(_IOC_WRITE, 'M', 2, sizeof(int))
让我们计算各个部分:
- 方向 (
_IOC_WRITE
):值为1U
- 类型 (
'M'
):ASCII 值为0x4D
- 编号 (
2
):值为2
- 大小 (
sizeof(int)
):在大多数平台上为4
计算
#define _IOC_WRITE 1U
#define 'M' 0x4D
#define nr 2
#define size 4
使用 _IOC
宏计算值:
#define DEVICE_CMD3 _IOC(_IOC_WRITE, 'M', 2, sizeof(int)) = \
(((1U) << _IOC_DIRSHIFT) | ((0x4D) << _IOC_TYPESHIFT) | ((2) << _IOC_NRSHIFT) | ((4) << _IOC_SIZESHIFT))
分解为二进制并位移:
- 方向 (
dir
):1U << 30
=0x40000000
- 类型 (
type
):0x4D << 8
=0x004D00
- 编号 (
nr
):2
=0x02
- 大小 (
size
):4 << 16
=0x00040000
组合这些部分:
0x40000000 | 0x00004D00 | 0x00000002 | 0x00040000 = 0x40044D02
解码
使用解码宏来验证这些值:
#include <stdio.h>
#include <linux/ioctl.h>
#define DEVICE_CMD3 _IOW('M', 2, int)
int main() {
printf("DEVICE_CMD3 value: 0x%x\n", DEVICE_CMD3);
printf("Type: %c\n", _IOC_TYPE(DEVICE_CMD3));
printf("Number: %d\n", _IOC_NR(DEVICE_CMD3));
printf("Direction: %d\n", _IOC_DIR(DEVICE_CMD3));
printf("Size: %d\n", _IOC_SIZE(DEVICE_CMD3));
return 0;
}
运行代码,输出如下:
DEVICE_CMD3 value: 0x40044d02
Type: M
Number: 2
Direction: 1
Size: 4
其中Write为 01 也就是代表方向为1
5.ioctl 设备使用
应用程序构成
ioctl
是一个用于设备控制的系统调用,允许用户空间应用程序与内核空间的设备驱动程序进行交互。它的主要功能是向设备发送控制命令或从设备读取状态信息。ioctl
的原型定义在 <sys/ioctl.h>
头文件中:
int ioctl(int fd, unsigned long request, ...);
参数分析
int fd
:- 表示文件描述符,是一个整数,通过
open
系统调用获取。该文件描述符指向需要控制的设备文件。
- 表示文件描述符,是一个整数,通过
unsigned long request
:- 表示控制命令,是一个无符号长整型。控制命令通常是一个通过宏定义的常量,用于指定需要执行的操作。命令值可以是
_IO
,_IOR
,_IOW
,_IOWR
之一。
- 表示控制命令,是一个无符号长整型。控制命令通常是一个通过宏定义的常量,用于指定需要执行的操作。命令值可以是
...
:- 表示可选参数。这些参数取决于
request
命令的类型,有些命令需要一个指针作为参数,有些则不需要。 - 如果命令不需要参数,这个位置可以省略;
- 如果需要参数,通常是一个指向数据结构的指针,用于传递数据或从设备读取数据。
- 表示可选参数。这些参数取决于
返回值
- 成功时,
ioctl
返回0
。 - 失败时,返回
-1
并设置errno
以指示错误类型。
无参数版
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/ioctl.h>
// Define IOCTL commands
#define DEVICE_CMD0 _IO('M', 0)
#define DEVICE_CMD1 _IO('M', 1)
#define DEVICE_CMD2 _IO('L', 0)
#define DEVICE_CMD3 _IOW('M', 2, int)
#define DEVICE_CMD4 _IOR('M', 3, int)
#define DEVICE_CMD5 _IOWR('M', 4, int)
#define DEVICE_PATH "/dev/countdown_device"
int main()
{
int fd;
int ret;
fd = open(DEVICE_PATH, O_RDWR);
if (fd < 0)
{
perror("open");
return 1;
}
// 每秒传递一次
while (1)
{
ret = ioctl(fd, DEVICE_CMD0); // 两个参数代表没有传递参数
sleep(1);
}
return 0;
}
驱动程序构成
驱动程序中,也要定义相同的命令 ,否则无法解析命令,可以提取出一个头文件,专门用来放置IOCTL宏命令
同样的文件结构体需要实现IOCTL操作
具体函数原型如下:
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
- file对应的是 应用程序的fd
- unsigned int cmd 对应的是具体宏命令
- 第三个参数对应的是参数
具体代码 (无命令参数版)
static long device_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
switch (cmd)
{
case DEVICE_CMD0:
printk(" null parmater cmd is invoke...");
break;
case DEVICE_CMD1:
break;
case DEVICE_CMD2:
break;
case DEVICE_CMD3:
break;
default:
return -ENOTTY; // 不支持的命令
}
return 0;
}
static struct file_operations fops = {
.open = device_open,
.release = device_release,
.read = device_read,
.write = device_write,
.unlocked_ioctl = device_ioctl
};
调用效果:(无参数版本)