Linux操作系统(1) – 系统初始化
系统在初始化过程中的操作大致可以分为以下几步
- 启动BIOS,准备实模式下的中断向量表以及中断服务程序
- 从启动盘加载操作系统程序到内存
- 内核初始化,系统调用
1 BIOS
设备加电,此时系统处于实模式,地址总线为20位,所以能寻址的范围是$2^{20}$即1M的空间,而BIOS时期的所有事情都是在这1M空间中完成的。
当设备加电之后,硬编码使得CS = 0xFFFF
,IP = 0x0000
所以第一条指令会指向0xFFFF0
x86系统中,会将1M空间最上面的0xF0000
到0xFFFFF
一共64k映射给ROM。
而BIOS启动之后,需要从磁盘上读取操作系统数据,那么就需要相应的服务,访问磁盘设备。因此,在BIOS时期,有以下几个功能:
- 基本的输入输出程序(从磁盘中读取信息,并在屏幕上显示信息)
- 系统设置信息
- 开机之后的自检程序
- 系统自启动程序
因此,在BIOS中,会做以下的事:
- 硬件设备自检
- 建立中断向量表和中断服务程序
之后再利用中断INT 19
将加载程序从磁盘引导扇区(512字节, 以xAA55结束) 加载到0x7c00
在这512个字节的加载程序会将操作系统的代码和数据从硬盘加载到内存中,然后跳转到操作系统的起始位置,将操作系统加载进内存并运行。
0x7c00相当于一个接口,BIOS始终会从这个位置加载操作系统的启动代码,而因为不同的操作系统的引导式不一样的,所以提供接口之后,不管什么样的操作系统BIOS中的程序都是一样的
小结
BIOS系统调用
- BIOS以中断调用方式提供了基本的I/O功能
- 只能在实模式下使用
系统启动过程
加电之后读BIOS,BIOS读取加载程序,加载程序将内核映像从磁盘加载进内存
2 系统启动流程
- CPU加电稳定之后从
0xFFFF0
读第一条指令- CS:IP = 0xF000:FFF0
- 第一条指令是跳转指令
- CPU初始状态是16位实模式
- CS:IP 是16位寄存器
- 指令指针
PC= 16 * CS + IP
- 最大寻址空间是1MB
- BOIS初始化
- 硬件自检
- 检测系统中关键部件的存在和工作状态
- 查找并执行IO设备BOIS并进行设备初始化
- 执行系统BIOS进行系统检测
- 更新CMOS中的扩展系统配置数据
- 硬件自检
- 按指令启动顺序从软盘,硬盘,光盘启动
在完成上面的操作之后,会读取启动盘第一块扇区 MBR 主引导扇区
MBR(boot.img
)一共有512字节,其中:
启动代码 446个字节,会完成以下的操作:
- 检查分区表的正确性
- 加载并跳转到磁盘上的引导程序
core.img
硬盘分区表:64个字节
- 描述分区状态和位置
- 每个分区的描述和信息占16个字节(所以一共有4个分区)(注:现在有BIOS-GPT,不受4个分区的约束)
结束标志:2个字节
- 55AA
分区引导扇区core.img
会有以下信息:
- 跳转指令:跳转到启动代码
- 文件卷头:文件系统的描述信息
- 启动代码
kernel.img
- 结束标志:55AA
在这个引导扇区中,会将操作系统内核读进内存,但是在实模式下,寻址范围只有1M,因此需要切换到保护模式。
切换的过程会有以下操作
启动分段
启动分页
这两部分在后续再详细讲。
设定了段页式内存访问机制之后,就会会打开Gate A20,即CR0寄存器置1,进入保护模式,使用32根地址总线,寻址范围为4GB
小结
系统启动过程总结出来大概有以下的过程:
加电进入BIOS
从BIOS启动bootloader
bootloader加载引导扇区
引导扇区在准备系统启动的过程中从实模式切换到保护模式,并将操作系统加载进入内核
启动内核进入操作系统
3 内核初始化
init/main.c
文件中的start_kernel()
函数是内核的入口函数,而在启动会有各种各样的初始化操作,主要的功能,是下面这样的:
1 | asmlinkage __visible void __init start_kernel(void) |
而在rest_init()中会完成以下的工作
3.1 启动1号进程
在内核中执行 kernel_thread(kernel_init, NULL, CLONE_FS)
创建第二个进程,也是1号进程,用户态的第一个进程。
一号进程通过系统调用,启动执行内存文件系统ramdisk的 /init
, 或者是普通文件系统上的/sbin/init
,/etc/init
,/bin/init
,/bin/sh
中的某一个,并返回用户态。
ramdisk的作用,由于在内核中不可能将所有类型的文件系统都写入,所以在内核启动的时候提供了一个内核文件系统ramdisk
,在它执行完init
之后就到了用户态,而在/init
里面,会根据存储系统的类型加载驱动,设置真正的根文件系统,并启动文件系统上的/init
1号进程初始化完成之后就到了用户态,完成各种操作系统初始化工作。
3.2 启动2号进程
在内核中执行kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES)
.
kthreadd 是内核态所有线程运行的祖先
小tips:
在内核态中,进程和线程都是task,使用的是一个数据结构,且在同一个链表中。而在用户态中,进程相当于一个项目,线程相当于一个项目的不同部分。
4 中断,异常,系统调用
(外部)中断: 外部设备的处理请求
异常:程序运行过程中出现意想不到的问题
(内部中断)系统调用:程序需要使用内核的某些功能主动向操作系统发出服务请求
5 系统调用
每一个中断或者异常都与一个中断服务例程(Interrupt Service Routine, ISR)关联,关联关系存储在中断描述符表(Interrupt Descriptor Table, IDT)中,IDT 的起始地址和大小保存在中断描述符表寄存器IDTR中,如上图所示。每一项都是一个中断门。根据中断号可以得到中断Trap Gate或者Interrupt Gate,然后进一步可以获取到相关的段选择子和段偏移。
从上图可以看到,产生中断后,可以得到对应的中断号,CPU根据中断号,选择确定的IDT表项,然后从中取出段选择子,在GDT中得到对应的段描述符,拿到Base Address,加上偏移,就得到了中断的线性地址,在中断线性地址的位置存放的就是中断例程。
特权级切换对堆栈的影响
内核态产生的中断还是在内核态
用户态产生中断到内核态
需要将用户栈的ESP和SS入栈
返回
iret 和 ret
iret 弹出 EFLAGS 和 SS/ESP
ret弹出EIP, retf弹出CS和EIP