0%

Linux启动过程

本文介绍了 Linux 启动的整个过程。

Linux Startup Process

Overview

总的来说我们可以把 Linux 系统启动的过程分为下面几个阶段:

  • ROM Stage
    • 在这个阶段没有内存,需要在ROM上运行代码。这时因为没有内存,没有C语言运行需要的栈空间,开始往往是汇编语言,直接在ROM空间上运行。在找到个临时空间(Cache空间用作RAM,Cache As Ram, CAR)后,C语言终于可以粉墨登场了,后期用C语言初始化内存和为这个目的需要做的一切服务。
  • RAM Stage
    • 经过 ROM阶段的困难情况后,我们终于有了可以大展拳脚的内存,很多额外需要大内存的东西可以开始运行了。在这时我们开始进行初始化芯片组、CPU、主板模块等等核心过程。
  • Find Something To Boot Stage
    • 终于要进入正题了,需要启动,我们找到启动设备。就要枚举设备,发现启动设备,并把启动设备之前需要依赖的节点统统打通。然后开始移交工作,Windows或者Linux的时代开始。

具体来说,

  • 在 ROM/RAM Stage,运行的代码我们一般称作固件,也就是 Firmware,最早是 BIOS,现在 UEFI 应用越来越广泛。
  • 固件初始化执行完毕后,Firmware 会将控制权交给 boot loader,最常见的是 GRUB。
  • boot loader之后会将OS内核加载进内存,开始进入操作系统。
  • OS 一般通过一个 init 进程启动所有的进程,最开始用的是 SysVinit,现在 Systemd应用越来越广泛。

Firmware

BIOS

BIOS, Basic Input-Output System

BIOS 是一组固化在计算机主板ROM里的程序代码,其主要功能是在计算机上电时对硬件进行初始化配置,并将硬件操作封装为BIOS中断服务。这样,各种硬件间的差异便由BIOS负责维护,程序直接调用BIOS中断服务即可实现对硬件的控制。

在系统上电后,CPU运行于实模式工作环境中,数据位宽为16位,最大物理地址寻址范围是0~1MB,其中的物理地址0x0C0000~0x0FFFFF保留给BIOS使用。开机后,CPU硬件逻辑设计为在加电瞬间强行将CS值置为0XF000,IP为0XFFF0,这样CS:IP就指向0XFFFF0这个位置,这个位置正是BIOS程序的入口地址。一般情况下,这里是一条跳转指令,CPU通过执行此处的跳转指令跳转到真正的BIOS入口地址处执行,以下是BIOS的启动流程:

BIOS Boot Process

加电自检

BIOS代码首先做的是POST(Power On Self Test,加电自检)操作,主要是检测关键设备是否正常工作,设备设置是否与CMOS中的设置一致。如果发现硬件错误,则通过喇叭报警;如果没有问题,屏幕就会显示出CPU、内存等信息。

BIOS POST

初始化设备

BIOS的第二步动作就是枚举本地设备并初始化

有一项对启动操作系统至关重要的工作,那就是BIOS在内存中建立中断向量表和中断服务程序

BIOS程序在内存最开始的位置(0x00000)用1KB的内存空间(0x00000~0x003FF)构建中断向量表,在紧挨着它的位置用256KB的内存空间构建BIOS数据区(0x00400~0x004FF),并在大约57KB以后得位置(0x0e05b)加载了8KB左右的与中断向量表相应的若干中断服务程序。

中断向量表有256个中断向量,每个中断向量占4个字节,其中两个字节是CS值,两个字节是IP值。每个中断向量都指向一个具体的中断服务程序。

MBR

硬件自检完成后,BIOS把控制权转交给下一阶段的启动程序。

这时,BIOS需要知道「下一阶段的启动程序」具体存放在哪一个设备。也就是说,BIOS需要有一个外部储存设备的排序,排在前面的设备就是优先转交控制权的设备。这种排序叫做”启动顺序”(Boot Sequence)。

打开BIOS的操作界面,里面有一项就是设定启动顺序

BIOS Boot Sequence

BIOS按照”启动顺序”,把控制权转交给排在第一位的储存设备。

CMOS又被称作互补金属氧化物半导体,电压控制的一种放大器件,是组成CMOS数字集成电路的基本单元。在计算机领域,CMOS常指保存计算机基本启动信息(如日期、时间、启动设置等)的位于微机主板上的一块可读写RAM芯片。在今日,CMOS制造工艺也被应用于制作数码影像器材的感光元件,尤其是片幅规格较大的单反数码相机。

CMOS主要用来保存当前系统的硬件配置和操作人员对某些参数的设定,CMOS RAM芯片由系统通过后备电池供电,在关机状态中,还是遇到系统掉电,CMOS信息不会丢失

这时,计算机读取该设备的第一个扇区,也就是读取最前面的512个字节。如果这512个字节的最后两个字节是0x55和0xAA,表明这个设备可以用于启动;如果不是,表明设备不能用于启动,控制权于是被转交给”启动顺序”中的下一个设备。

这最前面的512个字节,就叫做“主引导记录”(Master boot record,缩写为MBR)。

MBR Structure

MBR 只有 512 字节,由以下三个部分组成:

  • Bootstrap Code Area,前 446 字节,它里面就包含有可执行代码以及错误消息文本。
  • Partition Table,接下来的64字节,其中包含有四个分区的各自的记录(一个分区占16字节)。
  • Boot Signature,最后两个字节,0x55AA

简单来说,MBR 从 BIOS 获得控制权之后,主要做的事情就是寻找并加载 boot loader。MBR 的前 446 字节包含某个启动引导器,像 GRUB) 、Syslinux) 和 LILO 之类的第一启动阶段代码。

MBR 接管后,执行它之后的第二阶段代码,如果后者存在的话,它一般就是Boot Loader。在这之后,我们就进入了 Boot Loader Phase

UEFI

UEFI,Unified Extensible Firmware Interface,前身是EFI规范1.10。UEFI规范描述了操作系统平台固件之间的接口,其目的是为操作系统和平台固件定义一种通信方法。

UEFI规范仅提供操作系统引导过程所需的信息,旨在无需对平台或操作系统进行深入定制便可在处理器规范兼容的平台上运行操作系统。UEFI规范还允许平台引入创新的特性和功能,在无需为OS引导程序重新编程的情况下增强平台功能。UEFI规范适用于从移动系统到服务器的各种硬件平台,并允许原始设备制造商具有最大的扩展性和定制能力,以实现差异化。

UEFI接口的表现形式是数据表,其中包括与平台相关的信息,以及操作系统加载器操作系统可使用的引导服务运行时服务。它们一起为启动操作系统提供了一个标准环境。UEFI规范设计为纯接口规范。因此,UEFI规范定义了平台固件必须实现的一组接口和结构。

以下是UEFI设计的基本要素:

  • 重用现有接口表。为了让操作系统和固件中的代码可以在现有设计结构中持续使用。凡是兼容UEFI规范的处理器平台都必须遵照UEFI规范进行实现。
  • 系统分区。系统分区定义了一个独立的、可共享的分区和文件系统,这个系统分区可允许多个供应商之间安全共享数据,即使这些供应商出于不同目的去访问系统分区。
  • 引导服务。引导服务提供了在启动期间可以使用的设备和系统功能的接口。设备的访问是通过句柄(Handle)和协议(Protocol)抽象出来的。UEFI通过将基础实现隔离在规范之外,以避免给设备的访问者带来负担,进而促进现有BIOS代码的重用。
  • 运行时服务。运行时服务为操作系统提供了正常运行期间可以使用的基础平台硬件资源的接口。

UEFI 不仅能读取分区表,还能自动支持文件系统。所以它不像 BIOS,已经没有仅仅 440 字节可执行代码即 MBR 的限制了,它完全用不到 MBR。

不管第一块上有没有 MBR,UEFI 都不会执行它。相反,它依赖分区表上的一个特殊分区,叫 EFI 系统分区,里面有 UEFI 所要用到的一些文件。计算机供应商可以在 /EFI// 文件夹里放官方指定的文件,还能用固件或它的 shell,即 UEFI shell,来启动引导程序。EFI 系统分区一般被格式化成 FAT32,或比较非主流的 FAT16。

1
2
3
4
5
- 系统开机 - 上电自检(Power On Self Test 或 POST)。
- UEFI 固件被加载,并由它初始化启动要用的硬件。
- 固件读取其引导管理器以确定从何处(比如,从哪个硬盘及分区)加载哪个 UEFI 应用。
- 固件按照引导管理器中的启动项目,加载UEFI 应用。
- 已启动的 UEFI 应用还可以启动其他应用(对应于 UEFI shell 或 rEFInd 之类的引导管理器的情况)或者启动内核及initramfs(对应于GRUB之类引导器的情况),这取决于 UEFI 应用的配置。

UEFI Platform Initilization Boot Process

Security Phase

验证阶段 (Security,SEC)

系统上电后,CPU开始执行第一条指令,此时系统就进入SEC阶段。这个阶段的内存尚未被初始化,不可使用。所以,SEC阶段最主要的工作是建立一些临时内存并将CPU切换到保护模式,这里提到的临时内存可以是处理器的缓存,亦或者系统的物理内存。

Pre-EFI Initailization Phase

EFI环境预初始化阶段(Pre-EFI Initialization Environment,PEI)

PEI阶段最主要的工作就是对内存、CPU以及芯片组等关键设备进行初始化。由于这部分代码没有进行压缩,因此代码必须越精简越好。而且,在PEI阶段还要确定操作系统的引导路径,初始化UEFI驱动和固件需要的内存。

Driver eXecution Environment Phase

驱动运行环境阶段(Driver Execution Environment,DXE)

DXE是EFI最重要的阶段,大部分的驱动、固件加载工作都是在这个阶段完成的。

Boot Device Selection Phase

引导设备选择阶段(Boot Device Select,BDS)

BDS阶段的主要工作是初始化控制台设备的环境变量,尝试加载环境变量列表中记录的驱动,并尝试从环境变量列表中记录的启动设备中启动。

Transient System Load Phase

临时系统运行阶段(Transient System Load,TSL)

这个阶段将进入UEFI的临时Shell系统环境。

Run Time Phase

运行时阶段(RunTime,RT)

当操作系统调用 EFI_BOOT_SERVICES.ExitBootServices 服务后,系统进入RT阶段。此时,DXE与引导服务都将销毁,只有EFI运行时服务和EFI系统表可以继续使用。

After Life Phase

后世阶段(After Life,AL)

当操作系统调用 EFI_RUNTIME_SERVICES.ResetSystem 服务或者调用ACPI Sleep State,系统进入AL阶段。触发异步事件(比如:SMI、NMI)亦可使系统进入AL阶段,这在服务器和工作站中比较常见。

UEFI VS BIOS

  • 开发效率

    • BIOS开发一般采用汇编语言,代码大多与硬件控制相关。
    • 在UEFI中,绝大部分代码采用C语言编写,UEFI应用程序和驱动甚至可以使用C++编写。UEFI通过固件-操作系统接口(引导服务和运行时服务)为操作系统和操作系统加载器屏蔽了底层硬件细节,使得UEFI上层应用可以方便重用。
  • 可扩展性

    大部分硬件的初始化通过UEFI驱动实现。每个驱动是一个独立的模块,可以包含在固件中,也可以放在设备上,运行时根据需要动态加载。UEFI中的每个表和协议(包括驱动)都有版本号,这使得系统升级过程更加简单、平滑。

    UEFI系统的可扩展性体现在两个方面

    • 一是驱动的模块化设计
    • 二是软硬件升级的兼容性
  • 性能

    相比BIOS,UEFI有了很大的性能提升,从启动到进入操作系统的时间大大缩短。性能的提高源于以下几个方面:

    • UEFI提供了异步操作。基于事件的异步操作,提高了CPU利用率,减少了总的等待时间。
    • UEFI舍弃了中断这种比较耗时的操作外部设备的方式,仅仅保留了时钟中断。外部设备的操作采用“事件+异步操作”完成。
    • 可伸缩的设备遍历方式,启动时可以仅仅遍历启动所需的设备,进而加速系统启动。
  • 安全性

    UEFI的一个重要突破就是其安全方面的考虑。当系统的安全启动功能被打开后,UEFI在执行应用程序和驱动前会先检测程序和驱动的证书,仅当证书被信任时才会执行这个应用程序或驱动。UEFI应用程序和驱动采用PE/COFF格式,其签名放在签名块中。

Bootloader

bootloaderBIOSUEFI 启动的第一个程序。它负责使用正确的 内核参数 加载内核, 并根据配置文件加载 初始化 RAM disk。对于 UEFI,内核本身可以由 UEFI 使用 EFI boot stub 直接启动,也可以使用单独的引导加载程序或引导管理器来在引导之前编辑内核参数。

在 Linux 中,GRUB是最常用的一个boot loader。在 /boot目录下,我们可以看到,除了内核的四个文件,另外就是grub的相关文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vagrant@cosmos:/boot$ tree -L 2
.
├── config-4.4.0-140-generic
├── grub
│   ├── default
│   ├── fonts
│   ├── gfxblacklist.txt
│   ├── grub.cfg
│   ├── grubenv
│   ├── i386-pc
│   ├── locale
│   ├── menu.lst
│   └── unicode.pf2
├── initrd.img-4.4.0-140-generic
├── System.map-4.4.0-140-generic
└── vmlinuz-4.4.0-140-generic

在这里,内核的四个文件解释如下:

  • vmlinuz
    • vmlinuz是可引导的、压缩的内核。“vm”代表“Virtual Memory”。
    • vmlinuz的建立有两种方式。
      • zImage
        • 编译内核时执行make zImage
        • 然后通过:cp /usr/src/linux-2.4/arch/i386/linux/boot/zImage /boot/vmlinuz产生
        • zImage适用于小内核的情况,它的存在是为了向后的兼容性。
      • bzImage
        • 内核编译时执行make bzImage
        • 然后通过:cp /usr/src/linux-2.4/arch/i386/linux/boot/bzImage /boot/vmlinuz产生
        • bzImage是压缩的内核映像,需要注意,bzImage不是用bzip2压缩的,bzImage中的bz容易引起误解,bz表示big zImage
      • zImage和bzImage都是用gzip压缩的。它们不仅是一个压缩文件,而且在这两个文件的开头部分内嵌有gzip解压缩代码。所以你不能用gunzip 或 gzip –dc解包vmlinuz。
      • 内核文件中包含一个微型的gzip用于解压缩内核并引导它。两者的不同之处在于,老的zImage解压缩内核到低端内存(第一个640K),bzImage解压缩内核到高端内存(1M以上)。如果内核比较小,那么可以采用zImage 或bzImage之一,两种方式引导的系统运行时是相同的。大的内核采用bzImage,不能采用zImage。
      • vmlinux是未压缩的内核,vmlinuz是vmlinux的压缩文件。
  • initrd.img
    • initrd是initial ramdisk的简写,值得是一个临时文件系统,它在启动阶段被内核调用。
    • initrdinitramfs是实现的两种技术。
      • initrd 是kernel 2.4 及更早的用法
      • initramfs 是kernel 2.6的技术,现在看到的 initrd文件基本上都是initramfs 了。启动的时候加载内核和 initramfs 到内存执行,内核初始化之后,切换到用户态执行 initramfs 的程序/脚本,加载需要的驱动模块、必要配置等,然后加载 rootfs 切换到真正的 rootfs 上去执行后续的 init 过程。
    • Initrd是在实际根文件系统可用之前挂载到系统中的一个初始根文件系统。initrd 与内核绑定在一起,并作为内核引导过程的一部分进行加载。内核然后会将这个 initrd 文件作为其两阶段引导过程的一部分来加载模块,这样才能稍后使用真正的文件系统,并挂载实际的根文件系统。
    • initrd 中包含了实现这个目标所需要的目录和可执行程序的最小集合,例如将内核模块加载到内核中所使用的 insmod 工具。在桌面或服务器 Linux 系统中,initrd 是一个临时的文件系统。其生存周期很短,只会用作到真实文件系统的一个桥梁。在没有存储设备的嵌入式系统中,initrd 是永久的根文件系统。
  • System.map
    • System.map 内核符号映射表,顾名思义就是将内核中的符号(也就是内核中的函数)和它的地址能联系起来的一个列表。是所有符号及其对应地址的一个列表。之所以这样就使为了用户编程方便,直接使用函数符号就可以了,而不用去记要使用函数的地址。当你编译一个新内核时,原来的System.map中的符号信息就不正确了。随着每次内核的编译,就会产生一个新的 System.map文件,并且需要用该文件取代原来的文件。System.map是一个特定内核的内核符号表。它是你当前运行的内核的System.map的链接。
  • config
    • 内核编译时的配置选项。

下面是一个典型的 grub.cfg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# shutdown menu
menuentry "System shutdown" {
echo "System shutting down..."
halt
}

# restart menu
menuentry "System restart" {
echo "System rebooting..."
reboot
}

# UEFI shell
menuentry "UEFI Shell" {
insmod fat
insmod chain
search --no-floppy --set=root --file /shellx64.efi
chainloader /shellx64.efi
}

# linux boot menu
menuentry "Linux" {
set root=(hd0,1)
linux /boot/vmlinuz (add other options here as required)
initrd /boot/initrd.img (if the other kernel uses/needs one)
}

Kernel

Linux内核处理所有操作系统进程,如内存管理、任务调度、I/O、进程间通信和系统总体控制。

bootloader加载 kernel 和可用的 initramfs 文件,并执行 kernel 之后,kernel 将 initramfs(初始RAM文件系统)压缩包解压缩到(然后清空)rootfs(初始根文件系统,特别是ramfs或tmpfs)。首先提取的 initramfs 是在 kernel 构建过程中嵌入 kernel 二进制Update translation.的 initramfs,然后提取可用的外部 initramfs 文件。因此,外部 initramfs 中的文件会覆盖嵌入式 initramfs 中具有相同名称的文件。然后, kernel 执行 /init (在rootfs中)作为第一个进程。early userspace开始。

initramfs 之所以存在,是为了帮系统访问真正的根文件系统(参见 Arch filesystem hierarchy (简体中文)))。也就是说,那些硬件 IDE, SCSI, SATA, USB/FW 所要求的 kernel 模块,如果并没有内置在 kernel 里,就会被 initramfs 负责加载。一旦通过 udev (简体中文)) 之类的程序或脚本加载好模块,启动流程才会继续下去。所以,initramfs 只要有能够让系统访问真实根文件系统的模块就可以了,不用尽可能地包含一切模块。当然,其它真正有用的模块之后会在 init 流程中被 udev 加载好。

Init Process

在「早期用户空间」的最终环节里,真正的根文件系统被挂载好后,就会替换掉原来的根文件系统。接着 /sbin/init 被执行,同样也替换掉原来的 /init 进程。

SysVinit

1983 年以来,System V 便是 Unix 和类 Unix (例如 Linux)系统中的经典启动过程。它包括小程序 init 用于 启动诸如 login (由 getty 启动)这样的基础程序,并运行着名为 rc 的脚本。该脚本,控制着一众附加脚本的 执行,而那些附加脚本便是实施系统初始化所需要的任务的脚本。

程序 init 由文件 /etc/inittab 控制着,并且被组织成用户能够运行的运行级别形式:

1
2
3
4
5
6
7
0 — 停止
1 — 单用户模式
2 — 多用户,无网络
3 — 完整的多用户模式
4 — 用户可定义
5 — 完整的多用户模式,附带显示管理
6 — 重启

常用的默认运行级为 3 或 5。

  • 启动时间长,init是串行启动,只有前一个进程启动完,才会启动下一个进程
  • 启动脚本复杂,Init进程只是执行启动脚本,不管其他事情,脚本需要自己处理各种情况,这往往使得脚本变得很长
  • 由Linux内核加载运行,位于 /sbin/init ,是系统中第一个进程,PID永远为1

Systemd

  • 按需启动服务,减少系统资源消耗。
  • 尽可能并行启动进程,减少系统启动等待时间
  • 由Linx内核加载运行,位于 /usr/lib/systemd/systemd ,是系统中第一个进程,PID永远为1

命令对比

动作 SystemV Systemd
停止某服务 service httpd stop systemctl stop httpd
重启某服务 service httpd restart systemctl restart httpd
检查服务状态 service httpd status systemctl status httpd
删除某服务 chkconfig —del httpd 停掉应用,删除其配置文件
使服务开机自启动 chkconfig —level 5 httpd on systemctl enable httpd
使服务开机不自启动 chkconfig —level 5 httpd off systemctl disable httpd
显示所有已启动的服务 chkconfig —list systemctl list-unit-files
加入自定义服务 chkconfig —add test systemctl load test

Login

一般来说,用户的登录方式有三种:

  • 命令行登录
  • ssh登录
  • 图形界面登录 

这三种情况,都有自己的方式对用户进行认证。

  • 命令行登录:init进程调用getty程序(意为get teletype),让用户输入用户名和密码。输入完成后,再调用login程序,核对密码(Debian还会再多运行一个身份核对程序/etc/pam.d/login)。如果密码正确,就从文件 /etc/passwd 读取该用户指定的shell,然后启动这个shell。
  • ssh登录:这时系统调用sshd程序(Debian还会再运行/etc/pam.d/ssh ),取代getty和login,然后启动shell。
  • 图形界面登录:init进程调用显示管理器,Gnome图形界面对应的显示管理器为gdm(GNOME Display Manager),然后用户输入用户名和密码。如果密码正确,就读取/etc/gdm3/Xsession,启动用户的会话。

Reference