系统初始化
系统初始化
从 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)
andstruct 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()
完成其他初始化工作
- 创建第一个进程, 0 号进程,
- 创建管理/创建用户态进程的进程,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 返回用户态;