1、概述
信号量(semaphore)是一种用于提供不同进程间或一个给定进程的不同线程间同步手段的原语。本书讨论三种类型的信号量。
- Posix有名信号量:使用Posix IPC名字标识,可用于进程或线程间的同步。
- Posix基于内存的信号量:存放在共享内存区中,可用于进程或线程间的同步。
- System V信号量:在内核中维护,可用于进程或线程间的同步。
我们暂时只考虑不同进程间的同步。首先考虑二值信号量(binary semaphore):其值或为0或为1的信号量。图10-1展示了这种信号量。
图中画出该信号量是由内核来维护的(这对于System V信号量是正确的),其值可以是0或1。
Posix信号量不必在内核中维护。另外, Posix信号量是由可能与文件系统中的路径名对应的名字来标识的。因此,图10-2是Posix有名信号量的更为实际的图示。
在图10-1和图10-2中,我们注出了一个进程可以在某个信号量上执行的三种操作。
- 创建(create)一个信号量。这还要求调用者指定初始值,对于二值信号量来说,它通常是1,但也可以是0。
- 等待(wait)一个信号量。该操作会测试会这个信号量的值,如果其值小于或等于0,那就等待(阻塞),一旦其值变为大于0就将它减1。
- 挂出(post)一个信号量。该操作将信号量的值加1。
2、 sem_open、sem_close 和 sem_unlink 函数
函数 sem_open 创建一个新的有名信号量或打开一个已存在的有名信号量。有名信号量总是可用于线程间的同步,又可用于进程间的同步。
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag, ....
/* mode_t mode, unsigned int value */);
//返回:若成功则为指向信号量的指针,若出错则为SEM_FAILED
sem_close关闭使用sem_open打开的有名信号量。
#include <semaphore.h>
int sem_close (sem_t *sem);
// 返回:若成功则为0,若出错则为-1
一个进程终止时,内核还对其上仍然打开着的所有有名信号量自动执行这样的信号量关闭操作。不论该进程是自愿终止的(通过调用exit或_exit)还是非自愿地终止的(通过向它发送一个Unix信号),这种自动关闭都会发生。
关闭一个信号量并没有将它从系统中删除。这就是说,Posix有名信号量至少是随内核持续的:即使当前没有进程打开着某个信号量,它的值仍然保持。
sem_unlink从系统中删除有名信号量。
#include <semaphore.h>
int sem_unlink(const char *name);
// 返回:若成功则为0,若出错则为-1
每个信号量有一个引用计数器记录当前的打开次数(就像文件一样),sem_unlink类似于文件 l/0 的unlink函数:当引用计数还是大于0时,name就能从文件系统中删除,然而其信号量的析构(不同于将它的名字从文件系统中删除)却要等到最后一个sem_close发生时为止。
3、 sem_wait 和 sem_trywait 函数
sem_wait函数测试所指定信号量的值。
如果该值大于0,那就将它减1并立即返回。
如果该值等于0,调用线程就被投入睡眠中,直到该值变为大于0,这时再将它减1,函数随后返回。
我们以前提到过,考虑到访问同一信号量的其他线程, “测试并减1”操作必须是原子的。
#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
//均返回:若成功则为0,若出错则为-1
sem_wait和sem_trywait的差别是:当所指定信号量的值已经是0时,后者并不将调用线程投入睡眠。相反,它返回一个EAGAIN错误。
如果被某个信号中断,sem_wait就可能过早地返回,所返回的错误为EINTR。
4、 sem_post 和 sem_getvalue 函数
当一个线程使用完某个信号量时,它应该调用sem_post。本函数把所指定信号量的值加1,然后唤醒正在等待该信号量值变为正数的任意线程。
#include <semaphore.h>
int sem_post(sem_t *sem);
int sem_getvalue(sem_t *sem, int *valp);
//均返回:若成功则为0,若出错则为-1
sem_getvalue在由valp指向的整数中返回所指定信号量的当前值。如果该信号量当前已上锁,那么返回值或为0,或为某个负数,其绝对值就是等待该信号量解锁的线程数。
我们现在看到了互斥锁、条件变量和信号量之间的更多差别。首先,互斥锁必须总是由给它上锁的线程解锁。信号量没有这种限制:一个线程可以等待某个给定信号量(譬如说将该信号量的值由1减为0,这跟给该信号量上锁一样),而另一个线程可以挂出该信号量(譬如说将该信号量的值由0增为1,这跟给该信号量解锁一样)。
其次,既然每个信号量有一个与之关联的值,它由挂出操作加1,由等待操作减1,那么任何线程都可以挂出一个信号(譬如说将它的值由0增为1),即使当时没有线程在等待该信号量值变为正数也没有关系。然而,如果某个线程调用了pthread_cona_signal,不过当时没有任何线程阻塞在pthread_cond_wait调用中,那么发往相应条件变量的信号将丢失。
最后,在各种各样的同步技巧(互斥锁、条件变量、读写锁、信号量)中,能够从信号处理程序中安全调用的唯一函数是sem_post。
5、实例
5.1 secreate 程序
// semcreate.c
#include "unpipc.h"
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)
err_quit("usage: semcreate [ -e ] [ -i initalvalue ] <name>");
sem = Sem_open(argv[optind], flags, FILE_MODE, value);
printf("argv[optind]:%s,flags:%d,value:%d\n",argv[optind],flags,value);
Sem_close(sem);
exit(0);
}
5.2 semunlink 程序
// semunlink.c
#include "unpipc.h"
int main(int argc, char **argv)
{
if (argc != 2)
err_quit("usage: semunlink <name>");
Sem_unlink(argv[1]);
exit(0);
}
5.3 semgetvalue 程序
// semgetvalue.c
#include "unpipc.h"
int main(int argc, char **argv)
{
sem_t *sem;
int val;
if (argc != 2)
err_quit("usage: semgetvalue <name>");
sem = Sem_open(argv[1], 0);
Sem_getvalue(sem, &val);
printf("value = %d\n", val);
exit(0);
}
5.4 semwait 程序
// semwait.c
#include "unpipc.h"
int main(int argc, char **argv)
{
sem_t *sem;
int val;
if (argc != 2)
err_quit("usage: semwait <name>");
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(); /* blocks until killed */
exit(0);
}
5.5 sempost 程序
// sempost.c
#include "unpipc.h"
int main(int argc, char **argv)
{
sem_t *sem;
int val;
if (argc != 2)
err_quit("usage: sempost <name>");
sem = Sem_open(argv[1], 0);
Sem_post(sem);
Sem_getvalue(sem, &val);
printf("value = %d\n", val);
exit(0);
}
5.6 运行测试
窗口A:
窗口B:
窗口A:
6、文件上锁
使用Posix有名信号量实现my_lock和my_unlock函数。
#include "unpipc.h"
#define LOCK_PATH "pxsemlock"
sem_t *locksem;
int initflag;
void my_lock(int fd)
{
if (initflag == 0) {
locksem = Sem_open(Px_ipc_name(LOCK_PATH), O_CREAT, FILE_MODE, 1);
initflag = 1;
}
Sem_wait(locksem);
}
void my_unlock(int fd)
{
Sem_post(locksem);
}
7、sem_int 和 sem_destroy 函数
本章此前的内容处理的是Posix有名信号量。这些信号量由一个name参数标识,它通常指代文件系统中的某个文件。然而Posix也提供基于内存的信号量,它们由应用程序分配信号量的内存空间(也就是分配一个sem_t数据类型的内存空间),然后由系统初始化它们的值。
#include <semaphore.h>
int sen_init(sem_t *sem, int shared, unsigned int value);
// 返回:若出错则为-1
int sem_destroy(sem_t *sem);
// 返回:若成功则为0,若出错则为-1
基于内存的信号量是由seminit初始化的。sem参数指向应用程序必须分配的sem_t变量。如果shared为0,那么待初始化的信号量是在同一进程的各个线程间共享的,否则该信号量是在进程间共享的。当shared为非零时,该信号量必须存放在某种类型的共享内存区中,而即将使用它的所有进程都要能访问该共享内存区。跟semopen一样, value参数是该信号量的初始值。使用完一个基于内存的信号量后,我们调用sem_destroy摧毁它。
当不需要使用与有名信号量关联的名字时,可改用基于内存的信号量。彼此无亲缘关系的不同进程需使用信号量时,通常使用有名信号量。其名字就是各个进程标识信号量的手段。基于内存的信号量至少具有随进程的持续性,然而它们真正的持续性却取决于存放信号量的内存区的类型。只要含有某个基于内存信号量的内存区保持有效,该信号量就一直存在。
- 如果某个基于内存的信号量是由单个进程内的各个线程共享的(seminit的shared的参数为0),那么该信号量具有随进程的持续性,当该进程终止时它也消失。
- 如果某个基于内存的信号量是在不同进程间共享的(seminit的shared参数为1),那么该信号量必须存放在共享内存区中,因而只要该共享内存区仍然存在,该信号量也就继续存在。
例子:
基于内存的信号量 生产者-消费者
// prodcons2.c
#include "unpipc.h"
#define NBUFF 10
int nitems; /* read-only by producer and consumer */
struct { /* data shared by producer and consumer */
int buff[NBUFF];
sem_t mutex, nempty, nstored; /* semaphores, not pointers */
} shared;
void *produce(void *), *consume(void *);
int main(int argc, char **argv)
{
pthread_t tid_produce, tid_consume;
if (argc != 2)
err_quit("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);
Set_concurrency(2);
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); /* wait for at least 1 empty slot */
Sem_wait(&shared.mutex);
shared.buff[i % NBUFF] = i; /* store i into circular buffer */
Sem_post(&shared.mutex);
Sem_post(&shared.nstored); /* 1 more stored item */
}
return(NULL);
}
void * consume(void *arg)
{
printf("consume start...\n");
int i;
for (i = 0; i < nitems; i++)
{
Sem_wait(&shared.nstored); /* wait for at least 1 stored item */
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); /* 1 more empty slot */
}
printf("consume end...\n");
return(NULL);
}
执行结果:
8、小结
Posix信号量是计数信号量,它提供以下三种基本操作
(1)创建一个信号量;
(2)等待一个信号量的值变为大于0,然后将它的值减1;
(3)给一个信号量的值加1,并唤醒等待该信号量的任意线程,以此挂出该信号量。
Posix信号量可以是有名的,也可以是基于内存的。有名信号量总是能够在不同进程间共享,基于内存的信号量则必须在创建时指定成是否在进程间共享。这两类信号量的持续性也有差别:有名信号量至少有随内核的持续性,基于内存的信号量则具有随进程的持续性。