1. ioctl
ioctl是用于设备控制的公共接口,可以实现用户态系统调用、设备驱动程序进行通信。ioctl在字符设备、块设备、网络设备等设备驱动程序中广泛使用。
1 |
|
1.1 用户态ioctl和驱动ioctl
系统调用过程中,实际上就是处于用户空间的用户态ioctl对应上特定内核空间中ioctl的过程
但是用户态没有访问内核空间的权限,因此需要系统先通过SWI(Software Interrupt)的方式从用户态陷入内核态,并利用sys_ioctl通过文件操作集找到对应的驱动ioctl,三者共同构成了Linux
I/O控制的核心机制。
1)用户态ioctl
在Linux中,glibc/标准库中(/usr/include/sys)封装着为用户程序提供的统一ioctl接口,在<sys/ioctl.h>中定义了ioctl函数:
1 | extern int ioctl (int __fd, unsigned long int __request, ...) __THROW; |
fd:设备文件描述符。表示要操作的设备对象。request:也常将该参数写作cmd,表示对设备进行控制的命令,设备驱动将根据cmd参数执行对应的操作- 该参数定义了用户与驱动的“协议”,虽然可以是任意值,但是Linux中还是提供了统一格式,将32位的int型数据划分成4个位段,来保证参数的唯一性。
...:可选参数,表示对设备进行控制的命令参数,可以是整数、指针等类型,用于传递给驱动程序。
下面讲解一下
request/cmd参数中的4个位段信息: -
dir-2bit:表示数据传输方向(四种) -
_IOC_NONE:无数据传输 -
_IOC_READ:数据从内核空间(设备)读取到用户空间 -
_IOC_WRITE:数据从用户空间写入到内核空间(设备) -
_IOC_READ|_IOC_WRITE:数据在用户空间和内核空间之间双向传输
-
type-8bit:设备类型标识符,用于区分不同设备。也成为幻数/魔数
-
nr(number)-8bit:命令序号,用于区分同一设备的不同命令,可以从0~255之间进行编号
-
size-14bit:表示用户传入的用户数据...部分参数的数据类型和长度,单位是字节
- 系统并不强制使用这个位字段,因此内核不会检查该字段。
1 | 31 30 16 15 8 7 0 |
假设按照这4个字段来划分cmd参数,在宏定义时会定义_IOC_DIRSHIFT、_IOC_TYPESHIFT、_IOC_NRSHIFT、_IOC_SIZESHIFT这4个移位值,然后通过移位操作来获取这4个字段的值。
通常,我们不直接使用ioctl函数,而是使用一些宏定义,如_IOC:
1 |
并利用_IOC衍生的接口_IO _IOR
_IOW _IOWR 等来生成指定的ioctl命令:
1 |
2)sys_ioctl
ioctl会让用户态触发中断陷入内核,所以ioctl本身也有一个系统调用号__NR_ioctl,在<arch/arm64/include/asm/unistd32.h>中定义:
中断陷入内核态之后,会根据寄存器传递过来的系统调用号(ARM64中是29),执行系统调用表中的(29)操作,也就是就是调用sys_ioctl()函数。
1 |
并在arch/arm64/kernel/sys.c中构建系统调用表(也就是构建系统调用号所映射的系统调用函数):
1 | /* 第一步:定义系统调用函数原型 */ |
上述系统调用表是基于syscall_table_64.h中定义的__SYSCALL映射关系来构建的,ioctl
在syscall_table_64.h中定义的映射关系为:
1 | __SYSCALL_WITH_COMPAT(29, sys_ioctl, compat_sys_ioctl) |
也就是系统调用会调用以下函数来处理:
1 | static __attribute__((unused)) |
这里sys_ioctl函数的为:
1 | asmlinkage long sys_ioctl(unsigned int fd, unsigned int cmd, unsigned long arg); |
直接通过系统调用表跳转到SYSCALL_DEFINE3
1 | SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg) |
在SYSCALL_DEFINE3中执行fd_file(f),深挖到该接口里面会发现它将对应的驱动程序的ioctl加入到了文件操作集中,由此路由到对应的驱动ioctl函数中执行对应的操作。
3)驱动ioctl
内核通过前面的fd_file(f)以及所带的命令号cmd路由到指定的驱动程序中。根据文件描述符
fd 找到对应的文件对象
struct file *filp,然后通过这个文件对象的操作集
file_operations 中的 unlocked_ioctl
成员,最终调用设备驱动中你自己实现的 mydev_ioctl()
函数。
假设我们某个中断的目标是路由到led_ioctl驱动程序中,该fd注册的
1 | // 在file_operations中注册(注册到文件操作集中,供给sys_ioctl路由到这(通过`unlocked_ioctl`)) |
在驱动程序中用switch对该设备的cmd号进行解析,并执行对应的操作
1 | // 字符设备驱动示例 |
执行里面的驱动函数就能实现完整的系统调用,最后再将结果返回给用户态。
1.2 完整调用链
从用户态到驱动层的完整调用链为:
1 | ioctl(fd, cmd, arg) // 用户空间调用 ioctl() |
2. 驱动ioctl实现例子:KVM
用户态:qemu
以 KVM 中的 migration 热迁移中需要确定虚机中 KVM 的扩展能力为例子,在用户态 qemu 中会通过下面的函数来发起系统调用请求:
1 | // 用户态:qemu 中 |
其中用户态通过句柄kvm_fd,也就是
"/dev/kvm"来让内核识别出当前是要链路到 kvm
的系统调用中。
通过执行ioctl(kvm_fd, KVM_CHECK_EXTENSION, KVM_CAP_DIRTY_LOG_RING)发起系统调用,其中我们关注的
KVM
的扩展能力的cmd就是KVM_CHECK_EXTENSION。
kvm_fd是 /dev/kvm 的文件描述符。cmd= KVM_CHECK_EXTENSION → 实际值是 _IO(KVMIO, 0x03)。arg= KVM_CAP_DIRTY_LOG_RING → 查询的扩展能力编号。
在 qemu 中KVM_CHECK_EXTENSION定义为:
1 | /* |
内核驱动层:kvm 的驱动代码
KVM 驱动在初始化时注册了 /dev/kvm 的
file_operations:
1 | static struct file_operations kvm_chardev_ops = { |
意味着该系统调用最终在内核中会调用kvm_dev_ioctl函数来处理指令:
1 | static long kvm_dev_ioctl(struct file *filp, |
此时:
ioctl = KVM_CHECK_EXTENSION
arg = KVM_CAP_DIRTY_LOG_RING
在kvm_vm_ioctl_check_extension_generic中我们就可以看到所有可以查询的
KVM 扩展功能,当前我们要查的是 KVM
是否支持KVM_CAP_DIRTY_LOG_RING:
1 | static int kvm_vm_ioctl_check_extension_generic(struct kvm *kvm, long arg) |
KVM
中某些扩展能力的使能和关闭也是按照上面的路径来的,根据不同的ioctl来选择干不同的事。
其中,关于设备/dev/kvm与file_operations kvm_chardev_ops的映射关系是通过
misc device(杂项设备)机制注册给内核的:
1 | kvm_init |
执行misc_register(&kvm_dev) 时,内核会自动在
/dev/
目录下创建一个设备节点,根据kvm_dev的定义,这里的设备名就是
"kvm"。所以最终 qemu
中需要使用的句柄路径就是:/dev/kvm。