现在main函数开始执行了,可真正意义上的说linux操作系统开始运行了。main函数将设置程序在操作系统下运行所需的环境并创建进程0,操作系统才有了第一个进程。
设置根设备和硬盘信息:之前在setup程序中加载了一些硬件信息并存储在物理内存0x90000-0x901FC处,main函数从这些参数中设置了全局变量根设备ROOT_DEV、硬盘信息drive_info,这两个信息在设置操作系统环境时会被访问。
规划物理内存格局,设置缓冲区,虚拟盘和主内存:内存分为内核代码和数据所占空间(也就是物理内存从0x00000到0xFFFFF的1MB空间)、主内存区(进程代码运行的空间,以及一些申请内存的操作都是使用的该空间,包括内核管理内存的数据结构task_struct都是存放在此。16MB的物理内存情况下长度设置为10MB)、缓冲区(主机与外设数据交互的中转站,主要是为了外设的数据能够被多个进程共享,防止频繁访问外设,外设的访问速度比内存的访问速度慢多了。16MB的物理内存情况下长度设置为3MB)、虚拟盘(可选,物理内存不够时可以不设置,外设的数据可以先载入这里提高访问速度。16MB的物理内存情况下长度设置为2MB,现在一般都没有用到虚拟盘了)。
内存管理结构mem_map的初始化:主内存的访问需要通过mem_map来管理,内存中有多少的页mem_map就有多少的项。每次进程新申请的页,都要找mem_map报备,让该页的引用计数加1。如果还页的话,引用计数就减1。当需要释放页时,必须满足该页的P位(存在位)为0且在mem_map中释放,这个页才是真正的释放。简单来说mem_map就是为内存扮演记账的角色。初始化(mem_init)将主内存中的页面使用计数置为0,其它的置为100,之后系统申请内存只能够使用计数为0的页面。
异常处理类中断服务程序挂接:调用trap_init将一些异常类型的中断(如缺页、溢出、除0错误、边界检查等)的中断处理程序挂接到IDT上,以支持内核,进程在主机中的运算。
其他的初始化操作: 包括初始化块设备请求项结构、初始化字符设备、设置串口,显示器以及键盘、设置开机启动时间等等
初始化进程0
一、每个进程都有自己的管理结构task_struct,进程0也不例外。而它的task_struct的代码是事先设计好的。因为在Linux里认为是进程创建进程,进程0是系统的第一个进程,所以进程0相当于所有进程的祖先。所以我们需要把进程0的task_struct中的LDT,TSS挂接到GDT上(所有的进程都有一对LDT和TSS,新创建的进程需要把它们挂接到GDT上)。并对GDT、task[64](是否为一个进程的重要标志就是task_struct是否放到了task[64]中,此时task的第0项指向进程0,其他项清零)以及进程调度相关的寄存器进行初始化设置(TR寄存器指向进程0的tss地址,LDTR寄存器指向进程0的LDT地址)。
task_struct:task_struct和内核栈是一个union结构共同占用一个内存页面,进程进入内核态后,执行用的代码都是内核代码,但执行路径未必相同,导致数据压栈的顺序和内容不同,而这些栈又不能存储在每个进程的用户空间中,这样容易被用户覆盖和改动,因此需要为每一个进程专门准备一套内核栈。效果如下图所示:
三、进程0需要具备处理系统调用的能力。每个进程在运行时都可能需要与内核进行交互(比如读写文件等),而交互的接口就是系统调用程序。系统需要将system_call与IDT相挂接(系统调用本身也是中断的一种,属于0x80中断)。这样进程0就具备了处理系统调用的能力了,这个system_call就是系统调用的总入口。
初始化缓冲区管理结构:缓冲区的本质是以空间换时间。我们知道如果想获取数据,需要从硬盘上将数据搬进内存,而有了缓冲区,我们便可以从缓冲区获取数据,大大提升了效率。前面说到操作系统会在紧挨着内核代码和数据区为缓冲区分配3MB的内存,这个内存区域就是用来创建多个缓冲区,每个缓冲区可以存储1KB的数据,所以总共有3072个缓冲区。操作系统通过struct buffer_header*类型的free_list链表和hash_table[307]哈希表管理缓冲区,这两个都是内核数据区中的变量。当进程要从设备上读取数据时,操作系统先把数据从设备中按block的大小读取到缓冲区中,并设置哈希表和free_list,这样如果其他进程也要读取这个设备上相同的block可以直接从内存上读取而不必再次访问外设。系统申请缓冲区时是从free_list的头部开始遍历,找到第一个可用的buffer_head,也就是从缓冲区尾部往前查找,将其对应的缓冲区作为新的缓冲区。接着是进行硬盘和软盘的初始化。
开启中断:值得注意的是自从setup关中断以后到目前为止都没有开启中断,但是现在操作系统要使用的中断服务程序以及设置好了,意味着中断服务体系已经建立完毕,系统可以在32位保护模式下处理中断,所以现在可以开中断了。
翻转进程0的特权级
Linux规定,所有进程都要由一个已有进程在3特权级下创建。当前执行环境还是内核态,需要转变成用户态使进程0成为真正意义上的第一个用户进程。这里的模式变换是通过iret实现的(模仿中断返回动作move_to_user_mode,使进程0的特权级从0变到3)。
正常的中断处理方式:所有类型的中断处理时会先通过进程task_struct结构中的的tss找到进程的内核栈栈顶(现在的内核栈栈底就是在bootsect中设置的0x9000),依次把寄存器SS(用户栈栈底)、ESP(用户栈栈顶)、EFLAGS(标志位)、CS(代码段)、EIP(下条指令的地址)压入该进程的内核栈,并将系统从0x11用户态翻转为0x00内核态。执行完毕后通过iret返回。iret会从进程的内核栈中弹出五个值分别赋值给5个寄存器,并将系统从系统模式翻转回用户模式。这个保护现场和恢复现场以及翻转特权级的动作是由CPU硬件实现的。
而此时需要人为地制造现场,当前在操作系统里只有一个绝对内核栈。此时相当于把该绝对内核栈的SS和ESP压入绝对内核栈中。0x17和0x0f是均是3特权级的,说明入栈的SS和CS都是用户态的。在恢复现场后,绝对内核栈就变成了进程0的用户栈,特权级也由原来的0态变成了3态。这个设计真是太巧妙了!
至此,进程0激活结束。它的工作几乎做完了,接下来它要创建进程1,并将自己处于无限执行状态。
小知识:关中断和进程有关吗?有关,一个进程关中断是将EFLAGS中IF位置0,不同进程的EFLAGS是存在该进程的TSS中,所以一个进程关中断并不会影响另一个进程的中断使用。