Linux内核组成和编译

1. 系统与内核的关系

计算机科学语境中,内核即操作系统,kernel = OS,它直接与硬件交互、管理各种资源、调度其他进程。

日常语境中,操作系统是内核加上其他一些工具套件(如Windows的文件浏览器、控制面板、IE)组成的集合kernel ⊂ OS

虽然从用户的角度,那些“系统自带”的套件和普通的用户软件在信任程度、控制权限以及重要性方面都有很大差别,但是从内核的角度来说,这些套件其实和普通的程序其实是平等的,都只是内核调度之下的普通用户态程序。

2. Linux内核及相关部件

以下内容皆以Ubuntu为代表的发行版为例。

2.1. vmlinuz

内核一般位于/boot/vmlinuz-*位置。

  • vm即virtual memory,即支持虚拟内存的意思
  • linuz表示是linux的gzip压缩形式,而内核头部自带一段自解压代码,在加载后自动自我展开。
  • 后接版本号与系统信息等

系统中可以安置多个版本的内核,但引导程序只会选择其一进行实际启动。常用的引导程序是grub,他从设备固件(BIOS、UEFI)接过执行权,加载Linux内核并将之放置在系统物理内存的低地址处,然后将执行权交给内核功成身退。

Linux内核一般也就几十兆,gzip压缩后的vmlinuz一般不足十兆,相对动辄以GB计的“操作系统”,真实的内核其实并不大。

2.2. System.map

System.map-*位于内核同目录下。

它是内核的符号表,与内核一同编译产生,保存了每个符号的链接地址,对于系统调试有用。

nm命令的结果格式一直,大致如下:

0000000000000000 D __per_cpu_start
0000000000000000 D irq_stack_union
00000000000001e0 A kexec_control_code_size
...
ffffffff82107000 b .brk.early_pgt_alloc
ffffffff8210d000 B __brk_limit
ffffffff8210d000 B _end

2.3. initrd.img

initrd.img-*也位于内核同目录下。

其名称表示initial ramdisk,意义是充当内核启动初期阶段的文件系统。

内核一般除了核心常用功能外,很多功能可以做成模块外置,在启动后按需动态加载。以文件相关为例,实际的存储介质(磁盘、网络)、总线类型(USB、SATA、PCI-E)、文件系统(ext4、zfs)千差万别,甚至可能涉及加密等问题,内核肯定无法将之全部纳入,一般将之做成模块。但问题是要加载模块就要先挂载根目录,读取模块文件,二者就成了一个鸡生蛋、蛋生鸡的死结。

initrd.img就是为了解开这个死结而生,其本质是一个非常精简的系统镜像:

.
├── bin
├── conf
├── etc
├── init
├── lib
├── lib64
├── run
├── sbin
├── scripts
├── usr
└── var

它和内核一起被引导程序加载,然后内核将其用作早期的根文件系统,利用其中的系统环境,内核就可以挂载真正的系统根目录了。

也正因如此,通常可以只在内核中包含一些通用的功能,而差异化的功能只需要做成模块,必要时还需要置入initrd.img。所以在Ubuntu上,内核是从软件仓库下载的,因为它相对来说是通用的,而initrd.img是下载内核后本地自动生成的,因为它需要依据本地的环境和配置特殊生成。

值得一提,现今的initrd.img实际上已经是initramfs了,前者模拟一个磁盘块设备,后者直接模拟一个文件系统,少绕了一个弯路,因此性能更好,只不过在名称上出于历史原因继续沿用前者罢了。

3. Linux内核编译

3.1. 下载源码

Linux内核早有对应的Github仓库,不过鉴于其体积和网络速度,还是从清华镜像上下载比较快。

安全方便考虑,可以先克隆一个仓库到本地,之后有需要的话,就从本地仓库再克隆出一个仓库来使用。

克隆一个裸仓库(其没有工作目录、体积更小)到本地/opt/linux.git

sudo git clone --bare https://mirrors.tuna.tsinghua.edu.cn/git/linux.git /opt/linux.git

从本地仓库再克隆出5.0版本的仓库,--depth 1选项告诉git不需要历史commits,更小更快

git clone file:///opt/linux.git --depth 1 -b v5.0 linux-v5.0.0

3.2. 编译

内核编译一般有三步骤,

  1. 配置

    通过make ???config系列命令在源码根目录下生成一个.config文件,控制内核应该包含的功能。

  2. 编译

    生成编译出二进制文件,make -j 16,耗时最长,给make命令加上-j参数指定并行数量。以x86为例,完成后的内核会在arch/x86/boot/bzImage,这个bzImage就是/boot/vmlinuz-*,也就是真正的内核。

  3. 安装

    其实就是复制编译结果到目标路径

    mkdir -p ../install/boot
    make install INSTALL_PATH=../install/boot
    make modules_install INSTALL_MOD_PATH=../install
    .
    ├── boot
    │   ├── config-5.0.0
    │   ├── System.map-5.0.0
    │   └── vmlinuz-5.0.0
    └── lib
        └── modules
            └── 5.0.0
                ├── build
                ├── kernel
                ├── modules.alias
                ├── modules.alias.bin
                ├── modules.builtin
                ├── modules.builtin.bin
                ├── modules.dep
                ├── modules.dep.bin
                ├── modules.devname
                ├── modules.order
                ├── modules.softdep
                ├── modules.symbols
                ├── modules.symbols.bin
                └── source

需要注意,不过内核的编译与gcc的版本依赖性很强,比如4.0版本的内核在Ubuntu上用7.0版本的gcc无法通过编译,需要切换到gcc 4.8版本才行。