进程切换(上下文切换)
所谓进程切换,或者说上下文切换指的是,暂停当前运行的进程,从运行态变为其他状态,然后OS调度另一个进程从就绪态变成运行态的过程。
如此的进程切换,就需要保存一些进程的上下文,保存进程的生命周期信息(寄存器如PC,SP等等、CPU状态、地址空间)
之前提到过,OS为每个进程维护一个PCB,而这也是标志进程存在的标识,此外,OS会为每个处于相同状态的PCB放置在同一个队列中,如就绪队列、运行队列、阻塞队列等等。如下图所示:
有趣的是,上图中还有一个僵尸队列。什么是僵尸队列?——处于僵尸状态的一系列进程,举个栗子,在linux下,一个进程A中调用fork()
系统调用创建了一个子进程,子进程中休眠了10s,父进程在子进程休眠完成前就退出了。这就导致一个问题,子进程休眠完退出后找不到父进程了。
而我们又知道,PCB是进程存在的标识,一个进程自己肯定是不可能杀掉自己的,必须靠别人来帮忙,举个例子,一个人尝试提着自己的头发,把自己从站在地上提起来悬空,这显然不可能!也就类比于,一个进程既然存在它必然有PCB,而要执行相应的操作(哪怕是杀掉自己进程)也必须基于自己这个进程存在的前提之下,所以肯定是矛盾的。那怎么办呢?——交给创建该进程的父进程来完成。可是现在父进程又在子进程之前提前退出了。。这个进程就变成了僵尸进程。
进程的创建与加载
Unix进程创建系统调用:fork()、exec()
fork()函数把一个进程复制成两个进程,exec()用新的程序来重写当前进程,PID不会变。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18int pid=fork();
if(pid<0)
{
// 创建子进程时失败
return ;
}
else if(pid==0)
{
// 这里是子进程的操作
exec("/bin/calc",argc,argv0,argv1,/...);
printf("我刚才执行完了calc程序!");
}
else
{
// 这里是父进程的操作
child_status=wait(pid);
printf("子进程结束了!");
}
fork()函数的返回值如下:
- 子进程的fork()返回0
- 父进程的fork()返回子进程的标识符,即pid
- fork()返回值可以方便后续使用,字进程可以使用getpid()得到PID。
下面3张图演示了fork()和exec()两个系统调用的执行过程:
理论上来说,fork()创建一个子进程后,会复制父进程的所有变量和内存、CPU寄存器,但现代的计算机都运用了copy-on-write写时复制
的技术,这也归功于虚存的存在,只有在设计到一些写操作时,才会进行相应的copy操作,可以看下我之前转载的一篇文章>>fork之后子进程到底复制了父进程什么?<<
父进程等待子进程
之前说到,创建子进程中很有可能会产生僵尸进程的情况,于是,wait()这个系统调用就是用来父进程回收子进程资源的,不过wait()这个函数是一个同步的方法,效率不高,大致的执行机制如下:
子进程结束时,会向exit()函数中传一个值,也就是return的状态,这个返回的值会被父进程给接受到,父进程通过wait()
接受并处理这个返回值,然后把子进程清理掉。
注意,如果有子进程存活时,父进程进入等待状态,一直等到子进程调用EXEC()返回一个结果来唤醒父进程。如果没有子进程存活,wait()立马返回。