Linux内核与根文件系统
1. Linux内核映像的构成
Linux内核有多种格式的映像,包括vmlinux、Image、zImage、bzImage、uImage、xipImage、bootpImage等[1]
vmlinux
是可引导的, 可压缩的内核映像. vm表示virtual memory, 表示linux支持虚拟内存, 因此得名vmlinux. 它由用户对内核源码编译得到, 实质上是elf格式的文件, 也可以说vmlinux是编译出来的最原始的内核文件.
ELF(Executable and Linkable Format) 是可执行可链接格式, 是UNIX实验室作为应用程序二进制接口而发布的 (Unix/Linux上编译型语言最后编译的可执行文件就是ELF格式的),
vmlinux是可执行的Linux内核, 位于/boot/vmlinux, 一般是一个软链接, vmlinuz
是vmlinux
的压缩文件, vmlinuz
的创建方式有两种:
- 内核编译时通过
make zImage
创建 - 内核编译时通过
make bzImage
创建
回到最上面bzImage内核映像的构成, 使用航天器的例子. setup.bin
就像火箭的一级推进子系统, 最初这部分负责将内核加载进内存, 并为后面内核保护模式的运行建立基本的环境. 后来加载内核的功能被分离到Bootloader中, setup.bin则退化为辅助Bootloader将内核加载到内存.
再进一步拆分, 包围在32位保护模式部分外的是非压缩部分, 这部分可以看作是火箭的二级推进子系统, 负责将压缩的内核解压到合适的位置, 并进行内核重定位, 在完成这个环节后, 其从内核映像脱离.
最后是内核的32位保护模式部分vmlinux. 这部分相当于航天器的有效负载. 最终只有这部分会被留在内存中, 内存构建时, 将对vmlinux进行压缩, 然后与二级推进系统装配为vmlinux.bin
1.1 一级推进系统: setup.bin
在进行内核初始化时, 需要一些信息, 如显示信息、内存信息等。这些信息曾经由工作在实模式下的setup.bin
通过BIOS获取,保存在内核的变量boot_param
中.
1.2 二级推进系统: 内核非压缩部分
内核的保护模式部分是经过压缩的, 因此运行前需要解压缩. 既然内核在构建时自己压缩了自己, 解压缩时也需要由内核映像自己完成.
内核在压缩的映像外部包围了一部分非压缩的代码, Bootloader在加载内核映像后跳转至外围的这段非压缩部分. 这些没有经过压缩的指令可以直接给CPU执行, 这段指令恰恰负责解压内核的压缩部分.
除了解压外, 非压缩部分还负责内核重定位. 内核可以配置为可重定位的(relocatable)
. 所谓可重定位即内核可以被Bootloader加载到内存的任何区域, 但是在链接内核时, 链接器需要假定一个加载地址, 然后这个假定地址为参考, 为各个符号分配运行地址. 如果加载地址和链接时假定的地址不同, 那么需要对符号的地址进行重新修订, 这就是内核重定位.
内核的非压缩部分工作在保护模式下, 其占用的内存在完成使命后将被释放.
1.3 有效载荷: vmlinux
编译时, kbuild分别构建内核各个子目录中的目标文件, 然后将它们链接为vmlinux, 为了缩小内核体积, kbuild删除了vmlinux中一些不必要的信息, 并将其命名为vmlinux.bin, 最后将vmlinux.bin压缩为vmlinux.bin.gz. 默认情况下, 内核使用gzip压缩, 也可以配置为使用其他压缩格式, 但是gzip压缩速度最快(但是压缩比较小)
为什么内核需要进行压缩呢?
- 在最初, 因为某些体系架构上, 特别是i386, 系统启动时运行于
实模式状态
, 可以寻址空间只能在1MB以下, 如果内核尺寸过大, 将无法正常加载, 因此需要对内核进行压缩. 在内核加载完毕后, CPU切换到保护模式, 可以寻址更大的地址空间, 于是就可以将压缩过的内核展开了. - 2.4以及更早的内核版本, 需要可以容纳在一张软盘上, 所以内核也要进行压缩
但那些都是历史原因, 如今的一些Bootloader(譬如GRUB), 在加载内核期间就已经将CPU切换到保护模式了, 寻址空间的限制早已不是问题. 但是内存的压缩仍然保留下来, 仍然考虑到某些受限的情况, 以及现代CPU解压的速度要远大于IO速度, 更小的内核也能减少加载时间.
1.4 映像格式
不论是setup.bin、vmlinux.bin,还是vmlinux.bin.gz, 命名中都包含’bin’, 这是binary
的缩写. 然而可能会有个疑惑: Linux系统上二进制文件的格式不是用ABI(Application Binary Interface)规定的ELF吗?
Linux操作系统提供的hosted environment下, 二进制文件使用ELF格式, 操作系统也提供ELF文件的加载器. 但是Linux操作系统本身却是工作在freestanding environment下, 显然不能强求Bootloader也提供ELF加载器.
事实上, Linux 2.6.26开始, 内核的压缩部分(即有效负荷)就开始采用了ELF格式, 下面是Patch的提交者给出的原因:
This allows other boot loaders such as the Xen domain builder the opptunity to extract the ELF file.
在解压内核映像后, 会跳转到解压映像的开头执行, 但是ELF文件的开头并不是代码段的开始, 而是ELF文件头, 即并非CPU可执行的机器指令.因此需要一个ELF加载器来将ELF格式的内核映像转换为而裸二进制格式. 此时, 内核的非压缩部分调用函数decompress
解压内核后, 就紧接着调用函数parse_elf
来处理ELF格式的内核映像.
2. 根文件系统: rootfs
Linux的根文件系统是依照Filesystem Hierarchy Standard Group制定的Filesystem Hierarchy Standard(FHS)标准。服务器、个人计算机到嵌入式系统,虽然不会完全符合标准,但总体上还是遵循。
目录 | 内容 |
---|---|
/bin | 保存系统管理员与用户均会使用的重要命令 |
/boot | 系统开机使用的文件,如内核映像和boot loader相关的文件 |
/dev | 设备文件 |
/etc | 系统配置 |
/lib | 重要的库文件和内核模块 |
/media | 可移动存储介质的挂载点 |
/mnt | 临时挂载点,用户也可以自行选择一些临时挂载点 |
/opt | 用户自行安装软件的位置,通常用户也会选择将软件安装在/usr/local下 |
/sbin | 系统管理员使用的重要系统命令 |
/tmp | 正在执行的程序存放的临时文件 |
/usr | 包含系统中安装主要程序的相关文件,类似MS Windows中的“Program files”目录 |
/var | 针对的主要是系统运行过程中经常发生变化的一些数据,比如cache、log、临时的数据库、打印机的队列等 |
/home | 用户目录保存的地方 |
/root | root用户的用户目录 |
/srv | 在服务器版本上, 服务器软件用来保存数据的目录, 譬如www服务器使用的网页资料就可以放在/srv/www 下 |
对于
/bin
、/sbin
、/usr/bin
、/usr/sbin
:
系统管理元和普通用户都使用的重要命令保存在
/bin
下仅由系统管理员使用的重要命令则保存在
/sbin
下不是很重要的命令则分别放置在
/usr/bin
和/usr/sbin
下
- 重要的系统库一般放在
/lib
下- 其他库则存放在
/usr/lib
目录下
3. initramfs
initramfs
的作用很难简短描述清楚, 可以简单认为是内核与根文件系统之间的桥梁
事实上, 不需要initramfs, 内核也能成功的挂载根文件系统进入用户空间. 那是因为内核中必须有对应的硬盘驱动(或者其他硬件驱动). 然而除非是专用系统, 系统的硬件平台是多变的, 甚至同一个平台可能更换硬件. 为了兼容更多硬件平台, 如果将这些驱动全部编译进内核, 并不是好主意. 因为每一次, 系统只需要一种驱动, 当前时刻其他驱动根本用不上.
另一方面, rootfs可能不在一个简单的硬盘上, 而是使用了磁盘阵列RAID, 根文件系统可能横跨几个存储设备, 甚至根文件系统在网络设备上. 此时还需要网卡驱动、网络配置、网络认证,以及通信的加密解密。这些工作如果都由内核处理,内核将变得十分复杂。
这时就是先有鸡还是先有蛋的问题:内核要加载这些模块(驱动)才能正确识别rootfs所在的设备,但是保存这些模块的rootfs又存储在这些设备上。
为了解决这种热插拔适配问题,内核开发者设计了initramfs机制。initramfs是一个临时的文件系统
, 其中包含了必要的设备驱动以及加载驱动的工具和运行环境. 由第三方程序(如BootLoader)将initramfs从硬盘加载进内存, 以驱动硬盘为例, 内核转而从内存中的initramfs中获取硬盘控制相关的驱动了, 继而驱动硬盘, 访问硬盘上的根文件系统.
在初始化的最后, 内核运行initramfs中的init程序, 该程序将探测到硬件设备、加载对应驱动,挂载真正的文件系统,执行文件系统上的/sbin/init,从而切换到真正的用户空间。真正的文件系统被挂载后,initramfs即完成了使命,其占用的内存也会释放。
如果将initramfs的名字拆开:init ram fs,初始一个内存中的文件系统。
initrd是基于ramdisk技术的, 而ramdisk就是基于内存的块设备(一旦创建时设立了大小, 就不能动态调整; 访问块设备需要缓存机制, 但对于内存中的伪块设备, 这并不合适). 鉴于此缺点, Linus Torvalds提出直接将cache作为一个文件系统挂载使用, 基于此, 实现了
ramfs
. ramdisk本质是基于内存的块设备, 而ramfs是基于缓存的文件系统, 因此ramfs没有之前所说的一些缺点, 譬如ramfs可以根据文件大小自由伸缩容量, 且ramfs基于已有的缓存机制, 不必像ramdisk那样需要和缓存之间进行额外的一环.于是,从2.6开始, 伴随着ramfs的出现, initramfs取代了initrd.
然而在Ubuntu的/boot目录中, 至今可以看到
initrd.img
的身影