进程概念:批处理系统执行作业,分时系统执行用户程序或任务。每个作业或任务在很多方面均相似,称为进程。
进程与程序:进程不只是程序或代码段。程序是代码段,是被动实体,而进程是活动实体,拥有资源和指明下个命令的程序计数器。当一个程序被加载到内存中执行,程序就成为进程。
进程状态:新的(new):进程正在创建。运行(running):程序指令正在执行。等待(waiting):进程正在等待某个事件发生。就绪(ready):进程正在等待分配处理器。终止(terminated):进程完成执行。一次只能有一个进程在处理器上运行,但是有很多进程可处于等待或就绪状态。
进程控制块(process control block):用于描述进程。包含如:进程状态,程序计数器,cpu寄存器,cpu调度信息,内存管理信息和记账信息以及io状态信息等参数。pcb用于储存描述进程的这些信息。
进程表示:假设当前进程pid,那么创建它的进程称为其父进程,它创建的进程称为其子进程,它的父进程所创建的其他进程称为其兄弟进程。
进程调度器:进程调度采用进程调度器的方式,每次进程调度器都选择一个可用进程到cpu上执行。
调度队列:进程进入系统时,通常被加入调度队列中。调度队列分为就绪队列:包括驻留在内存中的进程,就绪的和等待运行的进程。通常采用链表实现。及设备队列:所有需要对IO进行操作的进程。
调度方式:进程进入系统时,被放入就绪队列,直到被选中。当进程被分配到cpu执行时,可能有以下事件:1.进程可能发出io请求,并被放入io队列。2.进程创建子进程,并等待子进程终止。3.进程由于中断被强制释放cpu,重新回到就绪队列。
进程调度算法:
1.短期调度程序:短期调度程序经常(高频率)为cpu选择新的进程,因此短期调度程序必须快速,即选择下一个执行进程所花费的调度时间要远远小于进程所花费的时间才可以。
2.长期调度程序:长期调度程序并不频繁,因此长期调度程序决定了多道程序程度(即内存中的进程数量)。
根据进程的io调用情况和cpu使用情况可以分为 io密集型进程:执行io要比执行计算花费更多的时间。cpu密集进程:执行大量计算,而很少产生io请求
如果进程都是io密集型进程,那么就绪队列几乎总是为空,短时调度程序没什么可做的。同理,如果进程都是cpu密集型进程,那么io队列几乎为空,长期调度程序基本没什么可做的。
3.中期调度程序:中期调度程序的核心思想是,进程可以从内存(或cpu竞争中)移出,从而降低多道程序复杂度,之后内存可以被重新调入内存,并从中断处继续执行。通过中期调度程序,进程可以换出和换入。
上下文切换:当中断发生时,系统需要保存当前在cpu上执行进程的上下文,以便中断结束后继续执行当前进程。上下文也采用pcb表述,包括cpu寄存器的值,进程状态,内存管理信息等。先状态保存,再状态切换。切换cpu到一个进行需要保存当前状态并恢复另一个进程的状态,这个任务称为上下文切换。上下文切换是纯粹的时间消耗,因为此时cpu不能执行其他操作。
进程树:创建进程称为父进程,每个被创建的进程还可以再创建进程,从而形成进程树。
进程标识符:对进程的识别通常采用唯一的进程标识符(process id,即pid)。除了向子进程提供物理和逻辑资源外,父进程也可以向子进程传递初始化数据。
创建子进程时的两种可能:1.父进程与子进程并发执行2.父进程等待,直到所有子进程终止。
fork()创建新进程,返回值为创建的子进程的pid。
wait()等待子进程终止,exec()将继承父进程的内存,因此可以采用exec()来进行父子进程间的通讯。
exit()终止自身进程。当需要终止其他进程时,需要提供其他进程的pid。
级联终止:有些系统不允许子进程在没有父进程的情况下存在,即如果一个父进程终止(正常或不正常),那么它的所有子进程都会被终止。
当一个进程使用exit()终止时,操作系统会释放其资源。但是它的进程标识符和位于进程表中的条目还在,知道它的父进程调用了wait()。因为进程表中包含进程退出时的状态。
僵尸进程:进程已经终止,而其父进程还未调用wait(),此时该进程称为僵尸进程。一般而言僵尸进程是短暂存在的。
孤儿进程:父进程没有调用wait()就终止了,此时子进程在进程表中的条目无法被删除,进程标识符也无法被释放。linux的做法是,将init进程作为其父进程,这样所有孤儿进程都成为init进程的子进程,而init进程定期调用wait()将孤儿进程的进程标识符释放,并删除其在进程表中的条目。
在进程间开辟一块内存区,需要通信的进程都可以访问。缓冲区的类型可以是无界缓冲区(不限制大小),这样生产者总是可以生产新的项,而消费者不得不等待生产者生产出新的项。或有界缓冲区(限制固定大小的缓冲区),如果缓冲区空,那么消费者必须等待生产者生产新的项,如果缓冲区满,那么生产者必须等待消费者消费。
在p和q之间传递消息,需要在p和q之间构建通讯链路,方法有:1.直接或间接的通讯 2.同步或异步的通讯 3.自动或显式的缓冲
构建链路的方式:命名,分为直接通信和间接通信
直接通信:即链路针对p和q,p和q可以是pid。寻址可以是对称的,即发送时必须指定接收方,接收时必须指定发送方。或者不对称的,发送时指定接收方,接收时不指定发送方。缺点是:当更改进程的标识符后,可能需要分析所有其他进程。
间接通信:通过邮箱或端口来发送和接收消息。进程可以向其中存放消息,也可以删除消息。
同步:以上命名完成了通信原语send()和receive()。而实现这些原语有不同的设计方案。消息传输可以使阻塞的或非阻塞的,又称为同步的或非同步的。
阻塞发送:发送进程阻塞,直到消息被接收。
非阻塞发送:发送进程发送消息后,不关心消息的接收情况,恢复操作,可以继续发送。
阻塞接收:接收进程阻塞,直到收到信息。
非阻塞接收:收到有效消息或空消息。
对于生产者-消费者模型,生产者调用阻塞发送,而消费者调用阻塞接收即可。
缓存:
无论采用直接通信还是间接通信,通信进程中的消息总是驻留在临时队列中。队列有三种方法:
1.零容量:链路中不能有任何消息处于等待。因此发送者应当阻塞。
2.有限容量:队列长度为n,若发送消息时,队列未满,则消息放入队列中,发送者可继续发送,无需阻塞。否则,发送者应当阻塞,直到队列中有可用空间。
3.无限容量:队列不限长度。此时发送者不阻塞。
零容量称为无缓冲的消息系统,有限容量和无限容量称为自动缓冲的消息系统。
邮箱模型:Mach操作系统实例
Mach的进程间通信,注意通过消息传输模型实现的,消息的收发,采用邮箱(Mach称之为端口)。在创建一个任务时,同时创建了两个邮箱:内核邮箱和通知邮箱。内核通过内核邮箱与任务通信,将消息放到通知邮箱中。
当进程分别位于网络中的不同计算机上时,也可以采用和进程间通信相同的方法。
套接字作为通信的端点,通过网络通信的每对进程需要使用一对套接字。套接字由ip地址和端口号组成。通常采用客户-服务器模型,服务器监听指定端口,等待客户请求。建立连接后,客户通过普通的io语句从套接字进行读取,收到服务器的回复后,客户关闭端口并退出。
优点:常用,高效。缺点:属于分布式进程之间一种低级形式的通信,因为只能传输没有结构的字节流。客户机和服务器程序需要自己加上数据结构。
RPC的过程:请求者将具有明确结构的消息传到rpc服务,rpc服务监听远程系统的端口号,这个消息用于指定:执行函数的标识符以及传输给函数的参数,然后函数进行执行,并将结果通过另一个消息返回请求者。
外部数据表示:通过RPC,用户可以调用位于远程主机上的进程,就像调用本地进程一样。但是由于服务器和本地机器可能有不同的数据表示,如大端服务器和小端客户机,因此需要统一数据。RPC使用的方式是外部数据表示XDR(eXternal Data Representation),独立于机器的数据表示,发送时,机器将数据打包为XDR发送至rpc,服务器通过RPC接收到XDR后,将XDR解包为本地机器数据。
大端(大端结尾big-endian),数据高位位于地址低位。如0x123456,12放置在地址0,34放在地址1,56放在地址2。即地址位从左向右变大,而数据位从左向右减小。
小端(小端结尾little-endian),与大端结尾相反。数据位的增减与地址位的增减同方向,即数据高位位于地址高位,数据低位位于地址低位。
确保RPC的准确性:为了防止rpc执行失败或者多次重复执行,需要保证rpc正好执行一次。
1.保证RPC最多执行一次。通过增加时间戳,可以使rpc最多执行一次
2.通过服务器端发送收到确认(ACK),保证rpc正好执行一次。
确保rpc的可行性:客户机应可以得知服务器上的端口
1.绑定消息按固定端口预先确定。但是这样一旦程序编译后,就无法改变端口。
2.通过交会机制动态进行。约定一个固定端口提供交会服务程序(或月老程序),这样客户端只需要发送包含rpc名称的消息到交会程序,交会程序会返回这个名称对应的端口。
RPC方案通常可以用于远程文件系统。
管道运行两个进程进行通信。管道是早期unix系统提供的一种IPC机制,为进程间通信提供了较为简单的方式,但是也有一定的局限性。
管道的四个属性:1.是否单双向通信 2.半双工or全双工 3.是否父子关系 4.是否网络通信
普通管道:允许两个进程进行标准的生产者-消费者模型通信:生产者向管道一方写入数据,消费者从管道另一方读取数据。因此普通管道是单向的,也就不存在双工的类型,如果要双向通信,必须使用两个管道。通过普通管道进行通信的进程必须具有父子关系。一旦进程完成通信且终止了,那么普通管道就不存在了。因此普通管道可以看作是进程的附属物。
在UNIX上,普通管道叫做pipe,通过pipe(int fd[])来创建,以文件描述符fd来访问。fd[0]为读出端,fd[1]为写入端。访问管道可以通过普通的read()和write()函数来进行。在windows下,普通管道被称为匿名管道。对于普通管道,UNIX和Windows下的性质相同。
访问:普通管道只能有创建进程访问,但是由于子进程继承了父进程打开文件,因此也就继承了管道。父进程创建管道,关闭不使用的一端,创建子进程,子进程关闭管道不使用的一端。这样可以使用管道在父子进程间单向通信(父子均可作为生产者或消费者,只要把对应不使用的一端关闭即可)。
普通管道的数据尾:由于管道的单向通信,因此要保证当管道写入者写入end-of-file时,管道读取端能检测到。
命名管道:与普通管道最大的区别在于,命名管道独立于某个进程存在,通信可以是双向的,因为有名字,所以不具有父子关系的多个进程可以通过命名管道进行通信,且通信可以是双向。
对于UNIX,命名管道被称为FIFO(按照特点先入先出命名),FIFO被创建后表现为文件系统的典型文件,会一直存在,直到被显式的从文件系统中删除。只支持字节流的数据。但是只需要半双工,如果要同时双向通信,必须使用两个FIFO。此外,通信的进程必须位于一台机器上,如果要进行不同系统间的通信,要使用套接字。管道的命令符为|,如ls | more,在ls和more之间建立了生产者-消费者关系,将ls的输出作为more的输入。创建mkfifo(),打开open(),读read(),写write(),关闭close()。
对于Windows,命名管道可以全双工,可以位于不同计算机上,可以支持字节流或消息流。