进程与线程(二)——进程的管理、创建与销毁

来源:互联网 发布:已断开此网络登陆超时 编辑:程序博客网 时间:2024/05/16 23:43


接上回:

我们介绍了进程的由来,进程的概念,进程的组成部分和它在运行过程中的状态。我们说进程最重要的部分是进程控制块PCB,操作系统通过PCB来管理各个进程有条不紊的在我们的机器中运行的。那么操作系统是怎么样管理这么多进程的呢?进程是怎么样创建、又是怎么样销毁的呢?

 


一.PCB的组织方式

 

一个系统中通常可有数十个甚至数百个乃至数千个PCB,为了能对他们加以有效的管理,应当用适合当的方式将这些组织起来。要回答这个问题,就要从PCB的组织方式来讲起。

我们说程序等于数据结构加算法,这句话并不是空穴来风,这里将涉及到我们数据结构的内容。

 

(1)  线性方式:

将系统中所有的PCB 都组织加一张线性表中将该表的手指放在内存当中的一个专用区域中。

 

 

(2)  链接方式:

一般使用链表的数据结构,为什么:因为进程在执行的过程是动态的执行过程,在管理的时候进程会一会创建一会儿结束,在组织里面可以动态的插入和删除,用链表的形式可以更好的实现动态的删除和插入功能。

索引的话动态的删除和插入开销会大一点,目前来说才去基于链表的的组织方式。如果一开始进程比较固定,不会频繁的创建和删除的话,采取索引的方式也是一种快捷的组织方式。 所以特殊的操作系统和通用的操作系统会有不同的组织方式。

 

 

(3)  索引方式

系统根据所有进程状态的不同,建立几张索引表。例如,就绪索引表,阻塞索引表等。并把个索引表在内存的首地址记录在内存的一些专用表单元中。每个索引表的表目中,记录具有相应状态的某个PCB在PCB表中的地址。

 


二. 状态队列

 

我们可以看出:

1.  操作系统来维护一组队列,用于表示系统当中所有进程的当前状态,

2.  不同的状态,分别用不同的队列来表示。就绪队列各种类型的阻塞队列。

3.  每个进程都根据他的状态加入到相应的队列当中。当进程的状态发生变化时,它的PCB从一个状态脱离出来,加入到另外一个队列。

 

如图所示:

我们还可以看到:多个优先队列,分优先级

例如:根据阻塞等待的不同的事件,会排不同的队列:事件1满足一个或者多个进程,进程就会变为就绪队列。

如果事件1只能满足一个进程的话,那么我们只能把队列中的一个进程从阻塞态变为就绪态。如果事件1产生之后,所有等待事件1的这些进程都可以得到满足,我们就需要把所有的进程都从阻塞态转变为就绪态,整个队列里的进程就会夸到就绪队列里去。

 

三. 创建进程,加载和执行进程

 

首先有这么一段代码。(终于看到代码了)

我们将讲解以下几个步骤:

1.创建fork()父进程创建子进程

2.加载:EXEC() 

3.等待:wait()

 


初始状态:

父进程的空间

 

 

执行Fork()后:新的子进程把父进程代码和数据都复制了一份,同时创建了一个新的属于自己的PID(128)。如图所示:


 


执行EXEC()后:

系统调用EXEC()加载执行新的程序,取代当前运行的进程,覆盖所有的代码和数据,都变成新的程序里面的内容。但是请注意:新的PID(128)没有变。


 

这个地方有点绕,多看几遍哪里没变化,哪里有变化。


我们再来看一遍内存布局图:

单独的父进程

fock()+exec()后:创建新的地址空间,新的代码段、新的进程:

 

一、fork子进程的地址空间完全是父进程的

 

二,但是执行完exec的时候可以看到:1.PCB里面信息变化 2. 用户态内存空间变化,代码段完全被新的程序替换。

           

calc_mian开始执行,也就是说:当子进程执行exec时候,整个程序的控制流发生了变化。

 

注意:Exec加载不同的程序,来执行新的程序,运行进程指定不同的控制流,这样可以使得在操作系统里面可以执行不同的应用程序,很好的一个方法,提供了一个设计思路:创建一个进程,继承父进程的所有的代码和数据,还是说完成一个新的程序的工作。这个可以根据应用程序的需求来做相应的处理。注意执行EXEC的时候,进程本身的代码段、数据段、堆栈都会被覆盖。

 作为了解:

Fork因为复制代码占用很多的系统开销,如果fork效率高,可以提高操作系统的效率。fork把父进程的地址空间完全复制一份到子进程的空间中来,有一个内存的大量的拷贝(代码段,数据段)。但是执行exec的时候,刚刚的拷贝全都是没有用的,因为要加载一个新的程序,要把代码段和数据段重新覆盖掉,前面做的fork工作其实是多余的。

有什么办法优化呢?

1.虚fork——vfork,复制的时候只是复制了一小部分的内容,绝大部分的内容没复制。——变成了两个(fork、vfork)

2.操作系统各个子系统之间 相互支持相互帮助,我们通过虚拟内存的管理,就可以出现一个高效的fork实现机制——Copy on Write技术。写的时候在进行复制。——当父进行创建子进程的时候 ,在实际的复制的时候,没有把整个地址空间真实的复制,只复制了父进程所需要的元数据(页表),它们指向了同一块地址空间。当父进程或者子进程对某一个地址单元进行写操作的时候,会触发一个异常,使不论是父进程还是子进程, 要把这个触发异常的这个页复制成两份 ,让父进程和子进程拥有两个个不同的地址。这种方式,实现按需写的复制,如果只是只读,确实没有必要复制,只有当写的时候,我们需要子进程和父进程需要有不同的页————很有效率。

不管后面是否执行EXEC(),这个系统调用,我们的fork:第一,还是和之前的语义是一样的,能完全创建子进程(而且执行效率很高,因为只复制了地址空间管理相关的所需要的那个源数据的页表等)。但是是根据是否完成写操作来决定是否要去复制。COW是我们进程管理和内存管理一个有效的相互支撑的一直机制。

 

四.等待和终止进程 


wait()系统调用是被父进程用来等待子进程的结束,一个子进程向父进程返回一个值,父进程必须接受这个值并处理。 
    
为什么要让父进程等?而不是直接结束? 
当进程执行完毕退出后,几乎所有资源都回收到OS中。但有个资源很难回收,就是PCB,PCB是代表进程存在的唯一标识,操作系统要依据PCB进程执行回收。这个功能由父进程完成。子进程exit()和父进程wait()匹配,父进程完成把子进程PCB释放:

Wait()使父进程睡眠,当子进程调用exit()时操作系统解锁父进程,将通过exit传递得到的返回值作为wait调用的一个结果(连同子进程的pid一起)。关闭所有打开的文件和连接,释放内存,释放大部分支持进程的OS结构,检查父进程是否存活。 
如果父进程存活,它保留exit结果的值直到父进程需要它,进入僵尸状态。 
还有一种情况:如果父进程挂了,子进程释放所有的数据结构,这个进程死亡。主终进程root 或者根进程会定期的扫描PCB进程控制块的链表,看是否有进程处于僵尸态的状态,如果有进程处于僵尸状态, 就代父进程来完成回收操作。

什么是僵尸状态: 

就是调用了子进程EXIT但父进程还没有执行到wait返回的时候。子进程将死,还没死。无法正常工作,只是等待被父进程回收。

 

现在,我们把fork、exit、wait加到进程状态图中。留一个问题,exec()应该在哪呢?


 

 

原创粉丝点击