IPC_信号量
问题的引入
- 如果有两个或两个以上的任务(进程/线程,并发的实体),去访问同一个共享资源(硬件上的,软件上的),那么我们必须要保证这个共享资源有序访问,否则会产生不可预知的后果。
例子:
important_i = 5
func()
{
important_i++;
}
有两个实例任务,调用了func()这个函数,那么请问,important_i最后的值是什么?
7 -> 有可能
8 -> 有可能,不是我们期望的结果
这种情况就不是有序访问,是有问题的,所以我们需要对这个共享资源进行某种方式的保护,以使它被有序的访问。---------避免竞争
- 分析:并发=>竞争=>共享资源的非法访问=>程序行为异常…
- 解决办法:
- 能不能不要并发?那肯定不行。在保留并发的前提下,“避免竞争”=>访问共享资源时,严格串行!!!!
信号量机制
- 信号量是个什么东西??
- 信号量的作用是什么?
- 为什么需要信号量??
- 信号量是怎么样达到目的的??
-
信号量(semaphore)是一种用于提供不同进程间或一个进程内部不同的线程间同步的一种机制。
- 进程/线程,任务:并发的实体
- “同步”:并发的实体间,相互等待相互制约,有序的,有条件的访问
-
信号量就是为了保护共享资源,让共享资源有序访问的一种机制。
-
信号量目标:为了保护共享资源,使共享资源有序的访问
-
信号量是我们程序界最高尚的一个东西,因为它不是为了自己而存在的,是为了别人(它保护的对象,共享资源)而存在,“保镖”
-
什么时候使用信号量?
- 有保护对象时,才需要信号量
- 首先要搞清楚,谁需要保护?保护谁?
- 一个被保护的对象,需要一个信号量
如何来保护呢?
-
“保护”是指:让这个被保护的对象(共享资源)有序访问。如:“互斥”“共享资源”:大家都访问的资源。
-
信号量机制其实是程序员之间的一种约定,用来保护共享资源的,比如说进程A和进程B,都要访问一个互斥设备,那么我们可以用一个信号量来表示能不能访问该设备,然后每个进程访问该设备时,先去访问信号量,如果能访问设备则先把信号量设成“NO”,然后再去访问该互斥设备,访问完互斥设备,然后再把信号量改成“YES”。
-
互斥的共享资源/不互斥的共享资源
- 互斥:同时只允许一个进程/线程 访问
- 不互斥:同时允许点个进程/线程 访问
-
在访问共享资源前,先去判断 共享资源是否能访问?
-
Lock
- ----能访问:你就获取了该信号量(变成“不可访问”),则进入下面的代码
- ----不能访问:wait,直到信号量变成“能访问”
- …//访问共享资源的代码区域,—>“临界区”
- 在访问完共享资源,要把信号量释放(变成“能访问”)
-
unlock
信号量是如何实现的呢?
-
信号量:大家都可以访问的 一个整数
-
一个进程或者线程可以在某个信号量上执行如下三种操作:
创建(create)
- 创建一个信号量:这还要求调用者指定信号量的初始值。初始值表示该信号量保护的共享资源,可以同时被多少个任务访问。
- sem --> 5 表示此刻有五个进程或线程去同时访问它所保护的资源
- sem --> 1 表示只有一个进程或者线程可以去访问它所保护的资源,“互斥信号量”
等待(wait)
- 该操作会测试这个信号量的值,如果其值<=0,那么会等待(阻塞),一旦其值>0,这个时候,将它-1,并且继续往下执行临界区的代码,其函数实现类似于如下代码:
while(semaphor_value <= 0)
{
sleep;//wait
}
semaphor_value--;
//表示该进程或线程,获取了该信号量
//上述必须必须上“原子操作”:不允许有两个及以上的进程同时进入。
//.....以下代码,获取了该信号量,就可以访问该信号量所保护的资源啦。
-
P操作:proberen(尝试),荷兰语
* down
* lock 上锁PV操作是原子操作,不允许被打断!!!!!
释放(+1)
- 该操作将信号量的值 +1 , 其函数实现类似于如下代码:
semaphore_value++; //原子操作
V操作:verhogen(增加),荷兰语
up
unlock 解锁
信号量保护的目标是通过:在临界区的前面加上一个 P操作 ,在临界区的会面加上一个 V操作。来实现
-
思考
-
1、在遇到“共享资源”在不同的进程或线程中访问的时候
- 考虑“避免竞争“
- (1)明确谁谁共享资源,即谁是需要保护的对象
- (2)确定”临界区“:操作共享资源的代码区域,我们称之为临界区。
- (3)一个保护的对象,就需要一个信号量。
- P
- 临界区
- V
-
2、现在有五个资源A,B,C,D,E需要保护,设计师决定用一个信号量S来同时保护这五个资源。
-
P(s)
-
----A
-
V(s)
-
P(s)
-
----B
-
P(s)
-
…
-
请问这种设计有什么问题?
-
可以达到“保护”互斥访问的目的,但是降低了并发度,A和B本身可以同时访问,。。。
死锁
-
所谓的死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵局状态时,若无外力作用,它们都无法再向前推进。因此我们举个例子来描述,如果此时有一个线程A,按照先锁a在获得锁b的顺序获得锁,而在此同时又有另外的一个线程B,按照先锁b在锁a 的顺序获得锁。A拿完a锁之后去拿b锁。但是b同时已经把b锁拿走了,那么A就在等b锁,而B在等a锁,就会陷入无限等待!!!
-
死锁产生的四个必要条件
- 1、互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 2、请求和保持条件:当进程因请求资源而阻塞时,对已获得得资源保存不放。
- 3、不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 4、环路等待条件:在发生死锁时,必然存在一个进程-资源的环形链。
-
预防死锁
- 1、(破坏请求条件)资源一次性分配:一次性分配所有资源,这样就不会再有请求了
- 2、(破坏请保存条件)只要有一个资源得不到分配,也不给这个进程分配其他的资源
- 3、(破坏不可剥夺条件)即当某进程获得部分资源,但得不到其他资源,则释放已占有资源
- 4、(破坏环路等待条件)系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反
-
避免死锁
- 预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。
- 银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程知道同意该请求后系统状态仍然是安全的。
-
检测死锁
- 首先为每个进程和每个资源指定一个唯一的号码;
- 然后建立资源分配表和进程等待表。
-
解除死锁:
-
当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:
- 剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
- 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
-
linux提供的信号量API
system V 信号量
Posix 信号量system V信号量过程:ftok-------->生存一个System V ipc对象的key-----semget----->创建或者打开一个System V信号量--------- >P/V操作-------->删除该信号量
- System V信号量:
- 计数信号量集(计数信号量集)。
- 计数信号量:该信号量的值可以是 > 1的值,它所保护的共享资源允许带个任务同时访问它。
- 计数值1,0 =》互斥信号量
- 互斥信号量:该信号量的值要么上1,要么是0,它所保护的共享资源同一时刻只允许一个任务去访问它。
- 为什么System V要把信号量,弄一个信号量集(信号量数组)呢?
API函数接口
ftok 用来创建一个System V IPC对象 的 key
key: 最主要的特征是什么?
唯一性。
在这里插入图片描述
-
key_t ftok(const char *pathname, int proj_id);
-
pathname: 一个文件系统中的路径名(要存在的)
-
proj_id: 整数。一般用工程的代号。
-
返回值:
- 成功生成一个唯一的 system v ipc的key(key_t)
- 失败返回-1, 同时errno被设置。
-
semget 用来创建或打开一个System V信号量
-
int semget(key_t key , int nsems ,int semflg)
-
key :system v ipc对象的key(一般由ftok是返回值)
-
nsems :你要创建的信号量集中的信号量的数量。如果我们不是创建而是打开一个已经存在的信号量集,此处参数可以指定为0,一旦创建完一个信号量集,其信号量的个数就不能改变了。
-
semflg :
- (1)创建
- IPC_CREAT|权限位
- (2)打开
- 0
- (1)创建
-
返回值:
- 成功返回system v信号量集的id
- 失败返回-1,同时errno被设置
-
the valuex of the semaphores in a newly created set are indeterminate. 在一个新创建的信号量集中的信号量的值,是不确定的!!!
-
so,我们在新创建一个信号量集后,马上指定他们的初始值!!!!
semctl 控制操作(设置或获取信号量集中某个或某些信号量的值)
- int semctl(int semid , int semnum , int cmd ,arg)
- semid : 要操作的信号集的id(semget返回值)
- semnum : 要操作的信号量集中的哪个信号量,就是信号量数组的下标,从0开始,0,1,2,3,4,5,。。。。,nsems-1
- cmd:命令号,常用的有:
- GETVAL:获取semnum个信号量的值
- SETVAL:设置第semnum个信号量的值
- GETALL:获取这个信号量 集中所有信号量的值
- SETALL:设置这个信号量集中所有信号量的值
- IPC_RMID:删除这个信号量集
- …
- arg:针对不同的命令行,第四个参数不一样。
-
cmd == IPC_RMID ,第四个参数不要。
- 如:semctl(semid,0,IPC_RMID);
-
cmd == GETVAL,第四个参数也不要,函数返回值(semctl)就表示哪个信号量集中第二个信号量的值
-
cmd == SETVAL,第四个参数应该为int,表示要设置信号量的值
- 如:
- int sem_val = 1;
- int r = semctl(semid,1,SETVAL,sem_val);
- 用来设置semid指定信号量集中第二个信号量的值为1;
- 如:
-
cmd == GETALL ,第四个参数应该为unsinged short vals[],这个数组是用来保存获取的每个信号量的值的。
- 如:unsigned short vals[10];
- int r = semctl(semid,0,GETALL,vals);
- //vals[0]保存的是 第一个信号量的值
- //vals[1]保存的是 第二个信号量的值
- //。。。。。。
- 如:unsigned short vals[10];
-
cmd ==SETALL,第四个参数应该为unsinged short vals[],这个数组是用来设置的每个信号量的值的
- 如:
- unsigned short vals[10] = {1,1,1,1,1,1,1,1,1};
- int r = semctl(semid,0,SETALL,vals);
- //第一个信号量的值 就为vals[0] 1
- //…
- 如:
-
- 返回值:
- 根据不同命令,semctl返回值含义不一样。
- cmd == GETVAL , 返回值就是表示要获取的那个信号量的值
semop : System V 信号量的P/V操作
- 这里要先明白一个结构体,在System V的信号量P/V操作,用一个结构体strucy sembuf来描述的。
struct sembuf
{
unsigned short sem_num;
//要进行P/V操作的信号量在信号量集中的编号(下标)
short sem_op; /* semaphore operation */
>0 : => V操作, up/unlock
=0 : try 一 try, 看是否会阻塞
<0 : => P操作, down/lock
semval(信号量的值) = 原semval + sem_op
short sem_flg; /* operation flags */
// 0 : 默认,如果 P操作获取不了,则会阻塞
PC_NOWAIT: 非阻塞,不等待
如果是P操作,能获取则获取,不能获取则走人(返回-1,)
SEM_UNDO: 撤销!!!!!!
为了防止进程带锁退出
if you set SEM_UNDO这个标志,
内核会额外记录该进程对该信号量的所有的p/v操作,
然后再该进程退出时,会还原对该信号的操作。
如:
p v p v kill
-1 +1 -1 +1 0
p v p kill
-1 +1 -1 (-1) 还原 +1
};
struct sembuf表示的对一个信号量的P/V操作
如果对两个信号量的P/V操作,就需要对应两个struct sembuf
如果对多个信号量的P/V操作,就需要对应多个struct sembuf
-
int semop(int semid , struct sembuf *sops , unsigned nsops);
- semid : 要操作的是哪个信号量集
- sops : struct sembuf的数组。struct sembuf[0]->表示第0个信号量的P/V操作…
- nsops:第二个参数sops数组中的元素个数
-
返回值:
- 成功返回0
- 失败返回-1,同时error被设置。
-
semop可能会阻塞当前进程/线程,如果是P操作,获取不了的时候,且IPC_NOWAIT没有设置时,会等待。“死等”。(我不知道为什么我的mac上找不到semtimedop这个函数的头文件和函数声明)
-
int semtimedop(int semid, struct sembuf *sops, size_t nsops,
const struct timespec *timeout); -
semtimedop和 semop的区别在于:
-
semtimedop 可以设置一个等待时间,在该时间内,按照规则来进行,超过时间,就不会等待
struct timespec
{
long tv_sec;//秒
long tv_nsec;//纳秒
};
//假设你要等待 2秒钟 零 200纳秒
struct timespec tv;
tv.tv_sec = 2;
tv.tv_nsec = 200;
关键:
1、明确共享资源
2、明确临界区
3、临界区前P操作,临界区后V操作
Posix信号量
单个信号量
- 单个信号量分为了有名信号量和无名信号量
无名信号量
- 没有名字,无名信号量存在于内存中。无名信号量, “基于内存的信号量”,如果这段内存在一个“内核的共享内存中”,进程可以访问,线程也可以访问。 => 既可以用于进程间同步/互斥,也可以用于线程间的同步/互斥。如果这段内存在一个进程的地址空间,此时,只能用于该进程内部所有线程的同步/互斥。POSIX 信号量:sem_t 来描述 POSIX信号量。
有名信号量
- 可以用于进程间或线程间 同步/互斥在文件系统中有一个入口(有一个文件名,inode),信号量的对象(值)却是存在于内核。=》既可以用用于任意进程间,也可以用于线程间的同步。
创建或打开一个POSIX信号量
- POSIX信号量用类型sem_t来表示,不管是有名信号量,还是无名信号量,都是用sem_t来表示
- 有名信号量:
- sem_t *sem_open(const char *name, int oflag);
- sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value);
- name:要创建或打开的POSIX有名信号量在文件系统中的路径名:
- 以‘/’开头的路径名(路径名中只能有一个/)
- oflag:
- (1)打开:0
- (2)创建:O_CREAT
- 第三个参数和第四个参数当你是创建一个有名信号量时,才需要
- mode:创建权限位,有两种方式指定:
- (1)S_IRUSR
- (2)0664
- value:指定创建的有名信号量的初始值。
- 返回值:
- 成功返回一个sem_t指针,指向POSIX有名信号量
- 失败返回一个SEM_FAILED,同时errno被设置。
Link with -pthread. -l pthread
- 无名信号量:
- (1)定义或分配一个无名信号量 sem_t
- sem_t t1;
- sem_t *psem = malloc(sizeof(sem_t));
- (2)初始化无名信号量 sem_init
POSIX信号量 P/V操作
P操作
-
“死等”
-
int sem_wait(sem_t *sem);
-
返回值:
- 返回0,获取了该信号量
- 返回-1,出错,同时errno被设置
-
try-try:非阻塞版本 “能获取则获取,不能获取则返回-1”
-
int sem_trywait(sem_t *sem)
- 返回值:
- 返回0,获取了该信号量
- 返回-1,出错,同时errno被设置
-
“限时等待”:
-
abs_timeout:指定等待超时的绝对时间。
-
“绝对时间”=当前绝对时间+相对时间
Link with -pthread
例子:
struct timespec ts;
clock_gettime(CLOCK_REALTIME , &ts); //获取当前时间
ts.tv_sec +=5;
ts.tv_nsec +=3000000000;
{
ts.tv_sec++;
ts.tv_nsec-=1000000000;
}
int r = sem_timedwait(sem,&ts);
V操作
其他操作
sem_getvalue用来获取一个POSIX信号量的值
POSIX有名信号量的 关闭和删除 操作
- sem_close:用来关闭一先·个POSIX有名信号量
- sem_unlink:用来删除一个POSIX有名信号量