1. 进程通信(Process Communication)
进程协作有其根本性的需求,主要是出于四点:信息共享,通过并行提升计算速度,利于模块化以及方便性。因此也衍生出了进程通信(IPC)的需求。下面将概述IPC的相关内容。
进程通信用于进程之间的数据交换,主要有两种抽象模型:其一共享内存模型(Shared memory),其二消息传递模型(Message passing)。
顾名思义,采用共享内存的进程间通信需要建立共享内存区域,它们通过在共享内存区域读或写来交换信息。我们通过生产者—消费者模型对共享内存模型稍作研究。这个模型里,生产者进程产生信息以供消费者进程“消费”。为了能使这两种进程并发执行,必须要有一个缓冲来被生产者填充并为消费者所使用。缓冲可以看作一个队列,生产和消费可以分别看作入队和出队。这里缓冲分为两类,有限缓冲和无限缓冲,二者区别在于后者情况下,生产者无需等待,总是有缓冲区域存放其生产的消息。
对于消息传递模型,则不需要共享地址空间。消息传递工具需要至少提供两种操作:发送消息和接收消息。而这两种操作的关键在于通信线路(Communication Link)。这里我们只分析其逻辑实现,关注以下几个问题。命名:在直接通信中,需要通信的每个进程必须明确的命名通信的接收者或者发送者。这种方案会在二者之间自动建立线路。但是这样限制了进程定义的模块化。而在间接通信中,通过邮箱或者端口来传递消息。同步:消息传递可以是阻塞非阻塞的,也称为同步或者异步。很容易理解,即进程是否会因为send()或者receive()而阻塞。缓冲:通信交换的信息驻留在临时队列里,有三种实现方法:零容量,有限容量,无限容量。关键区别在于队列的大小不同,对发送者的阻塞情况不同,零容量必须阻塞发送,有限容量会在队列满了的时候阻塞发送,无限容量不会阻塞。
通过网络通信的进程需要使用一对sockets作为端点,由一个iP地址和一个端口号组成。Socket一般采用客户端-服务器结构,服务器通过监听指定端口来等待客户请求,收到请求后会自动连接。一般,1024以下的端口用于标准服务。
除此之外,进程通信的方式还有管道(pipe)。管道分为普通管道和命名管道;两者都是半双工的。普通管道只能用于父子进程或兄弟进程间的通信,因为普通管道通过fork调用来拷贝文件描述符的,在文件系统中,普通管道并不对应物理文件。命名管道在文件系统中有物理文件存在,因此可以用于非亲属的进程间通信。
2. 进程同步(Process Synchronization)
系统的不同部分操作资源,自然需要这些变化不相互影响,这是就需要进程同步。
总体来说,进程同步通过临界区来实现,临界区问题必须确保:互斥,前进,有限等待。临界区前后分别由进入区和退出区,其它为剩余区。
临界区问题有两类处理方法:抢占内核和非抢占内核。容易理解,非抢占内核数据从根本上不会导致竞争条件,而抢占内核情况下则复杂得多。但是考虑到后者在响应时间和实时编程的优势,这种复杂值得花费力气解决。这里讨论一些solution proposals。(a)使临界区不可被打断,与非抢占内核类似,进程在临界区内时不允许上下文切换,我们可以通过一个系统调用来实现这个需求。(b)严格轮换,但是忙等会浪费CPU资源。而且轮换如此严格,使连续多次执行某个进程的临界区成为不可能。(c)对严格轮换进行改进得到Peterson’s solution,使用了两个共享数据项int turn和boolean flag。但是同样会导致忙等,而且可能会使进程的优先权错位(d)在硬件上实现互斥锁,同样忙等。
实际最终我们选择的方案是——信号量。信号量是一种数据类型,只能通过两个标准原子操作访问wait()和signal()。信号量通常分为计数信号量和二进制信号量,后者有时称为互斥锁。可以使用二进制信号量处理多进程的临界区问题,而计数信号量可以用来控制访问具有若干实例的某种资源,此时信号量表示可用资源的数量。
当一个进程位于临界区时,其他试图进入临界区的进程必须在进入代码中连续地循环,这种称为自旋锁,会导致忙等。为了克服这一点可以修改wait()和signal()地定义——当一个进程执行wait()需要等待的时候,改为阻塞自己而不是忙等。阻塞操作将此进程放入到与信号量相关的等待队列中,状态改为等待,然后会选择另一个进程来执行。
考虑进程同步时,很重要的一点是避免死锁。死锁的特征包括:互斥、占有并等待、非抢占、循环等待。当死锁发生时,进程永远不能完成,所以必须解决。有三种方法:(1)使用协议确保死锁不发生(2)允许死锁然后检测恢复(3)认为死锁不存在。
此外,进程同步中有三个经典问题,用来检验新的同步方案——生产者消费者问题、读者-写者问题、哲学家进餐问题,可以用来分析中的各种情况包括死锁问题,这里不作具体分析。
3. 进程调度
首先明确,进程执行由CPU执行和I/O等待周期组成,进程在这两种状态之间且切换。因为运算资源CPU是有限的,所以为了提高利用率,在CPU空闲时,必须从就绪队列中选择一个进程执行,具体到如何选择,这就产生了调度。
调度应当满足以下准则:CPU使用率和吞吐量最大化,周转时间、等待时间和响应时间最小化。
稍微概述几种调度算法。
a) 先到先服务算法:即先请求cpu的进程先分配到进程,实现简单,但平均等待时间通常较长。考虑FCFS调度在动态情况下,会产生护航效果,会导致cpu和设备使用率变得很低。
b) 最短作业优先调度:cpu空闲时,它会赋给具有最短cpu区间的进程,SJF算法可证为最佳,其平均等待时间最小。但是困难在于如何知道下一个cpu区间的长度。一种方法是近似SJF调度,可以用以前cpu长度的指数平均来预测下一个区间长度。
c) 优先级调度:每个进程都会有一个优先级,具有最高优先级的进程会被分配到cpu。这种算法的主要问题是无穷阻塞或者饥饿,可以使用老化的方法来处理。
d) 轮转法调度:专门为分时系统设计,与FCFS类似,但是增加了抢占。这里定义了一个时间片,就绪队列作为循环队列,每个进程分配不超过一个时间片的cpu。
e) 多级队列调度:将就绪队列分为多个独立队列,根据进程属性每个队列有自己的调度算法。而且队列之间必须有调度,通常采用固定优先级抢占调度。例如,前台队列可以比后台队列具有绝对的优先级,这样也符合交互的要求。
f) 多级队列反馈调度:与多级队列相比,差异在于允许进程在队列之间移动。
如果在多个处理器的情况下,负载分配成为可能。一种方法是让一个处理器处理所有调度决定,成为非对称多处理。这种方法,数据共享的需要较小。另一种是对称多处理(SMP),每个处理器自我调度。
此外,进程在不同处理器转移时,第一个处理器缓存必须无效,第二个处理器缓存需要重构,这样做代价太高,所以SMP系统尽量避免这样做,试图提高处理器亲和性,努力使一个进程在用同一个处理器上运行。
为了更好的利用多处理器,更重要的一点是负载平衡,负载平衡通常有两种方法:Push migration和Pull migration,篇幅所限,此不赘述。
参考文献:
[1] Abraham Silberschatz. 操作系统概念. 高等教育出版社, 2007.3.