系统初始化

系统初始化

从 BIOS 到 bootloader

在主板上,有一个东西叫 ROM(Read Only Memory,只读存储器)。这和咱们平常说的内存 RAM(Random Access Memory,随机存取存储器)不同。ROM 是只读的,上面早就固化了一些初始化的程序,也就是 BIOS(Basic Input and Output System,基本输入输出系统)。

在 Linux 里面有一个工具,叫 Grub2,全称 Grand Unified Bootloader Version 2。顾名思义,就是搞系统启动的。可以通过 grub2-mkconfig -o /boot/grub2/grub.cfg 来配置系统启动的选项。

grub2 第一个要安装的就是 boot.img。boot.img 做不了太多的事情。它能做的最重要的一个事情就是加载 grub2 的另一个镜像 core.img。diskboot.img 的任务就是将 core.img 的其他部分加载进来。先是解压缩程序 lzma_decompress.img,再往下是 kernel.img,最后是各个模块 module 对应的映像。

注意这里加载的是 grub 内核,不是 Linux 内核。

在这之前,我们所有遇到过的程序都非常非常小,完全可以在实模式下运行,但是随着我们加载的东西越来越大,实模式这 1M 的地址空间实在放不下了,所以在真正的解压缩之前,lzma_decompress.img 做了一个重要的决定,就是调用 real_to_prot,切换到保护模式,这样就能在更大的寻址空间里面,加载更多的东西。

从实模式到保护模式

实模式,只能寻址 1M,每个段最多 64K;保护模式,对于 32 位系统,能够寻址 4G。

从实模式到保护模式,将启用分段, 辅助进程管理;启动分页, 辅助内存管理;打开其他地址线。

内核初始化

运行 start_kernel() 函数(位于 init/main.c), 初始化做三件事:

  • 创建样板进程及各个模块初始化;
    • 创建第一个进程, 0 号进程, set_task_stack_end_magic(&init_task) and struct task_struct init_task = INIT_TASK(init_task)
    • 初始化中断, trap_init(),系统调用也是通过发送中断进行,由 set_system_intr_gate() 完成
    • 初始化内存管理模块, mm_init()
    • 初始化进程调度模块, sched_init()
    • 初始化基于内存的文件系统 rootfs, vfs_caches_init() ,VFS(虚拟文件系统)将各种文件系统抽象成统一接口
    • 调用 rest_init() 完成其他初始化工作
  • 创建管理/创建用户态进程的进程,1 号进程;
    • rest_init() 通过 kernel_thread(kernel_init,...) 创建 1号进程(工作在用户态)。
    • 权限管理
      • x86 提供 4个 Ring 分层权限
      • 操作系统利用: Ring0-内核态(访问核心资源); Ring3-用户态(普通程序)
    • 用户态调用系统调用: 用户态-系统调用-保存寄存器-内核态执行系统调用-恢复寄存器-返回用户态
    • 新进程执行 kernel_init 函数,先运行 ramdisk 的 /init 程序(位于内存中)
      • 首先加载 ELF 文件
      • 设置用于保存用户态寄存器的结构体
      • 返回进入用户态
      • /init 加载存储设备的驱动
    • kernel_init 函数启动存储设备文件系统上的 init
  • 创建管理/创建内核态进程的进程,2 号进程;
    • rest_init() 通过 kernel_thread(kthreadd,...) 创建 2号进程(工作在内核态).
    • kthreadd 负责所有内核态线程的调度和管理

0 号进程是唯一一个没有通过 fork 或者 kernel_thread 产生的进程,是进程列表的第一个。1 号进程管所有用户态,2 号进程管所有内核态。

当处于用户态的代码想要执行更高权限的指令,这种行为是被禁止的,要防止他们为所欲为。因此程序的执行是:用户态 - 系统调用 - 保存寄存器 - 内核态执行系统调用 - 恢复寄存器 - 返回用户态,然后接着运行

系统调用

用户进程调用 open 函数:

  • glibc 的 syscal.list 列出 glibc 函数对应的系统调用;
  • glibc 的脚本 make_syscall.sh 根据 syscal.list 生成对应的宏定义(函数映射到系统调用);
  • glibc 的 syscal-template.S 使用这些宏, 定义了系统调用的调用方式(也是通过宏);
  • 其中会调用 DO_CALL (也是一个宏), 32位与 64位实现不同;

32 位系统调用:

  • 用户态下:
    • 将请求参数保存到寄存器;
    • 将系统调用名称转为系统调用号保存到寄存器 eax 中;
    • 通过软中断 ENTER_KERNEL 进入内核态;
  • 内核态下:
    • 将用户态的寄存器保存到 pt_regs 中;
    • 在系统调用函数表 sys_call_table 中根据调用号找到对应的函数;
    • 执行函数实现, 将返回值写入 pt_regs 的 ax 位置;
    • 通过 INTERRUPT_RETURN 根据 pt_regs 恢复用户态进程;

64 位系统调用:

  • 用户态下:
    • 将请求参数保存到寄存器;
    • 将系统调用名称转为系统调用号保存到寄存器 rax 中;
    • 通过 syscall 进入内核态
  • 内核态下:
    • 将用户态的寄存器保存到 pt_regs 中;
    • 在系统调用函数表 sys_call_table 中根据调用号找到对应的函数;
    • 执行函数实现, 将返回值写入 pt_regs 的 ax 位置;
    • 通过 sysretq 返回用户态