给物理地址打补丁,转换成虚拟地址(P2V)

现在我们有了虚拟内存和物理内存之间的偏移量。接下来就会遇到第一个Kconfig符号:CONFIG_ARM_PATCH_PHYS_VIRT。

建立这个符号的原因是,开发人员需要让内核在不重新编译的情况下,在不同内存配置的系统中引导。内核可能被编译成在特定的虚拟地址(如 0xC0000000 )处执行,但实际可能被加载到 0x10000000 (如本文中的例子),也有可能是 0x40000000 或其他地址。

当然,内核中的绝大多数符号不需要担心这一点,因为它们都在虚拟内存中执行,对于它们而言,它们永远在 0xC0000000 处执行。但我们写的不是用户空间的程序,所以事情没那么简单。我们必须知道执行所处位置的物理地址,因为我们就是内核,意思就是我们需要在页表中建立物理地址到虚拟地址的映射,还需要经常更新这些页表。

而且,由于我们并不知道实际运行所处的物理地址,所以没办法依赖诸如编译时常量等技巧。这些技巧等于作弊,而且会造成非常难以维护的代码。

内核有两个函数可以在物理地址和虚拟地址之间进行转换: _virtto_phys() 和 _physto_virt() (仅限于内核内存使用的地址)。在内存空间中,这个转换是线性的(每个方向上只需使用一个偏移量),所以简单的加减法就可以实现。因此得名“P2V运行时补丁”。该方法由Nicolas Pitre、Eric Miao和Russell King于2011年发明,2013年Santosh Shilimkar将其扩展并应用到了LPAE系统上,特别是TI Keystone SoC上。

这里的重点是,如果对于一个物理地址 PHY 和一个内核虚拟地址 VIRT (两者的概念参见上一幅插图),以下关系成立的话:

PHY = VIRT – PAGE_OFFSET + PHYS_OFFSET
VIRT = PHY – PHYS_OFFSET + PAGE_OFFSET

那么根据算术定律可知,下述关系依然成立:

PHY = VIRT + (PHYS_OFFSET – PAGE_OFFSET)
VIRT = PHY – (PHYS_OFFSET – PAGE_OFFSET)

所以,只需给虚拟地址加上一个常量就可以得到物理地址,给物理地址减去一个常量就可以得到虚拟地址。所以最初的代码大概如下所示:

static inline unsigned long __virt_to_phys(unsigned long x)
{


    unsigned long t;


    __pv_stub(x, t, "add");


    return t;


}





static inline unsigned long __phys_to_virt(unsigned long x)


{


    unsigned long t;


    __pv_stub(x, t, "sub");


    return t;


}

_pvstub() 包含一个汇编宏,用于执行加法或减法。从那以后,LPAE开始支持多于32位的地址,因此这段代码变得更为复杂,但基本原理是不变的。

每当在内核中调用 _virtto_phys() 或 _physto_virt() 时,它们会被替换成一段内联汇编代码(位于 arch/arm/include/asm/memory.h),然后连接器就会将节切换到一个名为 .pv_table 的节上,然后在该节中添加一个指针,指向刚刚添加的汇编指令的位置。这就是说,.pv_table 接会扩展成一个指针的表格,指向所有这些内联汇编代码所在的位置。

在引导过程中,我们会遍历整个表格,取出每一个指针,检查指针所指位置的指令,然后利用物理和虚拟内存之间的偏移量对这些指令打补丁。

04_地址转换 - 图1


图:每个利用汇编宏将物理地址转换为虚拟地址的地方,都在引导过程的前期进行打补丁。

为什么要进行如此复杂的操作,而不是简单地将偏移量保存到一个变量中呢?这是为了提高效率,因为这些路径会被反复执行。更新页表以及从物理内存到虚拟内核内存的交叉引用的调用,其性能极其关键。所有访问内核虚拟内存的用例,不论是设备块层还是网络层的操作,甚至是用户空间到内核空间的转译,理论上任何流经内核的数据都会在某个时间点调用这些函数。所以它们必须非常非常快。

这个解决方案并不简单,实际上是非常复杂的解决方案,但效率非常高!