进程管理
进程概念
进程包括:
程序代码:有时候被称为文本段
当前活动:通过程序计数器的值和处理器寄存器的内容来表示
进程栈:包括临时数据,如函数参数,返回地址和局部变量
数据段:包括全局变量
堆:在运行期间动态分配的内存
内存中的进程结构如下图所示:
进程状态
新建:进程正在被创建
运行:指令正在被执行
等待:进程等待某个事件的发生(如I/O完成或收到信号)
就绪:进程等待分配处理器
-
终止:进程完成执行
进程状态图:
在任何一个处理器上一次只能有一个进程运行,但是许多进程可以处于就绪或者等待状态。
进程控制块
每个进程在操作系统内用进程控制块(PCB)来表示。如下图:
- 进程状态:状态可以为新建,就绪,运行,等待,终止等。
- 程序计数器:计数器表示进程要执行的下一条指令的地址。
- CPU寄存器:根据计算机体系结构的不同,寄存器的数量和类型也不同。包括累加器,索引寄存器,堆栈指针,通用寄存器和其他条件码信息寄存器。与程序计数器一起,这些状态信息在出现中断时也需要保存。
- CPU调度信息:这类信息包括进程优先级,调度队列的指针和其他调度参数。
- 内存管理信息:根据操作系统所使用的内存系统,这类信息包括基址和界限寄存器的值,页表或段表。
- 记账信息:这类信息包括CPU时间,实际使用时间,时间界限,记账数据,作业或进程数量
- I/O状态信息:这类信息包括分配给进程的I/O设备列表,打开文件的列表等
一个进程可能包含多个线程。
CPU在进程间的切换如下图所示:
进程调度
目的
多道程序设计的目的是无论何时都有进程在运行,从而使CPU的利用率达到最大。分时系统的目的是在进程之间频繁切换CPU,一遍用户在程序运行时能够与其交互。为了达到此目的,进程调度程序选择一个可用的进程到CPU上执行。
调度队列
进程进入系统时,会被加到作业队列中,该队列包括系统中的所有进程。
驻留在内存中就绪和等待运行的进程保存在就绪队列中。该队列通常用链表来存储,其头结点指向链表的第一个和最后一个PCB的指针。每个PCB包括一个指向就绪队列的下一个PCB的指针域。
假设进程向一个共享设备(如磁盘)发送I/O请求,由于系统有许多进程,磁盘可能会忙于其他进程的I/O请求,因此该进程可能需要等待磁盘。等待特定I/O设备的进程列表称为设备队列。每个设备都有自己的设备队列,如下图:
队列图
讨论进程调度的常用表示方法是队列图,如下图所示。每个长方形框表示一个队列。有两种队列:就绪队列和一组设备队列。圆形表示为队列服务的资源,箭头表示系统内进程的流向。
新进程开始处于就绪队列。它在就绪队列中等待直到被选中执行或分派。当进程分配到CPU并执行时,可能发生下面几种事件中的一种:
- 进程可能发出一个I/O请求,并被放到I/O队列中。
- 进程可能创建一个新的子进程,并等待其结束
- 进程可能会由于中断而强制释放CPU,并被放回到就绪队列。
对于前面两种情况,进程最终从等待状态切换到就绪状态,并被放回到就绪队列。进程继续这一循环直到终止。到时它将从所有队列中删除,其PCB和资源得以释放。
调度程序
长期调度程序
进程被放到大容量存储设备上(通常为磁盘)的缓冲池中,保存在那里以便于后来执行。长期调度程序从该池中选择进程,并装入内存以准备执行。
长期调度程序控制多道程序设计的程度。
短期调度程序
短期调度程序从准备执行的进程中选择进程,并为之分配CPU时间。
这两个调度程序的主要差别是他们执行的频率。短期调度程序必须频繁地为CPU选择新进程,而长期调度程序执行的并不频繁。
I/O绑定的进程&CPU绑定的进程
I/O绑定的进程在执行I/O方面比执行计算花费更多时间
CPU绑定的进程很少产生I/P请求,与I/O绑定的进程相比,将跟多时间用于执行计算上。
如果所有进程是I/O绑定的,那么多就绪队列几乎为空,从而短期调度程序没有什么事情可做。如果所有进程是CPU绑定的,那么多I/O等待队列几乎为空,从而设备并没有得到使用,因此系统会不平衡。为了达到最好的性能,长期调度程序需要选择一个合理的I/O绑定和CPU绑定的组合。
上下文切换
当发生一个中断时,系统需要保存当前运行在CPU中的进程的上下文,以便其处理完能够恢复上下文。
进程的上下文由进程的PCB表示,包括CPU寄存器的值,进程状态和内存管理信息等。
通常,通过执行一个状态保存来保存CPU的当前状态,之后执行一个状态恢复重新开始运行。
进程操作
进程创建
进程在其执行过程中,能够通过创建进程系统调用创建多个新进程。创建进程称为父进程,而新进程称为子进程。这些新进程中的每一个可以再创建其他进程,从而形成进程树。
大多数操作系统,根据唯一一个进程标识符(pid)来识别进程,它通常是一个整数值。
标识符为0的进程:sched。sched进程还生成init进程,它作为所有用户进程的根进程。
当一个进程创建新进程时,有两种执行可能:
- 父进程与子进程并发执行
- 父进程等待,直到某个或全部子进程执行完毕
新进程的地址空间也有两种可能:
- 子进程是父进程的副本(具有与父进程相同的程序和数据)
- 子进程装入另一个新程序
举例说明其差别:
通过fork()系统调用,可以创建新进程。新进程由原来进程的地址空间的副本组成。两个进程(父进程和子进程)都继续执行位于系统调用fork()之后的指令,但有一点不同:对于新(子)进程,系统调用fork()的返回值为0,而对于父进程,返回值为子进程的进程标识符(非零)。
通常,在系统调用fork()之后,一个进程会使用系统调用exec(),以新程序来取代进程的系统空间。系统调用exec()将二进制文件装入内存并开始执行。如果父进程在子进程运行时没有什么事情可做,那么它采用系统调用wait()把自己移出就绪队列来等待子进程的终止。当子进程完成时(通过显式或隐式调用exit()),父进程会从wait()调用处开始继续,并调用exit()以表示结束。
进程终止
当进程完成执行最后的语句并使用系统调用exit()请求操作系统删除自身时,进程终止。
在其他的情况下也会出现终止:父进程通过适当的系统调用能终止子进程。父进程需要知道其子进程的标识符。
有些系统,不允许子进程在父进程已经终止的情况下存在。对于这类操作系统,如果一个进程终止,那么它的所有子进程也必须终止。这种现象称之为级联终止。
进程间通信
与其他进程共享数据的进程称为协作进程。
协作进程需要一种进程间通信机制来允许进程之间相互交换数据与信息。
进程间通信有两种基本模型:共享内存和消息传递
消息传递对于交换较少量的数据很有用,更易于实现。共享内存比消息传递快。
共享内存系统
采用共享内存的进程间通信需要通信进程建立共享内存区域。
通常,一块共享内存区域驻留在生成共享内存段进程的地址空间。其他希望使用这个共享内存段进行通信的进程必须将此放到他们自己的地址空间中。
通常操作系统试图阻止一个进程访问另一个进程的内存。共享内存需要两个或者更多的进程取消这个限制,它们通过在共享区域内读或者写来交换信息。
生产者-消费者问题
为了说明协作进程这一概念,下面来研究一下生产者-消费者问题,这是协作进程的通用范例。
生产者进程产生信息以供消费者进程消费。
解决生产者-消费者问题的方法之一是采用共享内存。为了允许生产者进程和消费者进程能够并发执行,必须有一个缓冲区来被生产者填充并被消费者所使用。当生产者产生一项时,消费者使用另一项。生产者和消费者必须同步,以使消费者不会试图消费一个还没有生产出来的项。
可以使用两种类型的缓冲区:无限缓冲区:对缓冲区大小没有限制,消费者可能不得不等待新的项,但生产者总是可以产生新项。有限缓冲区:假设缓冲区大小固定。对于这种情况,如果缓冲区为空,那么消费者必须等待;并且如果缓冲区已满,那么生产者必须等待。
共享缓冲是通过循环数组和两个逻辑指针:in和out来实现的。变量in指向缓冲区的下一个空位,out指向缓冲区的第一个满位。count变量表示的是目前缓冲区的项目数。当count == 0时,缓冲为空;当count == BUFFER_SIZE的时候,缓冲为满。注意,生产者和消费者不能使用缓冲的时候,都有可能被阻塞到while循环中。
消息传递系统
消息传递提供一种机制以允许进程不必通过共享地址空间来实现通信和同步。这在分布式环境中特别有用。
消息传递工具提供至少两种操作:发送(消息)和接受(消息)。
如下是一些逻辑实现链路和send()/receive()操作的方法
命名
需要通信的进程必须有一个方法以互相引用。他们可以使用直接或者间接通信。
对于直接通信,需要通信的每个进程必须明确地命名通信的接收者或发送者。
这种方案,一个通信链路只与两个进程相关,且每对进程之间只有一个通信链路。可以采用对称寻址或非对称寻址。
在间接通信中,通过邮箱或端口来发送和接收消息。邮箱可以抽象成一个对象,进程可以向其中存放消息,也可以从中删除消息。每个邮箱都有一个唯一的标识符。对于这种方案,一个进程可能通过许多不同的邮箱与其他进程通信。但两个进程仅在共享至少一个邮箱时才能相互通信。原语send()和receive()定义如下:
- send(A, message):发送一个消息到邮箱A
- receive(A, message): 接受来自邮箱A的消息
对于这种方案,通信链路具有如下属性:
- 只有在两个进程共享一个邮箱时,才能建立通信链路
- 一个链路可以与两个或更多的进程相关联
- 两个通信进程之间可以有多条不同的链路,每条链路对应于一个邮箱。
现在假设进程P1,P2,P3都共享邮箱A。进程P1发送一个消息到A,而进程P2和P3都对A执行receive()。哪个进程能收到P1所发送的消息呢?答案取决于所选择的方案:
- 允许一个链路最多只能与两个进程相关联。
- 允许一次最多一个进程执行receive()操作
- 允许系统随意选择一个进程以接受消息(即进程P2和P3两者之一都可以接受消息,但不能两个都接收)。系统同样可以定义一个算法来选择哪个进程是接收者。
邮箱可以为进程或操作系统所有。如果邮箱为进程所有(即邮箱时进程地址空间的一部分),那么需要区分拥有者(只能通过邮箱接收消息)和使用者(只能向邮箱发送消息)。当拥有邮箱的进程终止时,邮箱消失。任何后来向该邮箱发送消息的进程,都会被通知邮箱不再存在。
与此相反,操作系统所拥有的邮箱是独立存在的,并不属于某个特定的进程。因此,操作系统必须提供机制以允许进程进行如下操作:
- 创建新邮箱
- 通过邮箱发送和接受消息
- 删除邮箱
创建新邮箱的进程默认为邮箱的拥有者。开始时,拥有者时唯一能够通过该邮箱接收消息的进程。不过,通过系统调用,拥有权和接收特权可能传递给其他进程。当然,该规定会导致每个邮箱有多个接收者。
同步
进程间的同学可以通过调用原语send()和receive()来进行。这些原语的实现有不同的设计选项。消息传递可以是阻塞或非阻塞。——也称为同步和异步。
- 阻塞发送:发送进程阻塞,直到消息被接受进程或邮箱所接收。
- 非阻塞发送:发送进程发送消息并继续操作
- 阻塞接收:接收者阻塞,直到有消息可用
- 非阻塞接收:接收者收到一个有效消息或者空消息。
send()和receive()的不同组合是有可能的。
缓冲
不管通信是直接的还是间接的,通信进程所交换的消息都驻留在临时队列中。队列的实现基本上有三种方法:
- 零容量:队列的最大长度为0;因此,链路中不能有任何消息处于等待。对于这种情况必须阻塞发送者,直到接收者接收到消息。
- 有限容量:队列具有有限的长度n;因此,最多只能有n条消息驻留其中。如果在发送新消息时队列未满,那么该消息可以放在队列中(或者复制消息,或者保存消息的指针),且发送者可以继续执行而不必等待。不过,链路只有有限容量。如果链路满,必须阻塞发送者直到队列中的空间可用为止。
- 无限容量:队列长度可以无限,因此不管多少消息都可以在其中等待,从不阻塞发送者。
零容量情况称为没有缓冲的消息系统,其他情况称为自动缓冲。
客户机-服务器通信
前面介绍了如何利用共享内存和消息传递进行通信。这些结束也可以用户客户机—服务器系统的通信。在本节中,将要探讨三种其他的客户机-服务器通信方法:套接字,远程过程调用(RPC)和Java的远程方法调用(RMI)。
套接字
套接字(socket)可以定义为通信的端点。一对通过网络通信的进程需要使用一对套接字,即每个进程各有一个。套接字由IP地址与一个端口号组成。(例如:表示允许在主机di为121.10.14.56上端口15的套接字表示为121.10.14.56:15)
通常,套接字采用客户机-服务器结构。服务器通过箭筒指定端口来等待进来的客户请求。一旦收到请求,服务器就接受来自客户套接字的链接,从而完成链接。服务器实现的特定服务(如Telnet,FTP和HTTP)是通过监听众所周知的端口进行的。所有低于1024的服务器端口都被认为是众所周知的,可以用它们来实现标准服务。
例如,如果IP地址为146.85.5.20的主机X的客户希望与渎职161.25.19.8的web服务器(监听端口80)建立链接,它会被赋予一个大于1024的任意数的端口,可能被赋予端口1625。该连接由一对套接字组成:主机X上的(146.86.5.20:1625)和web服务器上的(161.25.19.8:80)。根据目的端口,在主机间传输的数据包可分送给合适的进程。
所有的连接必须唯一。因此,如果主机X的另一个进程希望与同一Web服务器建立另一个连接,那么它会被富裕另一个大于1024但不等于1625的端口号。这确保了所有连接都有唯一的一对套接字。
例子:面向连接的TCP套接字的日期服务器
此操作允许客户机从服务器请求当前的日期和时间。服务器监听端口6013,当然端口号可以是任何大于1024的数字。当接收到连接请求时,服务器返回日期和时间给客户机。
服务器程序创建了ServerSocket对象以监听端口号6013.接着它通过采用accept()方法开始监听端口。服务器阻塞在accept()方法开始监听端口。服务器阻塞在accept()方法上,等待客户请求连接。当接收到连接请求时,accept()会返回一个套接字以供服务器来与客户进程通信。
有关服务器如何与套接字通信的细节如下:首先服务器建立PrintWriter对象,用来与客户机进行通信。PrintWriter对象允许服务器通过普通的输出方法print()和println()来向套接字进行写操作。服务器通过调用方法println()将日期时间发送给客户机。一旦完成了将日期时间写到套接字,服务器就关闭与客户端相连的套接字,并重新监听其他请求。
客户机通过创建套接字和服务器监听的端口相连来与服务器进行通信。客户机创建了socket,并请求与IP为127.0.0.1,端口号为6013的服务器建立连接。一旦建立了连接,客户就通过普通流I/O语句来对套接字进行读。在得到服务器的日期后,客户机关闭端口并退出。
缺点:使用套接字进行通信,虽然常用和高效,但是它属于较为低级的分布式进程通信。原因之一在于套接字只允许在通信进程之间交换无结构的字节流。客户机或服务器程序需要负责加上数据结构。下面两小节将介绍两种更高级的通信方法:远程过程调用(RPC)和远程方法调用(RMI)。
远程过程调用
与IPC消息不同,用于PRC交换的消息有很好的结构,因此不再是数据包。每个消息传递给位于远程系统上监听端口号的RPC服务器,每个都包含要执行的函数名称和传递给函数的参数。该函数根据请求而执行,任何结果通过另一个消息送回给请求者。
远程方法调用
远程方法调用是一个类似于RPC的Java特性。RMI允许线程调用远程对象的方法。
RMI和RPC在两个方面有根本的不同。
- 第一,RPC支持子程序编程,即只能调用远程的子程序或函数。而RMI是基于对象的——它支持调用远程对象的方法。
- 第二,在RPC中远程过程的参数是普通数据结构,而RMI可以将对象作为参数传递给远程方法。RMI通过允许Java程序调用远程对象的方法,使得用户能够开发分布在网络上的Java应用。