进程间通信的机制,它们最初由AT&T System V.2版本的UNIX引IPC (Inter-Process Communication,进程间通信)机制,或被更常见的称为System V IPC。正如我们所看到的,它们并不是进程间通信的唯一方法,但人们通常把这些特定的机制称为System V IPC。
信号量
当我们编写的程序使用了线程时,不管它是运行在多用户系统上、多进程系统上,还是运行在多用户多进程系统上,我们通常会发现,程序中存在着一部分临界代码,我们需要确保只有一个进程(或一个执行线程)可以进入这个临界代码并拥有对资源独占式的访问权。信号量有着复杂的编程接口,但幸运的是,我们可以很轻松地为自己提供一个更简单的接口,它足够应付大多数信号量编程的问题。
荷兰计算机科学家Edsger Dijkstra提出的信号量概念是在并发编程领域迈出的重要一步。信号量是一个特殊的变量,它只取正整数值,并且程序对其访问都是原子操作。
信号量的一个更正式的定义是:它是一个特殊变量,只允许对它进行等待( wait)和发送信号( signal)这两种操作。因为在Linux编程中,“等待”和“发送信号”都已具有特殊的含义,所以我们将用原先定义的符号来表示这两种操作。
- P(信号量变量):用于等待。
- V(信号量变量):用于发送信号。
这两个字母分别来自于荷兰语单词passeren(传递,就好像位于进入临界区域之前的检查点)和vrijigeven(给予或释放,就好像放弃对临界区域的控制权)。在与信号量关联的内容中,你可能还会看到术语“开”(up)和“关”( down),它们取自开、关信号标志的用法。
信号量的定义
最简单的信号量是只能取值0和1的变量,即二进制信号量。这也是信号量最常见的一种形式。可以取多个正整数值的信号量被称为通用信号量。
PV操作的定义非常简单。假设有一个信号量变量sv,则这两个操作的定义如表所示。
还可以这样看信号量:当临界区域可用时,信号量变量sv的值是true,然后P(sv)操作将它减1使它变为false以表示临界区域正在被使用:当进程离开临界区域时,使用V(sv)操作将它加1,使临界区域再次变为可用。注意,只用一个普通变量进行类似的加减法是不行的,因为在C、C++、C#或几乎任何一个传统的编程语言中,都没有一个原子操作可以满足检测变量是否为true,如果是再将该变量设置为false的需要。这也是信号量操作如此特殊的原因。
一个理论性的例子
我们用一个简单的理论性的例子来说明其工作原理。假设有两个进程proc1和proc2,这两个进程都需要在其执行过程中的某一时刻对一个数据库进行独占式的访问。我们定义一个二进制信号量sv,该变量的初始值为1,两个进程都可以访问它。要想对代码中的临界区域进行访问,这两个进程都需要执行相同的处理步骤,事实上,这两个进程可以只是同一个程序的两个不同执行实例。
两个进程共享信号量变量sv。一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区域。而第二个进程将被阻止进入临界区域,因为当它试图执行P(sv)操作时,它会被挂起以等待第一个进程离开临界区域并执行v(sv)操作释放信号量。
需要的伪代码对两个进程都是相同的,如下所示:
semaphore sv = 1;
loop forever
{
P(sv) ;
critical code section ;
V(sv) ;
noncritical code section;
}
这段代码相当简单,这是因为PV操作的功能非常强大。下图显示了Pv操作是如何把守代码中的临界区域的。
Linux的信号量机制
现在,我们已了解了信号量的含义及其工作原理,接下来我们来看看,在Linux系统中是如何实现这些功能的。Linux系统中的信号量接口经过了精心设计,它提供了比通常所需更多的机制。所有的Linux信号量函数都是针对成组的通用信号量进行操作,而不是只针对一个二进制信号量。乍看起来,这好像把事情弄得更复杂了,但在一个进程需要锁定多个资源的复杂情况中,这种能够对一组信号量进行操作的能力是一个巨大的优势。在本章中,我们将集中讨论单个信号量的使用,因为在绝大多数情况下,使用它就足够了。
信号量函数的定义如下所示:
#include <sys/sem.h>
int semctl(int sem_id,int sem_num,int command,...);
int semget(key_t key,int num_sems,int sem_flags);
int semop(int sem_id,struct sembuf *sem_ops, size_t num_sem_ops);
头文件sys/sem.h通常依赖于另两个头文件sys/types.h和sys/ipc.h。一般情况下,它们都会被sys/sem.h自动包含,因此不需要为它们明确添加相应的#include语句。
在逐个介绍这些函数时,请记住,这些函数都是用来对成组的信号量值进行操作的。这使得,对它们的操作要比单个信号量所需要的操作复杂得多。
参数key的作用很像一个文件名,它代表程序可能要使用的某个资源,如果多个程序使用相同的key值,它将负责协调工作。与此类似,由semget函数返回的并用在其他共享内存函数中的标识符