参考:《UNIX网络编程 · 卷2 : 进程间通信》
信号量(semaphore)是一种用于提供不同进程间或一个给定进程的不同线程间同步手段的原语。POSIX 有如下类型的信号量:
Posix 有名信号量:使用 Posix IPC 名字标识,可用于进程或线程间的同步。
Posix 基于内存的信号量(无名信号量):存放在共享内存区中,可用于进程或线程间的同步。
暂时只考虑不同进程间的同步。首先考虑二值信号量:其值或为 0 或为 1 的信号量。如下图:
该信号量是由内核来维护的(这对于 SystemV 信号量是正确的),其值可以是0或1。
Posix 信号量不必在内核中维护。Posix信号量是由可能与文件系统中的路径名对应的名字来标识的。如下图:
尽管 Posix 有名信号量是由可能与文件系统中的路径对应的名字来标识的,但是并不要求它们真正存放在文件系统内的某个文件中。系统可能使用这样的名字来标识信号量,但是真正的信号量值却存放在内核中的某个地方。然而,如果信号量的实现用到了映射文件,那么信号量的真正值确实出现在某个文件中,而该文件是映射到所有让该信号量打开着的进程的地址空间的。
一个进程可以在信号量上执行三种操作:
信号量的 P操作、V操作
P 操作:信号量中的等待(wait)操作还有其他常用名字:Edsger Dijkstra 称它为 P 操作,代表荷兰语单词 Proberen(意思是尝试)。它也称为递减(down,因为信号量的值被减掉1)或上锁 (lock),不过我们使用 Posix 术语等待(wait)。
V 操作:信号量的挂出操作还有其他常用名字:最初称为 V 操作,代表荷兰语单词 Verhogen(意思是增加)。它也称为递增(up,因为信号量的值被加上 1)、解锁(unlock)或发信号(signal)。我们使用 Posix 术语挂出(post)。
二值信号量和计数信号量实现中并没有区别,二值信号量可用于互斥作用,类似于互斥锁(mutex)。
把信号量初始化为 1,sem_wait 调用等待其值变为大于 0,然后将它减 1, sem_post 调用则将其值加 1(从 0 变为 1),然后唤醒阻塞在 sem_wait 调用中等待该信号量的任何线程。
信号量还有一个互斥锁没有提供的特性:互斥锁必须总是由锁住它的线程解锁,信号量的挂出却不必由执行过它的等待操作的同一线程执行。
信号量、互斥锁和条件变量的差异
Posix 标准声称,有了互斥锁和条件变量还提供信号量的原因是:提供一种进程间同步方式。这些进程可能共享也可能不共享内存区。互斥锁和条件变量是作为线程间的同步机制说明的,这些线程总是共享(某个)内存区。这两者都是已广泛使用了多年的同步范式。每组原语都有其特别适合的问题。信号量也可用于线程间,互斥锁和条件变量也可用于进程间。但应该使用适合具体应用的原语。
我们提到过 Posix 提供两类信号量:
有名信号量和基于内存的的信号量(也称为无名信号量)。如下图给出了这两类信号量的函数:
创建一个新的有名信号量或打开一个已存在的有名信号量。有名信号量既可用于线程间的同步,又可用于进程间的同步。其安函数声明如下:
#include /* For O_* constants */
#include /* For mode constants */
#include
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
返回值:
成功:返回指向 sem_t 数据类型信号量的指针,该指针将用作 sem_close、 sem_wait、sem_trywait、sem_post 以及 sem_getvalue 的参数。
出错:则为 SEM_FAILED。
参数:
name:信号量的名字。
oflag:参数可以是是 0、O_CREAT、O_CREAT|O_EXCL,但如果指定了 O_CREAT 标志,那么第三个和第四个参数是需要的。
mode:使用本参数前提是 oflag 指定了 O_CREAT 标志,mode 参数用来指定权限位。
value:指定信号量的初始值。该初始值不能超过 SEM_VALUE_MAX(这个常值必须至少为32767)。二值信号量的初始值通常为 1,计数信号量的初始值则往往大于 1。
若指定 O_CREAT 而没有指定 O_EXCL,只有当所需的信号量尚未存在时才初始化它。不过所需信号量已存在条件下指定 O_CREAT 并不错误。其意思是“如果所需信号量尚未存在,那就创建并初始化它”。但是所需信号量已存在条件下指定 O_CREAT | O_EXCL 却是一个错误。
当打开一个有名信号量时,要求对某个已存在的信号量具有读访问和写访问权限,这样sem_open 才能成功。原因也许是信号量的挂出与等待操作都需要读出并 修改信号量的值。
sem_open 和 sem_close 实例如下:
#include
#include
#include
#include
#include
#include
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
int main(int argc, char **argv)
{
int c, flags;
sem_t *sem;
unsigned int value;
flags = O_RDWR | O_CREAT;
value = 1;
while ((c = getopt(argc, argv, "ei:")) != -1)
{
switch (c)
{
case 'e':
flags |= O_EXCL;
break;
case 'i':
value = atoi(optarg);
break;
}
}
if (optind != argc - 1)
{
printf("usage: semcreate [ -e ] [ -i initalvalue ] " );
exit(1);
}
sem = sem_open(argv[optind], flags, FILE_MODE, value);
sem_close(sem);
exit(0);
}
注意:编译时需要链接 -lpthrea 库,否则报错。
sem_close 将关闭使用 sem_open 创建的信号量指针,函数声明如下:
#include
int sem_close(sem_t *sem);
返回值:
成功则为 0,出错则为 -1。
参数:
sem:使用 sem_open 创建的信号量指针。
一个进程终止时,内核对仍然打开着的所有有名信号量自动执行这样的信号量关闭操作。不论该进程是自愿终止的(如调用exit)还是非自愿地终止的(发送 kill 信号),这种自动关闭都会发生。
关闭一个信号量并没有将它从系统中删除。Posix 有名信号量至少是随内核持续:即使当前没有进程打开着某个信号量,它的值仍然保持。
有名信号量使用 sem_unlink 从系统中删除。
#include
int sem_unlink(const char *name);
每个信号量有一个引用计数器记录当前的打开次数,sem_unlink 类似于文件 I/O 的 unlink 函数:当引用计数还是大于 0 时,name 就能从文件系统中删除,然而其信号量的析构(不同于将它的名字从文件系统中删除)却要等到最后一个引用该信号量的进程 sem_close 发生时为止。
semunlink 实例如下:
#include
#include
#include
#include
#include
#include
int main(int argc, char **argv)
{
if (argc != 2)
{
printf("usage: semunlink " );
exit(1);
}
sem_unlink(argv[1]);
exit(0);
}
sem_wait 函数测试所指定信号量的值,如果该值大于 0,那就将它减 1 并立即返回。如果该值等于0,调用线程就被投入睡眠中,直到该值变为大于 0,这时再将它减 1,函数随后返回。其中“测试并减 1”这两步操作操作必须是原子的。函数原型如下:
#include
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
返回值:成功返回 0,出错则为 -1。
sem_trywait 和 sem_wait 的差别是:当所指定信号量的值已经是 0 时,后者并不将调用 线程投入睡眠。相反,它返回一个 EAGAIN 错误。
如果被某个信号中断,sem_wait 就可能过早地返回,所返回的错误为 EINTR。
sem_wait 实例如下:
#include
#include
#include
#include
#include
#include
int main(int argc, char **argv)
{
sem_t *sem;
int val;
if (argc != 2)
{
printf("usage: semwait " );
exit(1);
}
sem = sem_open(argv[1], 0);
sem_wait(sem);
sem_getvalue(sem, &val);
printf("pid %ld has semaphore, value = %d\n", (long)getpid(), val);
pause();
exit(0);
}
当一个线程使用完某个信号量时,它应该调用 sem_post。把所指定信号量的值加 1,然后唤醒正在等待该信号量值变为正数的任意线程。函数原型如下:
#include
int sem_post(sem_t *sem);
int sem_getvalue(sem_t *sem, int *sval);
返回值:成功返回 0,出错返回 -1。
sem_getvalue 在由 valp 指向的整数中返回所指定信号量的当前值。如果该信号量当前已上锁,那么返回值或为 0,或为某个负数,其绝对值就是等待该信号量解锁的线程数。
互斥锁是为上锁而优化的,条件变量是为等待而优化的,信号量既可用于上 锁,也可用于等待,因而可能导致更多的开销和更高的复杂性。
sem_post 实例如下:
#include
#include
#include
#include
#include
#include
int main(int argc, char **argv)
{
sem_t *sem;
int val;
if (argc != 2)
{
printf("usage: sempost " );
exit(1);
}
sem = sem_open(argv[1], 0);
sem_post(sem);
sem_getvalue(sem, &val);
printf("value = %d\n", val);
exit(0);
}
上面处理的都是有名信号量,这些信号量由一个 name 参数标识,它通常指代文件系统中的某个文件。然而 Posix 也提供基于内存的信号量,它们由应用程序分配信号量的内存空间(也就是分配一个 sem_t 数据类型的内存空间),然后由系统初始化它们的值。
基于内存的信号量是由 sem_init 初始化的。其函数原型如下:
#include
int sem_init(sem_t *sem, int pshared, unsigned int value);
返回值:出错返回 -1,成功并不一定返回 0,Posix.1 注解中说明将来的某个修订版可能指定调用成功时返回0。
参数:
sem:指向应用程序必须分配的 sem_t 变量。
pshared:如果为 0,那么待初始化的信号量是在同一进程的各个线程间共享的,否则该信号量是在进程间共享的。当 pshared 为非零时,该信号量必须存放在某种类型的共享内存区中,而即将使用它的所有进程都要能访问该共享内存区。
value:该信号量的初始值。
在使用完一个基于内存的信号量后,我们需要使用 sem_destory 摧毁它。其函数原型如下:
int sem_destroy(sem_t *sem);
返回值:成功返回 0,出错返回 -1。
sem_open 不需要类似于 shared 的参数,因为有名信号量总是可以在不同进程间共享的。
注意:基于内存的信号量不使用任何类似于 O_CREAT 标志的东西,因为 sem_init 总是初始化信号量的值。因此,对于一个给定的信号量,必须小心保证只调用 sem_init 一次。对一个已初始化过的信号量调用 sem_init,其结果是未定义的。
警告:对于一个基于内存的信号量,只有 sem_init 的 sem 参数指向的位置可用于访问该信号量,使用它的 sem_t 数据类型副本访问时结果将未定义。
基于内存的信号量至少具有随进程的持续性,然而它们真正的持续性却取决于存放信号量的内存区的类型。只要含有某个基于内存信号量的内存区保持有效,该信号量就一直存在。
比如某个基于内存的信号量是由单个进程内的各个线程共享的(sem_init 的 shared 的参数为 0),那么该信号量具有随进程的持续性,当该进程终止时它也消失。如果某个基于内存的信号量是在不同进程间共享的(sem_init 的 shared 参数为 1),那么该信号量必须存放在共享内存区中,因而只要该共享内存区仍然存在,该信号量也就继续存在。
使用内存信号量(无名信号量)实现的一个生产者和消费者实例:
#include
#include
#include
#include
#include
#include
#include
#define NBUFF 10
int nitems;
struct
{
int buff[NBUFF];
sem_t mutex, nempty, nstored;
} shared;
void *produce(void *), *consume(void *);
int main(int argc, char **argv)
{
pthread_t tid_produce, tid_consume;
if (argc != 2)
{
printf("usage: prodcons2 <#items>");
}
nitems = atoi(argv[1]);
sem_init(&shared.mutex, 0, 1);
sem_init(&shared.nempty, 0, NBUFF);
sem_init(&shared.nstored, 0, 0);
pthread_create(&tid_produce, NULL, produce, NULL);
pthread_create(&tid_consume, NULL, consume, NULL);
pthread_join(tid_produce, NULL);
pthread_join(tid_consume, NULL);
sem_destroy(&shared.mutex);
sem_destroy(&shared.nempty);
sem_destroy(&shared.nstored);
exit(0);
}
void *produce(void *arg)
{
int i;
for (i = 0; i < nitems; i++)
{
sem_wait(&shared.nempty);
sem_wait(&shared.mutex);
shared.buff[i % NBUFF] = i;
sem_post(&shared.mutex);
sem_post(&shared.nstored);
}
return (NULL);
}
void *consume(void *arg)
{
int i;
for (i = 0; i < nitems; i++)
{
sem_wait(&shared.nstored);
sem_wait(&shared.mutex);
if (shared.buff[i % NBUFF] != i)
printf("buff[%d] = %d\n", i, shared.buff[i % NBUFF]);
sem_post(&shared.mutex);
sem_post(&shared.nempty);
}
return (NULL);
}
改成有名信号量,实例如下:
#include
#include
#include
#include
#include
#include
#include
#include
#define PATH_MAX 1024
#define NBUFF 10
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define SEM_MUTEX "mutex"
#define SEM_NEMPTY "nempty"
#define SEM_NSTORED "nstored"
int nitems;
struct
{
int buff[NBUFF];
sem_t *mutex, *nempty, *nstored;
} shared;
void *produce(void *), *consume(void *);
char *px_ipc_name(const char *name)
{
char *dst;
//...
return (dst);
}
int main(int argc, char **argv)
{
pthread_t tid_produce, tid_consume;
if (argc != 2)
{
printf("usage: prodcons1 <#items>");
exit(1);
}
nitems = atoi(argv[1]);
shared.mutex = sem_open(px_ipc_name(SEM_MUTEX), O_CREAT | O_EXCL,
FILE_MODE, 1);
shared.nempty = sem_open(px_ipc_name(SEM_NEMPTY), O_CREAT | O_EXCL,
FILE_MODE, NBUFF);
shared.nstored = sem_open(px_ipc_name(SEM_NSTORED), O_CREAT | O_EXCL,
FILE_MODE, 0);
pthread_create(&tid_produce, NULL, produce, NULL);
pthread_create(&tid_consume, NULL, consume, NULL);
pthread_join(tid_produce, NULL);
pthread_join(tid_consume, NULL);
sem_unlink(px_ipc_name(SEM_MUTEX));
sem_unlink(px_ipc_name(SEM_NEMPTY));
sem_unlink(px_ipc_name(SEM_NSTORED));
exit(0);
}
void *produce(void *arg)
{
int i;
for (i = 0; i < nitems; i++)
{
sem_wait(shared.nempty);
sem_wait(shared.mutex);
shared.buff[i % NBUFF] = i;
sem_post(shared.mutex);
sem_post(shared.nstored);
}
return (NULL);
}
void *consume(void *arg)
{
int i;
for (i = 0; i < nitems; i++)
{
sem_wait(shared.nstored);
sem_wait(shared.mutex);
if (shared.buff[i % NBUFF] != i)
printf("buff[%d] = %d\n", i, shared.buff[i % NBUFF]);
sem_post(shared.mutex);
sem_post(shared.nempty);
}
return (NULL);
}
进程间共享内存信号量很简单,信号量本身(其地址作为 sem_init 第一个 参数的 sem_t 数据类型变量)必须驻留在希望共享它的进程所共享的内存区中,而 且 sem_init 的第二个参数必须为1,上面说过,只有该参数大于 0,该内存信号量的实现才是基于共享内存的。
这些规则与进程间共享互斥锁、条件变量或读写锁的规则类似:同步对象本身 (pthread_mutex_t 变量、pthread_cond_t 变量或 pthread_rwlock_t 变量)必须驻留在希望共享它的进程所共享的内存区中,而且该对象必须以 PTHREAD_PROCESS_SHARED 属性初始化。
有名信号量在不同进程(不论彼此间有无亲缘关系)总是能够访问同一个有名信号量,只要它们在调用 sem_open 时指定相同的名字就行。即使对于某个给定名字的 sem_open 调用在每个调用进程中可能返回不同的指针,使用该指针的信号量函数(例如 sem_post 和 sem_wait)所引用的仍然是同一个有名信号量。
如果我们在调用 sem_open 返回指向某个 sem_t 数据类型变量的指针后接着调用 fork,情况又会怎么样呢?
在父进程中打开的任何信号 量仍应在子进程中打开。如下程序是正确的:
set_t *mutex;
mutex = sem_open(px_ipc_name(NAME), O_CREAT | O_EXCL, FILE_MODE, 0);
if ((childpid = fork()) == 0)
{
//... child
sem_wait(mutex);
}
//... parent
sem_post(mutex);
我们必须清楚什么时候可以或者不可以在不同进程间共享某个信号量。
Posix 定义了两个信号量限制:
SEM_NSEMS_MAX:一个进程可同时打开着的最大信号量数(Posix要求至少为 256)。
SEM_VALUE_MAX:一个信号量的最大值(Posix 要求至少为 32767)。
这两个常值通常定义在头文件中,也可在运行时通过调用 sysconf 函数获取,如下面的例子所示。
int main(int argc, char **argv)
{
printf("SEM_NSEMS_MAX = %ld, SEM_VALUE_MAX = %ld\n",
sysconf(_SC_SEM_NSEMS_MAX), sysconf(_SC_SEM_VALUE_MAX));
exit(0);
}