我在一个简单的linux驱动程序测试(arm64)中看到一个奇怪的情况。
用户程序调用设备驱动程序的ioctl并传递uint64_t的数组'arg'作为参数。顺便说一下,arg[2]包含一个指向应用中变量的指针。下面是代码片段:
case SetRunParameters:
copy_from_user(args, (void __user *)arg, 8*3);
offs = args[2] % PAGE_SIZE;
down_read(¤t->mm->mmap_sem);
res = get_user_pages( (unsigned long)args[2], 1, 1, &pages, NULL);
if (res) {
kv_page_addr = kmap(pages);
kv_addr = ((unsigned long long int)(kv_page_addr)+offs);
args[2] = page_to_phys(pages) + offset; // args[2] changed to physical
}
else {
printk("get_user_pages failed!n");
}
up_read(¤t->mm->mmap_sem);
*(vaddr + REG_IOCTL_ARG/4) = virt_to_phys(args); // from axpu_regs.h
printk("ldd:writing %x at %pxn",cmdx,vaddr + REG_IOCTL_CMD/4); // <== line 248. not ok w/o this printk line why?..
*(vaddr + REG_IOCTL_CMD/4) = cmdx; // this command is different from ioctl cmd!
put_page(pages); //page_cache_release(page);
break;
case ...
我在上面的代码中标记了第248行。如果我注释掉那里的printk,就会出现一个陷阱,虚拟机崩溃(我在一个qemu虚拟机上这样做)。其中,cmdx
是根据应用程序的ioctl命令设置的整数值,vaddr
是设备的虚拟地址(从ioremap获取)。如果我保留printk,它就会像我期望的那样工作。什么情况下会发生这种情况?
通过简单的C结构(如*(vaddr + REG_IOCTL_ARG/4)
)访问内存映射寄存器是一个坏主意。如果访问是volatile
合格的,那么在某些平台上可以使用它,但是在某些平台上它不能可靠地工作,或者根本不能工作。访问内存映射寄存器的正确方法是通过#include <asm/io.h>
或#include <linux/io.h>
声明的函数。1
内存映射寄存器访问的函数在Linux内核文档Bus-Independent Device access中有描述。
这个代码:
*(vaddr + REG_IOCTL_ARG/4) = virt_to_phys(args);
*(vaddr + REG_IOCTL_CMD/4) = cmdx;
可以重写为:
writel(virt_to_phys(args), vaddr + REG_IOCTL_ARG/4);
writel(cmdx, vaddr + REG_IOCTL_CMD/4);
1特定总线类型(如PCI)的写顺序如果寄存器写的顺序很重要,则可能需要额外的代码在不同寄存器的写之间读取寄存器。这是因为写作是"张贴"的。异步地到PCI总线,并且PCI设备可能无序地处理对不同寄存器的写操作。在之前所有的写操作都被处理完之前,设备不会处理中间寄存器的读操作,所以它可以用来强制发布的写操作的顺序。