5 信号量
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。
- 背景
数据竞争 - 作用:
控制多进程共享资源的访问(资源有限并且不共享) - 本质
任一时刻只能有一个进程访问临界区(代码),数据更新的代码。 - 基本操作:PV
原子操作操作也被成为PV原语(P来源于Dutchproberen"测试",V来源于Dutchverhogen"增加")
- 信号
分类 | 取值 |
---|---|
P(信号量) | 0 :挂起进程;>0 :减1 |
V(信号量) | 0 :恢复进程;>0 :加1 |
- POSIX信号量
查看:man sem_overview
5.1 接口
- 头文件:
semaphore.h
- 库:
pthread
- 分类
信号量分为命名信号量(基于文件)与匿名信号量(基于内存)两种。
5.2 命名信号量/基于文件
操作 | 函数 |
---|---|
创建 | sem_t *sem_open(const char *name, int oflag, mode_t mode,unsigned int value) |
删除 | int sem_unlink(const char *name) |
打开 | sem_t *sem_open(const char *name, int oflag) |
关闭 | int sem_close(sem_t *sem) |
挂出 | int sem_post(sem_t *sem) |
等待 | int sem_wait(sem_t *sem) |
尝试等待 | int sem_trywait(sem_t *sem) |
获取信号量的值 | int sem_getvalue(sem_t *sem, int *sval) |
5.2.1创建
sem_t *sem_open(const char *name, int oflag, mode_t mode,unsigned int value)
- 参数
参数 | 含义 |
---|---|
name | 信号量IPC名字 |
oflag | 标志 |
mode | 权限位 |
value | 信号量初始值 |
- 返回值
返回值 | 含义 |
---|---|
非SEM_FAILED | 信号量的指针 |
SEM_FAILED | 出错 |
5.2.2 删除
int sem_unlink(const char *name)
-
参数:
name
: 信号量IPC名字 -
返回值
返回值 | 含义 |
---|---|
-1 | 出错 |
0 | 成功 |
5.2.3 打开
sem_t *sem_open(const char *name, int oflag)
- 参数:
参数 | 含义 |
---|---|
name | 信号量IPC名字 |
oflag | 标志 |
- 返回值
返回值 | 含义 |
---|---|
非SEM_FAILED | 信号量的指针 |
SEM_FAILED | 出错 |
5.2.4 关闭
int sem_close(sem_t *sem)
-
参数:
sem
:信号量的指针 -
返回值
返回值 | 含义 |
---|---|
-1 | 出错 |
0 | 成功 |
5.2.5 挂出
int sem_post(sem_t *sem)
-
参数:
sem
:信号量的指针 -
返回值
返回值 | 含义 |
---|---|
-1 | 出错 |
0 | 成功 |
5.2.6 等待
int sem_wait(sem_t *sem)
-
参数:
sem
:信号量的指针 -
返回值
返回值 | 含义 |
---|---|
-1 | 出错 |
0 | 成功 |
5.2.7 尝试等待
int sem_trywait(sem_t *sem)
-
参数:
sem
:信号量的指针 -
返回值
返回值 | 含义 |
---|---|
-1 | 出错 |
0 | 成功 |
5.2.8 获取信号量的值
int sem_getvalue(sem_t *sem, int *sval)
- 参数:
参数 | 含义 |
---|---|
sem | 信号量的指针 |
sval | 信号量的值 |
- 返回值
返回值 | 含义 |
---|---|
-1 | 出错 |
0 | 成功 |
5.3 匿名信号量/基于内存
操作 | 函数 |
---|---|
初始化 | int sem_init (sem_t *sem , int pshared, unsigned int value) |
销毁 | int sem_destroy(sem_t *sem) |
挂出 | int sem_post(sem_t *sem) |
等待 | int sem_wait(sem_t *sem) |
尝试等待 | int sem_trywait(sem_t *sem) |
获取信号量的值 | int sem_getvalue(sem_t *sem, int *sval) |
注:其中挂出和等待操作与命名信号量相同。
5.3.1 初始化
int sem_init (sem_t *sem , int pshared, unsigned int value)
- 参数
参数 | 含义 |
---|---|
sem | 信号量的指针 |
pshared | 共享方式。0:线程间共享;1:进程间共享,需要共享内存 |
value | 信号量初始值 |
- 返回值
返回值 | 含义 |
---|---|
-1 | 出错 |
0 | 成功 |
5.3.2 销毁
int sem_destroy(sem_t *sem)
-
参数:
sem
:信号量的指针 -
返回值
返回值 | 含义 |
---|---|
-1 | 出错 |
0 | 成功 |
5.4 命名信号量 vs. 匿名信号量
-
功能区别:
1、命名信号量一般是用在进程间同步,匿名信号量一般用在线程间同步。
2、命名信号量,其值保存在文件中, 所以它可以用于线程也可以用于进程间的同步。
3、匿名信号量,其值保存在内存中。 -
代码区别:
主要在于两种信号量初始化和销毁的方式不同。
5.5 实例
5.5.1 实例1:父子进程出现数据竞争
#include <iostream>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <cstdio>
#include <fcntl.h>
using namespace std;
int main(){
int* p = (int*)mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
if(MAP_FAILED == 0){
perror("mmap error");
return 1;
}
*p = 0;
if(fork()){
for(int i = 0;i < 10000;++i){
cout << "parent:" << p << ":" << ++*p << endl;
}
wait(NULL);
}else{
for(int i = 0;i < 10000;++i){
cout << "child:" << p << ":" << --*p << endl;
}
}
munmap(p,sizeof(int));
p = NULL;
}
数据竞争导致父子进程访问同一片内存时出错。
parent:0x7f933cc16000:3880
parent:0x7f933cc16000:3881
parent:0x7f933cc16000:3882
parent:0x7f933cc16000:3883
parent:0x7f933cc16000:3884
child:0x7f933cc16000:3723
child:0x7f933cc16000:3884
child:0x7f933cc16000:3883
child:0x7f933cc16000:3882
解决方案:
(1)加入命名信号量,同一时间只允许单个进程进行读写操作。
#include <iostream>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <cstdio>
#include <fcntl.h>
#include <semaphore.h>
#include <sstream>
using namespace std;
int main() {
sem_t* psem = sem_open("/sem.test",O_CREAT|O_RDWR,0666,1);
if(SEM_FAILED == psem) {
perror("sem_open error");
return 1;
}
int* p = (int*)mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
if(MAP_FAILED == p) {
perror("mmap error");
return 1;
}
*p = 0;
fork();
for(int i = 0; i < 10000; ++i) {
sem_wait(psem); //等待,本线程不结束,则下一线程无法进入
ostringstream oss;
oss << getpid() << " :"<< --*p << endl;
string s = oss.str();
write(STDOUT_FILENO,s.c_str(),s.size()+1);
sem_post(psem); //挂出
}
munmap(p,sizeof(int));
sem_close(psem);
psem = NULL;
p = NULL;
}
(2)匿名信号量的方式。
#include <iostream>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <cstdio>
#include <fcntl.h>
#include <semaphore.h>
#include <sstream>
using namespace std;
int main() {
sem_t* psem = (sem_t*)mmap(NULL,sizeof(sem_t),PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
sem_init(psem,1,1);//初始化,前面的1表示进程之间共享,后面1表示值
if(SEM_FAILED == psem) {
perror("sem_open error");
return 1;
}
int* p = (int*)mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
if(MAP_FAILED == p) {
perror("mmap error");
return 1;
}
*p = 0;
fork();
for(int i = 0; i < 2000; ++i) {
sem_wait(psem);
ostringstream oss;
oss << getpid() << " :"<< --*p << endl;
string s = oss.str();
write(STDOUT_FILENO,s.c_str(),s.size()+1);
sem_post(psem);
}
munmap(p,sizeof(int));
sem_destroy(psem);
psem = NULL;
p = NULL;
}
注:
1、编译时加-lrt -pthread
2、父子进程之间可以用匿名方式,不同进程之间只能用命名方式。
5.5.2 实例2:加入信号量的read.cpp
#include <iostream>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/shm.h>
#include <fcntl.h>
#include <semaphore.h>
using namespace std;
int main(int argc,char* argv[]){
if(2 != argc){
cout << "arguement error" << endl;
cout << "Usage: " << argv[0] << "shm file" << endl;
return 1;
}
int fd = shm_open(argv[1],O_RDWR|O_CREAT,0666);
if(-1 == fd){
perror("open error");
return 1;
}
sem_t* psem = sem_open("/sem.test",O_RDWR,0666,1);
int* p = (int*)mmap(NULL,sizeof(int),PROT_WRITE|PROT_READ,MAP_SHARED,fd,0);
if(MAP_FAILED == p){
perror("map error");
return 1;
}
for(int i = 0;i < 100;++i){
sem_wait(psem);
cout << getpid() << "read:" << --*p << endl;
sem_post(psem);
}
munmap(p,sizeof(int));
p = NULL;
sem_close(psem);
psem = NULL;
}