Linux - 架构与系统初始化
基本概念

CPU(Central Processing Unit)
运算单元 负责执行加法、减法、位移等操作。
数据单元 包括缓存和寄存器组,用于暂存数据和运算结果。根据数据地址,从数据段里读到数据寄存器,参与运算。
控制单元 指导运算单元执行指令操作。其中包含指令指针寄存器,存放下一条指令在内存中的地址。控制单元不断将代码段的指令取出,放入指令寄存器。而指令包括执行的操作(交由运算单元处理)、待操作的数据(交由数据单元处理)。
总线(Bus)
总线组成了 CPU 和其他设备的高速通道。包括 地址总线(Address Bus)和 数据总线(Data Bus)。
地址总线(Address Bus)
地址总线的位数决定可访问地址范围,比如 2 位总线可访问的地址为 2^2 == 4
个。
数据总线(Data Bus)
数据总线的位数决定每次可传输的数据量,体现为访问速度。比如要在 2 位总线取 8 位数据,则需要取 8 / 2 == 4
次。
内存(Memory)
…
I/O 设备
…
x86 架构
8086 处理器
源于 Intel 8086 CPU,其实现了 开放、统一、兼容 的平台。
型号 | 总线位宽 | 地址位 | 寻址空间 |
---|---|---|---|
8080 | 8 | 16 | 64K (2^16) |
8086 | 16 | 20 | 1M (2^20) |
8088 | 8 | 20 | 1M |
80386 | 32 | 32 | 4G |
8086 CPU 架构如图:

单元 | 寄存器 | 备注 | |
---|---|---|---|
运算单元 | |||
数据单元 | 通用寄存器 | AX、BX、CX、DX、SP、BP、SI、DI 各 16 位, 其中 AX、BX、CX、DX 可分成两个 8 位(AH、AL、BH、BL、CH、CL、DH、DL)。 |
|
控制单元 | 指令指针寄存器 IP | 指向代码段中下一条指令的位置。 CPU 根据它将指令从内存的代码段中加载到指令队列中,再交给运算单元去执行。 |
|
段寄存器 SR | 代码段寄存器 CS 数据段寄存器 DS |
CS 和 DS 中都存放着 段的起始地址,用于在内存中找到数据和代码,加载到通用寄存器。 代码段的偏移量在 IP 寄存器中,数据段的偏移量放在通用寄存器中。 寻址:起始地址 * 16 + 偏移量(即 16 位左移 4 位,再加上偏移量)。 **最大寻址空间 2^20 == 1M **。 |
|
附加段寄存器 ES | |||
栈寄存器 SS | 栈结构,用于处理函数调用。 |
32 位处理器
从 8086 升级到 32 位处理器要保持兼容:
将 8 个 16 位通用寄存器扩展到 8 个 32 位,保留 16 位和 8 位的使用方式。
指令指针寄存器 IP 扩展到 32 位。
段寄存器保留为 16 位,段的起始地址改为存放在内存中的 段描述符(Segment Descriptor) 表中,段寄存器存放 选择子(Selector),寻址方式改为从段寄存器找到选择子,再从段描述符表中拿到段起始地址。
为实现快速访问,段寄存器将段起始地址从内存中拿到 CPU 的描述符高速缓存器中。
实模式与保护模式
在 32 位系统架构下,系统启动时处于 实模式(Real Pattern),进行一系列操作后切换为 保护模式(Protected Pattern)。
实模式的寻址方式:从段寄存器中取段起始地址,从指令指针寄存器中取偏移量。
保护模式的寻址方式:从段寄存器取出选择子,根据选择子从内存中的段描述符表中取起始地址(存放在高速缓存中),再从指令指针寄存器中取偏移量。

除了可访问的地址空间更大,保护模式的意义还在于限制用户态代码执行更高权限的指令,即实现权限管理,这在稍后将会谈到。
BIOS
计算机主板上有 ROM(Read Only Memory,只读存储器)固定存放 BIOS(Basic Input and Output System,基本输入输出系统)程序。

在计算机启动时只有 1M 内存空间,其中 0xF0000 - 0xFFFFF 共 64K 的地址映射给 ROM。在实模式下执行以下过程:
将代码段寄存器 CS 设置为 0xFFFF,将指令指针寄存器 IP 设置为 0x0000,因此第一条指令寻址
0xFFFF * 16 + 0x0000 == 0xFFFF0
。该地址为 JMP 指令,跳到 ROM 中开始执行 BIOS 初始化。
BIOS 确认硬件正常后,建立 中断向量表 和 中断服务程序。
在 BIOS 完成后,进入 Bootloader 阶段。
Bootloader
此前 Linus 的 grub2(Grand Unified Bootloader Version 2)工具将 boot.img 安装到启动盘的 MBR 扇区(Master Boot Record,主引导记录,是启动盘的第一个扇区,共 512bytes,以 0xAA55 结束)。
grub2-mkconfig -o /boot/grub2/grub.cfg
配置系统启动的选项。
grub2-install /dev/sda
将启动程序安装到相应的位置。
在 BIOS 完成后,将 boot.img 从硬盘加载到内存 0x7c00 运行,即加载 grub2 的 core.img 镜像的各部分。

diskboot.img
负责加载 core.img 镜像的其它部分。
lzma_decompress.img
并调用 real_to_prot
,从 实模式 切换到 保护模式:
启用分段:在内存建立段描述符表,将段寄存器变成段选择子、指向某个段描述符,以支持进程却换。
启动分页:将内存分成相等大小的块,使能够管理更大的内存。
打开地址线:即 Gate A20,第 21 根地址线的控制线),以支持访问大于 1M 的地址空间。
切换到保护模式后将有足够的地址空间,此时开始 startup_raw.S
解压 kernel.img。
kernel.img
kernel.img 使 grub 内核,对应代码 startup.S
以及其它 c 文件:
调用 grub kernel 的主函数
grub_main()
调用
grub_load_config()
执行解析 grub.conf。调用
grub_command_execute("normal", 0, 0)
调用
grub_normal_execute()
调用
grub_show_menu()
,在列表中选择操作系统。调用
grub_menu_execute_entry()
,装载指定的内核文件并传递内核启动参数。调用
grub_cmd_linux()
,读取 Linux 内核镜像头部,放到内存中的数据结构以进行检查。检查通过则会读取 Linux 内核镜像到内存。调用
grub_command_execute("boot", 0, 0)
,开始启动操作系统内核。
其中 grub.conf 文件内容如下:
1 | 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' { |
内核初始化
Linux 内核启动的入口函数是 init/main.c 的 start_kernel()
,主要调用了以下函数:
INIT_TASK(init_task)
:初始化进程列表,创建 0 号进程。-
set_task_stack_end_magic(&init_task)
的参数init_task
(其定义是struct task_struct init_task = INIT_TASK(init_task)
)即 0 号进程,其不由fork
或者kernel_thread
产生,是进程列表(Process List)的第一个进程。
-
trap_init()
:初始化中断门。- 用于处理各种中断。其中
set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_32)
设置系统调用的中断门。
- 用于处理各种中断。其中
mm_init()
:初始化内存管理。sched_init()
:初始化调度器。vfs_caches_init()
:初始化 rootfs(基于内存的文件系统)。调用
mnt_init()->init_rootfs()
,在 VFS 虚拟文件系统里面注册了一种类型,定义为struct file_system_type rootfs_fs_type
。为了兼容各种文件系统,VFS(Virtual File System)作为抽象层,向上提供统一的接口。
rest_init
:其他方面的初始化(1 号进程和 2 号进程)。
分层权限管理
x86 提供了分层的权限机制,把区域分成了四个 Ring,越往里权限越高,越往外权限越低。

基于此权限管理的代码执行的逻辑:
用户态代码不允许执行更高权限的指令,如需要访问核心资源,则要通过系统调用。
发起系统调用,将停止当前运行(状态保存在寄存器),交由内核态代码执行。
内核态代码执行结束,从寄存器恢复状态,将结果返回给用户态,用户态代码恢复执行。

rest_init 启动 1 号进程
rest_init
调用 kernel_thread(kernel_init, NULL, CLONE_FS)
创建 1 号进程(第二个进程),用于管理 Ring3 的用户态(User Mode)。
执行 kernel_thread
时还处于内核态,要使得能在用户态运行程序,还需要执行以下操作。
从内核态到用户态
kernel_thread
会调用 kernel_init
函数,其执行文件:
1 | if (ramdisk_execute_command) { |
1 | static int run_init_process(const char *init_filename) |
do_execve
是一个系统调用, 尝试运行 ramdisk 的 /init
或普通文件系统上的/sbin/init
、/etc/init
、/bin/init
、/bin/sh
之其一。
以下可以理解为:内核态逻辑 -> 恢复寄存器 -> 系统调用返回 -> 用户态逻辑(“用户态发起系统调用”过程的后半部分)。调用链:do_execve
-> do_execveat_common
-> exec_binprm
-> search_binary_handler
。
1 | int search_binary_handler(struct linux_binprm *bprm) |
其中 search_binary_handler
会加载一个 ELF(Executable and Linkable Format,可执行与可链接格式)的二进制文件,在代码中定义为:
1 | static struct linux_binfmt elf_format = { |
其中 load_elf_binary
,最后调用 start_thread
:
1 | void start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp) { |
以上过程是直接从内核态跳转到用户态,之前没有用户态保存寄存器的操作,因此需要初始化用户态寄存器。
最后的 force_iret
用于从系统调用中返回。此时恢复寄存器,由于上面的函数已补上了寄存器,CS 和 IP 恢复,指向用户态下一个要执行的语句。DS 和 SP 也被恢复,指向用户态函数栈的栈顶,因此下一条指令就从用户态开始运行。
ramdisk
ramdisk 是基于内存的文件系统,区别于其它运行在存储设备上的文件系统,它不需要驱动就能运行。
因此执行 ramdisk 的 /init
,/init
在用户态根据存储系统的类型加载驱动,再启动真正根文件系统上的 /init
。
随后执行系统的初始化,启动系统服务、控制台等,此时所有用户态进程的祖先 init
创建完成。
rest_init 启用 2 号进程
调用kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES
:2 号进程(第三个进程),用于管理 Ring0 的内核态(Kernel Mode)。
对于内核态而言进程和线程没有区别,统称为任务(Task),存放在相同的数据结构中。
其中 kthreadd
负责所有内核态的进程的调度和管理,即内核态所有进程的祖先。