工作中的一个项目涉及到了内核模块和用户程序两部分,之间通过 ioctl() 进行通讯,现在需要 64 位(x86_64)的 Linux,所使用内核版本是固定的,2.6.9。
首先,直接编译运行以后,发现一个错误:kernel: ioctl32(${APP_NAME}:${PID}): Unknown cmd fd(6) cmd(c01844f01){00} arg(0816ed60) on ${file}。Google 了一下,明白这是由于用户程序是 32 位而内核是 64 位的缘故。问题的缘由是知道了,得开始找解决方法了。刚开始还以为是内核模块中自定义的 ioctl()(通过 struct file_operations 定义) 返回了错误,后来在其中加上了调试信息,发现根本就没有进到函数中去。
后来找到了 Linux 64Bit 下的 ioctl和compat_ioctl ioctl32 Unknown cmd fd 这篇文章,虽然也说的是 2.6,但我在 The Linux Cross Reference 2.6.9 的 struct file_operations 中根本就没有找到 compat_ioctl(),进一步发现这是在 2.6.11 中加入的。虽然没能解决问题,但至少大致了解了问题出现真正的原因。
但还是不明白为什么会没有进到我自己的函数中去,在 LXR 中寻找“ioctl32”,最后在 fs/compat.c 中找到了“unknown cmd”,其实调用的是compat_sys_ioctl(),看下来,大概知道,整个内核空间中对于 ioctl() 的 request 是全局唯一的,在这里是 cmd。每一个 cmd 对应一个处理器,其类型是 typedef int (*ioctl_trans_handler_t)(unsigned int fd, unsigned int cmd,unsigned long arg, struct file *),当对应的处理器没有找到的时候,就会打出“unknown cmd”错误。但当时并不明白这个处理器是干什么用的。所以认为是命令在用户程序和内核中不一样所导致的。那这个命令是如何产生的呢?通常都会调用 _IOR、_IOW、_IOWR 等宏所产生的,其中最后一个是一个结构,宏展开的时候会获取这个结构的大小作为命令的一部分。而项目的结构中有指针和 time_t,而命令在用户程序以及内核中都要使用,并且一个是 32 位,一个是 64 位,结构体大小肯定不一样,最后所获得的命令也必然不同。所以这时候在调用 _IOWR 生成命令值的时候,将最后一个结构体换成另外一个,并保证其大小在 32 位和 64 位下是一样的。但即使这样,仍然出现相同的错误。
接着 Google,找到的信息都是零零散散,最后找到了 32bit ioctl support for 64bit kernels,比较系统的介绍了同样的问题。简单来说,因为应用程序是 32 位,内核(包括内核模块)是 64 位,而 32 位和 64 位的很多数据类型大小是不同的,比如 long 和指针类型,当应用程序需要和内核打交道的时候(比如系统调用,比如本文提到的 ioctl),就需要将数据转换一下,才不会导致数据错误。一般的系统调用内核已经对其参数进行了转换,而 ioctl 是和自定义的内核模块通讯,所以相应的转换就必须自行处理。而前面提到的“handler”就是自定义的转换函数,并且需要和命令对应起来。那么就需要在模块初始化的时候使用 register_ioctl32_conversion(unsigned int cmd, ioctl_trans_handler_t handler) 进行注册,退出的时候调用 unregister_ioctl32_conversion(unsigned int cmd) 将其注销。
在处理器中,数据转换如下:
-
#if defined CONFIG_COMPAT
-
int my_handler(unsigned int fd, unsigned int cmd,
-
unsigned long arg, struct file *) {
-
-
struct myarg_32bit *arg32 = (struct myarg_32bit *)arg;
-
struct myarg arg64;
-
mm_segment_t old_fs;
-
int err;
-
-
/* 32 -> 64*/
-
/* for each field in myarg */
-
err = 0;
-
err |= get_user(arg64.field1, &arg32->field1);
-
...
-
-
if (err) return -EFAULT;
-
old_fs = get_fs();
-
set_fs(KERNEL_DS); /* tell kernel to accept arguments in kernel space */
-
-
/*
-
* 接着调用真正的 ioctl,最终会调到 file_operations 中的 .ioctl,
-
* 也就是我们自己的处理函数
-
*/
-
err = sys_ioctl(file, cmd, (unsigned long)&arg64)
-
-
set_fs(old_fs);
-
-
/* 64 -> 32 */
-
/* for each field in myarg */
-
err |= put_user(arg64.field1, &arg32->field1);
-
...
-
-
return err;
-
}
-
#endif
原来文档中提到的函数原型比较老,建议直接查看内核源码,保证函数参数都正确。内核中也有一些例子,比如 arch/x86_64/ia32/ia32_ioctl.c,可以作为参考。
最后总结一下步骤:
-
ioctl()中的命令代码使用_IOWR等宏生成,必须要保证在 32 位和 64 位下相同,这样才能根据命令找到对应的处理器,并且在真正的处理函数中才能正确处理。 - 必须要注册处理器,先 32 -> 64,再调用
sys_ioctl(),再 64 -> 32。




Post new comment