进程间通信
进程间通信
在进程间通信(IPC)所使用的方法同样可用于线程间通信
1、竞争条件
又两个或者多个进程同时读写某些共享数据的时候,而最后的结果取决于进行运行的精确时序称之为竞争条件,例如前面所提到的buffer写入的问题。
2、临界区
但凡涉及到共享内存、共享文件、共享任何资源的时候都会出现竞争条件,我们把对共享内存进行访问的程序片段称之为临界区。
对于一个好的解决临界区问题的方案应当:
1、任何两个进程不能同时位于临界区
2、不应该对CPU的速度和数量做出假设
3、临界区外的进程不得阻塞其它想进入临界区的进程
4、不得使进程无限期等待进入临界区
3、忙等待互斥的方法
3.1、屏蔽中断
在进程进程进入临界区的时候可以立马屏蔽所有中断,这样CPU就不会由于时钟中断或者其他中断而切换到其他进程了。
缺点:
1、屏蔽所有中断是不安全的行为,因为如果在本进程的临界区奔溃,整个系统就完了。
2、在多核处理器的时候屏蔽中断仅仅能够屏蔽本CPU的中断,其它的CPU照样可以更改共享区域的内容。
3.2、锁变量
可以为共享区域设置一个标志位即锁变量来指示是否有进程占据共享资源,但是这个变量本身据存在竞争问题。
3.3 、严格轮换法
将锁变量改为自旋锁变量来进行处理,即进程0仅仅在bit为0时才进入临界区,出临界区的时候将权限交给进行1,即将bit设为1,进程1在bit为1才会执行,并且在出临界区的时候将权限交给进程0,但是,在进程0第二次想进去临界区的时候(此时bit为1 )却是无法进入的,因为权限交给进程1了。此时就违反了评判标准3.
3.4、peterson解法
#define FALSE 0 #define TRUE 1 #define N 2 // 进程数量 int share; // 公共标记 int own[N]; // 线程自维护数据,初始化为FALSE void enter_region( int threadId ) // 线程号,取值为0,1 { int other = 1 - threadId; // 代表另外的那个线程的号 own[threadId] = TURE; // 设置当前线程的意向值为TURE share = threadId; // 设置标志位 while( share == threadId && own[other] == TURE ); // 当互斥条件不满足即while里面的条件满足时忙等待 } void leave_region( int threadId ) { own[threadId] = FALSE; // 设置意向值表示不感兴趣 }
使用peterson算法能够在纯软件层面上实现互斥操作,其中share这个全局变量的作用很是晦涩,我觉得作用应当是:假设没有share,在0号进程在进入临界区后会将own[0]=1,但是仅仅执行到while(还未执行),此时由于操作系统调用1号线程own[1],但是有由于own[0]=1,因此线程1处于忙等待,当操作系统再将CPU切回到0号线程会发现own[1]也为1,0号线程处于忙等待。。。此时整个系统就卡死了,但是加入share后再0号进程第二次获得所有权的时候会发现share!=threadId,此时0号线程不会忙等待,打破了刚刚两个
进程都处于忙等待的局面。
上述的方法在当前进程占用共享空间的时候,当另一个进程访问该共享空间的时候会处于等待状态,这会导致CPU一直得不到利用造成浪费,同样优先级也会存在问题。那么为了避免这个问题可以将忙等待while转为阻塞,这样操作系统就可以将CPU分配给其
他线程。
3.5 生产者消费者问题
1 semaphore mutex = 1; 2 semaphore fillCount = 0; 3 semaphore emptyCount = BUFFER_SIZE; 4 5 procedure producer() { 6 while (true) { 7 item = produceItem(); 8 down(emptyCount); 9 down(mutex); 10 putItemIntoBuffer(item); 11 up(mutex); 12 up(fillCount); 13 } 14 } 15 procedure consumer() { 16 while (true) { 17 down(fillCount); 18 down(mutex); 19 item = removeItemFromBuffer(); 20 up(mutex); 21 up(emptyCount); 22 consumeItem(item); 23 } 24 }
代码源于维基百科https://zh.wikipedia.org/wiki/%E7%94%9F%E4%BA%A7%E8%80%85%E6%B6%88%E8%B4%B9%E8%80%85%E9%97%AE%E9%A2%98。
其中fillCount表示缓冲区已有文件的数量,emptyCount代表可放置的文件的数量,mutex为二元信号量以实现互斥,有关顺序的问题,为什么需要先down,原因的需要先确认又可用的资源空间才能够进行真实的插入或者消费活动,同时down和up必须是原子操作。如果不需要信号量的计数能力,就可以使用互斥量来进行加锁和解锁 。
但是如果在内核中使用互斥信号量进行多线程处理的时候会造成大量的时间浪费,即无论当前的线程是否满足就绪状态,都会注册到内核中,但是实际上可以通过先判断该线程是否满足就绪状态来判断是否注册到内核中,当可以直接执行的时候直接在用户空间进行执行,当无法执行需要阻塞的时候将这个线程注册到内核中进行阻塞,这就体现了之前的混合模式的思想。
在互斥性信号量中使用该思想的就是futex,其在共享空间中设置一个互斥信号量,当前的线程检测到该信号量所指的空间可用的时候,便不陷入内核而直接使用共享资源,当检测到该信号量所指的资源不可用的时候即陷入内核,该进程或者线程被阻塞,由此看来陷入内核的最大好处在于消除忙等待的过程,使得需要忙等待的线程拥有同其进程一样的被调度的权利,但是这是要付出代价的。pthread提供了大量应用上述思想的函数,在此不介绍了 。
4、管程
管程:由变量、过程以及数据结构组成的数据集合
管程的一个重要特性是在任意时刻管程中只能存在有一个活跃的进程,何为活跃的进程,也就是说在某进程执行管程的代码中并未被挂起,仅仅是因为被系统剥夺了CPU,此时就称管程中存在活跃进程,注意管程是不具备原子性的,管程一般由条件变量和两个相关操作:wait和signal构成。
下面 BlockingCondition就是条件变量,其主要作用是申明当前进程阻塞与什么相关,在push代码中当size=capacity 的时候会使用wait,wait主要的作用是将当前的进程阻塞并且阻塞的原因与条件变量theStackIsNotFull相关,该变量维持一个与其相关的阻塞进程表。
signal用于激活与其后接的变量的阻塞进程表中的第一项如下signal theStackIsNotEmpty表示激活与条件变量theStackIsNotEmpty相关的阻塞进程。
monitor class SharedStack { private const capacity := 10 private int[capacity] A private int size := 0 invariant 0 <= size and size <= capacity private BlockingCondition theStackIsNotEmpty /* associated with 0 < size and size <= capacity */ private BlockingCondition theStackIsNotFull /* associated with 0 <= size and size < capacity */ public method push(int value) { if size = capacity then wait theStackIsNotFull assert 0 <= size and size < capacity A[size] := value ; size := size + 1 assert 0 < size and size <= capacity signal theStackIsNotEmpty and return } public method int pop() { if size = 0 then wait theStackIsNotEmpty assert 0 < size and size <= capacity size := size - 1 ; assert 0 <= size and size < capacity signal theStackIsNotFull and return A[size] } }
5、消息传递
在共享的内存中可用使用信号量或者管程的技术,但是在两个进程间通信不共享内存时其便不可用,此时便使用消息传递,主要由两个原语构成:send(destination,&message),receive(source,&message)。其中调用send表示向固定目标发送一条消息,接受方需要回应该消息,好像TCP协议,receice表示从固定的源接受消息,如果没有消息可用,接收者见会被阻塞,或者带错误码返回。不做详细的介绍,去看计算机网络吧。