前面复习到进程1的创建、调度以及执行,但系统启动工作还没有结束,未实现怠速状态(怠速意味着操作系统已经完成了所有的准备工作,随时可以响应用户的激励),所以,接着会由进程1创建进程2,然后由进程2的执行,最终加载和重建shell程序,创建update进程,从而实现怠速。
前记:
进程1开始执行就是进入init()函数,在前篇笔记里说过进程1会加载根文件系统等工作,这些工作全部在setup系统调用中执行,接下来的执行动作是在setup返回之后,即执行的是setup之后的代码。
四、进程2的创建及执行(参考init函数源码)
1) 进程2的创建及调度
此时,在加载根文件系统之后,进程1在其支持下,会通过系统调用分别创建或者复制标准输入设备、标准输出设备以及标准错误输出设备。这些设备文件的打开,都是为创建shell做准备,并且,此时意味着此后可以在程序中使用printf()函数...(系统says:啊,人艰不拆)
接着,从此处开始,进程1调用fork函数,创建进程2。创建过程与创建进程1一致,即调用find_empty_process()函数,获取可用进程号,之后调用copy_process()函数,申请task_struct及内核栈页,复制task_struct,个性化设置,共享文件设置,GDT设置等等,具体请参考笔记(三)。接下来的执行代码如下:
void init(void)
{
... // setup系统调用以及输入输出设备文件创建
if (!(pid=fork()))
{ // 子进程2执行部分
close(0);
if (open("/etc/rc",O_RDONLY,0))
_exit(1); // 如果打开文件失败,则退出
execve("/bin/sh",argv_rc,envp_rc); // 装入/bin/sh 程序并执行
_exit(2); // 若execve()执行失败则退出
}
// 父进程1执行部分
if (pid>0)
while (pid != wait(&i)) // 等待子进程2退出或者切换到子进程,此时进行进程切换
{/* nothing */;}
...
}
进程2创建完毕后,fork第一次返回,返回值为子进程号2,因此,父进程1执行wait()调用。此函数功能:如果父进程1有等待退出的子进程,就为该进程的退出做善后工作;如果有子进程,但并不等待退出,则进行进程切换;如果没有子进程,函数返回。
wait()函数实质仍是执行系统调用,跟前面的fork,pause,setup过程一致,其代码主体逻辑为:先遍历所有进程,确定父进程的所有子进程,再对子进程进行分析,确定是否准备退出,若退出则做善后工作,否则,切换到子进程执行。
2) 进程2的执行,Shell程序加载
此时子进程2并不退出,所以wait函数切换到进程2,进程2在此会从fork中的if (__res >= 0)语句执行(为什么?参考进程1的创建),亦即fork返回值为0,进入到if语句体内部分代码执行。从上面代码看到,子进程2首先关闭标准输入文件,并打开“/etc/rc ”脚本文件,这里实质就是将rc文件替换输入设备文件tty0,这一步的操作为后面shell程序的执行做准备(之后会再次说到);然后,调用execve()函数加载shell程序,balabala。。。
其实,笔记到这里就已经可以说,在Linux中创建一个完整进程的流程就出来了:
1,fork()通过拷贝当前进程创建一个子进程;
2,exec族函数负责读取可执行文件并将其加载至进程地址空间开始运行。
进入sys_execve系统调用,Shell程序加载过程。此时,子进程2有了自己对应的程序,因为之前是与父进程1共享一套内存页面管理结构的,所以,现在需要对自身的task_struct进行调整,关系解除,并重新组织自己的内存管理结构,比如定做LDT,重设代码段,数据段,栈段等控制变量。调整结束,还需要在sys_execve中设置好压栈的值,即用shell程序的起始地址设置EIP,用进程2新的栈顶地址值设置ESP。这样,当sys_execve返回时,进程2便开始从Shell程序执行。
3) Shell程序执行,update进程创建
Shell程序开始执行,但其线性地址空间对应的程序内容并未加载,也就不存在相应的页面,因此,产生“缺页”中断,并开始调用缺页中断服务程序分配页面,并加载Shell程序。载入Shell程序后,内核会将该页内容映射到Shell进程的线性地址空间内,建立页目录表→页表→页面的三级映射管理关系。
Shell程序执行的第一件事,就是读取标准输入设备文件上的信息,但此时,进程2刚创建时,就已经将tty0标准输入文件替换成rc脚本文件了,因此,Shell程序在这开始读rc脚本文件,并从rc文件中读取一些命令。根据第一条命令,Shell首先创建一个进程,即进程3,创建完毕再加载update程序,将此进程称为update进程,并最终将执行权转交给此进程(创建、加载、切换与进程2类似)。
update进程作用:将缓冲区的数据同步到外设上。由于主机与外设的数据交换速度远低于主机内部的数据处理速度,因此,当内核需要往外设上写数据的时候,为了提高系统的整体执行效率,并不把数据直接写入外设上,而是先写入缓冲区,之后再根数实际情况,将数据从缓冲区同步到外设。
每隔一段时间,uodate进程被唤醒,将数据往外设同步一次,之后再次挂起,即设置可中断等待状态,等待下次被唤醒,周而复始。
现在,uodate进程执行,因为并没有同步任务,进程挂起,系统进行进程调度,最终再次切换到Shell进程执行,处理rc文件第二条命令。。。读取结束后,Shell进程退出,执行exit()函数,同样是执行系统调用sys_exit。在sys_exit系统调用函数中,首先会将当前进程设置僵死状态,然后通知父进程,最后再次调用schedule()函数进行进程切换。
4)重建Shell,系统怠速
从一开始进程1调用wait()函数,执行sys_waitpid()系统调用,切换到进程2,这个时候,exit函数在执行通知父进程中的tell_father函数中,将父进程状态改为就绪态,接着,schedule函数切换,即从子进程2切换到父进程1中执行,亦即wait()函数返回,返回值为进程号2,所以即跳出上述的while循环,执行后续“大”的死循环,如下:
void init(void)
{
...
if (pid>0)
while (pid != wait(&i))
{ /* nothing */;}
while (1) {
if ((pid=fork())<0) {
printf("Fork failed in init\r\n");
continue;
}
if (!pid) { // 重建Shell,打开标准输入输出,错误输出设备文件,加载Shell程序
close(0);close(1);close(2);
setsid();
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);
_exit(execve("/bin/sh",argv,envp));
}
while (1)
if (pid == wait(&i))
break;
printf("\n\rchild %d died with code %04x\n\r",pid,i);
sync();
}
_exit(0);
}
注意,此时Shell重新打开的是标准输入设备文件(tty0),这样,Shell程序开始执行后,不再退出。至此,系统中目前已含有进程0,进程1,Shell进程,update进程,所有进程之后全部进入可中断等待状态,随即再次切换到进程0执行,从而实现系统怠速状态。
系统怠速以后,只要有用户通过Shell进程提供的终端与计算机进行交互,即有输入激励中断,对应的中断服务程序即给Shell进程发出信号,然后Shell进程被唤醒,读取数据信息并处理,处理结束,再次挂起,周而复始。。。
至此咧,整个系统的基本的启动工作全部搞定。两天时间将之前四章书连贯起来,感觉还不错,挺有收获的。本书后续就是一些文件操作,内存管理,进程间通信了,然后还有些操作系统设计的指导思想云云,虽已看过,但暂时感觉不至于用上,暂且略过咯。