信号量是一种用于提供不同进程间或一个给定进程的不同线程间同步手段的原语。三种类型的信号量:
Posix IPC
名字标识,可用于进程或线程间的同步。只考虑不同进程间的同步。首先考虑二值信号量: 其值或为0或为1的信号量。如下图所示:
图中画出该信号量是由内核来维护的(对于SystemV信号量是正确的),其值可以是0或1。 Posix信号量不必在内核中维护。
Posix信号量是由可能与文件系统中的路径名对应的名字来标识的。因此,下图是Posix有名信号量的更为实际的图示。
一个进程可以在某个信号量上执行的三种操作:
创建:调用者指定初始值,二值信号量,通常是1或0。
等待:测试信号量值,小于或等于0,阻塞;大于0将将其减1。(while(semaphore_value<=0) ; semaphore--;
)
挂出:将信号量的值加1。若有一些进程阻塞着等待该信号量的值变为大于0,其中一个进程现在就可能被唤醒。考虑到访问同一信号量的其他进程,挂出操作也必须是原子的。(semaphore_value++
)。
没有使用其值仅为0或1的二值信号量,这种信号量称为计数信号量,二值信号量可用互斥目的,就像互斥锁一样,如下所示:
信号量初始化为1,sem_wait调用等待其值变为大于0,然后将它减1,sem_post调用则将其值加1(从0变为1),然后唤醒阻塞在sem_wait调用中等待该信号量的任何线程。
除可以像互斥锁那样使用外,信号量还有一个互斥锁没有提供的特性:互斥锁必须总是由锁住它的线程解锁,信号量的挂出却不必由执行过它的等待操作的同一线程执行。
使用两个二值信号量和生产者-消费者问题的一个简化版本提供展示这种特性的一个例子。
下展示了往某个共享缓冲区中放置 一个条目的一 个生产者以及取走该条 目的一个消费者。为简单起见,假设该缓冲区只容纳一个条目。
下图显示生产者和消费者伪代码:
步骤:
生产者初始化缓冲区和两个信号量。
假设消费者接着运行 。 它阻塞在sem_wait调用中,因为get的值为0。
一 段时间后生产者接着运行:
sem_wait
调用中,因为put的值为0。生产者必须等待到消费者腾空缓冲区。消费者从set_wait
调用中返回,将get信号量的值由1减为0。然后处理缓冲区中的数据,然后调用sem_post,把put的值由0增为1。既然有一个线程(生产者)阻塞在该信号量上等待其值变为正数,该线程将被标记成准备好运行。但是假设消费者继续运行,消费者随后会阻塞在for循环顶部的sem_wait调用中,因为get的值为0。
生产者从sem_wait调用中返回,把数据放入缓冲区中,上述情形循环继续。若每次调用sem_post时,即使当时有一个进程正在等待并随后被标记成准各好运行,调用者也继续运行。是调用者继续运行还是刚变成准备好状态的线程运行无关紧要。
信号量、互斥锁和条件变量之间的三个差异:
互斥锁必须总是由给它上锁的线程解锁,信号量的挂出却不必由执行过它的等待操作的同一 线程执行。
互斥锁要么被锁住,要么被解开(二值状态,类似于二值信号量)。
既然信号量有一个与之关联的状态(它的计数值),则信号量挂出操作总是被记住。
当向条件变量发送信号时,如果没有线程等待在该条件变量上,那么该信号将丢失。
POSIX信号量有两种形式,差异在于创建和销 毁的形式上:
命名的:命名信号量可以通过名字访问,因此可以被任何已知它们名字的进程中的 线程使用。
未命名的:未命名信号量只存在于内存中,并要求能使用 信号量的进程必须可以访问内存。意味着只能应用在同一进程中的线 程,或者不同进程中已经映射相同内存内容到它们的地址空间中的线程。
下图显示某个进程由两个线程共享一个Posix基于内存的信号量。
下图显示某个共享内存区中由两个进程共享的一个Posix基于内存的信号量。
要使用命名信号量必须要使用下列函数:
sem_open()
函数打开或者创建一个信号量并返回一个句柄以供后继调用使用,如果这个调用会创建信号量的话还会对所创建的信号量进行初始化。
sem_post(sem)和 sem_wait(sem)
函数分别递增和递减一个信号量值。
sem_getvalue()
函数获取一个信号量的当前值。
sem_close()
函数删除调用进程与它之前打开的一个信号量之间的关联关系
sem_unlink())
函数删除一个信号量名字并将其标记为在所有进程关闭该信号量时删除该信号量。
//创建一个新的命名信号量或者使用一个现有信号量
//使用现有信号量,只需指定两个参数:信号量的名字和oflag参数的0值。
#include
sem_t *sem_open(const char *name,int oflag,../*mode_t mode,unsigned int value*/);
//返回值:若成功,返回指向信号量的指针;若出错,返回SEM_FAILED
oflag
参数有O_CREAT
标志集时,若命名信号量不存在,则创建新的;若存在被利用。指定O_CREAT
需提供额为两个参数。
mode
参数指定谁可以访问信号量,取值和打开文件的权限位相同,赋值给信号量的权限可以被调用者的文件创建屏蔽字修改。只有读和写访问要紧,但是打开一个现有信号量不允许指定模式。实现经常为读和写打开信号量。
value参数用来指定信号量1的初始值,取值范围为0~SEM_VALUE_MAX
。
确保创建的是信号量,可以设置参数为O_CREAT|O_EXCL
,若信号量已经存在,会导致sem_open
失败。
//释放任何信号量相关资源
#include
int sem_close(sem_t *sem);
//进程没有首先调用sem_close而退出,那么内核将自动关闭任何打开的信号量。
//返回值:若成功,返回0;若出错,返回-1
例:创建有名信号量,运行命令行选项指定独占创建的-e和指定1一个初始值的-i。
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 ] " );
sem = sem_open(argv[optind], flags, FILE_MODE, value);
sem_close(sem);
exit(0);
}
//来销毁一个命名信号量。
#include
int sem_unlink(const char *name);
//如果没有打开的信号量引用,则该信号量会被销毁。否则,销毁将延迟到最后一个打开的引用关闭。
// 返回值:若成功,返回0;若出错,返回-1
例:删除一个有名信号量名字。
int main(int argc, char **argv)
{
if (argc != 2)
err_quit("usage: semunlink " );
em_unlink(argv[1]);
exit(0);
}
//来实现信号量的减1操作
#include
int sem_trywait(sem_t *sem);
int sem_wait(sem_t *sem);
//两个函数的返回值:若成功,返回0;若出错则,返回-1
使用sem_wait函数时,如果信号量是0则会发生阻塞,,直到成功使用信号量减1或被信号中断才返回。
sem_trywait函数可以避免阻塞,调用此函数时,如果信号量为0,则不会阻塞,而是返回-1并且将errno置为EAGAIN。
下列函数是sem_wait函数的一个变体,允许调用者为被阻塞的时间指定一个限制。
//阻塞一段确定的时间。
#include
#include
int sem_timedwait(sem_t *restrict sem, const struct timespec *restrict tsptr);
//放弃等待信号量时,用tsptr指定绝对时间,超时是基于CLOCK_REALTIME时钟的。
//信号量可以立即减1,则超时不重要了,尽管指定是过去某个时间,信号量减1操作仍然会成功。
//超时到期并且信号量计数没能减1,此函数将返回-1且errno设置为ETIMEFOUT。
//返回值:若成功,返回0;若出错,返回−1
例:打开一个信号量,调用sem_wait(信号量的当前值小于或等于0,则阻塞结束阻塞信号量减1)。
int main(int argc, char **argv)
{
sem_t *sem;
int val;
if (argc != 2)
err_quit("usage: semwait " );
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);
}
//使信号量值增1
#include
int sem_post(sem_t *sem);
//调用此函数时,在调用sem_wait(或者sem_timedwait)中发生进程阻塞,那么进程会被唤醒并且被sem_post增1的信号量计数会再次被sem_wait(或者sem_timedwait)减1。
//返回值:若成功,返回0;若出错,返回−1
例:挂出有名信号量,然后取得并输出该信号量的值。
int main(int argc, char **argv)
{
sem_t *sem;
int val;
if (argc != 2)
err_quit("usage: sempost " );
sem = sem_open(argv[1], 0);
Sem_post(sem);
Sem_getvalue(sem, &val);
printf("value = %d\n", val);
exit(0);
}
//来检索信号量值。
#include
int sem_getvalue(sem_t *restrict sem, int *restrict valp);
//成功valp指向的整数值将包含信号量值。
//注:试图使用刚读出的值信号量的值可能改变了,除非使用额外的同步机制来避免这种竞争。否则此函数只能用于调式。
//返回值:若成功,返回0;若出错,返回−1
//例:获取POSIX信号量值
#include
#include "tlpi_hdr.h"
int main(int argc, char *argv[])
{
sem_t *sem;
if (argc