深度探索Linux操作系统 —— 构建内核

简介: 深度探索Linux操作系统 —— 构建内核

前言

    内核的构建系统 kbuild 基于GNU Make,是一套非常复杂的系统。

   对于编译内核而言,一条 make 命令就足够了。因此,构建内核最困难的地方不是编译,而是编译前的配置。配置内核时,通常我们都能找到一些参考。比如,对于桌面系统,可以参考主流发行版的内核配置。但是,这些发行版为了能够在更多的机器上运行,几乎选择了全部的配置选项,编译了全部的驱动,不仅增加了内核的体积,还降低了内核的运行速度。再比如,对于嵌入式系统,BSP(Board Support Package)中通常也提供内核,但他们通常也仅是个可以工作的内核而已。显然,如果要一个占用空间更小、运行更快的内核,就需要开发人员手动配置内核。而且,也确实存在着在某些情况下,我们找不到任何合适的参考,这时我们只能以手动方式从零开始配置。

一、内核映像的组成

1、一级推进系统——setup.bin

    在进行内核初始化时,需要一些信息,如显示信息、内存信息等。曾经,这些信息由工作在实模式下的 setup.bin 通过 BIOS 获取,保存在内核中的变量 boot_params 中,变量 boot_params 是结构体 boot_params 的一个实例。

2、二级推进系统——内核非压缩部分

   内核的保护模式部分是经过压缩的,因此运行前需要解压缩,但是谁来负责内核映像的解压呢?解铃还须系铃人,既然内核在构建时自己压缩了自己,当然解压缩也要由内核映像自己完成。


   内核在压缩的映像外包围了一部分非压缩的代码,Bootloader 在加载内核映像后跳转至外围的这段非压缩部分。这些没有经过解压缩的指令可以直接送给 CPU 执行,由这段 CPU 可执行的指令负责解压内核的压缩部分。


   除了解压以外,非压缩部分还负责内核重定位。内核可以配置为可重定位的(relocatable),所谓可重定位即内核可以被 Bootloader 加载到内存任何位置。但是在链接内核时,链接器需要假定一个加载地址,然后以这个假定地址为参考,为各个符号分配运行时地址。显

然,如果加载地址和链接时假定的地址不同,那么需要对符号的地址进行重新修订,这就是内核重定位。


   内核非压缩部分工作在保护模式下,其占用的内存在完成使命后将会被释放。

3、映像的格式

   在 Linux 作为操作系统的 hosted environment 环境下,二进制文件使用 ELF 格式,操作系统也提供 ELF 文件的加载器。但是,操作系统本身确是工作在 freestanding environment 环境下。操作系统显然不能强制要求 Bootloader 也提供 ELF 加载器。而且,操作系统映像也没有必要使用 ELF 格式来组织,将代码和数据顺次存放即可,即所谓的裸二进制格式。所以,内核映像都采用裸二进制格式进行组织。


   但是,从 Linux 2.6.26 版本开始,内核的压缩部分,即有效载荷部分,采用了 ELF 格式。这样做可以支持 “the Xen domain builder” 的 Bootloader。


   我们知道,在解压内核映像后,将会跳转到解压映像的开头执行。但是,ELF 文件的开头并不是代码段的开始,而是 ELF 文件头,也就是说,并不是 CPU 可执行的机器指令。显然,当内核映像不是裸二进制格式时,我们需要有一个 ELF 加载器来将 ELF 格式的内核映像转化为裸二进制格式。那么谁来充当这个 ELF 加载器呢?


   正所谓“螳螂捕蝉,黄雀在后”。内核的非压缩部分调用函数 decompress 解压内核后,紧接着就调用了函数 parse_elf 来处理ELF格式的内核映像。


   事实上,如果 Bootloader 不是所谓的 “the Xen domain builder” ,我们完全没有必要保留内核的压缩部分为 ELF 格式,并略去启动时进行的 “parse_elf” 。

二、内核映像的构建过程

    在编译内核时,通常我们只需要执行 “make bzImage” ,或者 make 后面不接任何目标。在没有接目标时,构建的内核映像也是 bzImage。

# linux-3.7.4/arch/x86/boot/Makefile:

$(obj)/bzImage: $(obj)/setup.bin $(obj)/vmlinux.bin \
  $(obj)/tools/build FORCE

    根据构建规则可见,bzImage 依赖于 setup.binvmlinux.bin,所以在构建 bzImage 前,make 将自动先去构建它们,以此类推,vmlinux 的构建也是同样的道理。因此,组成内核映像的各个部分的构建顺序如下:

  1. 构建有效载荷 vmlinux,并将其压缩为 vmlinux.bin.gz;
  2. 构建二级推进系统,并将二级推进系统装配到有效载荷上,组成 vmlinux.bin;
  3. 构建一级推进系统,即构建 setup.bin;
  4. 将 setup.bin 和 vmlinux.bin 组合为 bzImage。

三、配置内核

    内核提供了 make menuconfig、make xconfig、make gconfig 等具有图形界面的配置方式。make menuconfig 是图形界面配置方式中最简陋的一种,但是却非常方便易用,依赖也最小。其他如 make xconfig、make gconfig 需要 QT、GTK+ 等库的支持。在本书中,我们使用 make menuconfig 配置内核,其简单地基于终端的图形界面是使用 ncurses 编写的,因此需要安装 libncurses5-dev

apt install libncurses5-dev

1、交叉编译内核设置

    在默认情况下,内核构建系统默认内核是本地编译,即编译的内核是运行在与宿主系统相同的体系架构上。如果是为其他的架构编译内核,即交叉编译,我们需要设置两个变量:ARCHCROSS_COMPILE 。其中:

  • ARCH 指明目标体系架构,即编译好的内核运行在什么平台上,如 x86、arm 或 mips 等。
  • CROSS_COMPILE 指定使用的交叉编译器的前缀。对于我们的交叉工具链来说,其前缀是 i686-none-linux-gnu-

在顶层的 Makefile 中,我们可以看到工具链中的编译器、链接器等均以 $(CROSS_COMPILE) 作为前缀:

# linux-3.7.4/Makefile:

AS  = $(CROSS_COMPILE)as
LD  = $(CROSS_COMPILE)ld
CC  = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR  = $(CROSS_COMPILE)ar
NM  = $(CROSS_COMPILE)nm
STRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump

  可以使用多种方式定义这两个变量,比如通过在环境变量中定义 ARCH、CROSS_COMPILE ;或者每次执行 make 时,通过命名行为这两个变量的赋值,如:

make ARCH=i386 CROSS_COMPILE=i686-none-linux-gnu-

    也可以直接更改顶层 Makefile 。这种方法比较方便,但是要小心,以免破坏 Makefile 文件。本书中我们采用这种方式,将顶层 Makefile 中的如下脚本:

# linux-3.7.4/Makefile:

ARCH      ?= $(SUBARCH)
CROSS_COMPILE ?= $(CONFIG_CROSS_COMPILE:"%"=%)


更改为:

# linux-3.7.4/Makefile:

ARCH      ?= i386
CROSS_COMPILE ?= i686-none-linux-gnu-

2、基本内核配置

   在很多情况下,我们都会有一个目标系统的老版本内核配置文件,而不必每次都从零开始。在此种情况下,首先将已有的内核配置文件复制到顶层目录下,并命名为 .config;然后运行 make oldconfig,其将会询问用户如何处理变动的内核配置;最后用户可以使用 make menuconfig 进行微调。虽然内核提供 make oldconfig 的方法,但是这些方法并不是完美的,读者需要小心处理新内核中新增或改变的配置项。


   但是也有很多情况,已有配置并不理想,我们需要进行更彻底定制,或者我们根本找不到一个合适的已有配置。难道我们就别无选择,只能从零开始了吗?当然不是,内核构建系统已经为开发者考虑了这些。


   一方面内核为很多平台附带了默认配置文件,保存在 arch/<arch>/configs 目录下,其中 <arch> 对应具体的架构,如 x86、arm 或者 mips 等。比如,对于 x86 架构,内核分别提供了 32 位和 64 位的配置文件,即 i386_defconfig 和 x86_64_defconfig;对于 arm 架构,内核提供了如 NVIDA 的 Tegra 平台的默认配置 tegra_defconfig,Samsung 的 S5PV210 平台的默认配置 s5pv210_defconfig 等。


   如果我们打算使用x86的32位的默认配置,执行下面命令即可:

make i386_defconfig

  如果想使用Samsung的S5PV210平台的默认配置,则使用如下命令:

make ARCH=arm s5pv210_defconfig

  如果对这些内核内置的默认配置依然不满意,kbuild 还提供了创建一个最小配置的方法,从某种意义上讲,这是最彻底的定制方式了,命令如下:

make allnoconfig

3、内核启动的第一程序

    如果用户没有通过内核命令行参数 “init” 指定第一个进程运行的用户空间的程序,则内核依次尝试执行目录 /sbin、/etc、/bin下的 init,最后尝试执行目录 /bin 下的 sh 。因此,我们在目录 /bin 下建立一个指向 bash 的符号链接 sh,而且,这个符号链接也是 FHS 标准要求的。

四、构建基本根文件系统

1、根文件系统的基本目录结构

    Linux 的根文件系统的目录结构不是随意定义的,而是依照 Filesystem Hierarchy Standard Group 制定的 Filesystem Hierarchy Standard(FHS) 标准。从服务器、个人计算机到嵌入式系统,虽达不到完全符合,但大体上还是遵循这个标准的。


  根文件系统中主要有四处存放可执行程序的目录:/bin、/sbin、/usr/bin/usr/sbin 。系统管理员和普通用户都使用的重要命令保存在 /bin 目录下,而仅由系统管理员使用的重要命令则保存在 /sbin 目录下。相应的,不是很重要的命令则分别放置在 /usr/bin/usr/sbin 目录下。

    同样的道理,重要的系统库一般存放在 /lib 目录下,其他的库则存放在 /usr/lib 目录下。

2、安装C库

   几乎所有程序都依赖 C 库,它是整个系统的基础,因此,我们首先安装 C 库到根文件系统。在前面讨论编译构建系统的 C 库时,我们看到,C 库包含函数库、各种工具程序,以及开发所需的头文件等。而这里的文件系统只是个临时系统,所以 C 库中的各种实用工具及 $SYSROOT/usr/share 目录下的数据文件,都不需要安装。而且这个临时根文件系统亦不需要支撑开发,所以凡是开发时所需要的文件,包括头文件、静态库、启动文件等,也不需要安装。因此,最终我们只需要安装 $SYSROOT/lib 目录下的动态库及相应的动态链接/加载器需要的符号链接。


   我们新建一个保存目标系统的根文件系统的 rootfs 目录,并且按照 FHS 标准的规定,将 C 库安装在 rootfs/lib 目录下,命令如下:

mkdir rootfs
mkdir rootfs/lib
cp -d sysroot/lib/* rootfs/lib/

除了 Glibc 中包含的 C 库外,在前面编译 GCC 时,我们也看到,GCC 也将部分底层函数封装到库中,有些程序会使用 GCC 的这些库,因此,我们也将这部分程序安装到 rootfs/lib 目录中。同样,我们也只安装动态库及其对应的运行时符号链接,命令如下:

cp -d cross-tool/i686-none-linux-gnu/lib/lib*.so.*[0-9] rootfs/lib/

3、安装shell

    在安装 C 库后,构建基本的应用程序的基础已经具备了,接下来我们需要为内核准备用户空间的程序了。在 Linux 中,专门负责启动的软件包,如 System V initSystemd 等都提供一个二进制程序作为第一个进程执行的用户空间的程序,但是为简单起见,我们使用 bash shell 。安装 bash 的命令如下:

wget https://ftp.gnu.org/gnu/bash/bash-4.2.tar.gz
tar -xf ../source/bash-4.2.tar.gz
./configure --prefix=/usr --bindir=/bin --without-bash-malloc
make
make install DESTDIR=$SYSROOT
# /vita/cross-tool/bin/ldd:
#!/bin/bash
LIBDIR="${SYSROOT}/lib $(SYSROOT}/usr/lib ${CROSS_TOOL}/${TARGET}/lib"
find() {
  for d in $LIBDIR; do
    found=""
    if [ -f "${d}/$1" ]; then
      found="${d}/$1"
      break
    fi
  done
  
  if [ -n "$found" ]; then
    printf "%8s%s => %s\n" "" $1 $found
  else
    printf "%8s%s => (not found)\n" "" $1
  fi
}

readelf -d $1 | grep NEEDED \
  | sed -r -e 's/.*Shared library:[ ]+\[(.*)\]/\1/;' \
  | while read lib; do
    find $lib
  done
目录
相关文章
|
1天前
|
存储 人工智能 安全
操作系统的心脏——内核深度解析
【10月更文挑战第29天】 本文深入探讨了操作系统的核心组件——内核,包括其定义、功能、架构以及在现代计算中的重要性。通过对比不同操作系统内核的设计哲学和技术实现,揭示了内核如何影响系统性能、稳定性和安全性。此外,文章还讨论了未来内核技术的潜在发展方向,为读者提供了一个全面了解内核工作原理的平台。
|
1天前
|
Linux 数据库
Linux内核中的锁机制:保障并发操作的数据一致性####
【10月更文挑战第29天】 在多线程编程中,确保数据一致性和防止竞争条件是至关重要的。本文将深入探讨Linux操作系统中实现的几种关键锁机制,包括自旋锁、互斥锁和读写锁等。通过分析这些锁的设计原理和使用场景,帮助读者理解如何在实际应用中选择合适的锁机制以优化系统性能和稳定性。 ####
14 6
|
1天前
|
机器学习/深度学习 负载均衡 算法
深入探索Linux内核调度机制的优化策略###
本文旨在为读者揭开Linux操作系统中至关重要的一环——CPU调度机制的神秘面纱。通过深入浅出地解析其工作原理,并探讨一系列创新优化策略,本文不仅增强了技术爱好者的理论知识,更为系统管理员和软件开发者提供了实用的性能调优指南,旨在促进系统的高效运行与资源利用最大化。 ###
|
4天前
|
算法 Linux 开发者
深入探究Linux内核中的内存管理机制
本文旨在对Linux操作系统的内存管理机制进行深入分析,探讨其如何通过高效的内存分配和回收策略来优化系统性能。文章将详细介绍Linux内核中内存管理的关键技术点,包括物理内存与虚拟内存的映射、页面置换算法、以及内存碎片的处理方法等。通过对这些技术点的解析,本文旨在为读者提供一个清晰的Linux内存管理框架,帮助理解其在现代计算环境中的重要性和应用。
|
2天前
|
缓存 网络协议 Linux
Linux操作系统内核
Linux操作系统内核 1、进程管理: 进程调度 进程创建与销毁 进程间通信 2、内存管理: 内存分配与回收 虚拟内存管理 缓存管理 3、驱动管理: 设备驱动程序接口 硬件抽象层 中断处理 4、文件和网络管理: 文件系统管理 网络协议栈 网络安全及防火墙管理
19 4
|
1天前
|
安全 网络协议 Linux
Linux操作系统的内核升级与优化策略####
【10月更文挑战第29天】 本文深入探讨了Linux操作系统内核升级的重要性,并详细阐述了一系列优化策略,旨在帮助系统管理员和高级用户提升系统的稳定性、安全性和性能。通过实际案例分析,我们展示了如何安全有效地进行内核升级,以及如何利用调优技术充分发挥Linux系统的潜力。 ####
14 1
|
4天前
|
人工智能 算法 大数据
Linux内核中的调度算法演变:从O(1)到CFS的优化之旅###
本文深入探讨了Linux操作系统内核中进程调度算法的发展历程,聚焦于O(1)调度器向完全公平调度器(CFS)的转变。不同于传统摘要对研究背景、方法、结果和结论的概述,本文创新性地采用“技术演进时间线”的形式,简明扼要地勾勒出这一转变背后的关键技术里程碑,旨在为读者提供一个清晰的历史脉络,引领其深入了解Linux调度机制的革新之路。 ###
|
6天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
30 4
|
4天前
|
物联网 Linux 云计算
Linux操作系统的演变与未来趋势####
【10月更文挑战第29天】 本文深入探讨了Linux操作系统从诞生至今的发展历程,分析了其在服务器、桌面及嵌入式系统领域的应用现状,并展望了云计算、物联网时代下Linux的未来趋势。通过回顾历史、剖析现状、预测未来,本文旨在为读者提供一个全面而深入的视角,以理解Linux在当今技术生态中的重要地位及其发展潜力。 ####
|
4天前
|
存储 程序员 Linux
深入理解操作系统:从用户到内核的旅程
【10月更文挑战第31天】本文将带领读者踏上一场从应用层到内核层的探索之旅,揭示操作系统如何协调硬件资源、管理进程和提供系统服务。我们将通过具体代码示例,展示如何在Linux环境下编写简单的程序来与操作系统交互,并解释背后的原理。文章旨在为非专业读者提供一个易于理解的操作系统概念框架,同时为有志于深入了解计算机科学核心的读者打下坚实基础。
下一篇
无影云桌面