正常 Linux 启动流程大体如下:
- BIOS 启动,完成自检,选择启动硬件
- 如果是磁盘系统读取 MBR
- 从 MBR 指示,找到 GRUB 所在分区,加载 GRUB 显示菜单
- 加载 Linux 内核到内存中
- 执行 INIT 程序
- 进入用户界面
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。
电脑刚上电的时候,会做一些重置工作,比如将 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 分区格式如下所示,
MBR 扇区主要包含如下数据:
- 主引导记录(bootloader),负责从活动分区加载并运行系统引导程序。446 字节
- 硬盘分区表项(DPT——disk partition table),由四个分区表项组成,负责记录磁盘的分区情况。64 字节。
- 硬盘有效标志(magic number),代表引导扇区结束,占用 2 字节。
**Bootloader:**这部分记录了一段较小引导代码,用于去启动硬盘其他分区位置上更大的引导文件,例如 linux 操作系统的 grub。
硬盘的每个分区的第一个扇区叫做 boot sector(不同于 MBR),这个扇区存放的就是操作系统的 loader。如下图,第一个分区的 boot sector 存放着 windows 的 loader,第二个分区放着 Linux 的 loader,第三个第四个由于没有安装操作系统所以空着。MBR 的 bootloader 一般有以下三个功能:
- 提供选单:让用户选择进入哪个系统。
- 读取内核文件:默认启动的 loader 会被拷贝一份到 MBR 中,这样就可以直接读取内核了,图中 1 部分
- 转交给其他 loader:图中 2,3 部分
但是现在的 bootloader 一般用于启动更大的引导文件。
**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 损坏
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 和一系列模块组成,这些可以做很多事情。
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 文件里的配置信息。
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() 会显示出一个让你选择操作系统的列表界面。
当选择某个操作系统之后,就开始调用 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。整个流程如下所示:
6.1. 初始化系统环境
start_kernel() 的第一个函数调用是下面这条。其中,init_task 是系统创建的第一个进程,称其为 0 号进程,这是唯一一个没有通过 fork 或者 kernel_thread 产生的进程。
0 号进程其实是一个空闲进程,它执行的是 cpu_idle() 函数,该函数仅有一条 hlt 汇编指令,这条指令用来在系统闲置时降低电力的使用和减少热的产生。当 linux 的就绪队列中没有其他进程的时候,空闲进程就会被调用,从而实现节电、减少热量的产生。除此之外,0 号进程使用的 PCB 叫做 init_task,在很多链表中起到了表头的作用。
// init\main.c
set_task_stack_end_magic(&init_task);
接下去,比较重要的是中断的初始化。在 trap_init() 这个函数中,设置了很多中断门,用于处理各种中断。需要注意的是,这个函数需要由各个体系分别定义,不同体系结构对这个函数的定义是不同的。
// init\main.c
trap_init();
再接下去是内存管理的初始化。
// init\main.c
mm_init();
之后是初始化调度模块。
// 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。
// 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 号进程将运行一个用户进程。
// 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 系统中其他所有的进程,并且管理这些进程。
// 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() 函数。
// 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 在内核态的一系列操作都完成之后,准备回到用户态时,使用的就是这些值。从而,成功进入用户态并开始运行。
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 阶段进行初始化:
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。因此,对于内核态来说,进程和线程并无特别大的差别。
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 就是运行级别)
这里的文件都采用“字母 S 或 K+两位数字+程序名”的命名方式。其中 S 开头的表示在这个级别需要执行 start 命令,K 开头需要执行 Stop 命令,数字越小越优先执行。系统会依次执行相应的软件和服务,负责用户界面的程序也被启动你就有了 X11 界面,然后是 SSH 服务你就可以使用 ssh 登录。这样系统就完成了启动。
当然啦现在这种方式已经过时了,目前基本使用 systemd 方式用 systemctl 命令管理。
8. 总结
-
未按下开机键之前:在机子没接通电源之前(台式机没有接入电源,笔记本电池没电)的情况下,工作的只有 RTC 和 CMOS,这两个都是靠内部电源维护支持的。当接通电源之后(台式机接入电源,笔记本电池有电或者接入电源)的情况,RTC 和 CMOS 的电源供应变成外部供电了,同时 EC 以及南桥部分组件也会在工作。EC 在工作状态下等待用户按下开机键,并且还负责电池的充放电检测、指示灯、功能键等。
-
BIOS 阶段:比如将 CS 设置为 0xFFFF,将 IP 设置为 0x0000(也有一说是 0xF000 和 0xFFF0),所以 CPU 执行的第一条指令位于地址 0xFFFF0 处。而该地址处,是一个 JMP 指令(jmp [0xF000,0xE05B])会跳转到 ROM 中初始化工作的代码中(这个地址才是 BIOS 代码真正开始的地方)。BIOS 完成一些初始化操作之后,会加载 bootloader 程序。(BIOS 阶段一般管不了)
-
bootloader 阶段,bootloader 程序位于启动盘的第一个扇区。在 Linux 中可以使用 grub 相关的程序安装相应的 bootloader 程序。由于 bootloader 程序通常比较小,比较难胜任 linux 内核的加载工作,因此 bootloader 会先加载系统引导程序,比如 linux 平台中的 grub2 。grub2 会完成实模式到保护模式的切换、加载并且初始化 initrd(其实会运行一个内存文件系统),同时也加载相应的 Linux 内核到内存中。
grub2 和 bootloader(前面提到了 bootloader 可以由 grub 安装,因此这两个是相当于可以捆绑的)从广义上来说都算是 “bootloader” 阶段。
Linux 内核执行文件一般会放在 /boot 目录下,文件名类似 vmlinuz*。
-
内核初始化阶段:内核初始化阶段会依次进行 trap、mem、sched、VFS 等初始化。在内核完成各种初始化之后,将会创建 1 号进程,而这个 1 号进程首先会使用 initrd 中的 init 程序,这个 init 程序会挂载真正的根文件系统,之后再加载并运行真正的根文件系统中的 init 程序(在没有外面参数指定程序路径的情况下,一般会从几个缺省路径尝试执行 1 号进程的代码。这几个路径都是 Unix 常用的可执行代码路径)。之后,再从内核态切换到用户态。
除此之外,还会创建并启动 2 号进程。
9. 巨人的肩膀
- https://mp.weixin.qq.com/s/6vFXphhkPpyYEDSgibl_vA
- 极客时间-趣谈 Linux 操作系统