11|main方法:整个操作系统就三十几行代码
你好,我是闪客。
第一部分,我们通过一大堆讨厌的汇编代码,把进入 main 方法前的苦力工作都完成了。
我们的程序终于跳到第一个由 c 语言写的文件 main.c 中,这里有个名字叫做 main 的方法,写得非常精简,把操作系统的整个骨架都勾勒出来了。
第二部分,我们的主题是“大战前期的初始化工作”,学习重点就是 main 方法中的各种初始化函数,这些函数是操作系统各个模块得以运作的基础。
在详细分析这些函数之前,这一讲,我先作为“导游”,结合 main 方法的代码,带你建立初始化过程的整体印象。
// init/main.c
void main(void) {
ROOT_DEV = ORIG_ROOT_DEV;
drive_info = DRIVE_INFO;
memory_end = (1<<20) + (EXT_MEM_K<<10);
memory_end &= 0xfffff000;
if (memory_end > 16*1024*1024)
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024)
buffer_memory_end = 4*1024*1024;
else if (memory_end > 6*1024*1024)
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024;
main_memory_start = buffer_memory_end;
mem_init(main_memory_start,memory_end);
trap_init();
blk_dev_init();
chr_dev_init();
tty_init();
time_init();
sched_init();
buffer_init(buffer_memory_end);
hd_init();
floppy_init();
sti();
move_to_user_mode();
if (!fork()) {
init();
}
for(;;) pause();
}
数一数看,总共也就 30 几行代码,而且还有一些可以精简的 if else 分支。
但就这么几行代码,的确蕴含着操作系统启动流程的全部秘密了,我用回车将这个代码分成了几个部分,我们依次来看看。
第一部分:参数取值与计算
第一部分是一些参数的取值和计算。
// init/main.c
void main(void) {
ROOT_DEV = ORIG_ROOT_DEV;
drive_info = DRIVE_INFO;
memory_end = (1<<20) + (EXT_MEM_K<<10);
memory_end &= 0xfffff000;
if (memory_end > 16*1024*1024)
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024)
buffer_memory_end = 4*1024*1024;
else if (memory_end > 6*1024*1024)
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024;
main_memory_start = buffer_memory_end;
...
}
包括根设备 ROOT_DEV,之前在汇编语言中获取的各个设备的参数信息 drive_info,以及通过计算得到的表示内存边界的值:
- main_memory_start、main_memory_end
- buffer_memory_start、buffer_memory_end
这些边界值我们下一讲再研究,你先留个印象就行。
从哪获得之前的设备参数信息呢?如果你看过前面第一部分的内容,那一定还记得由 setup.s 这个汇编程序调用 BIOS 中断获取的各个设备的信息,并保存在约定好的内存地址 0x90000 处。
当时约好“存取”位置,就是给后续工作打基础。现在这不就来取了么,我们再次放出第5讲的表格。

第二部分:表示初始化的init方法
第二部分是表示各种初始化的一系列 init 方法。我们还是边看代码边分析。
// init/main.c
void main(void) {
...
mem_init(main_memory_start,memory_end);
trap_init();
blk_dev_init();
chr_dev_init();
tty_init();
time_init();
sched_init();
buffer_init(buffer_memory_end);
hd_init();
floppy_init();
...
}
这段代码非常规整,但需要逐个击破,因为每一个 init 都可能包含着操作系统某个模块的运作秘密,包括内存初始化 mem_init、中断初始化 trap_init、进程调度初始化 sched_init 等等。
我们知道学操作系统知识的时候,其实就分成这么几块来学的,看来在操作系统源码上看,也确实是这么划分的,那我们之后照着源码慢慢品,就好了。
第三部分:切换用户态
第三部分是切换到用户态模式,并在一个新的进程中做一个最终的初始化 init。
// init/main.c
void main(void) {
...
sti();
move_to_user_mode();
if (!fork()) {
init();
}
...
}
这个 init 函数是在一个新的进程里执行的,我们把这个进程叫做进程 1。
这个 init 函数会设置终端的标准 IO,并且又创建出一个执行 shell 程序的进程,用来接受用户的命令,这个新创建的进程叫做进程 2。
到这里其实就出现了我们熟悉的 shell 画面(下面是 bochs 启动 Linux 0.11 后的画面)。

在这里我们就可以不断输入命令,交给操作系统去执行了,而操作系统最大的作用,就是如此。
第四部分:死循环
如果没有任何任务可以运行,操作系统会一直陷入后面这个死循环无法自拔。
void main(void) {
...
for(;;) pause();
}
OK,不要细品每一句话,我们本回就是要你有个整体印象,之后我会细细讲这里的每一个部分。
这里再放上目前的内存布局图。

这个图我们一定要牢记在心,操作系统说白了就是在内存中放置各种的数据结构,来实现“管理”的功能。
所以之后我们的学习过程,主心骨其实就是看看,操作系统在经过一番折腾后,又在内存中建立了什么数据结构,而这些数据结构后面又是如何用到的。
比如进程管理,就是在内存中建立好多复杂的数据结构用来记录进程的信息,最重要的就是那个 task_struct。再配合上进程调度的小算法,就可以在操作系统层面实现进程这个概念了。
总结时刻
为了让你对目前的流程心里有个底,我们把操作系统前面的工作再做一个回顾。只要把后面这张图记下来就行。

看到了吧?我们已经把 boot 文件夹下的三个汇编文件的全部代码都一行一行品读过了,其主要功能就是三张表的设置:全局描述符表、中断描述符表、页表。
同时,我们还设置了各种段寄存器,栈顶指针。并且还为后续的程序提供了设备信息,保存在 0x90000 处往后的几个位置上。
最后,一个华丽的跳转,就将程序跳转到了 main.c 文件里的 main 函数中。
欲知后事如何,且听下回分解。