概念
提供不同进程间或给定的一个特定进程的不同线程间的同步手段。
信号量本质上是一个计数器(不设置全局变量是因为进程间是相互独立的,而这不一定能看到,看到也不能保证++引用计数为原子操作),用于多进程对共享数据对象的读取,它和管道有所不同,它不以传送数据为主要目的,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。
有名信号量,是根据外部名字标识,通常指代文件系统中的某个文件,但不要求真正存放在文件系统的某个文件中(如果信号量的实现用到了映射文件);随内核持续,可用于进程或线程间的同步
基于内存的信号量,放在共享内存中,基于内存的信号灯,同步多线程时,可以放到该多线程所属进程空间里;如果是同步多进程,那就需要把信号灯放入到共享内存中(方便多个进程访问)。随进程持续
有名信号灯和基于内存的信号灯,具体区别体现在创建和销毁两个函数。有名信号灯使用sem_open和sem_close函数。基于内存的信号灯使用sem_init和sem_destroy函数。sem_init的参数可以控制是同步多线程,还是多进程;且该函数只能调用1次,因为调用后信号灯就存在了( 内存指针存在)。一般,使用基于内存的信号灯同步同进程多线程,使用有名信号灯同步多进程。
创建信号量:调用者指定一个初始值,对于二值信号量来说通常是1
等待一个信号量:该操作会测试这个信号量,如果其值小于等于0那么就等待阻塞,一旦值大于0就将他减一,访问同一信号量的其它线程或进程,在while测试其值然后将他减一(如果该值大于0)这两个步骤需为原子操作,这就使信号量成为内核系统调用,使其成为原子操作就变得容易
挂出一个信号量:将信号量的值加一
有名信量
一个信号量的状态可能包含在它本身的sem_t数据类型中,还包含其他的信息(如:文件描述符),知道该标识符的任何进程都可以访问该信号量,它的整数标识符只是告诉内核具体引用哪个信号量
不同进程(无论彼此有无亲缘关系)总是能够访问同一个有名信号量,只要他们在调用sem_open时指定相同的名字就行,即使对于某个特定的名字sem_open调用在每个进程中可能返回不同的指针,使用该指针的信号量函数(sem_post,sem_wait)引用的任然是同一个信号量
父子进程共享一个全局信号量
sem_t *mutex;//全局变量 mutex=sem_open(...); if((cpid=fork())==0) { sem_wait(mutex); } sem_post(mutex);
sem_open
创建并初始化信号灯,如果存在就返回存在的信号灯。
#include <semaphore.h> sem_t * sem_open(const char * name,int oflag,mode_t mode,unsigned int value); sem_t * sem_open(const char * name,int oflag);
name是给信号灯指定一个名字。oflag的值为O_CREAT,表示如果信号灯不存在,创建信号灯;为O_CREAT|O_EXCL,如果信号灯不存在报错。后面两个参数,只有新建信号灯时使用。mode为信号灯的权限(0644),value为信号灯的值。
返回值:成功时,返回信号灯的指针,错误返回SEM_FAILED
oflag:
- O_CREAT:若文件不存在则创建,创建文件时设定第三个参数和第四个参数,指定访问权限和初始值,初始值不能超过SEM_VALUE_MAX(通常为32767),二值信号量初始值通常为1,计数信号量初始值往往大于1,如果指定O_CREATE而没有指定O_EXCL当所需信号量尚未存在初始化,所需信号量已存在并不出错
- O_EXCL:将测试文件是否存在和创建文件组成原子操作,如果所需信号量已存在下指定O_CREATE|O_EXCL出错(这样的设计就不会出现两个进程同时创建覆盖的后果了);
- O_NONBLOCK:如果path引用的是FIFO、块特殊文件或字符特殊文件,将I/O操作设置为非阻塞状态;
默认都具备读写权限不需要指定O_RNONLY 和O_WRONLY,因为信号量的挂出与等待操作都要改变信号量的值,不具备读写权限的信号量会导致sem_open返回EACCES(访问权限不符合)
sem_close
关闭引用信号灯,信号灯引用计数减1。
#include <semaphore.h>
int sem_close(sem_t * sem)
参数:sem为信号灯的指针 返回值:成功时,返回0,失败,-1 注:每个信号灯有一个引用计数器记录当前打开次数.关闭一个信号灯并没有将它从系统中删除因为他是随内核持续的,而是信号灯引用计数减1,进程的终止都会引发此操作(自愿终止exit或非自愿终止),不显示调用也可以
可以为每个线程调用该函数,这一操作终止进程时会自动发生,然而删除一个有名信号量必须显示完成
sem_unlink
信号灯引用计数为大于0时,name从系统中删除,而其信号量的析构要等到最后一个sem_close发生。
#include <semaphore.h> int sem_close(const char *name)
参数:name为信号灯的外部名字 返回值:成功时,返回0,失败,-1,需要显示调用
sem_wait/sem_try_wait
等待共享资源,信号灯值为0就睡眠直到该值大于0时再将它减一,信号灯值大于0,就使用共享资源,信号灯值减1。sem_trywait当信号灯值为0时,不睡眠,返回EAGAIN;sem_wait检测死锁并会返回EDEADLK,过早返回错误为EINTR.
#include <semaphore.h> int sem_wait(sem_t *sem) int sem_trywait(sem_t *sem)
参数:sem为信号灯指针 返回值:成功时,返回0,失败,-1
当持有某个信号量的锁的进程没有释放它就终止时,内核并不给该信号量解锁
sem_getvalue
获得信号灯当前值
#include <semaphore.h> int sem_getvalue(sem_t *sem,int *valp)
参数:sem为信号灯指针,valp为信号灯的值 返回值:成功时,返回0,失败,-1,valp返回信号量当前值,如果当前信号量以上锁,返回0或返回某个负数,其绝对值等于等待该信号量解锁的线程数
sem_post
#include <semaphore.h>
int sem_post(sem_t *sem)
参数:sem为信号灯指针 返回值:成功时,返回0,失败,-1
任何线程都可以挂出一个信号,即使当前没有线程在该信号上阻塞,然而如果某个线程调用pthread_cond_signal,当时没有任何线程阻塞在pthread_cond_wait上时,那么发送相应的条件变量的信号将丢失
互斥锁,信号量条件变量区别
- 互斥锁必须总是由锁住他的线程解锁,信号量的挂出不必由执行过它的等待操作的同一线程执行
- 互斥锁要么被锁住要没被解开
- 既然信号量有一个与之关联的状态(它的计数值)那么信号量的挂出总是被记住,然而向一个条件变量上发送信号时,如果没有线层等待该条件变量那么信号将丢失
总结:
当某个进程持有信号量的锁时进程没有释放他就终止,内核并不给信号量解锁,这与锁不同,当进程持有某个记录的锁没有释放就终止,内核自动释放。
能够从信号处理程序中安全调用的唯一函数时sem_post,互斥锁是为上锁而优化,条件变量为等待而优化,信号量即可用于上锁,也可用于等待,01信号量就相当于互斥锁
sem_wait在一个信号灯上等待(以锁住一个资源或等待一个时间),如果信号灯计数比0大,sem_wait函数将计数器减一并马上返回,否则,线程阻塞,一个阻塞的线程通过sem_post来发布一个信号灯(开锁一个资源或等待一个线程),
如果一个以上的线程在信号灯上等待,sem_post将唤醒其中一个(优先级最高的或最早等待的线程),如果没有线程在等待,信号灯计数器被加一。
假如有个代码段,期望最多有两个线程同时在里面执行(其他线程必须等待),可以使用一个信号灯而不需要附加任何状态,初始化信号灯值为2,然后再代码段开始调用sem_wait,结尾处调用sem_post,然后两个线程可以再信号登上等待而不被阻塞,
但是第三个线程将发现信号灯计数器为0,并且阻塞,每个系那成退出代码区时,他信号灯释放一个等待吸纳城(如果有的话恢复计数器)。
不同进程(不管是否彼此有无亲缘关系),他们都可以访问同一个信号灯,只是需要在sem_open的时候传入的名字是一样就行。在有亲缘关系时,Posix中的fork如是描述,在父进程中打开的任何信号灯,仍应在子进程中打开。
生产者与消费者问题:
#include "unpipc.h" #define BUFF 10 #define SEM_MUTEX "/tmpmutex" #define SEM_NEMPTY "/tmpnempty" #define SEM_NSTORED "/tmpnstored" struct shared { int buffer[BUFF]; sem_t *mutex,*nempty,*nstored; }shared; void *produce(void *arg) { int i; for(i=0;i<BUFF;++i) { sem_wait(shared.nempty); sem_wait(shared.mutex); shared.buffer[i%BUFF]=i; sem_post(shared.mutex); sem_post(shared.nstored); } return NULL; } void *consume(void *arg) { int i; for(i=0;i<BUFF;++i) { sem_wait(shared.nstored);//这两个wait交换会造成死锁 sem_wait(shared.mutex); printf("shared.buffer[%d]= %d\n",i,shared.buffer[i%BUFF]); sem_post(shared.mutex); sem_post(shared.nempty); } return NULL; } int main() { pthread_setconcurrency(2); pthread_t pid_consume,pid_produce; /*每个信号量都需要正确的初始化,如果先前建立的信号里因本程序终止没有 * 删除,那么,可以在sem_open之前调用sem_unlink,并忽略任何错误,也 * 可以指定O_EXCL,检查是否返回EEXIST错误,若是,调用sem_unlink,并 * 且再次调用sem_open。如果想验证本程序只有一个副本在运行,可以调用 * fcntl文件锁 */ shared.mutex=sem_open(SEM_MUTEX,O_CREAT|O_EXCL,FILE_MODE,1); shared.nempty=sem_open(SEM_NEMPTY,O_CREAT|O_EXCL,FILE_MODE,BUFF); shared.nstored=sem_open(SEM_NSTORED,O_CREAT|O_EXCL,FILE_MODE,BUFF); pthread_create(&pid_produce,NULL,produce,NULL); pthread_create(&pid_consume,NULL,consume,NULL); pthread_join(pid_produce,NULL); pthread_join(pid_consume,NULL); sem_unlink(SEM_MUTEX); sem_unlink(SEM_NEMPTY); sem_unlink(SEM_NSTORED); exit(0); }
基于内存的信号量
#include <semaphore.h> int sem_init(sem_t * sem,int shared,unsigned int value); int sem_destroy(sem_t * sem)
参数:sem为指向应用程序必须分配的sem_t变量,shared是指同步多线程还是多进程(0:同一进程间的线程共享;其他:进程间共享,该信号量必须放在在某个类型的共享内存中,而即将使用他的所有进程都要能访问共享内存),value为信号量值 返回值:成功时,不返回0,失败时,返回-1
注意:
- 必须保证该函数只调用一次,对一个已经初始化的信号量调用sem_init结果是未定义的。
- sem_open与sem_init:前者返回一个指向某个sem_t变量的指针,该变量由sem_open函数本身分配并初始化;后者的第一个参数时指向某个sem_t变量的指针,该变量由调用者分配,然后由sem_int初始化
- 一个基于内存的信号量,只有sem_init的参数指向的位置可用于访问该信号量,使用他的sem_t数据类型副本访问时为定义的结果
- 只要某个基于内存的信号量的内存区保持有效,该信号量就一直存在,随进程持续,进程终止其消失
- 基于内存的信号量在不同进程间共享,该信号量必须放在共享内存区中,只要该共享内存存在,信号量就一直存在
- fork的子进程通常不共享父进程内存空间,所以不能用sem_init
- 在父进程中打开的信号量在子进程中仍然打开
#include "unpipc.h" #include <pthread.h> #include <string.h> #define BUFF 10 struct shared { struct buff { char data[BUFFSIZE]; size_t n; }buffer[BUFF]; sem_t mutex,nempty,nstored; }shared; int fd; void *produce(void *arg) { int i=0; while(1) { //printf("fd is:%d\n",fd); sem_wait(&shared.nempty); sem_wait(&shared.mutex); //如果数据缓冲区要是在一个连表上维护,此处是链表中移除某个缓冲区的 //地方,把该操作放在临界区是避免生产者消费者对链表操作发生冲突 sem_post(&shared.mutex); shared.buffer[i].n=read(fd,shared.buffer[i].data,BUFFSIZE); if(shared.buffer[i].n==0)//读取结束 { sem_post(&shared.nstored); return NULL; } if(++i>=BUFF) i=0;//循环缓冲 sem_post(&shared.nstored); } return NULL; } void *consume(void *arg) { int i=0; while(1) { sem_wait(&shared.nstored); sem_wait(&shared.mutex); //同上 sem_post(&shared.mutex); if(shared.buffer[i].n==0) return NULL; write(1,shared.buffer[i].data,shared.buffer[i].n); if(++i>BUFF) i=0; sem_post(&shared.nempty); } return NULL; } int main() { printf("请输入文件名\n"); char pathname[100]; fgets(pathname,100,stdin); int len=strlen(pathname); if(pathname[len-1]=='\n') pathname[len-1]='\0'; fd=open(pathname,O_RDONLY); //printf("pathname:%s",pathname); //printf("fd is main: %d\n",fd); sem_init(&shared.mutex,0,1); sem_init(&shared.nempty,0,BUFF); sem_init(&shared.nstored,0,0); pthread_setconcurrency(2); pthread_t pid_produce,pid_consume; pthread_create(&pid_consume,NULL,consume,NULL); pthread_create(&pid_produce,NULL,produce,NULL); //pthread_create(&pid_consume,NULL,consume,NULL); pthread_join(pid_produce,NULL); pthread_join(pid_consume,NULL); sem_destroy(&shared.mutex); sem_destroy(&shared.nempty); sem_destroy(&shared.nstored); exit(0); }
代码github:https://github.com/tianzengBlog/test/tree/master/ipc2/sem
信号量的限制
在<unistd,h>中定义,也可运行时通过sysconf获取
SEM_NSEMS_MAX:一个进程可以最大打开的信号量数(posix要求至少为256)
SEM_VALUE_MAX:一个信号量的最大值(posix至少要求为32767)