本文分析Minix3内核的初始化过程。如Boot系列中所说的,内核启动并作了少量初始化之后,会
开始执行main.c文件中的main()函数,本文就是从这个函数开始看的。
* big_kernel_lock
目前,Minix3支持SMP。在SMP的情况下,big_kernel_lock是一个自旋锁,用于防止多个CPU同时执行某
段Kernel代码,也就是说,用于多个CPU之间的互斥。在main()函数开始时,会抢占这个锁。
* proc_init()
main()会调用这个函数(定义在proc.c文件中)来初始化全局的进程表和特权表,以及各CPU对应的IDLE任务。
对于进程表:
1. 初始化标识字段,表示slot空闲,初始化magic字段;
2. 初始化进程号,从-NR_TASKS开始,依此递增;
3. 初始化slot对应的消息地址endpoint号;
4. 初始化调度器为null,优先级和时间片都是0;
对于权限表:
1. 初始化对应的进程字段为NONE,表示slot为空闲;
2. 初始化自己的id为自己的下标;
3. 初始化ppriv_addr数组;
4. 初始化信号处理器为null;
** Idle任务
在Minix3中,每个CPU都有一个对应的idle任务,所有的idle任务共享一个权限结构:定义在proc.c中
的idle_priv。这个结构体仅仅设置了其s_flags成员为(SYS_PROC|BILLABLE)。
idle任务的p_rts_flags中设置了RTS_PROC_STOP的标志,使其不能被调度执行。
idle任务的名字是"idle" + XXX,其中,XXX是CPU的编号。
** 其它
人工对齐每个进程表项中的fpu_state_s结构体。
这个要求16字节对齐,此处用一段代码对它们进行对齐操作。
下面,一大段代码主要的工作是为boot image中的进程设置进程表项。
* 栈
所有内核任务(不包括kernel任务,它的栈是汇编分配的)的栈都分配在一个数组中,该数组是t_stack,
定义在table.c中。
boot image中其它进程的栈已经被boot monitor分配在它们的数据空间中了,所以,它们的栈指针指向其
数据空间的末尾。
这里有个问题,如果这样的话,这些进程的栈大小在编译时就是知道的。难道是最大栈大小?
如前面的文章所述,每个boot image中进程的某些属性定义在表image中。
* 主要流程
对于每个在image中的进程,找到其对应的进程表入口地址,做如下操作(如果没有特别指出,都是指对
进程表项的设置操作):
1. 根据进程表中的endpoint设置image表中的字段;
2. 清0剩余CPU时间;
3. 设置进程的名字;
4. 清空account;
5. 此时,检查当前进程是不是应该被立刻调度运行。如果是,则设置它对应的特权,使得它立刻可以被
调度执行,否则,不设置它的特权,而是设置其RTS标志RTS_NO_PRIV,使得它暂时不能运行。从条件
可以看到,只有内核任务和root system process(也就是再生服务器RS)是立刻可以运行的。
设置特权的主要流程如下:
a. 调用get_priv()函数为当前进程分配并设置对应的特权表项。对于这些进程,其对应的特权ID是
NR_TASKS + proc_nr,对于内核任务而言,分别是0,1,2,3。事实上,get_priv()支持动态
或者静态地分配特权表项,这里,传递进去的特权id是大于等于0的,表示静态分配指定的特权表
项,如果传递进来的id是-1,则表示动态地查找一个空闲表项并分配。
b. 设置进程特权表项的内容:
6. 如果当前任务是内核任务,设置它的栈,这些栈都是分配在t_stack中的;
7. 读取当前进程的头信息,放在局部的一个struct exec结构体中;
8. 构造当前进程的内存映射,即p_memmap数组,下面都是以click为单位的:
a. 代码段的mem_phys设置为aout中符号表的大小;
b. 代码段的mem_len设置为aout中代码段的大小;
c. 数据段的mem_phys设置为代码段的base+length;
d. 数据段的mem_len设置为aout中数据段加bss段的大小;
e. 栈段的mem_phys设置为代码段的base+length;
f. 栈段的mem_len设置为0;
g. 栈段的mem_vir设置为aout中a_total的大小;
9. 设置进程表项中寄存器的值:
a. pc设置为0;
b. 设置psw;
c. 设置用户进程的栈指针sp,它指向p_memmap[S].mem_vir对应的字节地址 - 3*sizeof(reg_t)的位置,
之所以空出3个字长的空间,是用于放置进程main函数的argc、argv和envp三个参数。
10. 在Minix内核中,有一个CPU-LOCAL全局变量proc_ptr,它指向当前正在运行的进程的进程表项。调度器
依赖于这个变量。此时,我们检查一下当前CPU的proc_ptr是否为空,如果为空,则将它设置为当前进程。
11. 如果当前进程的运行需要虚拟地址,设置其RTS标志的RTS_VMINHIBIT位,表示,在VM为它设置好页表
之前,该进程不能被调度执行;
12. 设置当前进程的RTS标志的RTS_PROC_STOP位,表示不能运行;
13. 调用alloc_segments()函数,为该进程分配段(段寄存器,在386上使用);
至此,一个进程表项就初始化结束了。
* 后续初始化流程
1. 设置ipc_call_names数组,包含SEND, RECEIVE, SENDREC, NOTIFY, SENDNB, SENDA。
2. 调用arch_init()完成体系结构相关的初始化;
对于i386而言,这里主要做设置好内核栈环境,初始化acpi、idt、tss等工作。
3. 调用system_init()完成系统任务的初始化;
该函数主要在初始化一个call_vec数组,该数组中保存了kernel-call的句柄。该数组将
将SYS_XXX号对应到do_xxx()函数上。例如,SYS_FORK对应do_fork()函数。
除上面之外,该函数还做了另外两件事:
a. 将irq_hooks表项的proc_nr_e清成NONE;
b. 初始化特权表中的s_alarm_timer成员,设置为永不超时,没有next;
4. 调用bsp_finish_booting(),该函数从不返回,进入系统正常运行过程。
现在,内核最原始的初始化过程已经结束了,后续的工作交给了bsp_finish_booting()函数。
* bsp_finish_booting()
该函数执行结束后,系统就开始正常运行并调度各个进程了。该函数主要完成以下操作:
1. 调用cpu_identify()获取CPU的相关信息,保存在全局变量cpu_info中;
2. 设置全局标志vm_running为0,初始化全局变量krandom,设置随机种子信息;
此时,Minix已经基本ready了,所有boot image中的进程也都处于ready队列中了,后面的主要动作及时
启动调度并启动用户态的进程了。
3. 设置当前CPU的计费和进程,都指向当前cpu的idle_proc;
4. 打印minix启动时的信息;
5. 清楚用户态进程中的RTS_PROC_STOP标志,使得它们可以开始执行(至少不被这个标志阻挡);
6. 调用boot_cpu_init_timer函数,启用时钟中断,并设置中断处理函数;
7. 初始化fpu;
8. 调用switch_to_user()函数,开始调度用户态进程运行,该函数不会返回。
至此,Minix内核所有的启动初始化完成了,开始调度用户态进程并进入正常执行的状态。