Kyle's Notebook

Linux - 架构与系统初始化

Word count: 2.8kReading time: 11 min
2021/04/01

Linux - 架构与系统初始化

基本概念

img

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 架构如图:

img
单元 寄存器 备注
运算单元
数据单元 通用寄存器 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)

实模式的寻址方式:从段寄存器中取段起始地址,从指令指针寄存器中取偏移量。

保护模式的寻址方式:从段寄存器取出选择子,根据选择子从内存中的段描述符表中取起始地址(存放在高速缓存中),再从指令指针寄存器中取偏移量。

img

除了可访问的地址空间更大,保护模式的意义还在于限制用户态代码执行更高权限的指令,即实现权限管理,这在稍后将会谈到。

BIOS

计算机主板上有 ROM(Read Only Memory,只读存储器)固定存放 BIOS(Basic Input and Output System,基本输入输出系统)程序。

img

在计算机启动时只有 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 镜像的各部分。

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
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
}

内核初始化

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,越往里权限越高,越往外权限越低。

img

基于此权限管理的代码执行的逻辑:

  • 用户态代码不允许执行更高权限的指令,如需要访问核心资源,则要通过系统调用。

  • 发起系统调用,将停止当前运行(状态保存在寄存器),交由内核态代码执行。

  • 内核态代码执行结束,从寄存器恢复状态,将结果返回给用户态,用户态代码恢复执行。

img

rest_init 启动 1 号进程

rest_init 调用 kernel_thread(kernel_init, NULL, CLONE_FS) 创建 1 号进程(第二个进程),用于管理 Ring3 的用户态(User Mode)。

执行 kernel_thread 时还处于内核态,要使得能在用户态运行程序,还需要执行以下操作。

从内核态到用户态

kernel_thread 会调用 kernel_init 函数,其执行文件:

1
2
3
4
5
6
7
8
9
10
11
  if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
......
}
......
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"))
// 调用 run_init_process()
return 0;
1
2
3
4
5
6
7
static int run_init_process(const char *init_filename)
{
argv_init[0] = 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 是一个系统调用, 尝试运行 ramdisk 的 /init 或普通文件系统上的/sbin/init/etc/init/bin/init/bin/sh 之其一。

以下可以理解为:内核态逻辑 -> 恢复寄存器 -> 系统调用返回 -> 用户态逻辑(“用户态发起系统调用”过程的后半部分)。调用链:do_execve -> do_execveat_common -> exec_binprm -> search_binary_handler

1
2
3
4
5
6
7
8
int search_binary_handler(struct linux_binprm *bprm)
{
......
struct linux_binfmt *fmt;
......
retval = fmt->load_binary(bprm);
......
}

其中 search_binary_handler 会加载一个 ELF(Executable and Linkable Format,可执行与可链接格式)的二进制文件,在代码中定义为:

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

其中 load_elf_binary,最后调用 start_thread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp) {
// 通过设置代码段和数据段的起始位置,IP 的起始模拟上下文。
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);

以上过程是直接从内核态跳转到用户态,之前没有用户态保存寄存器的操作,因此需要初始化用户态寄存器。

最后的 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 负责所有内核态的进程的调度和管理,即内核态所有进程的祖先。

CATALOG
  1. 1. Linux - 架构与系统初始化
    1. 1.1. 基本概念
      1. 1.1.1. CPU(Central Processing Unit)
      2. 1.1.2. 总线(Bus)
        1. 1.1.2.1. 地址总线(Address Bus)
        2. 1.1.2.2. 数据总线(Data Bus)
      3. 1.1.3. 内存(Memory)
      4. 1.1.4. I/O 设备
    2. 1.2. x86 架构
      1. 1.2.1. 8086 处理器
      2. 1.2.2. 32 位处理器
        1. 1.2.2.1. 实模式与保护模式
    3. 1.3. BIOS
    4. 1.4. Bootloader
      1. 1.4.1. diskboot.img
      2. 1.4.2. lzma_decompress.img
      3. 1.4.3. kernel.img
    5. 1.5. 内核初始化
      1. 1.5.1. 分层权限管理
      2. 1.5.2. rest_init 启动 1 号进程
        1. 1.5.2.1. 从内核态到用户态
        2. 1.5.2.2. ramdisk
      3. 1.5.3. rest_init 启用 2 号进程