当多个进程同时访问系统上的某个资源的时候,就需要考虑进程的同步问题,以确保任一时刻只有一个进程可以拥有对资源的独占式访问。POSIX信号量不仅可以用于进程之间的同步,也可以用于线程之间的同步。
概述
SUSv3规定了两种类型的POSIX信号量。
- 命名信号量:这种信号量拥有一个名字。通过使用相同的名字调用sem_open(),不相关的进程能够访问同一个信号量。
- 未命名信号量:这种信号量没有名字,相反,它位于内存中一个预先商定的位置处。未命名信号量可以在进程之间或一组线程之间共享。当在进程之间共享时,信号量必须位于一个共享内存区域中。当在线程之间共享时,信号量可以位于被这些线程共享的一块内存区域中(如在堆上或在一个全局变量中)。
- POSIX信号量是一个整数,其值不能小于0。
命名信号量
要使用命名信号量必须要使用下列函数。
- sem_open()函数打开或创建一个信号量并返回一个句柄以供后续调用使用,如果这个调用会创建信号量的话还会对所创建的信号量进行初始化。
- sem_post()和sem_wait()函数分别递增和递减一个信号量值。
- sem_getvalue()函数获取一个信号量的当前值。
- sem_close()函数删除调用进程与它之前打开的一个信号量之间的关联关系。
- sem_unlink()函数删除一个信号量名字并将其标记为在所有进程关闭该信号量时删除该信号量。
打开一个命名信号量
sem_open()函数创建和打开一个新的命名信号量或打开一个既有信号量。
#include<fcntl.h>
#include<sys/stat.h>
#include<semaphore.h>
sem_t *sem_open(const char *name,int oflag,mode_t mode,unsigned int value);
name标识出了信号量。
oflag参数是一个位掩码,它确定了是打开一个既有信号量还是创建并打开一个新信号量。如果oflag为0,那么将访问一个既有信号量。如果在oflag中指定了O_CREAT,并且与给定的name对应的信号量不存在,那么就创建一个新的信号量。如果在oflag中同时指定了O_CREAT和O_EXCL,并且与给定的name对应的信号量已经存在,那么sem_open()就会失败。
如果sem_open()被用来打开一个既有信号量,那么调用就只需要两个参数。但如果在flags中指定了O_CREAT,那么就还需要另外两个参数:mode和value。
- mode参数是一个位掩码,它指定了施加于新信号之上的权限。这个参数能取的位值与文件上的位值是一样的并且与open()一样,mode参数中的值会根据进程的umask来取掩码。
- value参数是一个无符号整数,它指定了新信号量的初始值。
不管是创建一个新信号量还是打开一个既有信号量,sem_open()都会返回一个指向一个sem_t值的指针,而在后续的调用中则可以通过这个指针来操作这个信号量。
SUSv3声称当在sem_open()的返回值指向的sem_t变量的副本上执行操作时结果是未定义的。换句话说,像下面这种使用sem2的做法是不允许的。
sem_t *sp,sem2;
sp=sem_open(...);
sem2=*sp;
sem_wait(&sem2);
通过fork()创建的子进程会继承其父进程打开的所有命名信号量的引用。在fork()之后,父进程和子进程就能够使用这些信号量来同步它们的动作了。
关闭一个信号量
当一个进程打开一个命名信号量时,系统会记录进程与信号量之间的关联关系。sem_close()函数会终止这种关联关系(即关闭信号量),释放系统为该进程关联到该信号量之上的所有资源,并递减引用该信号量的进程数。
#include<semaphore.h>
int sem_close(sem_t *sem);
打开的命名信号量在进程终止或进程执行了一个exec()时会自动被关闭。
关闭一个信号量并不会删除这个信号量,而要删除信号量则需要使用sem_unlink()。
删除一个命名信号量
sem_unlink()函数删除通过name标识的信号量并将信号量标记成一旦所有进程都使用完这个信号量时就销毁该信号量。
#include<semaphore.h>
int sem_unlink(const char *name);
等待一个信号量
sem_wait()函数会递减(减小1)sem引用的信号量的值。
#include<semaphore.h>
int sem_wait(sem_t *sem);
如果信号量的当前值大于0,那么sem_wait()会立即返回。如果信号量的当前值等于0,那么sem_wait()会阻塞直到信号量的值大于0为止,当信号量大于0时该信号量值就被递减并且sem_wait()会返回。
如果一个阻塞的sem_wait()调用被一个信号处理器中断了,那么它就会失败并返回EINTR错误。
sem_trywait()函数是sem_wait()的一个非阻塞版本。
#include<semaphore.h>
int sem_trywait(sem_t *sem);
如果递减操作无法立即被执行,那么sem_trywait()就会失败并返回EAGAIN错误。
发布一个信号量
sem_post()函数递增(增加1)sem引用的信号量的值。
#include<semaphore.h>
int sem_post(sem_t *sem)l
如果在sem_post()调用之前信号量的值为0,并且其他某个进程(或线程)正在因等待递减这个信号量而阻塞,那么该进程会被唤醒,它的sem_wait()调用会继续往前执行来递减这个信号量。如果多个进程(或线程)在sem_wait()中阻塞了,并且这些进程的调度采用的是默认的循环时间分享策略,那么哪个进程会被唤醒并允许递减这个信号量是不确定的。
POSIX信号量仅仅是一种同步机制,而不是一种排队机制。
获取信号量的当前值
sem_getvalue()函数将sem引用的信号量的当前值通过sval指向的int变量返回。
#include<semaphore.h>
int sem_getvalue(sem_t *sem,int *sval);
如果一个或多个进程(或线程)当前正在阻塞以等待递减信号量的值,那么sval中的返回值将取决于实现。在Linux下,返回0。
注意在sem_getvalue()返回时,sval中的返回值可能已经过时了。
未命名信号量
未命名信号量(也被称为基于内存的信号量)是类型为sem_t并存储在应用程序分配的内存中的变量。通过将这个信号量放在由几个进程或线程共性的内存区域中就能够使这个信号量对这些进程或线程可用。
操作未命名信号量所使用的函数与操作命名信号量使用的函数是一样的。此外,还需要两个函数;
- sem_init()函数对一个信号量进行初始化并通知系统该信号量会在进程间共享还是在单个进程中的线程间共享。
- sem_destory()函数销毁一个信号量。
这些函数不应该被应用到命名信号量上。
未命名与命名信号量对比
- 在线程间共享的信号量不需要名字。将一个未命名信号量作为一个共享(全局或堆是上的)变量自动会使之对所有线程可访问。
- 在相关进程间共享的信号量不需要名字。如果一个父进程在一块共享内存区域中分配了一个未命名信号量,那么作为fork()操作的一部分,子进程会自动继承这个映射,从而继承这个信号量。
- 如果正在构建的是一个动态数据结构(如二叉树),并且其中的每一项都需要一个关联的信号量,那么最简单的做法是在每一项中都分配一个未命名的信号量。为每一项打开一个命名信号量需要为如何生成每一项中的信号名字和管理这些名字设计一个规则。
初始化一个未命名信号量
sem_init()函数使用value中指定的值来对sem指向的未命名信号量进行初始化。
#include<semaphore.h>
int sem_init(sem_t *sem,int pshared,unsigned int value);
pshared参数表明这个信号量是在线程间共享还是在进程间共享。
- 如果pshared等于0,那么信号量将会在调用进程中的线程间进行共享。在这种情况下,sem通常被指定成一个全局变量的地址或分配在堆上的一个变量的地址。线程共享的信号量具备进程持久性,它在进程终止时会被销毁。
- 如果pshared不等于0,那么信号量将会在进程间共享。在这种情况下,sem必须是共享内存区域中的某个地址的位置。信号量的持久性与它所处的共享内存的持久性是一样的。由于通过fork()创建的子进程会继承其父进程的内存映射,因此进程共享的信号量会被通过fork()创建的子进程继承,这样父进程和子进程也就能够使用这些信号量来同步它们的动作了。
未命名信号量不存在相关的权限设置。对一个未命名信号量的访问将由进程在底层共享内存区域上的权限来控制。
SUSv3规定对一个已初始化过的未命名信号量进行初始化操作将会导致未定义的行为。换句话说,必须要将应用程序设计成只有一个进程或线程来调用sem_init()以初始化一个信号量。
与命名信号量一样,在sem_t变量的副本上执行操作的结果是未定义的。
销毁一个未命名信号量
sem_destroy()函数将销毁信号量sem,其中sem必须是一个之前使用sem_init()进行初始化的未命名信号量。只有在不存在进程或线程在等待一个信号量时才能够安全销毁这个信号量。
#include<semapthore.h>
int sem_destroy(sem_t *sem);
当使用sem_destroy()销毁了一个未命名信号量之后就能够使用sem_init()来重新初始化这个信号量了。
一个未命名信号量应该在其底层的内存被释放之前被销毁。
代码示例
使用一个未命名线程共享的信号量来保护对全局变量的访问。
#include <semaphore.h>
#include <pthread.h>
#include "tlpi_hdr.h"
static int glob = 0;
static sem_t sem;
static void * /* Loop 'arg' times incrementing 'glob' */
threadFunc(void *arg)
{
int loops = *((int *) arg);
int loc, j;
for (j = 0; j < loops; j++) {
if (sem_wait(&sem) == -1)
errExit("sem_wait");
loc = glob;
loc++;
glob = loc;
if (sem_post(&sem) == -1)
errExit("sem_post");
}
return NULL;
}
int
main(int argc, char *argv[])
{
pthread_t t1, t2;
int loops, s;
loops = (argc > 1) ? getInt(argv[1], GN_GT_0, "num-loops") : 10000000;
/* Initialize a semaphore with the value 1 */
if (sem_init(&sem, 0, 1) == -1)
errExit("sem_init");
/* Create two threads that increment 'glob' */
s = pthread_create(&t1, NULL, threadFunc, &loops);
if (s != 0)
errExitEN(s, "pthread_create");
s = pthread_create(&t2, NULL, threadFunc, &loops);
if (s != 0)
errExitEN(s, "pthread_create");
/* Wait for threads to terminate */
s = pthread_join(t1, NULL);
if (s != 0)
errExitEN(s, "pthread_join");
s = pthread_join(t2, NULL);
if (s != 0)
errExitEN(s, "pthread_join");
printf("glob = %d\n", glob);
exit(EXIT_SUCCESS);
}