操作系统-PV操作的原理和几种常见问题

信号量是一种变量类型,用一个记录型数据结构表示,有两个分量:信号量的值和信号量队列指针
除了赋初值外,信号量仅能通过同步原语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

 

posted @ 2018-11-19 15:27  扬羽流风  阅读(5363)  评论(0编辑  收藏  举报