信号量是一种变量类型,用一个记录型数据结构表示,有两个分量:信号量的值和信号量队列指针
除了赋初值外,信号量仅能通过同步原语PV对其进行操作
s.value为正时,此值为封锁进程前对s信号量可施行的P操作数,即s代表实际可用的物理资源
s.value为负时,其绝对值为对信号量s实施P操作而被封锁并进入信号量s等待队列的进程数,即登记排列在s信号量队列之中等待的进程个数
s.value为0时,无资源且无进程等待
信号量按其取值可分为二值信号量和一般信号量(计数信号量),记录型信号量和PV操作定义为如下数据结构和不可中断过程:
typedef struct semaphore { int value; struct pcb *list; //信号量队列指针 } void P(semaphore s){ s.value--; if(s.value<0) sleep(s.list); //若信号量值小于0,执行P操作的进程调用sleep(s.list)阻塞自己,被置成等待信号量s状态并移入s信号量队列,转向进程调度程序 } void V(semaphore s){ s.value++; if(s.value<=0) wakeup(s.list); //若信号量小于等于0,则调用wakeup(s.list)从信号量s队列中释放一个等待信号量s的进程并转换成就绪态,进程则继续运行 }
二值信号量虽然仅能取值0、1,但它和其它记录型信号量有一样的表达能力
- 机票问题
注意:(1)P操作与V操作在执行路径上必须一一对应,有一个P操作就有一个V操作;(2)输出一张票的操作不应放在临界区内
int A[m]; Semaphore s = 1; cobegin process Pi { int Xi; Li:按旅客定票要求找到A[j]; P(s); Xi = A[j]; If (Xi>=1) { Xi=Xi-1; A[j]=Xi;V(s); 输出一张票;} else {V(s); 输出票已售完;} goto Li; } coend;
只有相同航班的票数才是相关的临界资源,所以用一个信号量s处理全部机票会影响进程并发度
可以让每一个航班都有自己的临界区,把信号量改为s[m]
int A[m]; Semaphore s[m]; //每一个航班都有自己的临界区 For (int j=0;j<m;i++) s[j] = 1; cobegin process Pi { int Xi; L1:按旅客定票要求找到A[j]; P(s[j]); Xi = A[j]; If (Xi>=1) { Xi=Xi-1; A[j]=Xi;V(s[j]); 输出一张票; } else {V(s[j]); 输出票已售完;} goto L1; } coend;
- 生产者消费者问题
(1)1生产者1消费者1缓冲区问题
int B; semaphore sput = 1; /* 可以使用的空缓冲区数 */ semaphore sget = 0; /* 缓冲区内可以使用的产品数 */ process producer { L1: produce a product; P(sput); B = product; V(sget); goto L1; } process consumer { L2: P(sget); product = B; V(sput); consume a product; goto L2; }
(2)1生产者1消费者N缓冲区问题
必须引入放入和取出产品的循环队列指针putptr指针和getptr指针进行管理,因为只有1生产者1消费者所以它们不需要是共享变量
int B[k]; // 共享缓冲区队列 semaphore sput = N; //可以使用的空缓冲区数 semaphore sget = 0; //缓冲区内可以使用的产品数 int putptr = getptr = 0; process producer { L1:produce a product; P(sput); B[putptr] = product; putptr = (putptr + 1) mod k; V(sget); goto L1; } process consumer { L2:P(sget); product = B[getptr]; getptr = (getptr + 1) mod k; V(sput); consume a product; goto L2; }
⭐️(3)N生产者N消费者N缓冲区问题
必须引入新的信号量s1和s2对putptr指针和getptr指针进行管理
int B[k]; semaphore sput = N; /* 可以使用的空缓冲区数 */ semaphore sget = 0; /* 缓冲区内可以使用的产品数 */ int putptr = getptr = 0; semaphore s1 = s2 = 1; /* 互斥使用putptr、getptr */ process producer_i { L1:produce a product; P(sput); P(s1); B[putptr] = product; putptr = ( putptr + 1 ) mod k; V(s1); V(sget); goto L1; } process consumer_j { L2:P(sget); P(s2); Product = B[getptr]; getptr = ( getptr + 1 ) mod k; V(s2); V(sput); consume a product; goto L2; }
若要求一个消费者取10次,其它消费者才能开始取,则需要再增加一个信号量mutex1并对消费者进程进行改造,循环10次后再V(mutex1)。
这里通过一个互斥信号量mutex2互斥访问缓冲区,而不是像前面那样通过两个互斥信号量s1和s2分别互斥使用缓冲区存指针putptr、取指针getptr
process consumer_j { P(mutex1); for(int i=0; i<10; i++){ P(full); P(mutex2); 互斥访问缓冲区; V(mutex2); V(empty); } V(mutex1); }
(4)苹果橘子问题
父亲只放评估、母亲只放橙子;儿子只吃橙子、女儿只吃苹果
同步关系1:有苹果
同步关系2:有橘子
同步关系3:有空位
Semaphore sp; /* 盘子里可以放几个水果*/ Semaphore sg1; /* 盘子里有桔子*/ Semaphore sg2; /* 盘子里有苹果*/ sp = 1; /* 盘子里允许放入一个水果*/ sg1 = 0; /* 盘子里没有桔子*/ sg2 = 0; /* 盘子里没有苹果*/ process father { L1: 削一个苹果; P(sp); 把苹果放入plate; V(sg2); goto L1; } process mother { L2: 剥一个桔子; P(sp); 把桔子放入plate; V(sg1); goto L2; } process son { L3: P(sg1); 从plate中取桔子; V(sp); 吃桔子; goto L3; } process daughter { L4: P(sg2); 从plate中取苹果; V(sp); 吃苹果; goto L4; }
(5)吸烟者问题
三个消费者各有一种材料、缺另外两种材料,供应者每次随机供应两种不同材料
想法:生产者每次供应两种不同材料,其实可以理解为有三种不同的生产材料提供方式,分别供三种消费者消费
int random; Semaphore offer1 = offer2 = offer3 = 0; Semaphore finish = 1; process producer(){ while(1){ random = 任意随机整数; random = random % 3; P(finish); 对应两种材料放在桌子上; if(random==0) V(offer1); else if(random==1) V(offer2); else V(offer3); } } process consumer_1(){ while(1){ P(offer1); 拿自己缺的那两种材料; 卷成烟抽掉; V(finish); } }
(6)1生产者2消费者N缓冲区,生产者随机生成正整数,2个消费者分别取奇数和偶数
思路:这个问题和抽烟者问题很类似,关键是要定义缓冲区里有奇数的互斥信号量odd、缓冲区里有偶数的互斥信号量even。每次生产者随机生成一个数之后,判断奇偶,分别V(odd)和V(even)
- 哲学家就餐问题
Semaphore fork[5]; for (int i = 0; i < 5; i++) fork[i] = 1; cobegin process philsopher_i() { while (true) { think(); P(fork[i]); P(fork[(i+1)%5]); eat(); V(fork[i]); V(fork[(i+1)%5]); } } coend
如果5位哲学家同时拿起他们左手/右手的叉子,将出现死锁,可以:
(1)至多允许四个哲学家同时拿叉子
(2)奇数号哲学家先取左边叉子,再取右边叉子;偶数号哲学家则相反
(3)每位哲学家取到手边两把叉子才开始吃,否则一把也不取:可以通过引入互斥信号量mutex每次只允许一个哲学家拿叉子
Semaphore fork[5]; for (int i = 0; i < 5; i++) fork[i] = 1; Semaphore mutex = 1; cobegin process philsopher_i() { while(true){ P(mutex); P(fork[i]); P(fork[(i+1)%5]); V(mutex); eat(); V(fork[i]); V(fork[(i+1)%5]); } } coend
- 写者问题
一次只允许一个写者写,但可以N个读者同时读。写者完成写操作前不允许其它写者、读者操作
引入表示是否允许写的信号量writeblock,相当于任何进程在工作的时候都不允许写。不过单纯引入信号量不能解决此问题,还必须引入计数器readcount对读进程进行计数,读之前检查计数器,如果为1(自己是唯一的读进程)才需要P(writeblock),读之后检查计数器,如果为0(当前已无读进程)则需要V(writeblock)。
mutex是用于对计数器readcount操作的互斥信号量
int readcount = 0; Semaphore writeblock = 1 ; Semaphore mutex = 1; cobegin process read_i() { P(mutex); readcount++; if(readcount==1) P(writeblock); //自己是唯一的读进程,写者在写文件时自己不能开始读,自己开始读后不允许写操作 V(mutex); /*读文件*/ P(mutex); readcount-—; if(readcount==0) V(writeblock); //自己是唯一的读进程,读文件完成后允许写操作 V(mutex); } process write_j() { P(writeblock); /*写文件*/ V(writeblock); } coend
此写法读者优先,当存在读者时写者将被延迟。且只要有一个读者活跃,随后而来的读者都将被允许访问文件,从而导致写者长时间等待。
改进方法:增加信号量,确保当一个写进程声明想写时,不允许后面的新读者访问共享文件。
- 男女共浴问题、汽车过桥问题
这个问题的关键是设置一把性别锁mutex,第一个到的男人要负责抢这把锁,因此mutex的PV操作必须放在修改mancount的临界区内。一旦男人抢到了这把锁,试图抢这把锁的女人就会停留在womancount的临界区内出不来,而每次又只允许一个女人进入womancount的临界区。
最后一个走的男人要负责把这把锁释放掉。
Semaphore mutex; Semaphore mutex_man = mutex_woman= 1; int mancount = womancount =0; Process man(){ P(mutex_man) mancount++; if(mancount==1) P(mutex); V(mutex_man); 洗澡; P(mutex_man); mancount—; if(mancount==0) V(mutex); V(mutex_man) }
南大18年真题过桥问题:汽车只能单向过桥,最多12辆车同时过桥。
- 理发师问题
引入一个计数器waiting记录等待理发的顾客坐的椅子数,初值为0最大为N。mutex是用于对计数器waiting操作的互斥信号量
引入信号量customers记录等候理发的顾客数,并用于阻塞理发师进程,初值为0
引入信号量barbers记录正在等候顾客的理发师数,并用于阻塞顾客进程,初值为0
想法:其实和N生产者1消费者N缓冲区的生产者消费者问题有些类似,但是多了P(barbers)和V(barbers);此外,椅子数改用了int数waiting再用信号量mutex进行互斥。
int waiting = 0; Semaphore customers = 0; Semaphore barbers = 0; Semaphore mutex = 1; cobegin process barbers() { while(true) { P(customers);//判断是否有顾客,没有的话理发师睡眠 P(mutex); waiting--; V(barbers);//理发师准备为顾客理发 V(mutex); cuthair(); //理发师理发,不应放在临界区 } } process customer_i() { P(mutex); if(waiting<N) { waiting++; V(customers);//唤醒理发师 V(mutex); P(barbers);//如果理发师忙则等待 get_haircut(); } else V(mutex);//人满了,顾客离开 } coend