目录

Linux Kernel | Linux Kernel 启动全过程

正常 Linux 启动流程大体如下:

  1. BIOS 启动,完成自检,选择启动硬件
  2. 如果是磁盘系统读取 MBR
  3. 从 MBR 指示,找到 GRUB 所在分区,加载 GRUB 显示菜单
  4. 加载 Linux 内核到内存中
  5. 执行 INIT 程序
  6. 进入用户界面

1. 电源键按下前后

1.1. 开机之前

在没有外部电源的情况下,基本只有 RTC 和 CMOS 在 RTC 电源供电的情况下才能正常工作。电源一般是个纽扣电池。RTC 芯片保持机器内部的时钟,这也是为什么我们的计算机关机之后再次启动时间还是准确的(不考虑操作系统启动后利用网络同步时间)。而 CMOS 是电脑主板上的一块可读写的RAM芯片。因为可读写的特性,所以在电脑主板上用来保存 BIOS 设置计算机硬件参数后的数据。在计算机领域,CMOS 常指保存计算机基本启动信息(如日期、时间、启动设置等)的芯片。

外部的 ATX 电源接入之后,ATX 会使用 +5VSB(Stand-By) 电源唤醒一部分硬件,例如南桥(系统 IO 芯片)、EC(嵌入式控制器、单片机)等。+5VSB 电压为开机电路和需要唤醒机器的 WOL 和 USB 等设备提供电源,但是不会为 CPU 等提供工作电压。这些更多是指台式机。对于笔记本来说,由于存在锂电池的原因,在没有接入电源之前,EC 和南桥就处于工作状态了。这也是为什么我们打开笔记本电脑,按下开机键,系统能接收到这个信号,并准备开始工作。

实际上不同的主板、应用场景都会采用不同的电源时序控制方案,笔记本采用 EC,台式机很多用SIO(Super I/O)或者定制芯片,嵌入式设备以及手机采用PMIC(Power Management IC)。

综上:在机子没接通电源之前(台式机没有接入电源,笔记本电池没电)的情况下,工作的只有 RTC 和 CMOS,这两个都是靠内部电源维护支持的。当接通电源之后(台式机接入电源,笔记本电池有电或者接入电源)的情况,RTC 和 CMOS 的电源供应变成外部供电了,同时 EC 以及南桥部分组件也会在工作。EC 在工作状态下等待用户按下开机键,并且还负责电池的充放电检测、指示灯、功能键等。

1.2. 按下电源键

  • 按下电源键之后,EC 接收到 PWRSW# 信号。
  • EC 向南桥发送 PM_PWRBTN#! PM_PWRBTN#!信号。
  • 南桥回应 EC 三个信号 SLP_S5#(退出 S5 关机状态),SLP_S4#(退出 S4 休眠状态),SLP_S3#(退出 S3 待机状态)。
  • EC 收到南桥发来的信号之后,了解南桥已经准备就绪。
  • ATX 电源收到信号,开始工作,发出各路基本电压给主板上的各个元件。然后发送 PWROK# 信号给 EC。
  • 之后由 EC 转发给南桥和北桥(这个过程还很多,直到准备通知 CPU)……
  • 最后,南桥发送 PLT_RST# 给北桥,紧接着向 CPU 发送 PWRGOOD# 信号。
  • 北桥接收到南桥的信号后,过 1 秒钟向 CPU 发送 CPU_RST# 信号。

2. BIOS 和 UEFI 阶段

BIOS 和 UEFI,他们是计算机按下电源后最先被执行的程序。

在主板上有一块内存区域,叫做 ROM(Read Only Memory),它是只读的,上面往往已经固化了一些初始化程序,也就是 BIOS(Basic Input and Ouput)。在 BIOS 阶段,可使用的内存地址空间是非常有限的。在 x86 系统中,仅有 1MB,其中 1MB 空间最上面的 0xF0000-0xFFFFF 这 64K 映射给 ROM。也就是说访问这部分地址的时候,会访问 ROM。

https://img.dawnguo.cn/Linux/image-20201109161144270.png

电脑刚上电的时候,会做一些重置工作,比如将 CS 设置为 0xFFFF,将 IP 设置为 0x0000(书上有说是 0xF000 和 0xFFF0),所以 CPU 执行的第一条指令位于地址 0xFFFF0 处,该地址正好在 ROM 的范围内。而该地址处,是一个 JMP 指令(jmp [0xF000,0xE05B])会跳转到 ROM 中初始化工作的代码中(这个地址才是 BIOS 代码真正开始的地方)。于是,BIOS 开始进行初始化的工作。

jmp [0xF000,0xE05B] 这条指令的执行流程:通过 FSB(前端总线)将这个地址发送到北桥,然后通过 HUB-LINK 到南桥,通过LPC(Low Pin Count Bus)到 EC,再通过 X-BUS 一直到达 BIOS 取指令。在CPU读到所发出的地址内的指令后,开始 BIOS 程序执行。

BIOS 主要做以下工作:

  • 自诊断程序:读取 CMOS 中的内容获得硬件配置信息,并对其进行自检和初始化;完整的 POST(Power On Self Test) 自检将包括CPU、640K基本内存、1M以上的扩展内存、ROM、主板、 CMOS RAM、串并口、显示卡、软硬盘子系统及键盘测试。
  • CMOS 设置程序:引导过程中,用特殊热键启动,进行设置后,存入CMOS RAM中;对应 Dell 计算机启动时我们按F12,进入的 BIOS 设置界面。
  • 主要 I/O 设备的驱动程序和中断服务:要建立一个中断向量表和中断服务程序,因为这个 BIOS 阶段也会用到键盘和鼠标,这些都是需要通过中断进行的。在内存中 0x000~0x3FF 处建立数据结构,中断向量表 IVT 并填写中断例程。
  • 输出内容,也就是在内存空间映射显存的空间,从而可以在显示上显示一些字符。
  • 系统自举 bootloader:在自检成功后将磁盘相对 0 道 0 扇区上的 bootloader 装入内存。

x86 有两种模式,一种是实模式,只能寻址 1MB,每个段最多 64K。另一种是保护模式,能够寻址 4G。

3. bootloader 阶段 + MBR/GPT 分区格式

BIOS 阶段会让你选择启动盘。启动盘的特点是:在它的第一个扇区,通常占 512 个字节,并且以 0x55 和 0xaa 结尾。当满足这个条件时,那么就说明是启动盘。假如不是以 0x55 和 0xaa 结尾,那就不是启动盘。

启动盘的第一个扇区中,存放着 bootloader 程序,这第一个扇区又被称为主扇区(Master Boot Record, MBR)。MBR 的任务一般是加载某个程序到指定位置,这个程序一般是内核加载器,很少是直接加载内核的。

3.1. MBR 分区格式

MBR 分区格式如下所示,

https://img.dawnguo.cn/Linux/%2525E5%2525BE%2525AE%2525E4%2525BF%2525A1%2525E5%25259B%2525BE%2525E7%252589%252587_20201109084830.png

MBR 扇区主要包含如下数据:

  • 主引导记录(bootloader),负责从活动分区加载并运行系统引导程序。446 字节
  • 硬盘分区表项(DPT——disk partition table),由四个分区表项组成,负责记录磁盘的分区情况。64 字节。
  • 硬盘有效标志(magic number),代表引导扇区结束,占用 2 字节。

https://img.dawnguo.cn/Linux/image-20201107170801382.png

**Bootloader:**这部分记录了一段较小引导代码,用于去启动硬盘其他分区位置上更大的引导文件,例如 linux 操作系统的 grub。

硬盘的每个分区的第一个扇区叫做 boot sector(不同于 MBR),这个扇区存放的就是操作系统的 loader。如下图,第一个分区的 boot sector 存放着 windows 的 loader,第二个分区放着 Linux 的 loader,第三个第四个由于没有安装操作系统所以空着。MBR 的 bootloader 一般有以下三个功能:

  • 提供选单:让用户选择进入哪个系统。
  • 读取内核文件:默认启动的 loader 会被拷贝一份到 MBR 中,这样就可以直接读取内核了,图中 1 部分
  • 转交给其他 loader:图中 2,3 部分

但是现在的 bootloader 一般用于启动更大的引导文件。

https://img.dawnguo.cn/Linux/%2525E5%2525BE%2525AE%2525E4%2525BF%2525A1%2525E5%25259B%2525BE%2525E7%252589%252587_20201109084830-20210421112923769-20210421112925954.png

**Disk Partition table:**这一部分 64 字节大小被均分为 4 份,每份大小 16 字节,每当我们在硬盘上创建出一个新的主分区或者扩展分区时,便会占用 1 个 16 字节的大小用于记录这个分区的相关信息(例如起始和截止柱面位置、分区文件系统类型等等)。这就是为什么 mbr 分区模式最多只能有 4 个主分区的原因。

MBR 的局限:

  • 最多只支持 4 个主分区,超过 4 个就需要使用扩展分区。
  • 磁盘的最大容量只能到 2.2TB

3.2. GPT 分区格式

为了解决 MBR 的问题,GPT 分区诞生,GPT 全称 Globally Unique Identifier Partition Table,也叫 GUID 分区表,它是 UEFI 规范的一部分(但这并不是说它只支持 UEFI,它也支持 BIOS 方式的引导)。

GPT 分区结构如下:

  • Protective MBR:GPT 分区表的最前面部分也保存了和 MBR 相同的格式和内容称为 Protective MBR,这极大的提高了 GPT 分区表的兼容性。
  • 主 GPT Header:这里记录了分区表项目数和每项目大小。
  • 主 GPT 分区表:包含分区的类型 GUID,名称,起始终止位置,该分区的 GUID 以及分区属性
  • 实际分区
  • 备份 GPT 分区表: 用于提高安全性,防止主 GPT 分区表损坏
  • 备份 GPT Header: 用于提高安全性,防止主 GPT Header 损坏

https://img.dawnguo.cn/Linux/%2525E5%2525BE%2525AE%2525E4%2525BF%2525A1%2525E5%25259B%2525BE%2525E7%252589%252587_20201109085645.png

4. 系统引导程序:Syslinux 和 GRUB

前文说到 MBR 的 bootloader 主要功能是交棒内核,但是 bootloader 不会直接拉起 linux 内核,400K 太小,它没有能力将 linux 内核直接加载到内存。这时需要系统引导程序登场,它的主要目的就是将系统内核镜像和 initrd 镜像加载到内存并将控制权交给它们。目前常用的有两种 Syslinux 和 GRUB:

  • Syslinux 是一个启动加载器集合,可以从硬盘、光盘或通过 PXE 的网络引导启动系统。支持的文件系统包括 FAT,ext2,ext3,ext4 和非压缩单设备 Btrfs 文件系统。
  • GRUB ,即 Grand Unified Bootloader(大一统启动加载器),是一个多重启动加载器,承自 PUPA 项目。今的 GRUB 也被称作 GRUB 2,而 GRUB Legacy 表示 0.9x 版本。

对于普通用户来说他们有什么用呢?它可以提供选单选择 Linux 内核版本,此外加载程序使得我们可以向 Linux 内核传递参数。Syslinux 已经不支持 bios64 位系统了,目前使用 GRUB2 的比较多。

5. 实例:Linux 中的 bootloader/系统引导阶段

BIOS 阶段完成之后,接下去会启动 bootloader,将 CPU 的使用权交给 bootloader。为了实现这个过程:BIOS 会将 bootloader 会被加载到内存中 0x7c00 处,之后再通过 jmp 0:0x7c00 指令进行跳转(在这之前 CS 寄存器会被替换)。

Linux 中可以使用 grub 程序来安装 bootloader,grub2 第一个安装的就是 boot.img(也就是 bootloader)。它由 boot.S 编译而成,一共 512 字节,正式安装到启动盘的第一个扇区,也就是 MBR。

使用 grub2-install /dev/sda,可以将启动程序安装到相应的位置。

5.1. boot.img 阶段

boot.img 做不了太多事情(只有 512 字节),它要做的一个事情就是加载 grub2 的另一个镜像 core.img。core.img 由 lzma_decompress.img、diskboot.img、kernel.img 和一系列模块组成,这些可以做很多事情。

https://img.dawnguo.cn/Linux/image-20201109170435672.png

5.2. diskboot.img 阶段

boot.img 先加载的是 core.img 的第一个扇区内容。如果从硬盘启动的话,这个扇区里面就是 diskboot.img,对应的代码是 diskboot.S。boot.img 将控制交给 diskboot.img 之后,diskboot.img 就是将 core.img 的其他部分加载进来。先是用于解压缩的程序 lzma_decompress.img(对应的代码是 startup_raw.S),这个是因为 kernel.img 是经过压缩的,需要将 kernel.img 解压缩;之后就是加载 kernel.img 了,需要解压缩(注意,这个 kernel 不是 Linux 的内核,而是 grub 的内核);最后是各个模块 module 对应的映像加载。

5.3. lzma_decompress.img 阶段

在 BIOS 阶段,是运行在实模式下的,但是随着加载的东西变多之后,1MB 的内存空间放不下这么多的镜像文件。因此, lzma_decompress.img 会调用 real_to_prot 切换到保护模式,之后再进行解压缩。

实模式切换到保护模式需要干很多工作,大部分工作都与内存的访问方式相关。

  • 启用分段,就是在内存中建立段描述符,将寄存器里面的段寄存器变成段选择子,指向某个段描述符,这样就能实现不同进程的切换了。
  • 启动分页,将内存分成相等大小的页。
  • 打开 Gate A20,也就是第 21 根地址线的控制线。在实模式 8086 下面,一共就 20 个地址线,可访问 1M 的地址空间。假如超过这个限度之后就会绕回来。在保护模式下,第 21 根及之后的地址线要起作用了,所以需要打开 Gate A20。

5.4. kernel.img 阶段

(处于保护模式下了)之后,对 kernel.img 进行解压缩,然后跳转到 kernel.img 开始运行。kernel.img 对应的代码是 startup.S 以及一些 C 文件,在 startup.S 中会调用 grub_main(),这是 grub kernel 的主函数。在这个函数里面,grub_load_config() 开始解析 grub.conf 文件里的配置信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
menuentry 'CentOS Linux (3.10.0-862.el7.x86_64) 7 (Core)' --class centos --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-3.10.0-862.el7.x86_64-advanced-b1aceb95-6b9e-464a-a589-bed66220ebee' {
  load_video
  set gfxpayload=keep
  insmod gzio
  insmod part_msdos
  insmod ext2
  set root='hd0,msdos1'
  if [ x$feature_platform_search_hint = xy ]; then
    search --no-floppy --fs-uuid --set=root --hint='hd0,msdos1'  b1aceb95-6b9e-464a-a589-bed66220ebee
  else
    search --no-floppy --fs-uuid --set=root b1aceb95-6b9e-464a-a589-bed66220ebee
  fi
  linux16 /boot/vmlinuz-3.10.0-862.el7.x86_64 root=UUID=b1aceb95-6b9e-464a-a589-bed66220ebee ro console=tty0 console=ttyS0,115200 crashkernel=auto net.ifnames=0 biosdevname=0 rhgb quiet 
  initrd16 /boot/initramfs-3.10.0-862.el7.x86_64.img
}

如果是正常启动,grub_main() 最后会调用 grub_command_execute(“normal”, 0, 0),最终会调用 grub_normal_execute() 函数。在这个函数里面,grub_show_menu() 会显示出一个让你选择操作系统的列表界面。

https://img.dawnguo.cn/Linux/image-20201109212019480.png

当选择某个操作系统之后,就开始调用 grub_menu_execute_entry(),开始解析并执行你选择的那一项。

  • linux 命令表示装载指定的内核文件,并传递内核启动参数,这个会使得 grub_cmd_linux() 函数被调用,在经过一些检查之后将整个 linux 内核镜像加载到内存。

  • initrd 命令将 init ramdisk 路径作为参数传递给启动的内核,这个会使得 grub_cmd_initrd() 函数被调用,从而将 initramfs 加载到内存中。

    initrd 是“initial ramdisk”的简写。initrd 字面上的意思就是"boot loader initialized RAM disk",这是一块特殊的 RAM disk,会运行内存文件系统。

当这些事情都完成之后,grub_command_execute(”boot’, 0, 0) 会被执行,这时候才真正启动内核。

6. 内核初始化阶段

系统引导阶段交棒之后进入内核初始化阶段。内核初始化阶段会进行相应的 trap、mem、sched、VFS 等初始化,然后创建 1 号进程先执行 initrd 中的 init 程序,initrd 中的 init 程序完成阶段性目标后,会挂载真正的根文件系统,然后再去执行相应的 init 程序,比如 /sbin/init 程序。同时,还会创建并启动 2 号进程。

vmlinuz 是可引导的、压缩的内核。其中 vm 代表 Virtual Memory,这是 Linux 能够使用硬盘空间作为虚拟内存。

Linux 内核的启动从位于 init/main.c 的入口函数 start_kernel() 开始,start_kernel() 相当于内核的 main() 函数。而这个函数中是各种各样的初始化函数 xxx_init。整个流程如下所示:

https://img.dawnguo.cn/Linux/image-20200523224648541.png

6.1. 初始化系统环境

start_kernel() 的第一个函数调用是下面这条。其中,init_task 是系统创建的第一个进程,称其为 0 号进程,这是唯一一个没有通过 fork 或者 kernel_thread 产生的进程。

0 号进程其实是一个空闲进程,它执行的是 cpu_idle() 函数,该函数仅有一条 hlt 汇编指令,这条指令用来在系统闲置时降低电力的使用和减少热的产生。当 linux 的就绪队列中没有其他进程的时候,空闲进程就会被调用,从而实现节电、减少热量的产生。除此之外,0 号进程使用的 PCB 叫做 init_task,在很多链表中起到了表头的作用。

1
2
// init\main.c
set_task_stack_end_magic(&init_task);

接下去,比较重要的是中断的初始化。在 trap_init() 这个函数中,设置了很多中断门,用于处理各种中断。需要注意的是,这个函数需要由各个体系分别定义,不同体系结构对这个函数的定义是不同的。

1
2
// init\main.c
trap_init();

再接下去是内存管理的初始化。

1
2
// init\main.c
mm_init();

之后是初始化调度模块。

1
2
// init\main.c
sched_init();

再之后是初始化 VFS,这个时候会先初始化一个基于内存的文件系统 rootfs。函数 vfs_caches_init() 就是用来初始化基于内存的文件系统 rootfs。在这个函数会有这样的调用流程: mnt_init()->init_rootfs(),而函数 init_rootfs() 里有一行代码,register_filesystem(&rootfs_fs_type),它会在 VFS 虚拟文件系统里面注册了一种类型,我们定义为 struct file_system_type rootfs_fs_type。

1
2
// init\main.c
vfs_caches_init();

最后,start_kernel() 调用函数 rest_init() 用来做其他方面的初始化:

  • 第一个就是初始化 1 号进程,1 号进程是所有用户态进程的祖先了。在内核准备好之后,这个进程会切换到用户态。
  • 第二个是初始化 2 号进程,2 号进程是所有内核态线程的祖先(Linux 中线程和进程都是用 task_struct 结构体,放在一个链表中)。

6.2. 初始化 1 号进程

res_init() 函数会使用 kernel_thread() 创建第二个进程,也就是 1 号进程,1 号进程将运行一个用户进程。

1
2
// init\main.c---rest_init()
pid = kernel_thread(kernel_init, NULL, CLONE_FS);

由于,执行 kernel_thread() 的时候还在内核态,而我们需要切换到用户态。此时,需要借助 kernel_thread() 中的第一个参数 kernel_init,这其实是一个函数。新创建的 1 号进程会运行这个函数。而在 kernel_init() 函数中会调用 kernel_init_freeable() 函数,而在 kernel_init_freeable() 函数中会根据情况对 ramdisk_execute_command 这个变量进行赋值。之后,假如 ramdisk_execute_command 有被设置的话,那么会调用 run_init_process() 函数,而这个函数会调用 do_execve() 函数去运行 ramdisk 的 /init。同理,try_to_run_init_process() 函数最终调用的也是 do_execve() 函数,后者最终是去运行普通文件系统上的 /sbin/init、/etc/init、/bin/init、/bin/sh,不同版本的 Linux 会运行不同的文件,但是只要将一个文件运行起来之后就可以了。

目前主流的 Linux 发行版,无论是 RedHat 系的还是 Debian 系的,都会把 /sbin/init 作为符号链接指向 Systemd。Systemd 是目前最流行的 Linux init 进程,在它之前还有 SysVinit、UpStart 等 Linux init 进程。但无论是哪种 Linux init 进程,它最基本的功能都是创建出 Linux 系统中其他所有的进程,并且管理这些进程。

 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
27
28
29
30
31
32
33
// init\main.c
static int __ref kernel_init(void *unused) {
	......
	kernel_init_freeable();
    
    if (ramdisk_execute_command) {
        ret = run_init_process(ramdisk_execute_command);
        if (!ret)
            return 0;
        pr_err("Failed to execute %s (error %d)\n", ramdisk_execute_command, ret);
    }
    
    if (!try_to_run_init_process("/sbin/init") ||
	    !try_to_run_init_process("/etc/init") ||
	    !try_to_run_init_process("/bin/init") ||
	    !try_to_run_init_process("/bin/sh"))
		return 0;
}

static noinline void __init kernel_init_freeable(void) {
	......
	if (!ramdisk_execute_command)
		ramdisk_execute_command = "/init";
    ......
}

static int run_init_process(const char *init_filename) {
	argv_init[0] = init_filename;
	pr_info("Run %s as init process\n", init_filename);
	return do_execve(getname_kernel(init_filename),
		(const char __user *const __user *)argv_init,
		(const char __user *const __user *)envp_init);
}

调用 do_execve() 之后,会依次调用 do_execveat_common() —__do_execve_file()—exec_binprm()—search_binary_handler(),而在 search_binary_handler() 中又会通过 fmt->load_binary() 加载 binary 文件。针对 ELF 格式来说,如下所示,也就是调用的是 load_elf_binary() 函数。

1
2
3
4
5
6
7
8
// fs\binfmt_elf.c
static struct linux_binfmt elf_format = {
	.module		= THIS_MODULE,
	.load_binary	= load_elf_binary,
	.load_shlib	= load_elf_library,
	.core_dump	= elf_core_dump,
	.min_coredump	= ELF_EXEC_PAGESIZE,
};

load_elf_binary() 函数最终会调用函数 start_thread() 设置相应的值,这些值原本是发生中断或系统调用之后由系统自动保存的,由于现在不涉及到用户态陷入到内核态,所以这边相当于手动补上这些值。那么,当 do_execve 在内核态的一系列操作都完成之后,准备回到用户态时,使用的就是这些值。从而,成功进入用户态并开始运行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp) {
	set_user_gs(regs, 0);
	regs->fs		= 0;
	regs->ds		= __USER_DS;
	regs->es		= __USER_DS;
	regs->ss		= __USER_DS;
	regs->cs		= __USER_CS;
	regs->ip		= new_ip;
	regs->sp		= new_sp;
	regs->flags		= X86_EFLAGS_IF;
	force_iret();
}
EXPORT_SYMBOL_GPL(start_thread);

6.3. ramdisk 的作用

init 从内核态到用户态,首先执行的程序时是 ramdisk 的 init。ramdisk 是一个内存上的文件系统,在内核启动的时候(bootloader 阶段),会配置 ramdisk。也就相当于在执行内核初始化的那块代码的之前,已经配置好了。之后根据 ramdisk 上的 init 程序会先根据存储系统的类型加载存储系统所需的驱动(ramdisk 中包含了各种存储设备驱动),设置真正的根文件系统。然后从这个根文件系统中启动 init 程序,而启动的 init 程序将成为所有用户态进程的祖先。

ramdisk 在内核启动之前的 grub 阶段通过 initrd16 指令进行指定,那么会在 grub 阶段进行初始化:

1
initrd16 /boot/initramfs-3.10.0-862.el7.x86_64.img

为什么又需要 ramdisk 呢?而需要 ramdisk 是因为 do_execve 要加载 init 程序,那么 init 程序得在文件系统上才行,而文件系统一定是在一个存储设备上,比如硬盘。Linux 访问存储设备,又必须需要驱动才能访问。因此,假如直接先访问需要驱动的存储设备上的文件系统上的 init 程序的话,那么需要把驱动都放进内核。这将会带来很大问题,一是因为市面上的存储驱动很多,都放入内核的话,会很大;二是因为存储驱动会更新和增加,更新和添加新的驱动会很麻烦。因此,Linux 在刚开始初始化的时候(没有文件系统)先弄一个基于内存的文件系统,这个就是 ramdisk,毕竟内存访问是不需要驱动的。

此时,ramdisk 就是临时的根文件系统。会先从 ramdisk 中加载 /init 程序,而 /init 这个程序会根据存储系统的类型加载驱动,有了驱动就可以设置真正的根文件系统了,接下去 ramdisk 上的 /init 程序会启动真正根文件系统上的 init 了。那么,此时 1 号进程也算是真正意义上成为了用户态所有进程的祖先了。

ramdisk 是可以配置的而且也不属于内核代码,所以这样一来不仅使得 Linux 内核代码减少,而且具有灵活性,以后只需要更换 ramdisk 即可,而不需要更换内核代码。也就是相当于假如换了一个存储设备的话,只要换一个 ramdisk 就好了,内核代码还是一样的。但是假如驱动程序都放在内核的话,那么每次更新存储设备都要重新编译内核,效率极低,很不方便。

6.4. 初始化 2 号进程

rest_init() 第二个大事情就是创建第 3 个进程,也就是 2 号进程,它是所有内核态进程的公共祖先。同样,系统使用函数 kernel_thread 来创建进程。这里创建出来的进程会执行 kthreadd() 函数,它负责内核态所有线程的调度和管理,是内核态所有线程运行的祖先。

kernel_thread 中的 thread 可以翻译成线程,那么这边到底是创建线程还是进程呢?其实站在内核态角度,无论是进程和线程,它们使用的结构体都是一样的,都平放在同一个链表中,都可以被称为 task。因此,对于内核态来说,进程和线程并无特别大的差别。

1
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);

7. init 阶段

内核初始化阶段完成以后,系统已挂载真正的根文件系统,执行 /sbin/init 程序初始化系统环境。

/sbin/init 会首先确定运行级别,这个配置在/etc/inittab 中,一般 Linux 有 7 种运行级别(0-6)。一般来说,0 是关机,1 是单用户模式(也就是维护模式),6 是重启。运行级别 2-5,各个发行版不太一样,对于 Debian 来说,都是同样的多用户模式(也就是正常模式)。确定运行级别后会访问/etc/rcN.d(这里的 N 就是运行级别)

https://img.dawnguo.cn/Linux/image-20201109095039930.png

这里的文件都采用“字母 S 或 K+两位数字+程序名”的命名方式。其中 S 开头的表示在这个级别需要执行 start 命令,K 开头需要执行 Stop 命令,数字越小越优先执行。系统会依次执行相应的软件和服务,负责用户界面的程序也被启动你就有了 X11 界面,然后是 SSH 服务你就可以使用 ssh 登录。这样系统就完成了启动。

当然啦现在这种方式已经过时了,目前基本使用 systemd 方式用 systemctl 命令管理。

8. 总结

  1. 未按下开机键之前:在机子没接通电源之前(台式机没有接入电源,笔记本电池没电)的情况下,工作的只有 RTC 和 CMOS,这两个都是靠内部电源维护支持的。当接通电源之后(台式机接入电源,笔记本电池有电或者接入电源)的情况,RTC 和 CMOS 的电源供应变成外部供电了,同时 EC 以及南桥部分组件也会在工作。EC 在工作状态下等待用户按下开机键,并且还负责电池的充放电检测、指示灯、功能键等。

  2. BIOS 阶段:比如将 CS 设置为 0xFFFF,将 IP 设置为 0x0000(也有一说是 0xF000 和 0xFFF0),所以 CPU 执行的第一条指令位于地址 0xFFFF0 处。而该地址处,是一个 JMP 指令(jmp [0xF000,0xE05B])会跳转到 ROM 中初始化工作的代码中(这个地址才是 BIOS 代码真正开始的地方)。BIOS 完成一些初始化操作之后,会加载 bootloader 程序。(BIOS 阶段一般管不了)

  3. bootloader 阶段,bootloader 程序位于启动盘的第一个扇区。在 Linux 中可以使用 grub 相关的程序安装相应的 bootloader 程序。由于 bootloader 程序通常比较小,比较难胜任 linux 内核的加载工作,因此 bootloader 会先加载系统引导程序,比如 linux 平台中的 grub2 。grub2 会完成实模式到保护模式的切换、加载并且初始化 initrd(其实会运行一个内存文件系统),同时也加载相应的 Linux 内核到内存中

    grub2 和 bootloader(前面提到了 bootloader 可以由 grub 安装,因此这两个是相当于可以捆绑的)从广义上来说都算是 “bootloader” 阶段。

    Linux 内核执行文件一般会放在 /boot 目录下,文件名类似 vmlinuz*。

  4. 内核初始化阶段:内核初始化阶段会依次进行 trap、mem、sched、VFS 等初始化。在内核完成各种初始化之后,将会创建 1 号进程,而这个 1 号进程首先会使用 initrd 中的 init 程序,这个 init 程序会挂载真正的根文件系统,之后再加载并运行真正的根文件系统中的 init 程序(在没有外面参数指定程序路径的情况下,一般会从几个缺省路径尝试执行 1 号进程的代码。这几个路径都是 Unix 常用的可执行代码路径)。之后,再从内核态切换到用户态。

    除此之外,还会创建并启动 2 号进程。

9. 巨人的肩膀

  1. https://mp.weixin.qq.com/s/6vFXphhkPpyYEDSgibl_vA
  2. 极客时间-趣谈 Linux 操作系统