一、概述
- 信号量( semaphore)是一种用于提供不同进程间或一个给定进程的不同线程间同步手段。
- 信号量有三种类型:
① Posⅸ有名信号量:可用于进程或线程间的同步。
② Posix基于内存的信号量(无名信号量) 存放在内存区中,可用于进程或线程间的同步。常用于多线程间同步。
③ System V信号量(IPC机制):在内核中维护,可用于进程或线程间的同步。 常用于进程间同步。 - 有名信号量通过文件系统中的路径名对应的文件名进行维护(信号量只是通过文件名进行标识,信号量的值并不存放到这个文件中,除非信号量存放在空间映射到这个文件上)。
- 无名信号量通过用户空间内存进行维护,无名信号量要想在进程间通信,该内存必须为共享内存区。
- System V信号量由内核进行维护。
- 信号量可分为:
① 二值信号量( binary semaphore):其值或为0或为1的信号量。这与互斥锁类似,若资源被锁住则信号量值为0,若资源可用则信号量值为1。
② 计数信号量( counting semaphore):其值在0和某个限制值(对于Posiⅸ信号量,该值必须至少为32767)之间的信号量。信号量的值就是可用资源数。 - Posix信号量为单个计数信号量,System V信号量为计数信号量集,偏集合的概念。
- 由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。 - 除可以像互斥锁那样使用外,信号量还有一个互斥锁没有提供的特性:互斥锁必须总是由锁住它的线程解锁,信号量的挂出却不必由执行过它的等待操作的同一线程执行。
二、System V信号量
2.1 简介
- 常用于进程间同步,也可以用于线程间
- 信号量的使用规则流程:
(1)检测控制该资源的信号量。
(2)若信号量的值为正,则进程可以使用该资源。然后将信号量值减 1,表示它使用了一个资源单位。此进程
使用完共享资源后对应的信号量应该加 1。以便其他进程使用。
(3)若对信号量进行减一时,信号量的值为 0,则进程进入阻塞休息状态,直至信号量值大于 0。进程被唤醒,
返回第(1)步。
为了正确地实现信号量,信号量值的测试及减 1 操作应当是原子操作(原子操作是不可分割的,在执行完毕
不会被任何其它任务或事件中断)。为此信号量通常是在内核中实现的
- 内核为每个信号量集合维护的 struct semid_ds
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct semid_ds
{
struct ipc_perm sem_perm; /* permissions .. see ipc.h */
__kernel_time_t sem_otime; /* last semop time */
__kernel_time_t sem_ctime; /* last change time */
struct sem *sem_base; /* ptr to first semaphore in array */
struct sem_queue *sem_pending; /* pending operations to be processed */
struct sem_queue **sem_pending_last; /* last pending operation */
struct sem_undo *undo; /* undo requests on this array */
unsigned short sem_nsems; /* no. of semaphores in array */
};
/* One semaphore structure for each semaphore in the system. */
struct sem
{
int semval; /* current value *信号量的值*/
int sempid; /* pid of last operation *最后一个操作信号量的进程号*/
struct list_head sem_pending; /* pending single-sop operations */
};
2.2 接口
(1) semget
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
/* @function 创建一个信号量集或访问一个已存在的信号量集。
* @param[in] key 长整型(唯一非零),系统建立IPC通讯 ( 消息队列、 信号量和 共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到,由内核变成标识符,要想让两个进程看到同一个信号集,只需设置key值不变就可以。
* @param[in] nsems 指定集合中的信号量数,通常为1。如果我们不创建一个新的信号量集,而只是访问一个已存在的集合,那就可以把该参数指定为0。一且创建完一个信号量集,我们就不能改变其中的信号量
* @param[in] flags 一组标志,当想要当信号量不存在时创建一个新的信号量,可以将flag设置为IPC_CREAT与文件权限做按位或操作。
* 设置了IPC_CREAT标志后,即使给出的key是一个已有信号量的key,也不会产生错误。
* 而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。也可以是SEM_R和ISEM_A常值的组合。
* @return 成功:信号量标识符,失败: -1
*/
int semget(key_t key, int nsems, int flags)
(2) semctl
/* @function 用来直接控制信号量信息
* @param[in] semid 由semget返回的信号量标识符
* @param[in] semnum 为要进行操作的集合中信号量的编号,当要操作到成组的信号量时,
* 从 0 开始。一般取值为 0,表示这是第一个也是唯一的一个信号量
* @param[in] cmd
* @arg IPC_RMID(立即删除信号集,唤醒所有被阻塞的进程)
* @arg GETVAL(根据semun 指定的编号返回相应信号的值, 此时该函数返回值就是你要获得的信号量的值,不再是 0 或-1)
* @arg SETVAL(根据 semun 指定的编号设定相应信号的值)
* @arg GETALL(获取所有信号量的值,此时第二个参数为 0,并且会将所有信号的值存入 semun.array 所指向的数组的各个元素中,此时需要用到第四个参数 union semun arg)
* @arg SETALL(将 semun.array 指向的数组的所有元素的值设定到信号量集中,此时第二个参数为 0,此时需要用到第四个参数 union semun arg)
* @return 成功 0, 失败 -1
*/
int semctl(int semid, int semnum, int cmd, ...)
//第四个参数一般为是一个 union semun 类型(具体的需要由程序员自己定义),一般包含以下几个成员:
union semun {
int val; //使用的值
struct semid_ds *buf; //IPC_STAT、IPC_SET 使用的缓存区
unsigned short *arry; //GETALL,、SETALL 使用的数组
struct seminfo *__buf; // IPC_INFO(Linux特有) 使用的缓存区
};
(3) semop
struct sembuf{
short sem_num; //要操作的信号量在信号量集中的编号,第一个信号量的编号是 0。
short sem_op; //sem_op 成员的值是信号量在一次操作中需要改变的数值。通常只会用到两个值,一个是-1,也就是 p(减一)操作,它等待信号量变为可用;一个是+1,也就是 v(加一)操作,它发送信号通知信号量现在可用。
short sem_flg; //通常设为:SEM_UNDO,程序结束,信号量为 semop 调用前的值。
};
/* @function 对semid标识的信号量集中的一个信号量或多个信号量进行操作
* @param[in] semid 由 semget 返回的信号量标识符
* @param[in] sops 指向一个结构体数组的指针。可以指向单个或者多个结构体变量(即结构体数组)。
* @param[in] nsops 为sops 指向的 sembuf 结构数组的长度
* @return 成功 0, 失败 -1
*/
int semop(int semid, struct sembuf *sops, size_t nsops)
2.3 示例
参考:https://www.cnblogs.com/fangshenghui/p/4039946.html
- 这位例程写得挺好的,借鉴参考一下
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#include<errno.h>
#include<string.h>
#include<stdlib.h>
#include<assert.h>
#include<time.h>
#include<unistd.h>
#include<sys/wait.h>
#define MAX_SEMAPHORE 10
#define FILE_NAME "test2.c"
union semun{
int val ;
struct semid_ds *buf ;
unsigned short *array ;
struct seminfo *_buf ;
}arg;
struct semid_ds sembuf;
int main()
{
key_t key ;
int semid ,ret,i;
unsigned short buf[MAX_SEMAPHORE] ;
struct sembuf sb[MAX_SEMAPHORE] ;
pid_t pid ;
pid = fork() ;
if(pid < 0)
{
/* Create process Error! */
fprintf(stderr,"Create Process Error!:%s\n",strerror(errno));
exit(1) ;
}
if(pid > 0)
{
/* in parent process !*/
key = ftok(FILE_NAME,'a') ;
if(key == -1)
{
/* in parent process*/
fprintf(stderr,"Error in ftok:%s!\n",strerror(errno));
exit(1) ;
}
semid = semget(key,MAX_SEMAPHORE,IPC_CREAT|0666); //创建信号量集合
if(semid == -1)
{
fprintf(stderr,"Error in semget:%s\n",strerror(errno));
exit(1) ;
}
printf("Semaphore have been initialed successfully in parent process,ID is :%d\n",semid);
sleep(2) ;
printf("parent wake up....\n");
/* 父进程在子进程得到semaphore的时候请求semaphore,此时父进程将阻塞直至子进程释放掉semaphore*/
/* 此时父进程的阻塞是因为semaphore 1 不能申请,因而导致的进程阻塞*/
for(i=0;i<MAX_SEMAPHORE;++i)
{
sb[i].sem_num = i ;
sb[i].sem_op = -1 ; /*表示申请semaphore*/
sb[i].sem_flg = 0 ;
}
printf("parent is asking for resource...\n");
ret = semop(semid , sb ,10); //p()
if(ret == 0)
{
printf("parent got the resource!\n");
}
/* 父进程等待子进程退出 */
waitpid(pid,NULL,0);
printf("parent exiting .. \n");
exit(0) ;
}
else
{
/* in child process! */
key = ftok(FILE_NAME,'a') ;
if(key == -1)
{
/* in child process*/
fprintf(stderr,"Error in ftok:%s!\n",strerror(errno));
exit(1) ;
}
semid = semget(key,MAX_SEMAPHORE,IPC_CREAT|0666);
if(semid == -1)
{
fprintf(stderr,"Error in semget:%s\n",strerror(errno));
exit(1) ;
}
printf("Semaphore have been initialed successfully in child process,ID is:%d\n",semid);
for(i=0;i<MAX_SEMAPHORE;++i)
{
/* Initial semaphore */
buf[i] = i + 1;
}
arg.array = buf;
ret = semctl(semid , 0, SETALL,arg);
if(ret == -1)
{
fprintf(stderr,"Error in semctl in child:%s!\n",strerror(errno));
exit(1) ;
}
printf("In child , Semaphore Initailed!\n");
/* 子进程在初始化了semaphore之后,就申请获得semaphore*/
for(i=0;i<MAX_SEMAPHORE;++i)
{
sb[i].sem_num = i ;
sb[i].sem_op = -1 ;
sb[i].sem_flg = 0 ;
}
ret = semop(semid , sb , 10);//信号量0被阻塞
if( ret == -1 )
{
fprintf(stderr,"子进程申请semaphore失败:%s\n",strerror(errno));
exit(1) ;
}
printf("child got semaphore,and start to sleep 3 seconds!\n");
sleep(3) ;
printf("child wake up .\n");
for(i=0;i < MAX_SEMAPHORE;++i)
{
sb[i].sem_num = i ;
sb[i].sem_op = +1 ;
sb[i].sem_flg = 0 ;
}
printf("child start to release the resource...\n");
ret = semop(semid, sb ,10) ;
if(ret == -1)
{
fprintf(stderr,"子进程释放semaphore失败:%s\n",strerror(errno));
exit(1) ;
}
ret = semctl(semid ,0 ,IPC_RMID);
if(ret == -1)
{
fprintf(stderr,"semaphore删除失败:%s!\n",strerror(errno));
exit(1) ;
}
printf("child exiting successfully!\n");
exit(0) ;
}
return 0;
}
三、Posix 信号量
3.1 简介
- POSIX 信号量标准定义了有名信号量和无名信号量(基于内存的信号量)两种
- 常用于线程间的同步互斥操作
3.2 接口
- 有名信号量创建与销毁
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>
/* @function 创建一个新的有名信号量或打开一个已存在的有名信号量。有名信号量总是既可用于线程间的同步,又可用于进程间的同步
* @param[in] name
* @param[in] oflag 可以是0、O_CREAT或O_ CREAT|O_EXCL,如果指定了O_CREAT,需要第三、四个参数
* @param[in] mode 读写权限 eg:0666
* @param[in] value 指定信号量的个数
* @return 成功 信号量标识, 失败 -1
*/
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
/* @function 关闭信号量
* @param[in] psem sem_open的返回值
* @return 成功 0, 失败 -1
*/
int sem_close(sem_t *psem);
//关闭一个信号量并没有将它从系统中刑除。
//这就是说, Posix有名信号量至少是随内核持续的:即使当前没有进程打开着某个信号量,它的值仍然保持。
//有名信号量使用sem_ unlink从系统中删除。
/* @function 销毁信号量
* @param[in] name 信号量关联的文件
* @return 成功 0, 失败 -1
*/
int sem_unlink(const char *name);
//每个信号量有一个引用计数器记录当前的打开次数(就像文件一样)
//sem_unlink类似于文件I/O的 unlink函数:当引用计数还是大于0时,name就能从文件系统中删除.
//然而其信号量的析构(不同于将它的名字从文件系统中删除)却要等到最后一个sem_close发生时为止
- 无名信号量创建与销毁
#include <semaphore.h>
/* @function 初始化sem_t 变量的值。
* @param[out] sem sem_t变量的地址
* @param[in] pshared 信号量的作用域
* @arg: 为0时,信号量在同一进程的多线程间是共享的,用于线程间
* @arg: 非0时,信号量用于多进程间,且分配的sem_t内存必须处于多进程的共享内存区
* @param[in] value 信号量的初始值
* @return 成功 信号量标识, 失败 -1
*/
int sem_init(sem_t *sem, int pshared, unsigned int value);
/* @function 销毁sem指向的信号量,并释放所占用的资源
* @param[in] sem 指向信号量的指针
* @return 成功 信号量标识, 失败 -1
*/
int sem_destroy(sem_t * sem);
– sem_open与sem_init的区别:
前者分配内存并初始化,然后返回一个指向该信号量的地址,后者只对已经存在的sem_t变量进行初始化。
/*基于文件的有名信号量初始化*/
sem_t *sem_p;
sem_p = sem_open("test.c", O_ CREAT|O_EXCL, 0666, 1);
/*基于内存的无名信号量初始化*/
sem_t sem;
sem_init(&sem, 0, 1);
//尽量避免sem_init对同一个信号量多次初始化,因为对初始化过的信号量再进行初始化,结果是未知的
- P 操作(申请资源或叫等待资源)
– sem walt函数测试所指定信号量的值,如果该值大于0,那就将它减1并立即返回。如果该值等于0,调用线程就被投入睡眠中,直到该值变为大于0,这时再将它减1,函数随后返回。我们以前提到过,考虑到访问同一信号量的其他线程,“测试并减1”操作必须是原子的。
#include <semaphore.h>
/* @function 阻塞式等待信号量的值大于0时,并减1,线程执行后续操作,如果为0,使线程进入睡眠
* @param[in] sem 指向信号量的指针或信号量的地址值
* @return 成功 0, 失败 -1
* @note 如果被某个信号中断, sem wait就可能过早地返回,所返回的错误为EINTR
*/
int sem_wait(sem_t *sem);
/* @function 非阻塞式等待信号量,没有等到不会进入睡眠,会返回一个EAGAIN错误
* @param[in] sem 指向信号量的指针或信号量的地址值
* @return 成功 0, 失败 -1
*/
int sem_trywait(sem_t *sem);
/* @function 定时阻塞等待信号量,时间到达没有等到不会进入睡眠,会返回一个ETIMEDOUT错误
* @param[in] sem 指向信号量的指针或信号量的地址值
* @param[in] abs_timeout 设置时间
* @return 成功 0, 失败 -1
*/
struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
};
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
- V 操作(释放资源)
/* @function 增加(解锁)sem指向的信号量,+1,唤醒正在等待该信号量值为正数的任一线程。
* @param[in] sem 指向信号量的指针或信号量的地址值
* @return 成功 0, 失败 -1
*/
sem_post( sem_t *psem ); // V 操作(加一)
- 其他
/* @function 得到信号量当前值
* @param[in] sem 指向信号量的指针或信号量的地址值
* @param[out] sval 由sval 指向的整数中返回所指定信号量的当前值
* @return 成功 0, 失败 -1
* @note 如果该信号量当前已上锁,那么返回值或为0,或为某个负数,其绝对值就是等待该信号量解锁的线程数
*/
int sem_getvalue(sem_t * sem, int * sval);
3.3 示例
- 售票系统
#include<pthread.h>
#include<stdio.h>
#include <semaphore.h>
int ticketcount = 10;
sem_t lock;
void *pthread1(void *args)
{
for(;;) {
sem_wait(&lock); //因为要访问全局的共享变量 ticketcount,所以就要加锁
if(ticketcount > 0) {
printf("windows1 start sale ticket!the ticket is:%d\n",ticketcount);
ticketcount --;//则卖出一张票
sleep(3);
printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);
} else {
sem_post(&lock);
break;
}
sem_post(&lock);
sleep(1); //要放到锁的外面
}
pthread_exit(NULL);
}
void *pthread2(void *args)
{
for(;;) {
sem_wait(&lock); //因为要访问全局的共享变量 ticketcount,所以就要加锁
if(ticketcount > 0) {//如果有票
printf("windows2 start sale ticket!the ticket is:%d\n",ticketcount);
ticketcount --;//则卖出一张票
sleep(3);
printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);
} else {
sem_post(&lock);
break;
}
sem_post(&lock);
sleep(1); //要放到锁的外面
}
pthread_exit(NULL);
}
int main(int argc, char **argv)
{
pthread_t pthid1,pthid2;
sem_init(&lock,0,1); //信号灯值初始为 1,表示资源可用
pthread_create(&pthid1,NULL,pthread1,NULL);
pthread_create(&pthid2,NULL,pthread2,NULL);
pthread_join(pthid1,NULL);
pthread_join(pthid2,NULL);
sem_destroy(&lock);
exit(0);
}
四、总结
-
我们现在看到了互斥锁、条件变量和信号量之间的更多差别:
① 首先,互斥锁必须总是由给它上锁的线程解锁。信号量没有这种限制:一个线程可以等待某个给定信号量(譬如说将该信号量的值由1减为0,这跟给该信号量上锁一样),而另一个线程可以挂出该信号量(如说将该信号量的值由0增为1,这跟给该信号量解锁一样)。
② 其次,既然每个信号量有一个与之关联的值,它由挂出操作加1,由等待操作减1,那么任何线程都可以挂出一个信号(譬如说将它的值由0增为1),即使当时没有线程在等待该信号量值变为正数也没有关系。然而,如果某个线程调用了 pthread_cond_ signa,不过当时没有任何线程阻塞在 othread_ cond wait调用中,那么发往相应条件变量的信号将丢失。
③ 最后,在各种各样的同步技巧(互斥锁、条件变量、读写锁、信号量)中,能够从信号处理程序中安全调用的唯一函数是 sem post。 -
我们已看过的所有同步原语(互斥锁、条件变量、读写锁、信号量以及记录上锁)都有它们各自的位置。对于一个给定应用我们已有很多选择,因而需要了解各种原语之间的差别,还要从刚刚列出的比较中意识到的是,互斥锁是为上锁而优化的,条件变量是为等待而优化的,信号量既可用于上锁,也可用于等待,因而可能导致更多的开销和更高的复杂性。
(1) 互斥锁必须总是由给它上锁的线程解锁,信号量的挂出却不必由执行过它的等待操作的同一线程执行。这是我们的例子刚展示过的。
(2) 互斥锁要么被锁住,要么被解开(二值状态,类似于二值信号量)。
(3) 既然信号量有一个与之关联的状态(它的计数值),那么信号量挂出操作总是被记住。然而当向一个条件变量发送信号时,如果没有线程等待在该条件变量上,那么该信号将丢失。