你们好,我是老吴。
明天继续跟你们分享linus的文章,
文章有点长,都是linus的锅。
我的翻译策略是这样:
不会一字一句翻译,会改变抒发形式,
会简化代码剖析,必要的地方会使用英语术语。
水平有限,建议搭配原文阅读。
本文将讨论ARMLinux内核在自解压后,怎样在数学显存中执行自引导,直至才能在虚拟显存中执行用C编撰的通用内核代码。这儿默认你是了解一点ARM汇编语言和Linux内核基础知识的。
这一切的开始
ARMLinux内核在自解压并处理完设备树的更新后,会将程序计数器pc设置为stext()的化学地址,这儿是内核的代码段。这段代码可以在arch/arm/kernel/head.S中找到。
arch/arm/kernel/head.S
/*
* Kernel startup entry point.
* ---------------------------
*
* This is normally called from the decompressor code. The requirements
* are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
* r1 = machine nr, r2 = atags or dtb pointer.
*
[...]
__HEAD
ENTRY(stext)
[...]
__HEAD是定义在链接脚本里的一个宏:section:".head.text"。
通过查看ARM体系结构的链接脚本arch/arm/kernel/vmlinux.lds.S,可以晓得这个宏会将目标代码放置在内核最开始的位置。
这个位置对应的数学地址为:16MB的倍数+TEXT_OFFSET(32KB)。比如,你可能会在0x10008000之类的地址处找到stext(),前面的示例会基于这个假定的地址进行剖析。
head.S包含了一些针对不同的旧ARM平台的特殊处理代码,这促使我们很难从捉住程序的主干。ATAG和设备树的标准是后来才出现的,所以这种特殊代码多年来显得越来越复杂。
要理解后续的内容,你须要对分页虚拟显存(pagedvirtualmemory)有基本的了解。假如维基百科过分简约,请参阅Hennesy&Patterson的书:ComputerArchitecture:AQuantitativeApproach。这儿默认你是了解一点ARM汇编语言和Linux内核基础知识的。
虚拟显存的界定
首先,让我们先弄清楚内核是在虚拟显存中那个地址开始执行的。内核的虚拟显存基地址(kernelRAMbase)由PAGE_OFFSET决定,你可以对其进行配置。从名子上理解PAGE_OFFSET:firstpageofkernelRAM在虚拟显存中的偏斜位置。
你可以从4种显存界定方案中选择其中1个,这让我想起了快餐店的餐牌。目前在arch/arm/Kconfig中是这样定义的:
config PAGE_OFFSET
hex
default PHYS_OFFSET if !MMU
default 0x40000000 if VMSPLIT_1G
default 0x80000000 if VMSPLIT_2G
default 0xB0000000 if VMSPLIT_3G_OPT
default 0xC0000000
注意,假若芯片没有MMU(比如在ARMCortex-R类设备或旧的ARM7芯片上运行时),内核将在数学和虚拟显存之间创建1:1映射。之后页表将仅用于填充缓存但是地址不会被重画。这些情况下,PAGE_OFFSET的典型值就是0x00000000。没有使用虚拟显存的Linux内核被称为“uClinux”,在合并在主线内核之前,多年来它都是Linux内核的一个分支。
在使用Linux或任何POSIX类型的系统时,不使用虚拟显存被觉得是一种奇特的行为。因而,从现今开始,我们只考虑使用虚拟显存的情况。
PAGE_OFFSET,即virtualmemorysplitsymbol,在其上方的地址处创建一个虚拟显存空间,供内核留驻。内核将其所有代码、状态和数据结构(包括虚拟到化学显存转换表,即pagetable)都保存在这一区域的虚拟显存中:
0x40000000-0xFFFFFFFF
0x80000000-0xFFFFFFFF
0xB0000000-0xFFFFFFFF
0xC0000000-0xFFFFFFFF
这4种不同大小的内核空间里,0xC0000000-0xFFFFFFFF是迄今为止最常见的。这些方法下,内核有1GB的地址空间可供使用。
内核下方的虚拟显存空间,从0x00000000-PAGE_OFFSET-1,即一般地址0x00000000-0xBFFFFFFF(3GB)用于用户空间代码。这意味着您可以豁达地为程序提供比可用化学显存更多的虚拟显存空间,这些做法被称为overcommit。每次启动一个新的用户空间进程时,它都觉得它有3GB的显存可以使用!overcommit仍然是Unix系统自1970年代诞生以来的一个特点。
嵌入式物联网须要学的东西真的特别多,千万不要学错了路线和内容,引起薪水要不起来!
无偿分享你们一个资料包,差不多150多G。上面学习内容、面经、项目都比较新也比较全!某鱼上买恐怕起码要好几十。
点击这儿找小助理0元发放:加陌陌发放资料
为何有四种不同的界定方法?
答案很显著:ARM大量用于嵌入式系统,这种系统可以是用户空间密集型(比如普通平板笔记本或手机,甚至台式计算机)或内核空间密集型(比如路由器)。大多数系统都是用户空间密集型,或化学显存太小以至于分拆并不重要,因而最常见形式是PAGE_OFFSET=0xCxC0000000。
关于那些插图的注意事项:当我说显存“高于”某物时,我的意思是图片中的较高位置,顺着箭头,朝向更高的地址。我晓得有些人觉得这是不合逻辑的,并将数字倒置,底部为0xFFFFFFFF,但这是我个人的偏好,也是大多数硬件指南中使用的约定。
当你有足够大的显存和而且应用场景是内核密集型,比如大容量的显存(比如4GB显存)路由器或NAS的话,假如你希望内核能否将其中一些显存用于pagecache和networkcache以提高系统的性能,可以选择更大的内核空间,比如在极端情况下:PAGE_OFFSET=0x40000000。
内核空间的映射会仍然存在,即使是内核正在执行用户空间代码时也是这么。这个看法是这样的,通过保持内核空间永久映射,从用户空间到内核空间的上下文切换会显得十分快:当用户空间进程想要向内核寻问个别东西时,不须要替换任何页表。只需发出一个软中断(softwaretrap)来切换到特权模式(supervisormode)并执行内核代码,无需改动虚拟显存相关的设置。
不同用户空间的进程之间的上下文切换也显得更快:你只须要替换页表的较低部份。内核空间的映射一般很简单,它映射的是预先确定的化学显存块而且是线性映射linux 开发arm,甚至储存在一个特殊的地方:translationlookasidebufferlinux 开发arm,因而能更快地步入内核空间。内核空间的地址总是存在的,而且总是线性映射,永远不会形成pagefault。
目前我们是在那里运行?
我们继续查看arch/arm/kernel/head.S里的stext()。
下一步是处理我们目前正在显存的某个未知位置运行的事实。内核可以被加载到任何地址(只要它是一个合理的质数地址)并直接执行,所以如今我们须要处理它。因为内核代码不是位置无关的,它在编译后被链接器链接到某个地址处执行,而我们还不晓得是那个地址。
内核首先检测一些特殊功能,如虚拟化扩充和LPAE(小型化学地址扩充),之后做了下边这件事:
arch/arm/kernel/head.S
adr r3, 2f
ldmia r3, {r4, r8}
sub r4, r3, r4 @ (PHYS_OFFSET - PAGE_OFFSET)
add r8, r8, r4 @ PHYS_OFFSET
[...]
2: .long .
.long PAGE_OFFSET
.long.是在链接的时侯就分配给lable2的地址,也就是说我们可以通过label2获得其链接地址,这个地址属于内核空间,通常在0xCxC0000000之上的某个位置。
然后是常量PAGE_OFFSET,它大机率是0xCxC0000000。
其余的几行汇编代码是在通过lable2的运行地址和链接地址相乘的形式来推断出数学显存的起始偏斜(PHYS_OFFSET),将其保存在r8中,假定其值为0x10000000。
旧的ARM内核有一个名为PLAT_PHYS_OFFSET的符号,它包含这个偏斜量,这是在编译时时指定的。我们如今不再这样做了,正如我们后面听到的那样,动态地估算下来。倘若您使用的操作系统不如Linux这么成熟,您会发觉开发人员一般都会在编译时指定,使事情显得简单些:化学显存的起始偏斜量是一个常数。Linux发展成现今这样,是由于我们须要在各类显存布局上处理单个内核映像的启动。
化学显存到虚拟显存映射。
一些关于PHYS_OFFSET的规定:它须要遵循一些基本的对齐要求。当我们要确定第一个数学显存块的位置时,是通过执行PHYS=pc&0xFxF8000000来确定的,这意味着数学显存必须是128MB对齐。诸如,假如它从0x00000000开始,那就太好了。
当内核是以XIP“executeinplace”的形式执行时,就须要有有一些特殊的考虑,但我们把这些情况放到一边,这是另一个奇怪的地方,甚至比不使用虚拟显存更不常见。
请注意另一件事:你可能尝试加载未压缩的内核并启动它,之后发觉内核对放置它的位置非常挑剔。此时,你最好将其加载到0x00008000或0x10008000之类的数学地址(假定你的TEXT_OFFSET是0x8000)。假如你使用压缩内核,则可以防止此问题,由于解压缩器会将内核解压缩到合适的位置(一般为0x00008000)并为你解决此问题。这正是人们认为压缩内核正常工作是一种常态的另一个诱因。
给P2V打补丁(PatchingPhysicaltoVirtual)
如今我们有了运行时应处于的虚拟显存地址和实际执行时的数学显存地址之间的偏斜量(PHYS_OFFSET-PAGE_OFFSET),接出来我们第一个要处理的东西就是CONFIG_ARM_PATCH_PHYS_VIRT。
创建此符号是由于内核开发者想实现这样的功能:无需重新编译,也能让同一个内核在不同显存配置的系统上启动。内核被编译成在某个虚拟地址处执行,比如0xCxC0000000,并且始终可以被加载到化学显存0x10000000处,或则在0x40000000处,或其他某个地址处去执行。
内核中的大多数符号是不须要我们额外关心的:它们运行时的地址就是其链接时的虚拟地址上,即0xCxC0000000以后的这些地址。并且如今我们不是在编撰用户空间的程序,事情没这么容易。我们必须晓得我们正在执行的数学显存,由于我们是内核,这意味着我们须要在页表中设置数学到虚拟的映射,并定期更新那些页表。
内核不晓得自己将在数学显存中的那个位置运行,而我们也不能依赖任何廉价的方法,比如编译常常量,这是作弊,那会创建无法维护的饱含幻数的代码。
为了在数学地址和虚拟地址之间转换,内核有两个函数:__virt_to_phys()和__phys_to_virt()用于相互转换内核地址(不会用于非内核地址)。
这些转换在显存空间中是线性的,可以通过简单的乘法或除法来实现。这就是我们正要做的事情,我们给它起了个名子叫“P2Vruntimepatching”。该方案由NicolasPitre、EricMiao和RussellKing在2011年发明,2013年SantoshShilimkar将该方案扩充到适用于LPAE系统,非常是TIKeystoneSoC。
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;
}
__pv_stub()是用汇编实现加减操作的宏。LPAE对超过32位地址的支持使此代码显得愈发复杂,但总体思路是相同的。
每每在内核中调用__virt_to_phys()或__phys_to_virt()时,它就会被替换为来自arch/arm/include/asm/memory.h的一段内联汇编代码,之后链接器会换到名为.pv_table的section嵌入式linux论坛,之后向该section里添加一个条目,条目的内容是一个表针,它指向上面提及的汇编代码。这意味着.pv_tablesection似乎就是一个表,上面的每一个条目都是一个表针,每位表针都指向内核调用了__virt_to_phys()或__phys_to_virt()处的汇编代码。
在启动过程中,我们将遍历这个表,取出每位表针,检测它指向的每条指令,之后用(PHYS_OFFSET-PAGE_OFFSET)去给那些指令打补丁。
在初期启动过程中,每位调用了执行数学显存到虚拟显存的转换汇编宏的地方都须要打补丁
相关的代码:
__fixup_pv_table:
adr r0, 1f
ldmia r0, {r3-r7}
mvn ip, #0
subs r3, r0, r3 @ PHYS_OFFSET - PAGE_OFFSET
add r4, r4, r3 @ adjust table start address
add r5, r5, r3 @ adjust table end address
add r6, r6, r3 @ adjust __pv_phys_pfn_offset address
add r7, r7, r3 @ adjust __pv_offset address
mov r0, r8, lsr #PAGE_SHIFT @ convert to PFN
str r0, [r6] @ save computed PHYS_OFFSET to __pv_phys_pfn_offset
(...)
b __fixup_a_pv_table
1: .long .
.long __pv_table_begin
.long __pv_table_end
2: .long __pv_phys_pfn_offset
.long __pv_offset
核心内容就是,先估算出pv_table的起始地址和结束地址linux系统官网,之后遍历该表,对每一个条目都调用__fixup_a_pv_table,给该条目所指向的汇编代码打补丁。
为何我们进行如此复杂的操作,而不仅仅是将偏斜量储存在变量中?
这是出于效率缘由:它坐落内核的热数据路径上。更新页表和交叉引用数学到虚拟内核显存的操作对性能的要求是及其严苛的,所有使用内核虚拟显存的场景,无论是blocklayer或networklayer的操作,还是用户到内核空间的转换,原则上任何通过内核的数据会在某个时间点调用那些函数。所以,她们必须很快。
里面的做法不能称为简单的解决方案,事实上,它是一个十分复杂的解决方案。并且它能正常工作,而且十分高效!