进程是由操作系统创建的工作单元。值得注意的是进程和程序未必是等同的。一个程序可能由多个任务组成,而每个任务可以和一个或多个进程相关联。程序是由程序员创建的,而进程是由操作系统创建的。一个工作单元要想被称作进程,它必须要有操作系统指派给他的地址空间,必须拥有进程ID,必须拥有状态和进程表中的表项。进程和线程之间最大的区别是进程有着自己的地址空间,而线程共享创建它们的进程的地址空间。
在将C++任务映射为操作系统能够理解的执行单元时,结果证明线程更易于编程,其主要原因是线程共享相同的地址空间,使得线程间的通信和同步都要易于进程。创建或终止线程所要做的工作都要少于创建进程的相关工作,而且速度也要快于进程。
那为什么还有使用进程呢?
首先,进程有自己的地址空间,可以有效的提供安全性和隔离性,阻止流氓进程的干扰。
其次,线程所能使用的打开的文件数目受限于一个进程能拥有的打开的文件数目。这使得使用进程时能够对更多的资源进行访问。
第三,对于多用户的程序,每个用户程序还是应该更独立一些,如果一个用户进程失败,其他用户还是可以继续工作的,但如果一个用户线程失败,则有可能影响所有用户的操作。
但使用多进程时进程间通信和启动时间是主要的代价。
当执行进程时,操作系统将它指定到一个处理器上。进程在一个时间片(quantum)内执行他的指令。进程是可抢占的。分为操作系统进程和用户进程。
操作系统进程:是执行的系统代码,也被称作内核进程。执行的是系统管理的任务。
用户系统进程:是执行的用户代码。
进程控制块 PCB
进程拥有一些标识来描述他们运行的特性。内核维护数据,并提供可以允许用户访问的接口。这些信息被保存在进程控制块中(Process Control Block)。
PCB中的信息:
- 进程当前的状态和优先级
- 进程标识,父进程的标识,子进程的标识
- 指向以分配资源的指针
- 指向进程内存位置的指针
- 指向父进程和子进程的指针
- 进程所使用的处理 器
- 控制和状态寄存器
- 栈指针
在进程的地址空间内分为3个逻辑段: 代码段,数据段,栈段。
进程的地址空间是虚拟的。虚拟存储使得在执行的进程中引用的地址同内存中的实际可用的地址是无关的。这使得可寻址的存储空间大小远远大于实际的内存大小。进程的虚拟地址空间的段是连续的。每个段及物理地址空间被分成页。每个页有唯一的帧号(page frame number)。虚拟页帧号(virtual page frame number) 用作进程页表中的索引。虚拟地址空间是连续的,但可以以任何顺序映射到物理页面。虽然进程的虚拟地址空间都受到保护,但进程的代码段是可以在不同进程间共享的。
要想运行任何一个程序,操作系统必须首先创建一个进程。当创建新的进程之后,在主进程表中会加入一个新的条目,创建并初始化一个新的PCB。PCB中会包含进程id,父进程id,进程的入口等。
创建进程有着不同的方法 fork() system() posix_spawn(),这里将主要讨论posix_spawn()方法。
一个简单的posix_spawn() 的示例:
posix_spawn() 函数的说明:
file_atction: 包含了新进程将要对文件描述符执行的动作的信息的数据结构。
进程在运行期间,会有不同的状态
运行(Running)
就绪(runnable, ready)
僵死(zombied)
等待(waiting, block)
停止(stopped)
进程状态的改变取决于操作系统所创造的环境。
当一个就绪队列包含多个进程时,调度器就会决定首先将那个进程指派给处理器。每个进程都会被赋予一个优先级,有相同优先级的进程被放在一个队列中。进程的优先级可能是静态的,也可能是动态的。调度器根据不同的调度策略来处理进程。在POSIX API中主要包含2中调度策略,FIFO(先进先出),RR(轮询)
FIFO 先进先出:进程是根据到达队列的时间被指派给处理器的。当正在运行的进程时间片耗尽时他会被放到队列的头部,当一个休眠进程变为可运行时,它将被放在队列的尾部。
RR 轮询:所有的进程被同等对待,当进程时间片被耗尽后进程被放到队列的后端。
异步进程间是相互独立的。进程A运行到结束不需要考虑其他进程。
同步进程定义为交错进程,进程A运行一段时间后将会挂起自身,等待进程B运行结束。
fork() fork-exec() posix_spawn等函数创建的进程都是异步进程,父进程在成功创建子进程后并不挂起,两个进程将会各自独立的运行。
system() 创建的进程是同步进程,它会创建一个shell来执行命令或可执行文件。父进程将会被挂起,直到子进程结束,且system()调用返回。
wait() 函数:异步进程可以通过执行它将自身挂起,等到子进程结束。当子进程结束后,等待中的父进程收集子进程的退出状态,防止出现僵死进程。