摘自 Linus Torvalds 于 1996 年 9 月 27 日发送给 linux-kernel 邮件列表的消息,并经过编辑。我将借此机会向所有设备驱动程序编写者讲述可移植性背后丑陋的秘密。情况实际上比物理地址和虚拟地址更糟糕。
aha1542 是一个总线主控设备,并且[发布到 linux-kernel 列表的补丁]使驱动程序向控制器提供缓冲区的物理地址,这在 x86 上是正确的,因为所有总线主控设备都直接看到物理内存映射。
然而,在许多设置中,实际上有三种不同的方式来看待内存地址,在这种情况下,我们实际上想要第三种,即所谓的“总线地址”。
本质上,内存寻址的三种方式是(这是“真实内存”,即普通 RAM;稍后会介绍其他细节)
现在,举个例子,在 PReP(PowerPC 参考平台)上,CPU 看到的内存映射类似于这样(以下来自记忆)
因此,当 CPU 希望任何总线主控器写入物理内存 0 时,它必须向主控器提供地址0x80000000作为内存地址。
因此,例如,根据内核在 PPC 上的实际映射方式,您最终可能会得到像这样的设置
类似地,在 alpha 上,正常的转换是
无论如何,要查找所有这些转换,您需要执行
#include <asm/io.h> phys_addr = virt_to_phys(virt_addr); virt_addr = phys_to_virt(phys_addr); bus_addr = virt_to_bus(virt_addr); virt_addr = bus_to_virt(bus_addr);现在,您何时需要这些地址?
当您实际上要从内核访问该指针时,您需要虚拟地址。因此,您可以有类似这样的代码(来自 aha1542 驱动程序)
/* * this is the hardware "mailbox" we use to communicate with * the controller. The controller sees this directly. */ struct mailbox { __u32 status; __u32 bufstart; __u32 buflen; .. } mbox; unsigned char * retbuffer; /* get the address from the controller */ retbuffer = bus_to_virt(mbox.bufstart); switch (retbuffer[0]) { case STATUS_OK: ...另一方面,当您有一个想要提供给控制器的缓冲区时,您需要总线地址
/* ask the controller to read the sense status into "sense_buffer" */ mbox.bufstart = virt_to_bus(&sense_buffer); mbox.buflen = sizeof(sense_buffer); mbox.status = 0; notify_controller(&mbox);并且您通常永远不想使用物理地址,因为您无法从 CPU 使用它(CPU 只使用翻译后的虚拟地址),也无法从总线主控器使用它。
那么,我们为什么要关心物理地址呢?在某些情况下,我们确实需要物理地址,只是在正常代码中不常用。如果您使用内存映射,则需要物理地址,例如,因为remap_page_range()mm 函数需要重新映射的内存的物理地址(内存管理层不知道 CPU 外部的设备,因此它不需要知道“总线地址”等)。
注意 注意 注意! 以上只是整个方程式的一部分。以上仅讨论“真实内存”,即 CPU 内存,即 RAM。
还有一种完全不同类型的内存,那就是 PCI 或 ISA 总线上的“共享内存”。这通常不是 RAM(尽管在视频显卡的情况下,它可以是仅用于帧缓冲区的普通 DRAM),但可以是网卡中的数据包缓冲区等。
这种内存称为“PCI 内存”或“共享内存”或“IO 内存”等等,并且只有一种访问它的方式:readb/writeb和相关函数。您永远不应该获取此类内存的地址,因为实际上您无法使用这样的地址做任何事情:它在概念上与“真实内存”不在同一内存空间,因此您不能只是解引用指针。(可悲的是,在 x86 上,它确实在同一内存空间中,因此在 x86 上,实际上可以直接解引用指针,但这不具有可移植性)。
对于此类内存,您可以执行以下操作
/* * read first 32 bits from ISA memory at 0xC0000, aka * C000:0000 in DOS terms */ unsigned int signature = readl(0xC0000);
/* * remap framebuffer PCI memory area at 0xFC000000, * size 1MB, so that we can access it: We can directly * access only the 640k-1MB area, so anything else * has to be remapped. */ char * baseptr = ioremap(0xFC000000, 1024*1024); /* write a 'A' to the offset 10 of the area */ writeb('A',baseptr+10); /* unmap when we unload the driver */ iounmap(baseptr);
/* get the 6-byte ethernet address at ISA address E000:0040 */ memcpy_fromio(kernel_buffer, 0xE0040, 6); /* write a packet to the driver */ memcpy_toio(0xE1000, skb->data, skb->len); /* clear the frame buffer */ memset_io(0xA0000, 0, 0x10000);
请注意,内核版本 2.0.x(和更早版本)错误地称为ioremap() "vremap()". ioremap()才是正确的名称,但我在最初编写时没有考虑清楚。必须同时支持两者的用户可以执行类似以下的操作
/* support old naming sillyness */ #if LINUX_VERSION_CODE < 0x020100 #define ioremap vremap #define iounmap vfree #endif在其源文件的顶部,然后即使在 2.0.x 系统上,他们也可以使用正确的名称。
以上听起来比实际情况更糟糕。大多数实际驱动程序实际上并没有做那么复杂的事情(或者更确切地说:复杂性不在于实际的 IO 访问,而在于错误处理和超时等)。修复驱动程序通常不难,并且在许多情况下,代码实际上看起来更好
unsigned long signature = *(unsigned int *) 0xC0000;与
unsigned long signature = readl(0xC0000);我认为第二个版本实际上更具可读性,不是吗?
Linus