在进程模型中,
计算机上所有可运行的
软件,通常也包括
操作系统,被组织成若干顺序进程(sequential process),简称
进程(process)。操作系统中最核心的概念是进程, 进程也是
并发程序设计中的一个最重要、 最基本的概念。进程是一个动态的过程, 即进程有生命周期, 它拥有资源, 是程序的执行过程, 其状态是变化的。 Windows、
unix和
Linux是最流行的几个操作系统。
运行原理
一个进程就是一个正在执行程序的实例,包括
程序计数器、
寄存器和
变量的当前值。从概念上说,每个进程拥有它自己的
虚拟CPU。当然,实际上真正的CPU在各进程之间来回切换。但为了理解这种系统,考虑在(伪)
并行情况下运行的进程集,要比我们试图跟踪CPU如何在程序间来回切换简单得多。正如我们所看到的,这种快速的切换称作
多道程序设计。
在图中a我们看到,在一台多道程序计算机的
内存中有4道程序。在图中b,这4道程序被抽象为4个各自拥有自己控制流程(即每个程序自己的逻辑程序计数器)的进程,并且每个程序都独立地运行。当然,实际上只有一个物理程序计数器,所以在每个程序运行时,它的逻辑程序计数器被装入实际的程序计数器中。当该程序执行结束(或暂停执行)时,物理程序计数器被保存在内存中该进程的逻辑程序计数器中。在图中c我们看到,在观察足够长的一段时间后,所有的进程都运行了,但在任何一个给定的瞬间仅有一个进程真正在运行。
在此,我们假设只有一个
CPU。然而,逐渐这个假设就不为真了,因为新的芯片经常是
多核的,包含2个、4个或更多的CPU。但是,一次只考虑一个CPU会更简单一些。因此,当我们说一个CPU只能真正一次运行一个进程的时候,即使有2个核(或CPU),每一个核也只能一次运行一个进程。
由于CPU在各进程之间来回快速切换,所以每个进程执行其运算的速度是不确定的。而且当同一进程再次运行时,其运算速度通常也不可再现。所以,在对进程编程时决不能对时序做任何确定的假设。例如,考虑一个
I/O进程,它用流式
磁带机恢复
备份的文件,它执行一个10 000次的空循环以等待磁带机达到正常速度,然后发出命令读取第一个记录。如果CPU决定在空循环期间切换到其他进程,则磁带机进程可能在第一条记录通过
磁头之后还未被再次运行。当一个进程具有此类严格的实时要求时,也就是一些特定事件一定要在所指定的若干毫秒内发生,那么必须采取特殊措施以保证它们一定在这段时间中发生。然而,通常大多数进程并不受CPU
多道程序设计或其他进程相对速度的影响。
值得注意的是,如果一个程序运行了两遍,则算作两个进程。例如,我们可能经常两次去启动同一个字处理软件,或在有两个可用的打印机的情况下同时打印两个文件。像“两个进程恰好运行同一个程序”这样的事实其实无关紧要,因为它们是不同的进程。操作系统能够使它们共享代码,因此只有一个副本放在内存中,但那只是一个技术性的细节,不会改变有两个进程正在运行的概念。
区别理解
进程和程序间的区别是很微妙的,但非常重要。用一个比喻可以使我们更容易理解这一点。想象一位有一手好厨艺的计算机科学家正在为他的女儿烘制生日蛋糕。他有做生日蛋糕的食谱,厨房里有所需的原料:面粉、鸡蛋、糖、香草汁等。在这个比喻中,做蛋糕的食谱就是程序(即用适当形式描述的算法),计算机科学家就是
处理器(CPU),而做蛋糕的各种原料就是输入数据。进程就是厨师阅读食谱、取来各种原料以及烘制蛋糕等一系列动作的总和。
假设计算机科学家的儿子哭着跑了进来,说他的头被一只蜜蜂蛰了。计算机科学家就记录下他照着食谱做到哪儿了(保存进程的当前状态),然后拿出一本急救手册,按照其中的指示处理蛰伤。这里,我们看到处理机从一个进程(做蛋糕)切换到另一个高
优先级的进程(实施医疗救治),每个进程拥有各自的程序(食谱和急救手册)。当蜜蜂螫伤处理完之后,这位计算机科学家又回来做蛋糕,从他离开时的那一步继续做下去。
这里的关键思想是:一个进程是某种类型的一个活动,它有程序、输入、输出以及状态。单个处理器可以被若干进程共享,它使用某种调度算法决定何时停止一个进程的工作,并转而为另一个进程提供服务。
具体模型介绍
Windows、 unix和Linux是最流行的几个操作系统, 由于Linux和unix有很多的相似性, 这里仅分析Windows和Linux操作系统中的进程模型。
Linux进程模型
1.1 Linux进程描述符
为了管理进程,
内核必须对每个进程所做的事情进行清楚的描述,这正是进程描述符的功能。进程描述符都是task_struct类型结构,它的字段包含了与一个进程相关的所有信息。进程描述符包含了进程的所有信息 。task_struct是一个非常复杂的结构 。这里重点分析其包含的信息:
进程状态 (Stak);
进程调度信息 (SchedulingInfoMh);
各种知识符 (Identifiers);
进程通信有关信息 (IPC, Inter_Process Com-
munication);
时间和
定时器信息 (Times and Timers);
进程链接信息 (Links);
文件系统信息 (File System);
虚拟内存信息 (Virtual Memory);
页面管理信息 (page);
和处理器相关的环境 (上下文) 信息 (Pro-cessor Specific Context)。
下面对task_struct结构进行描述:
(1) 进程状态
进程描述符中的state字段描述了进程当前所处的状态。它由一组标志组成,其中每个标志描述一种可能的进程状态。下面是可能的6种状态:
① 可运行状态
进程要么在CPU上执行,要么准备执行。正在运行的进程就是当前进程 (由current所指向的进程),而准备运行的进程只要得到CPU就可以运行,CPU是这些进程唯一等待的系统资源。
② 可中断的等待状态
进程被挂起 (睡眠),直到某个条件变为真。
③ 不可中断的等待状态
与可中断的等待状态类似, 但有一点不同,把信号传递到睡眠进程不能改变它的状态。
④ 暂停状态
进程的执行被暂停。通常当进程接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU信号后就处于这种状态。
⑤ 跟踪状态
进程的执行已由debugger程序暂停。
进程的执行被终止,但在发布wait4 () 系统调用前, 在进程表中仍然有它的任务结构。
(2) 进程调度信息
调度程序利用这部分信息决定系统中哪个进程最应该运行,并结合进程的状态信息保证系统运转的公平和高效。这一部分信息通常包括进程的类别 (普通进程还是实时进程)、 进程的优先级等。
(3) 标识符
Linux操作系统中允许用户使用一个叫做进程标识符process ID (或PID) 的数来标志进程,PID存放在进程描述符的pid字段中。
PID被顺序编号,新创建进程的PID通常是前一个进程的PID加1。不过PID有一个上限值,当内核使用的PID达到这个上限值的时候就必须开始循环使用已闲置的小PID号。
(4) 进程通信有关信息
为了使进程能在同一项任务上协调工作,进程之间必须能进行通信即交流数据。Linux支持多种不同形式的通信机制。它包含如下域:
spinlock_t sigmask_lock: 信号掩码的自旋锁
long blocked: 信号掩码
struct signal *sig: 信号处理函数
struct sem_undo *semundo: 为避免死锁而在信号量上设置的取消操作
struct sem_queue *semsleeping: 与信号量操作相关的等待队列
程序创建的进程具有父/子关系。如果一个进程创建多个子程序时,则子进程之间具有兄弟关系。
(6) 时间和定时器信息
一个进程从创建到终止叫做该进程的
生存期。进程在其生存期内使用CPU的时间,内核都要对其进行记录,以便进行统计、计费等有关操作。进程耗费CPU的时间由两部分组成:一是用户模式 (或称
用户态) 下耗费的时间、一是在系统模式 (或称为系统态) 下耗费的时间。每个时钟滴答,也就是每个时钟中断,内核都要更新当前进程耗费CPU的时间信息。进程有3种类型的定时器:实时定时器、虚拟定时器和概况定时器。这3种定时器的特征共有3个:到期时间、定时间隔和要触发的事件。到期时间就是定时器到什么时候完成定时操作,从而触发相应的事件;定时间隔就是两次定时操作的时间间隔,它决定了定时操作是否继续进行。如果定时间隔大于o,则在定时器到期时,该定时器的到期时间被重新
赋值,以使定时操作能继续进行下去,直到进程结束或停止用定时器,只不过对不同的定时器,到期时间的重新赋值操作是不同的。实时定时器不管其所属的进程是否运行都要更新,所以,时钟中断来临时,系统中所有进程的实时定时器都要被更新,如果有多个进程的实时定时器到期,则内核要一一处理这些定时器所触发的事件。而虚拟定时器和概况定时器只在进程运行时更新,所以,时钟中断来临时,只有当前进程的概况定时器得到更新,如果当前进程运行于用户态,则其虚拟定时器也会得到更新。
(7) 文件系统信息
进程可以打开或关闭文件,文件属于
系统资源,Linux内核要对进程使用文件的情况进行记录。task_struct结构中有两个
数据结构用于描述进程与文件相关的信息。其中,fs_struct中描述了两个vFs索引节点 (vFS inode),这两个
索引节点叫做root和pwd,分别指向进程的可执行映像所对应的
根目录 (Home Directory) 和
当前目录或工作目录。file_struct结构记录了过程打开的文件的描述符 (Descrlptor),
(8) 虚拟内存信息
除了内核线程,每个进程都拥有自己的
地址空间 (也叫虚拟空间),用mm_struct来描述。其内存信息如下:
struct mm_struct *mm: 描述进程的地址空间
struct mm_struct *active_mm: 内核线程所借用的地址空间
(9) 页面管理信息
当物理内存不足时,Linux内存管理子系统需要把内存中的部分页面交换到
外存,其交换是以页为单位的。
与对称多处理机相关的域如下:
int has_cpu: 进程当前是否拥有CPU
int processor: 进程当前正在使用的CPU
int lock_depth: 上下文切换时内核锁的深度
(11) 和处理器相关的环境信息
进程作为一个执行环境的综合,当系统调度某个进程执行, 即为该进程建立完整的环境时,处理器的寄存器、
堆栈等是必不可少的。因为不同的处理器对内部寄存器和堆栈的定义不尽相同,所以叫做 “和处理器相关的环境”,也叫做“处理机状态”。当进程暂时停止运行时,处理机状态必须保存在进程的task_struct结构中,当进程被调度重新运行时再从中恢复这些环境,也就是恢复这些寄存器和堆栈的值。
处理机信息的定义形式为struct thread_struct*tss: 任务切换状态
1.2 Linux进程描述符处理
进程是动态实体,其生命周期范围从几毫秒到几个月。 因此,内核必须能同时处理很多进程,并把进程描述符存放在动态内存中。Linux把两个不同的数据结构紧凑地放在一个单独为进程分配的存储区域内:一个是内核态的进程堆栈,另一个是紧挨进程描述符的小数据结构thread_info,叫做线程描述符,这块存储区域的大小通常为8192个
字节 (两个页框)。内核使用alloc_thread_info和free_thread_info宏分配和释放存储thread_info结构和内核的内存区。
1.3 进程链表
进程链表把所有的进程的描述符链接起来。每个task_struct结构都包含一个list_head类型的tasks字段,这个类型 的prev和next字段分别指向前面和后面的task_struct元素。进程链表的头是init_task描述符,它是所谓的0进程(process 0) 或swapper进程的进程描述符。init_task的tasks.prev字段指向链表中最后插入的进程描述符的tasks字段。当内核寻找一个新进程在CPU上运行时, 必须只考虑可运行进程 (即处在TASK_RUNNING状
态的进程)。Linux2.6实现的运行队列建立多个可运行进程链表,每种进程优先权对应一个不同的链表,其目的是让调度程序能在固定的时间内选出 “最佳” 可运行进程,与队列中可运行的进程数无关。每个task_struct描述符包含一个list_head类型的字段run_list。这样,内核就必须为系统中每个运行队列保存大量的数据,不过运行队列的
主要数据结构还是组成运行队列的进程描述符链表,所有这些链表都由一个单独的prio_array_t数据结构来实现。
Windows进程模型
2.1 Windows进程的特点
Windows进程设计的目标是对多种操作系统环境提供支持。不同操作系统环境支持的进程在很多方面都是不同的,包括:
◇ 进程如何命名
◇ 进程中是否提供线程
◇ 进程如何表示
◇ 如何保护进程资源
◇ 进程间的通信和同步使用什么机制
◇ 进程之间如何联系
因此,Windows内核所提供的进程结构和服务是相当简单和通用的,同时允许每个
OS子系统模拟某种特定的进程结构和功能。Windows进程的重要特点如下:
◇ Windows进程作为对象实现
◇ 一个可执行的进程可能含有一个或多个线程
◇ 进程对象和线程对象都具有内置的同步能力
图4显示了进程与它所控制或使用的资源相关联的方式。每个进程都被指定一个安全访问标志,称为进程的基本标志。当用户初次登陆时,Windows会创建一个包括用户安全ID的访问标志。每个由用户创建的进程或代表用户运行的进程都有该访问标志的一个
副本。Windows使用这个标志,使得用户可以访问受保护的对象,或者在系统上和受保护的对象上执行限定功能。访问标志控制该进程是否可以改变它自己的属性,在这种情况下,该进程没有已打开的自身访问标志的
句柄。如果进程试图打开这样的一个句柄,则
安全系统确定是否允许这样做,即确定该进程是否可以改变自己的属性。与进程相关的还有定义当前分派给该进程的虚拟地址空间的一系列块。进程不能直接修改这些结构,而必须依赖于虚拟存储管理器,它为进程提供了内存分配任务。进程还包括一个对象表, 表中有该进程知道的其他对象的句柄。对象中包含的每个线程都有一个句柄。对象句柄即对象
标识符,当一个进程通过名称创建或打开一个对象时,它会接收到一个句柄,此后通过此句柄来访问该对象。对象句柄实际上是一个索引,指向与进程相关的句柄表中的表项。
2.2 Windows进程的组成
从最高抽象层次来看, Windows有以下几个方面组成:
◇ 一个私有的虚拟地址空间
◇ 一个可执行的程序: 定义了
代码和数据,并被
映射到进程的虚拟地址空间
◇ 一个已经打开句柄的列表: 指向各种资源,比如信号量、文件,该进程的所有线程都可访问这些系统资源
◇ 一个被称为访问令牌的安全环境: 标识与该进程关联的用户、 安全组和特权
◇ 一个被称为进程ID的唯一标识
◇ 至少一个执行线程
2.3 Windows进程的关键数据结构
Windows进程的数据结构主要有以下几块:
◇ 执行体进程
块 (EPROCESS, Executive Process Block):执行体进程对象的对象体,包括进程ID、 父进程ID、程序名、
进程优先级、内存管理信息、设备映像等。
◇ 核心进程块 (KPROCESS, Kernel Process Block):内核进程对象的对象体,又称
PCB,包括线程调度时需要的信息,如进程状态、线程时间片等。
◇ 进程环境块 (PEB, Process Environment Block):包括用户态代码需要和修改的信息。
◇ Windows环境子系统核心态部件
win32k.sys为每个进程建立的进程信息数据结构WIN32KPROCESS。
◇ Windows环境子系统进程
csrss (用户态空间) 为每个进程建立的进程信息数据结构。每个Windows进程用一个对象表示。每个进程由许多属性定义,并且封装了它可以执行的许多行为或服务。一个进程在收到相应的消息后将执行一个服务,调用这类服务的唯一方法是给提供该服务的进程对象发送消息。当Windows创建一个进程后,它使用为Windows进程定义的、用做模板的对象类或类型来产生一个新的对象实例。并且在创建对象时,赋予其属性值。下面简单给出进程对象中每个对象属性的定义。
◇ 进程ID: 为操作系统标志该进程的惟一的值
◇
安全描述符: 描述创建了对象、可以访问或使用该对象以及不允许访问该对象的用户ID标志
◇ 基本优先级: 进程中线程的基准执行优先级
◇ 默认处理器关联: 可以运行进程中线程的默认处理器集合
◇ 定额限制: 已分页的和未分页的系统内存的最大值、分页文件空间的最大值、用户进程可以使用处理器时间的最大值
◇ 执行时间: 进程中所有线程已执行的时间总量
◇ I/O计数器: 记录进程中线程已经执行的
I/O操作的数量和类型的变量
◇
VM操作计数器: 记录进程中线程已经执行的虚拟内存操作的数量和类型的变量
◇ 异常/调试
端口: 当进程中的一个线程引发异常时,用于进程管理器发送消息的
进程间通信通道
◇ 退出状态: 进程终止的原因一个Windows进程必须至少包含一个执行线程,该线程可能会创建别的线程。在Windows操作系统中,进程是资源分配的最小单位,而线程则是操作系统调度的最小单位。
Windows线程有六种状态, 以下分别予以介绍:
① 就绪态:可以被调度执行。微内核分派器跟踪所有就绪线程,并按优先级顺序进行调度。
② 备用态:备用线程已经被选择下一次在一个特定的处理器上运行。该线程在这个状态等待,直到那个处理器可用。如果备用线程的优先级足够高,正在那个处理器上运行的线程可能被这个备用线程抢占。否则,该备用线程要等到正在运行的线程被阻塞或结束其时间片。
③ 运行态:一旦微内核处理线程或
进程切换,备用线程将进入运行状态并开始执行,执行过程一直持续到被抢占、时间片期满、被阻塞或终止。在前两种情况下,它将回到就绪态。
④ 等待态:当线程被一个事件 (如阻塞、为了同步自愿等待) 或者一个环境子系统指引它把自身挂起时,该线程进入等待状态。当等待的条件满足时,如果它的所有资源都可用,则线程转到就绪态。
⑤ 过渡态:一个线程在等待后,如果准备好运行但资源不可用时,进入该状态。例如,一个线程的栈被换出存储器。当该资源可用时,线程进入就绪态。
⑥ 终止态:一个线程可以被自己或者被另一个线程终止,或者当它的
父进程终止时终止。一旦完成了清理工作,该线程从系统中移出,或者被执行体保留,供以后重新
初始化。
Windows进程和Linux进程比较
Linux 和 Windows 系统的进程结构都相当复杂。在Linux里,只有进程的概念,但在
WIN32里却还有一个 “线程” 的概念,那么Linux和WIN32在这里究竟有着什么区别呢?在WIN32里,“进程” 是指一个程序,而 “线程” 是一个 “进程”里的一个执行 “线索”。从核心上讲,WIN32的多进程与Linux并无多大的区别,在WIN32里的线程才相当于Linux的进程,是一个实际正在执行的代码。但是,WIN32里同一个进程里各个线程之间是共享数据段的。这才是与Linux的进程最大的不同。在WIN32下,使用
CreateThread函数创建线程,与Linux下创建进程不同,WIN32线程不是从创建处开始运行的,而是由 CreateThread指定一个函数,线程就从那个函数处开始运行。在WIN32中,全局变量是子线程与父线程共享的,这就是与Linux最大的不同之处。从上面的分析可以看出Windows的进程/线程要比Linux复杂。对于多任务系统,共享数据区是必要的,但也是一个容易引起混乱的问题,在WIN32下,线程之间的数据是共享的,一个线程修改过一个变量后,另一个线程却又修改了它,结果引起程序出问题。 但在Linux下,由于变量本来并不共享,而由程序员来显式地指定要共享的数据,使程序变得更清晰与安全。在进程管理及调度方面,Linux要比Windows的开销小。Linux是一个单块式的操作系统,操作系统通常在用户进程的内存空间内进行,可免去发生系统调用时的
进程切换开销。Windows是一个准微内核操作系统,许多功能以单独的进程实现,从而提高了系统的模块化程度,但进程切换上的开销要大一些。