概述
本文在结合赵炯博士的《Linux内核0.11完全注释》和对应源代码的基础上,总结而成。
Linux0.11的大致启动流程如下:
- pc机器的BIOS程序启动,开始加载硬盘中的bootsect.s对应的机器码程序到内存中,将CPU的程序计数器指向bootsect.s对应机器码
在内存中的起始位置,开始执行bootsect.s代码。 - 执行bootsect.s代码的过程中,从硬盘加载了setup.s对应的机器码到程序的内存中,并跳转到setup.s机器码的开始处开始执行setup.s
对应机器码。setup的主要工作是将硬盘,内存等硬件的参数信息读取并放置到约定的安全位置。加载head.s和main.c共同组成的system模块
机器码到内存中,将程序计数器指定到system代码开头的地方,指定CPU进入保护模式工作。准备开始执行head.s代码。 - head.s对应的机器码主要完成全局描述符表、中断描述符表、页表等信息的内存分配工作,具体的赋值需要到后续完成。
- 执行完head.s之后,就该执行main.c函数中的内容了,这也是本文要分析的代码内容。
1
2
3
4
5
6
7after_page_tables:
push 0 ;// These are the parameters to main :-)
push 0 ;// 这些是调用main 程序的参数(指init/main.c)。
push 0
push L6 ;// return address for main, if it decides to.
push _main_rename ;// '_main'是编译程序对main 的内部表示方法。
jmp setup_paging
之所以有这一段流程分析,是为了有个上下文,因为对于好奇的同学来讲,总要问机器是怎么执行到main函数的,之前都干了什么。有了这里的上下文,
感兴趣的同学就可以自行去探索一下上述几个汇编程序的内容。
还有一点需要说明,对于汇编语言,本身大家都学习过,在遇到不懂的助记符的时候,百度一下,很多时候都能找到答案。我的博客中也有我在看汇编代码时候
在网上查找到的一些汇编语言的知识记录,本身赵炯博士的书里也较为系统的介绍了一部分重要的语言特性,大家感兴趣可以看看。
main函数的主体内容
1 |
|
mem_init函数
该函数是对主内存区进行管理初始化的。有以下一些点需要get到:
- Linux0.11将主内存按照4K的大小划分为多个页面来管理
- 那么,内核如何知道哪些页面时可用的,哪些页面还为使用?
- 如何处理内存页面的共享问题?
下面的初始化代码可以大致解答这些问题:
- 使用全局数组mem_map与主内存中的每个页面进行对应。
- 使用数字表示页面被使用的次数,当某个下标对应的数字为0的时候,表示
其下标对应页面未使用,可以被分配。
那么,mem_init函数的主要工作就是讲mem_map初始化为0。
1 | void mem_init(long start_mem, long end_mem) |
trap_init
要理解这个函数在做什么,首先要理解中断之于操作系统的意义:
CPU在一刻不停的运转,假设我们正在执行用户程序,那么内核怎么来调度CPU来执行其他的程序呢?
在单核CPU中,用户程序正在执行,答案是无法运行内核程序要调度CPU去执行其他的程序。那么内核调度是如何实现的?
答案是,时钟中断。CPU会按照一定的频率去接受来自外部的时钟中断,CPU都会根据时钟中断的中断向量号到中断向量表中查找时钟中断
对应的中断处理程序,时钟中断的中断处理程序会调用内核的调度方法schedule方法,决定是否进行进程切换工作。讲到这里,我们大概就理解了trap_init函数的作用,就是向中断向量表注册每个中断向量号对应的中断处理函数,以便CPU在接收到中断后能够正确处理中断。
看下面代码,可以看到该函数初始化一些我们可以猜到的一些中断陷入处理老程序0除,调试,溢出,页错误等等。
1 | void trap_init(void) |
blk_dev_init
该函数式块设备初始化函数,什么是块设备?就是可以按照一大块数据进行读取数据的设备,典型的有硬盘,U盘等,与之对比,键盘属于字符设备。
要理解这段函数,我们先来看看request数组吧。
- request是request结构体类型的数组。
- request结构体是对一个块设备读取请求的一个抽象,表示了要读取哪个块设备的哪个起始扇区的,要读几个扇区,哪个任务在等待这个请求,
该任务数据的缓冲区和缓冲区头分别是哪个,并且指向了下一个request请求。至于为什么指向下一个请求,我暂时无法回答,猜测与io调度有关系。
分析到这里,基本可以说明blk_dev_init函数在做什么了。初始化request数组而已,以备后用
1 | //// 块设备初始化函数,由初始化程序main.c 调用(init/main.c,128)。 |
chr_dev_init
在Linux0.11源代码中,该函数为空。
tty_init
该函数大致看了一遍。实现的功能也比较简单。暂时不做进一步分析。
1 | //// tty 终端初始化函数。 |
time_init
该函数设置开机时间
sched_init
该函数顾名思义,是调度程序的初始化子程序,我们需要仔细分析一下。首先这需要了解中断描述符表的概念,我的这篇博客有一个摘抄性质的记录,大家可以进一步参考相关书籍,形成对中断描述符表
的整体认识。
首先来理解一下TSS(Task Status Segment),任务状态段,该内存段是用来存储一个进程的所有上下文信息的,在进程被切换掉的时候,该段可以保存对应进程的所有的寄存器信息,程序
计数器信息等等,以便下一次该任务被切换执行的时候,可以从被中断的位置开始重新执行。
- 这就不难理解set_tss_desc的意义了,在全局描述符表中,注册任务0的任务状态段信息,set_ldt_desc则在全局描述符表中注册了任务0的局部描述符表信息。
- 接下来的初始化全局描述符表的表项,全部设置为0。
- 将任务0的TSS描述符和局部描述符表分别加载到tr寄存器和ldtr寄存器中。
- 接下来设置时钟中断程序向量,开始时钟中断。这个很重要,timer_interrupt函数定义在kernel/system_call.s中,是进程调度的入口函数。
- 最后设置了系统调用的中断程序向量。这里需要理解,我们使用的所有的系统调用,最终都是通过向CPU发出一个中断向量号为0x80的软中断来完成的,至于每个系统调用的参数等,则需要
在进行系统调用的时候存储到寄存器中即可。
1 | // 调度程序的初始化子程序。 |
buffer_init
buffer_init函数对缓冲区进行初始化,至于缓冲区,前文有专门提到,这里再讲一下,高速缓冲区是用来进行硬盘数据缓冲的,这个区由
缓冲区头和缓冲区块共同组成。缓冲区头是缓冲区块的元数据,用来标记对应缓冲区块是哪个硬盘的哪个扇区的数据缓冲,缓冲区块则存储了
磁盘中多个扇区的完整数据,具体多少个扇区需要根据操作系统的设置来决定,在Linux0.11来说,一个缓冲区块存储了1024字节的数据,而
一个扇区一边是512字节的数据,因此一个缓冲区块存储了两个扇区的数据。
在该函数中:
- 程序从缓冲区的开头和末端分别开始,初始化一个缓冲区头同时再设置一个缓冲区,并让缓冲区头将缓冲区管理起来,如此循环,知道缓冲区头
和缓冲区相遇结束。 - 第一个缓冲区头指针为内核代码最末端,保存在start_buffer指针中。
- 在缓冲区初始化完成后,内核将缓冲区头指针赋予了一个称之为free_list的全局变量之后。
- 将一个哈希表初始化。
free_list和哈希表存在是为了更加有效的使用缓冲区内存,这个后面的文章会深入分析。
1 | //// 缓冲区初始化函数。 |
hd_init
该函数进行了如下操作:
- 对blk_dev数组中硬盘对应的request_fn进行了赋值,指向了do_hd_request对应的代码。用于处理硬盘数据请求。
- 设置了硬盘中断向量,并且设置了CPU,允许硬盘控制器发送硬盘中断请求。
后续的文章可以进行深入分析。
1 | // 硬盘系统初始化。 |
floppy_init
软盘和硬盘类似,并且我没有接触过,其代码并没有深入分析。
sti
开启中断
move_to_user_mode
切换到用户模式运行。
该函数利用iret 指令实现从内核模式切换到用户模式(初始任务0)。
该段代码,似懂非懂
1 | #define move_to_user_mode() \ |
fork 和 init
在后续的执行中,任务0执行fork系统调用,fork出来的任务1执行init方法,而任务0将会循环执行pause()。
总结
本文是我在阅读Linux0.11源码中main函数时的一些总结,有自己的一些理解。还有以下几个方面都有一些遗留问题
- buffer_init。其中free_list和哈希表的用途需要进一步分析。
- hd_init。其中do_hd_request函数需要进一步分析
- move_to_user_mode。该函数的具体意义的进一步分析。