Linux内核设计艺术笔记(四)

前面复习到进程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进程被唤醒,读取数据信息并处理,处理结束,再次挂起,周而复始。。。

至此咧,整个系统的基本的启动工作全部搞定。两天时间将之前四章书连贯起来,感觉还不错,挺有收获的。本书后续就是一些文件操作,内存管理,进程间通信了,然后还有些操作系统设计的指导思想云云,虽已看过,但暂时感觉不至于用上,暂且略过咯。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值