POSIX 信号量
POSIX信号量,它允许进程和线程同步对共享资源的访问。
本章内容的过程中将会对POSIX信号量和System V信号量进行比较以阐明这两组信号量API的相同之处和相异之处。
概述
SUSv3规定了两种类型的POSIX信号量。
-
命名信号量:这种信号量拥有一个名字。通过使用相同的名字调用sem_open(),不相关的进程能欧访问同一个信号量。
-
未命名信号量:这种信号量没有名字,相反,它位于内存中的一个预先商定的位置处。未命名信号量可以在进程之间或者一组线程之间共享。当在进程之间共享时,信号量必须位于一个共享内存区域中(SystemV,POSIX或mmap())。当在线程之间共享时,信号量可以位于被这些线程共享的一块内存区域中(如堆上或者一个全局变量中)。
POSIX信号量是一个整数,其值是不能小于0 的。如果一个进程试图将一个信号量的值减少到小于0,那么取决于所使用的函数,调用会阻塞或返回一个表明当前无法执行相应操作的错误。
一些系统并没有完整地实现POSIX信号量,一个典型的约束是只支持未命名线程共享的信号量,在linux2.4上也是同样的情况;只有在linux2.6以及带NPYTL的glibc上,完整的POSIX信号量实现才可用。
在带NPTL的linux2.6上,信号量操作(递增和递减)是使用futex(2)系统调用来实现的。
命名信号量
要使用命名信号量必须要使用下列函数。
-
Sem_open()函数打开或创建一个信号量并返回一个句柄以供后续调用使用,如果这个调用会创建信号量的话还会对所创建的信号量进行初始化。
-
Sem_Post(sem), sem_wait(sem)函数分别递增和递减一个信号量值。
-
Sem_getvalue()函数获取一个信号量的当前值。
-
Sem_close()函数删除调用进程与它之前打开的一个信号之间的关联关系。
-
sem_unlink()函数删除一个信号量名字并将其标记为在所有进程关闭信号量时删除该信号量。
SUSv3并没有规定如何实现命名信号量。一些UNIX实现将它们创建成位于标准文件系统上一个特殊位置处的文件。在liunx上,命名信号量被创建成小型POSIX共享内存对象,其名字的形式为sem.name.这些对象将被放在一个挂载在/dev/shm目录之下的专用tmpfs文件系统。这个文件系统具有内核持久性---它所包含的信号量对象将会持久,即使当前没有进程打开它们,但如果系统被关闭的话,这些对象就会丢失。
在linux上内核2.6起开始支持命名信号量。
Sem_open()函数创建和打开一个新的命名信号量或打开一个既有信号量。
1 #include <fcntl.h> // defines 0_* constants 2 #include <sys/stat.h> // defines mode constants 3 #include <semaphore.h> 4 5 sem_t *sem_open(const char* name, int oflag, ... 6 /* mode_t model, unsigned int value */); 7 Returns pointer to semaphore on success, or SEM_FAILED on error
name参数标示出量信号量,其取值需符合51.1节中给出的规则。
oflag参数是一个位掩码,它确定了是打开一个既有信号量还是创建并打开一个新信号量。如果oflag为0,那么将访问一个既有信号量。如果在oflag中指定了O_CRATE,并且与给定的name对应的信号量不存在,那么就创建一个新信号量。如果在oflag中同时指定了O_CREAT和O_EXCL,并且与给定的name对应的信号量已经存在,那么sem_open()就会失败。
如果sem_open()被用来打开一个既有信号量,那么调用只需要两个参数。但如果在flags中指定了O_CREAT,那么还需要另外两个参数:mode和value。(如果与name对应的信号量已经存在,那么这两个参数会被忽略)。
-
-
value参数是一个无符号整数,它指定了新信号量的初始值。信号量的创建和初始化操作是原子的,这样就避免了SystemV信号量初始化时所需要完成的复杂工作了。
不管是创建一个新信号量还是打开一个既有信号量,sem_open()都会返回一个sem_t值的指针,而在后续的调用中则可以通过这个指针来操作这个信号量。sem_open()在发生错误时会返回SEM_FAILED值。(在大多数实现上,SEM_FAILED被定义成(sem_t * )0 或 (sem_t * )-1;liunx采用量前面一种定义。
SUSv3声称当在sem_open()的返回值指向的sem_t变量的副本上执行操作(sem_post(), sem_wait())等时结果是为定义的。换句话说,像下面这种使用sem2的做法是不允许的。
1 sem_t *sp, sem2; 2 sp = sem_open(...); 3 sem2 = *sp; 4 sem_wait(&sem2);
通过fork()创建的子进程会继承父进程打开的所有命名信号量的引用。在fork()之后,父进程和子进程就能够使用这些信号量来同步它们的动作了。
1 #include <semaphore.h> 2 #include <sys/stat.h> 3 #include <fcntl.h> 4 #include "tlpi_hdr.h" 5 6 static void 7 usageError(const char* progName) 8 { 9 fprintf(stderr, "Usage: %s [-cx] name [octal-perms [value]] ", progName); 10 fprintf(stderr, " -c Create semaphor (O_CREATE) "); 11 fprintf(stderr, " -x Xreate exclusively (O_EXCL) "); 12 exit(EXIT_FAILURE); 13 } 14 15 int 16 main(int argc, char *argv[]) 17 { 18 int flags, opt; 19 mode_t perms; 20 usigned int vlaue; 21 sem_t *sem; 22 23 flags = 0; 24 while((opt = getopt(argc, argv, "cx")) != -1){ 25 switch (opt){ 26 case 'c': flags |= O_CREAT; break; 27 case 'x': flags |= O_EXCL; break; 28 default: usageError(argv[0]); 29 } 30 } 31 if(optind >= argc) 32 usageError(argv[0]); 33 34 /* Defalut permissions are rw-------; default semaphore initialization value is 0 */ 35 perms = (argc <= optind + 1) ? (S_IRUSR | S_IWUSR): 36 getInt(argv[optind+1], GN_BASE_8, "octal-perms"); 37 value = (argc <= optind + 2 )? : getInt(argv[optind+2], 0, "value"); 38 sem = sem_open(argv[optind], flags, perms, value); 39 if(sem == SEM_FAILED) 40 errExit("sem_open"); 41 exit(EXIT_SUCCESS); 42 }
关闭一个信号量
当一个进程打开一个命名信号量时,系统会记录进程与信号量之间的关联关系。sem_close()函数会终止这种关联关系(即关闭信号量),释放系统为该进程关联到该信号量之上的所有资源,并递减引用该信号量的进程数。
1 #include <semphore.h> 2 3 int sem_close(sem_t* sem); 4 5 Returns 0 on success, or -1 on error
打开的命名信号量在进程终止或进程执行量一个excel()时会自动被关闭。
关闭一个信号量并不会删除这个信号量,而要删除信号量则需要使用sem_unlink().
删除一个命名信号量
1 #include <semphore.h> 2 3 int sem_unlink(const char* name); 4 5 Return 0 on success, or -1 on error 6 7 #include <semaphore.h> 8 #include "tlpi_hdr.h" 9 10 int 11 main(int argc, char* argvp[]) 12 { 13 if (argc != 2 || strcmp(argv[1], "--help") == 0) 14 usageErr("%s sem-name ", argv[0]); 15 if(sem_unlink(argv[1]) == -1) 16 errExit("sem_unlink"); 17 exit(EXIT_SUCCESS); 18 }
信号量操作
一个POSIX信号量也是一个整数并且系统不会允许其值小于0。
但POSIX信号量的操作不同于SystemV信号量的操作,具体包括:
-
修改信号量值的函数---sem_post()和sem_wait(), 一次只操作一个信号量。与之对比的是,SystemV semop()系统调用能够操作一个集合中的多个信号量。
-
Sem_post()和sem_wait()函数只对信号量值加1和减1.与之形成对比的是,semop()能够加上和减去任意一个值。
-
System V信号并没有提供一个wait for zero的操作,(将sops.sem_op字段指定为0的semop()调用)
能够通过SystemV信号量完成的工作都可以使用POSIX信号量来完成。在一些情况下,使用POSIX信号量可能需要多做一些编程工作,但一般应用场景中,使用POSIX信号量实际所需的编程量要更少。(对于大多数应用程序来讲,System V信号量API过于复杂了)
等待一个信号量
sem_wait()函数会递减(减少1)sem引用的信号量的值。
1 #include <semaphore.h> 2 3 int sem_wait(sem_t *sem); 4 5 Return 0 on success, or -1 on error
如果信号量的当前值大于0,那么sem_wait()会立即返回。如果信号量的当前值等于0,那么sem_wait()会阻塞直到信号量的值大于0为止,当信号量值大于0时该信号量值就被递减并且sem_wait()会返回。
如果一个阻塞的sem_wait()调用被一个信号处理器中断了,那么它就会失败并返回EINTR错误,不管在使用sigaction()建立这个信号处理器时是否采用了SA_RESTART标记(在其他一些UNIX实现上,SA_RESTART会导致sem_wait()自动重启。)
1 #include <semaphor.h> 2 #include "tlpi_hdr.h" 3 4 int main(int argc, char *argv[]) 5 { 6 sem_t *sem; 7 8 if(argc < 2 || strcmp(argv[1], "--help") == 0) 9 usageErr("%s sem-name ", argv[0]); 10 sem = sem_open(argv[1], 0); 11 if(sem == SEM_FAILED) 12 errExit("sem_open"); 13 if(sem_wait(sem) == -1) 14 errExit("sem_wait"); 15 printf("%ld sem_wait() succeeded ", (long)getid()); 16 exit(EXIT_SUCCESS); 17 }
Sem_trywait()函数是sem_wait()的一个非阻塞版本。
1 #include <semaphore.h> 2 3 int sem_trywait(sem_t *sem); 4 5 Return 0 on success, or -1 on error
如果递减操作无法立即执行,那么sem_trywait()就会失败并返回EAGAIN错误。
sem_timedwait()函数是sem_wait()的另一个变体,它允许调用者为调用被阻塞的时间量设定一个限制。·
1 #define _XOPEN_SOURCE 600 2 #include <semaphore.h> 3 4 int sem_timedwait(sem_t *sem, const struct timespec* abs_timeout); 5 6 Return 0 on success, or -1 on error
如果sem_timedwait()调用因超时而无法递减信号量,那么这个调用就会失败并返回ETIMEOUT错误。
Abs_timeout参数是一个结构,它将超时时间表示成了自新纪元到现在为止的秒数和纳秒的绝对值。如果需要指定一个相对超时时间,那么就必须要使用colck_gettime()获取CLOCK_REALTIME时钟的当前值并在该值上加上所需的时间量来生成一个适合在sem_timedwait()中使用的timespec结构。
发布一个信号量
Sem_post()函数递增(增加1)sem引用的信号量的值
1 #include <semaphore.h> 2 3 int sem_post(sem_t *sem); 4 5 Return 0 on success, or -1 on error
如果在sem_post()调用之前信号量的值为0,并且其他某个进程(或线程)正在等待递减这个信号量而阻塞,那么该进程会被唤醒,它的sem_wait()调用会继续往前执行来递减这个信号量。如果多个进程(或线程)在sem_wait()中阻塞了,并且这些进程的调度采用默认的循环时间分享策略,那么那个进程会被唤醒并允许递减这个信号量是不确定的(与SystemV信号量一样,POSIX信号量仅仅是一种同步机制,而不是一种排队机制)。
SUSv3规定如果进程或线程执行在实时调度策略下,那么优先级最高等待时间最长的进程或线程将会被唤醒。
与SystemV信号量一样,递增一个POSIX信号量对应释放一些共享资源以供其他进程或线程使用。
1 #include <semaphore.h> 2 #include "tlpi_hdr.h" 3 4 int main(int argc, char* argv[]) 5 { 6 sem_t *sem; 7 8 if(argv != 2) 9 usageErr("%s sem-name ", argv[0]); 10 11 sem = sem_open(argv[1], 0); 12 if(sem == SEM_FAILED) 13 errExit("sem_open"); 14 if(sem_post(sem) == -1) 15 errExit("sem_post"); 16 exit(EXIT_SUCCESS); 17 }
获取信号量的当前值
Sem_getvalue()函数将sem引用的信号量的当前值通过sval指向的int变量返回
1 #include <semaphore.h> 2 3 int sem_getvalue(sem_t *sem, int *sval); 4 5 Return 0 on success, or -1 on error
如果一个或多个进程(或线程)当前正在阻塞以等待递减信号量值,那么sval中返回值将取决于实现,SUSv3允许两种做法,0或一个绝对值等于在sem_wait()中阻塞的等待者数目的负数,linux和其他一些实现采用了第一种行为,而另一些实现采用了后一种行为。
注意在sem_getvalue()返回时,sval中的返回值可能已经过时了。依赖于sem_getvalue()返回的信息在执行后续操作时未发生变化的程序将会碰到检查时,使用时(time-of-check,time-of-use)的竞争条件。
1 #include <semaphore.h> 2 #include "tlpi_hdr.h" 3 4 int main(int argc, char *argv[]) 5 { 6 int value; 7 sem_t *sem; 8 9 if(argc != 2) 10 usageErr("%s sem-name ", argv[0]); 11 sem = sem_open(argv[1], 0); 12 if(sem == SEM_FAILED) 13 errExit("sem_open"); 14 if(sem_getvalue(sem, &value) == -1) 15 errExit("sem_getvalue"); 16 17 printf("%d ", value); 18 exit(EXIT_SUCCESS); 19 }