Linux操作系统(1) -- 系统初始化

Linux操作系统(1) – 系统初始化

系统在初始化过程中的操作大致可以分为以下几步

  • 启动BIOS,准备实模式下的中断向量表以及中断服务程序
  • 从启动盘加载操作系统程序到内存
  • 内核初始化,系统调用

1 BIOS

设备加电,此时系统处于实模式,地址总线为20位,所以能寻址的范围是$2^{20}$即1M的空间,而BIOS时期的所有事情都是在这1M空间中完成的。

当设备加电之后,硬编码使得CS = 0xFFFF,IP = 0x0000 所以第一条指令会指向0xFFFF0

img

x86系统中,会将1M空间最上面的0xF00000xFFFFF一共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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
asmlinkage __visible void __init start_kernel(void)
{
...

/* init_task定义为
* struct task_struct init_task = INIT_TASK(init_task)
* 它是系统创建的第一个进程,0号进程
* 也是第一个没有通过fork或者kernel_thread产生的进程
*/
set_task_stack_end_magic(&init_task);

...
/* 初始化基于内存的文件系统rootfs
* 会调用 mnt_init()->init_rootfs()
* 有
* register_filesystem(&rootfs_fs_type)
* rootfs_fs_type定义为
* struct file_system_type rootfs_fs_type;
*/
vfs_caches_init();

...

/* 设置 Interrupt Gate 用于处理系统中断
* set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_32)
* 这个中断后面会讲解,它是32位系统调用的中断门
*/
trap_init();

/* 初始化内存管理模块
*
*/
mm_init();

...

/* 初始化调度模块
* Set up the scheduler prior starting any interrupts (such as the
* timer interrupt). Full topology setup happens at smp_init()
* time - but meanwhile we still have a functioning scheduler.
*/
sched_init();

....

/* 其他初始化*/
arch_call_rest_init();
}

而在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 中断,异常,系统调用

(外部)中断: 外部设备的处理请求

异常:程序运行过程中出现意想不到的问题

(内部中断)系统调用:程序需要使用内核的某些功能主动向操作系统发出服务请求

1587202783152

5 系统调用

1587210589127

每一个中断或者异常都与一个中断服务例程(Interrupt Service Routine, ISR)关联,关联关系存储在中断描述符表(Interrupt Descriptor Table, IDT)中,IDT 的起始地址和大小保存在中断描述符表寄存器IDTR中,如上图所示。每一项都是一个中断门。根据中断号可以得到中断Trap Gate或者Interrupt Gate,然后进一步可以获取到相关的段选择子和段偏移。

1587211062813

从上图可以看到,产生中断后,可以得到对应的中断号,CPU根据中断号,选择确定的IDT表项,然后从中取出段选择子,在GDT中得到对应的段描述符,拿到Base Address,加上偏移,就得到了中断的线性地址,在中断线性地址的位置存放的就是中断例程。

特权级切换对堆栈的影响

内核态产生的中断还是在内核态

1587211596716

用户态产生中断到内核态

需要将用户栈的ESP和SS入栈

1587211611732

返回

iret 和 ret

iret 弹出 EFLAGS 和 SS/ESP

ret弹出EIP, retf弹出CS和EIP