ioctl基于接口
ioctl()是应用程序与设备驱动程序进行接口交互的最常见方式。它灵活且易于通过添加新命令进行扩展,并且可以通过字符设备、块设备以及套接字和其他特殊文件描述符进行传递。
然而,很容易错误地定义ioctl命令,并且很难在不破坏现有应用程序的情况下修复它们,因此本文档试图帮助开发人员正确使用它。
命令编号定义
命令编号或请求编号是传递给ioctl系统调用的第二个参数。虽然这可以是任何32位数字,用于唯一标识特定驱动程序的操作,但在定义它们时有一些约定。
include/uapi/asm-generic/ioctl.h提供了四个宏,用于定义遵循现代约定的ioctl命令:_IO、_IOR、_IOW和_IOWR。对于所有新命令,应使用这些宏,并使用正确的参数:
_IO/_IOR/_IOW/_IOWR
宏名称指定参数的使用方式。它可以是指向要传递到内核的数据的指针(_IOW),从内核传出的数据的指针(_IOR),或者两者都有的指针(_IOWR)。_IO可以表示没有参数的命令或传递整数值而不是指针的命令。建议仅对没有参数的命令使用_IO,并使用指针传递数据。- type
一个8位数字,通常是特定子系统或驱动程序的字符字面值,并在Ioctl Numbers中列出 - nr
一个8位数字,用于标识特定命令,在给定'type'值的情况下是唯一的 - data_type
参数指向的数据类型的名称,命令编号将sizeof(data_type)值编码为13位或14位整数,从而限制了参数的最大大小为8191字节。注意:不要将sizeof(data_type)类型传递给_IOR/_IOW/IOWR,因为这将导致编码sizeof(sizeof(data_type)),即sizeof(size_t)。_IO没有data_type参数。
接口版本
某些子系统在数据结构中使用版本号,以重载具有不同参数解释的命令。
这通常是一个不好的做法,因为更改现有命令往往会破坏现有应用程序。
更好的方法是添加一个具有新编号的新ioctl命令。为了兼容性,旧命令仍然需要在内核中实现,但可以将其包装在新实现的周围。
返回代码
ioctl命令可以返回负错误代码,如errno(3)中所述;这些错误代码在用户空间中转换为errno值。成功时,返回代码应为零。也可以返回正的'long'值,但不建议这样做。
当使用未知命令编号调用ioctl回调时,处理程序返回-ENOTTY或-ENOIOCTLCMD,这也导致从系统调用返回-ENOTTY。一些子系统在这里返回-ENOSYS或-EINVAL是出于历史原因,但这是错误的。
在Linux 5.5之前,compat_ioctl处理程序需要返回-ENOIOCTLCMD,以便使用回退转换为本机命令。由于现在所有子系统都负责处理兼容模式,因此不再需要这样做,但在将错误修复移植到旧内核时,考虑到这一点可能很重要。
时间戳
传统上,时间戳和超时值被传递为struct timespec或struct timeval,但由于64位time_t的移动后,这些结构在用户空间中的定义不兼容,因此存在问题。
当需要单独的秒/纳秒值时,可以使用struct __kernel_timespec
类型嵌入到其他数据结构中,或直接传递给用户空间。然而,这仍然不是理想的解决方案,因为该结构既不匹配内核的timespec64,也不完全匹配用户空间的timespec。可以使用get_timespec64()和put_timespec64()辅助函数来确保布局与用户空间兼容,并正确处理填充。
由于将秒转换为纳秒很简单,但相反需要昂贵的64位除法,因此简单的__u64纳秒值可能更简单和更高效。
超时值和时间戳应理想地使用CLOCK_MONOTONIC时间,如ktime_get_ns()或ktime_get_ts64()所返回的那样。与CLOCK_REALTIME不同,这使得时间戳免受由于闰秒调整和clock_settime()调用而向前或向后跳动的影响。
对于需要在重启或多台机器之间持久存在的CLOCK_REALTIME时间戳,可以使用ktime_get_real_ns()。
32位兼容模式
为了支持在64位机器上运行的32位用户空间,每个实现ioctl回调处理程序的子系统或驱动程序也必须实现相应的compat_ioctl处理程序。
只要遵循数据结构的所有规则,这很容易,只需将.compat_ioctl指针设置为诸如compat_ptr_ioctl()或blkdev_compat_ptr_ioctl()之类的辅助函数即可。
compat_ptr()
在s390架构上,31位用户空间对于数据指针具有模糊的表示,忽略了最高位。在以compat模式运行此类进程时,必须使用compat_ptr()辅助函数来清除compat_uptr_t的最高位,并将其转换为有效的64位指针。在其他架构上,此宏仅执行到void __user *
指针的转换。
在compat_ioctl()回调中,最后一个参数是一个unsigned long,可以根据命令将其解释为指针或标量。如果它是标量,则不能使用compat_ptr(),以确保64位内核对于带有设置了最高位的参数的行为与32位内核相同。
对于仅接受指向兼容数据结构的指针的驱动程序,可以使用compat_ptr_ioctl()辅助函数来替代自定义的compat_ioctl文件操作。
结构布局
兼容的数据结构在所有架构上具有相同的布局,避免了所有问题成员:
- long和unsigned long的大小与寄存器相同,因此它们可以是32位或64位宽,并且不能用于可移植的数据结构。固定长度的替代类型是__s32、__u32、__s64和__u64。
- 指针也有同样的问题,除了需要使用compat_ptr()之外。最好的解决方法是在指针的位置使用__u64,这需要在用户空间中进行uintptr_t的转换,并在内核中使用u64_to_user_ptr()将其转换回用户指针。
- 在x86-32(i386)架构上,64位变量的对齐方式只有32位,但在包括x86-64在内的大多数其他架构上,它们是自然对齐的。这意味着在x86-64上,像这样的结构:
struct foo { __u32 a; __u64 b; __u32 c; };
- 在a和b之间有4个字节的填充,在结尾处还有另外4个字节的填充,但在i386上没有填充,并且它需要一个compat_ioctl转换处理程序来在这两种格式之间进行转换。
- 为了避免这个问题,所有的结构都应该有自然对齐的成员,或者在隐式填充的位置添加显式保留字段。可以使用pahole工具来检查对齐情况。
- 在ARM OABI用户空间,结构被填充到32位的倍数,如果它们不以32位边界结束,这会导致一些结构与现代EABI内核不兼容。
- 在m68k架构上,结构成员不能保证具有大于16位的对齐方式,这在依赖隐式填充时会成为一个问题。
- 位字段和枚举通常按照预期工作,但它们的一些属性是实现定义的,因此最好完全避免在ioctl接口中使用它们。
- char成员可以是有符号的或无符号的,这取决于架构,因此应该使用__u8和__s8类型来表示8位整数值,尽管对于固定长度的字符串,使用char数组更清晰。
信息泄漏
未初始化的数据不能被复制回用户空间,因为这可能导致信息泄漏,从而可以用来破解内核地址空间布局随机化(KASLR),从而帮助攻击。
因此(并且为了兼容性支持),最好避免数据结构中的任何隐式填充。在现有结构中存在隐式填充的情况下,内核驱动程序必须小心地在将结构的实例复制到用户空间之前完全初始化该结构的实例。通常在分配给各个成员之前调用memset()来完成这个操作。
子系统抽象
虽然一些设备驱动程序实现了自己的ioctl函数,但大多数子系统为多个驱动程序实现了相同的命令。理想情况下,子系统具有一个.ioctl()处理程序,它将参数从用户空间复制到子系统特定的回调函数中,通过正常的内核指针传递给它们。
这有助于解决以下问题:
- 如果用户空间ABI没有微妙的差异,那么为一个驱动程序编写的应用程序更有可能适用于同一子系统中的另一个驱动程序。
- 用户空间访问和数据结构布局的复杂性在一个地方完成,减少了实现错误的可能性。
- 当ioctl在多个驱动程序之间共享时,有经验的开发人员更有可能对接口中的问题进行审查,而当它仅在单个驱动程序中使用时则不太可能。
ioctl的替代方案
有许多情况下,ioctl并不是解决问题的最佳方案。替代方案包括:
- 系统调用是一个更好的选择,用于不与物理设备绑定或受字符设备节点的文件系统权限限制的系统范围功能。
- netlink是通过套接字配置任何与网络相关的对象的首选方式。
- debugfs用于调试功能的临时接口,不需要将其作为稳定接口暴露给应用程序。
- sysfs是一种很好的方式,用于公开与文件描述符无关的内核对象的状态。
- configfs可用于比sysfs更复杂的配置。
- 自定义文件系统可以提供简单的用户界面,但会给实现带来很多复杂性。