Linux0.11源码趣读

用源码打开操作系统“黑盒子”

Linux,Linux 0.11,内核,源码,操作系统,

闪客 “低并发编程”公号作者

11|main方法:整个操作系统就三十几行代码

讲述:闪客 大小:4.94MB 时长:00:05:09
00:00
1.0×

你好,我是闪客。

第一部分,我们通过一大堆讨厌的汇编代码,把进入 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 函数中。

欲知后事如何,且听下回分解。