操作系统学习笔记(4)——操作系统初始化

Posted by 皮皮潘 on 05-13,2022

整体视角

Linux 0.11的入口函数一共就二十几行代码
1943_1.png
主要划分为3个部分:

  1. 进行一些参数的取值与计算,主要在于memory_end,buffer_memory_end以及main_memory_start三个与内存分布有关的值的计算
  2. 各种初始化init操作
  3. 从内核态切换为用户态,创建真正工作的进程1并初始化它,然后进程0进入死循环

本博客主要大致讲解第二块内容,也即几个核心的初始化init操作

mem_init

mem_init的核心代码如下:
1949_1.png

mem_init就是对一个mem_map数组的各个位置上进行赋值,赋值为为 100 的部分就是 USED,也就表示内存被占用,如果再具体说是占用了 100 次,这个之后再说。剩下赋值为 0 的部分就表示未被使用,也即使用次数为零。然后每个Entry都代表一个大小为4KB的物理页,就这么简单。

在赋值的同时,mem_init其实也进行了内存空间的划分:

  1. 1M 以下的内存这个数组干脆没有记录,这里的内存是无需管理的,或者换个说法是无权管理的,也就是没有权利申请和释放,因为这个区域是内核代码所在的地方,不能被“污染”。
  2. 1M 到 2M 这个区间是缓冲区,2M 是缓冲区的末端,这些地方不是主内存区域,因此直接标记为 USED,产生的效果就是无法再被分配了。
  3. 2M 以上的空间是主内存区域,而主内存目前没有任何程序申请,所以初始化时统统都是零,未来等着应用程序去申请和释放这里的内存资源。

trap_init

trap_init的核心代码如下:
1947_1.png
其中set_trap_gate以及set_system_gate都是通过内联汇编实现的函数,其作用就是在IDT上针对对应的中断号设置对应的中断处理函数,其实现就是把中断处理函数地址设置到对应中断号代表的索引的Entry上

整段代码执行下来,内存中目前的布局会变成如下所示,也即在IDT中设置好了各个中断号对应的终端处理函数:
1951_1.png

blk_dev_init

blk_dev_init的代码非常简单,它就是完成了块设备请求项数组的初始化工作:
1961_0.png

其真正的核心在于request结构体:
1963_0.png
总的来讲,这个request结构就代表了一次读盘请求,其中:

  1. dev 表示设备号,-1 就表示空闲。
  2. cmd 表示命令,其实就是 READ 还是 WRITE,也就表示本次操作是读还是写。
  3. errors 表示操作时产生的错误次数。
  4. sector 表示起始扇区。
  5. nr_sectors 表示扇区数。
  6. buffer 表示数据缓冲区,也就是读盘之后的数据放在内存中的什么位置。
  7. waiting 是个 task_struct 结构,这可以表示一个进程,也就表示是哪个进程发起了这个请求。
  8. bh 是缓冲区头指针。
  9. next 指向了下一个请求项。

简单的来讲,request块设备请求项是读取块设备与内存缓冲区之间的桥梁,这个在之后的针对文件读写的博文中会再次讲到

tty_init

tty_init中将内存中的图形视频缓冲区与显存进行映射,从而使得往该内存区域写数据,相当于写在了显存中,进而就相当于在屏幕上输出文本了,也就是可以通过简单的内存写入实现显示屏输出文本,进而实现控制台输出字符的功能

time_init

time_init 方法非常的简单,它使用inout指令与 CMOS 硬件端口进行读写交互进而获取到了年月日时分秒等数据,最后通过这些计算出了开机时间 startup_time 变量

sched_init

在sched_init方法中完成大名鼎鼎的进程调度初始化,其核心步骤如下:

  1. 往全局描述符表写了两个结构,TSS 和 LDT,作为未来进程 0 的任务状态段和局部描述符表信息,TSS 叫任务状态段,就是保存和恢复进程的上下文的,而 LDT 叫局部描述符表,是与 GDT 全局描述符表相对应的,内核态的代码用 GDT 里的数据段和代码段,而用户进程的代码用每个用户进程自己的 LDT 里得数据段和代码段。写入了TSS和LDT之后,GDT中的内存布局如下图所示:
    1953_1.png
  2. 初始化一个task_struct数组,task_struct 结构就是代表每一个进程的信息,未来在task_struct数组这里会存放所有进程的信息,并且给数组的第一个位置附上了 init_task.task 这个具体值,也是作为未来进程 0 的信息
  3. 开启了可编程时钟硬件,并设置了时钟中断 0x20 和系统调用 0x80,一个作为进程调度的起点,一个作为用户程序调用操作系统功能的桥梁

虽然目前还没有建立起进程调度的机制,但当前正在运行的代码就是会作为未来的一个进程的指令流,也就是当未来进程调度机制一建立起来,正在执行的代码就会化身成为进程 0 的代码,而进程 0 也是唯一一个不是通过fork调用创建的进程

buf_init

buf_init方法的核心代码如下所示:
1957_0.png

这里有个外部变量 end,而我们的缓冲区开始位置 start_buffer 就等于这个变量的内存地址。 这个外部变量 end 并不是操作系统代码写就的,而是由链接器 ld 在链接整个程序时设置的一个外部变量,帮我们计算好了整个内核代码的末尾地址,简单来讲end就是缓冲区和内核程序之间的分界线,如下图所示:
1955_0.png

buf_init的代码虽然很长,但是其实就是创建了一个buff_header双向链表的数据结构而已,每个buff_header对应一个buff_block缓冲块,并记录了对应缓冲块的元数据,另外buff_header从start_buffer开始往高地址增长,而buff_block则从buffer_end开始往低地址增长,直到两者碰到一块为止,如下图所示:
1959_0.png

在buf_init的最后简单地初始化了hash_table数组,其作用就是为了快速搜索定位对应的缓冲块,仅此而已

参考

  1. 《闪客——你管这破玩意叫操作系统源码》