译者注:原文为LWN上的系列文章,作者为Joël Porquet。 这篇文章是第一篇,一共三篇。 作者在2016年的Embedded Linux Conference上有的讲解视频,可见Youtube或Bilibili。
尽管一个简单的移植只需要大约4000行代码——具体来说,将最新(译者注:该博客发表于2015年8月26日)的Linux 4.2-rc1移植到没有MMU的Hitachi 8/300上只需要3775行代码——但是将Linux内核移植到新的处理器体系结构上仍然是一个困难的过程。
在花费了无数个小时熟悉许多支持的架构后,我发现了一个适用于大多数移植精妙设计的框架。这样一个框架逻辑上可以分为两个相互交织的部分。第一部分是引导代码(boot code),即当内核从bootloader手中接管那一刻开始到init
执行为止期间执行的架构特定的代码。第二部分涉及在引导阶段完成且内核正常运行后定期执行的架构特定代码。第二部分包括了开启一个新线程,处理硬件中断或软件异常,向用户应用中读取/写入数据,服务系统调用等等。
一个新的移植是必要的吗?
就像LWN在去年(2014年)发表的一篇文章中的另一个移植经验中所说,做移植有三个意思:
它可以是一直到一个带有已支持的处理器的新板子上,或者是它可以是一个从已支持的处理器家族产生的新处理器,或者是移植到一个全新架构上。
有时关于一个人是否应该从零开始一个新的移植这个问题是非常清楚的——如果新的处理器是新的指令集,那么它肯定需要从零开始移植。但有时候又不那么清晰,在我的案例中,我花费了两周时间来搞清楚这个问题。
在2013年5月,我刚刚被法国学术计算机实验室LIP6雇佣,来将linux移植到这个SoC实验室正在设计的学术性质的处理器架构TSAR上。TSAR是一个遵循很多当前流行的趋势的架构:许多小的,单发射,低功耗处理器核组成一个相当规模的NoC。它同时也又一些革命性创新:一个用于数据和指令缓存的纯硬件的缓存一致性协议,以及TLB和分布式共享内存。
我的问题在于处理器核兼容MIPS32指令集,意味着移植可能属于第二个范畴:“从一个已支持的处理器家族产生的新处理器”。但是由于TSAR有一个和任何MIPS处理器都非常不同的虚拟内存模型,有时必须把整个文件用#ifdef TSAR ... #endif
来包裹起来。
很快,它归结于最符合逻辑也最有趣的结论:
1
mkdir linux/arch/tsar
认识你的硬件
真正认识你的硬件很明显是将Linux移植过去的基础和前提。
一个处理器的描述文档一般从逻辑上和物理上划分为两个部分(比如就像最新发布的RISC-V处理器的描述文档)。第一部分通常详细描述用户级指令集,这也是处理器能够理解和执行的用户级指令列表;第二部分描述特权架构,包括内核级指令和控制处理器状态的多个系统寄存器。
这些描述文档需要回答的关键问题包括:
-
该处理器架构的虚拟内存模型是什么,页表的格式,地址翻译机制如何?
许多处理器架构(比如x86,ARM或TSAR)定义了灵活的虚拟内存布局。尽管Linux的32位处理器默认的布局为低3GiB为用户空间,剩下的高1GiB为内核空间,他们的虚拟地址空间理论上可以在用户空间和内核空间之间任意地划分。在一些其它的架构中,这个布局被硬件设计所约束。比如说,在MIMPS32架构中,虚拟地址空间被划分成两个相同大小的区域:低2GiB为用户空间,高2GiB为内核空间,后者甚至包含了物理地址空间中的预定义窗口。
页表的格式私下里和处理器使用的翻译机制相关。在硬件管理机制的情况下,当TLB(一个有限大小的包含最近使用的虚拟地址和物理地址的翻译物理缓存)中不包括一个特定的虚拟地址(即TLB miss)时,一个硬件状态机将会从页表中取出该项翻译并放入TLB中。这意味着页表的格式必须由处理器的描述文档确定。在基于软件的机制中,一个TLB miss异常由一段代码处理,这就给页表的组织方式提供了很大的自由度,只有TLB entry的格式是指定的。
-
如何启用/禁用中断,在特权模式和用户模式之间切换,获得一个异常的原因等?
尽管所有的这些操作通常只涉及到读或者修改一套系统寄存器的特定bit域,但他们在不同的体系结构下总是不同的。因此,大多数情况下,他们是由一小段精细设计的汇编代码完成。
-
ABI是什么?
尽管由于应用程序二进制接口(Application Binary Interface, ABI)定义了栈被格式化为栈桢的方式以及函数参数和返回值被传递的方式,可以被认为只和编译器相关;但实际上在移植Linux的时候当然很有必要熟悉它。比如说,作为系统调用的容器,内核必须知道怎样获取参数并返回值;或者当上下文切换时,内核必须知道什么需要保存和恢复,以及什么代替了线程的上下文等等。
着手了解内核
学习一些尤其是与Linux内存布局相关的内核概念是非常有用的。我承认我花了一段时间才搞清楚low memory和high memory之间以及direct mapping和vmalloc区域的真正区别。
对于一个内核占据高1GiB虚拟地址空间的常规而简单的移植(到一个32位处理器),通常是很直接的。在这1GiB中,Linux定义了它的较低部分直接映射到系统内存的较低地址部分(因此叫low memory):意味着如果内核访问了内存地址0xC0000000,它将会被重定向到0x00000000。
相反,在含有比能够直接映射的部分更多的物理内存的系统中,系统内存较高的部分(也叫high memory)内核是不能够正常访问的。必须使用其他的机制,比如kmap()
和kmap_atomic()
,来获得对这些high memory页的临时访问权限。
在直接映射区之上是vmalloc区域,由vmalloc()
控制。这个分配机制提供了将物理地址不连续的内存分配到连续虚拟地址空间的能力。这在分配一大块内存地址时很有用,因为不可能在物理空间中找到如此大块的连续空闲内存地址。
关于Linux内存管理的扩展阅读请见Linux Device Divers(PDF)和这篇LWN文章。
如何开始?
当你学习完处理器的描述文档和内核原则后,终于到了给新增加的arch目录添加一些文件的时候了。稍等…,我们怎么开始呢?就像任何移植甚至任何代码都必须遵循一个特定的API一样,这个过程分为两步。
首先,定义了符号的最小集合(函数,变量,宏定义)的一小组文件对于内核的编译都是必要的。这一组文件和符号经常来自编译失败:如果由于缺少文件或符号而编译失败,这是一个好的提示,它可能需要实现(或者某些配置选项需要修改)。在移植Linux的案例中,在实现架构特定的代码以及内核其他部分的API的众多的头文件时,这个方法尤其重要。
在内核最终被编译并能够在目标硬件上执行以后,知道引导代码是连续的很重要。那使得许多函数一开始留空,直到系统最终变得稳定并且达到init
过程时逐渐被实现。这种方法大概对于所有的在其汇编引导代码执行后的C函数都适用。然而建议先实现early_printk()
函数否则debug过程将会很难。
终于可以开始:最小非代码文件集合
将编译工具一直到新的处理器架构上式移植Linux内核的前提条件,但是这里假设这件事已经完成了。剩下的事情就是构建一个交叉编译器。因为此时已知一个标准C库还没有完成(甚至刚刚开始),只能创建一个第1阶段的交叉编译器。
这样一个交叉编译器只能够为裸机执行编译代码,这非常适合于内核(因为它不依赖与任何外部库)。相反,一个第2阶段的交叉编译器对于标准C库有內建支持。
移植Linux到一个新的处理器的第一步就是在arch/
目录下建立一个新的目录,arch/
位于kernel树的根目录(在我的例子中叫做linux/arch/tsar
)。在这个新建立的目录中,布局是相当标准化的:
- configs/:对于待支持系统的默认配置(比如
*_deconfig
文件) - include/asm/:只在内部使用的头文件,比如Linux源码
- include/uapi/asm:被导出到用户空间的头文件(比如libc)
- kernel/:通用内核管理
- lib/:优化的实用程序(比如
memcpy()
、memset()
等等) - mm/:内存管理
重要的事情是一旦新架构目录存在了,Linux自动地知道它。它只会提示没有找到Makefile,不会说关于新架构的事。
1
2
~/linux $ make ARCH=tsar
Makefile: ~/linux/arch/tsar/Makefile: No such file or directory
就像下面的例子中展示的,一个最小的架构Makefile只有几个变量需要指定:
KBUILD_DEFONFIG := tsar_deconfig
KBUILD_CFLAGS += -pipe -D__linux__ -G 0 -msoft-float
head-y := arch/tsar/kernel/head.o
core-y += arch/tsar/kernel/
core-y += arch/tsar/mm/
LIBGCC := $(shell $(CC) $(KBUILD_CFLAGS) -print-libgcc-file-name)
libs-y += $(LIBGCC)
libs-y += arch/tsar/drivers/
KBUILD_DEFCONFIG
必须为有效默认配置的名字,它是configs
目录中的defconfig
文件之一(比如configs/tsar_deconfig
)KBUILD_CFLAGS
和KBUILD_AFLAGS
定义了编译标志,分别是给编译器和汇编器的。{head, core, libs, ...}-y
列出了内核映像中需要编译的目标或包含目标的子目录(详见Documentation/kbuild/makefiles.txt)。
另一个在架构目录根目录中的是Kconfig
,这个文件主要有两个目的:定义了描述架构特征的新的架构专用配置选项,以及选择该架构适用的通用配置选项(比如在Linux源码中其他地方定义的选项)。
由于这将成为新建架构的主要配置文件,它的内容也决定了menuconfig命令的布局(比如make ARCH=tsar menuconfig
。直接写出这个文件很困难,毕竟它很大程度上取决于目标架构,但是查看其它(简单的)架构的相同文件肯定会有所帮助。
defconfig
文件(比如configs/tsar_defconfig
)对于完成和LInux内核构建系统(kbuild)相关的文件是必要的。它的角色就是为该架构定义默认配置,这基本上意味着指定被用作种子来产生完整Linux内核编译配置的一系列配置选项。再一次,从其它架构的defconfig文件开始是很有帮助的,但还是建议精简它们,因为它们可能启用了比最小系统所需的更多功能,比如USB、IOMMU、文件系统等这些对于移植的这个阶段太早的东西。
最后需要创建的一个“不是真正的代码但是仍然很重要”的文件是一个脚本(通常位于kernel/vmlinux.lds.S
)来指导连接器怎样把代码和数据的不同部分放到最终的内核映像中。比如说,早期汇编引导代码需要放置在二进制的开头,这就是这个脚本让我们做的。
结论
到此为止,构建系统真正被用到了:现在可以产生一个初始化内核配置,定制它甚至开始从它编译。然而,因为移植还没有包含任何代码,编译很快就会停止。
在下一篇文章中,我们将会在移植的第二部分深入一些代码:头文件,早期汇编引导代码以及在内核线程创建之前执行的重要的架构函数。