L16 进程同步与信号量

例子:生产者和消费者

通过程序“走走停停”来保证多进程合作的合理有序即为进程同步

  1. #define BUFFER_SIZE 10
  2. typedef struct {...} item;
  3. item buffer[BUFFER_SIZE];
  4. int in = out = counter = 0;
  5. //========生产者==========
  6. while (true) {
  7. while(counter== BUFFER_SIZE)
  8. ;
  9. buffer[in] = item;
  10. in = (in + 1) % BUFFER_SIZE;
  11. counter++;
  12. }
  13. //==========消费者============
  14. while (true) {
  15. while(counter== 0)
  16. ;
  17. item = buffer[out];
  18. out = (out + 1) % BUFFER_SIZE;
  19. counter--;
  20. }

:::tips 如果是多个用户,那么上述的仅使用信号的方式不可行 ::: :::info 信号只有有和没有两种状态;需要用信号量来保证更丰富的信息 :::

信号量

不仅希望知道有多少剩余的资源;还需要知道有多少进程在停滞等待 :::info 信号量:记录一些信息,并根据这个信息决定睡眠还是唤醒 :::

  • 缓冲区满,P1执行,P1sleep,sem=-1;
  • P2执行,P2sleep,sem=-2
  • C执行,wakeupP1,sem=-1
  • C执行,wakeupP2,sem=0;
  • C执行,sem=1;(还有一个资源or缓冲区)

    实现

  1. struct semaphore
  2. {
  3. int value; //记录资源个数
  4. PCB *queue;//记录等待在该信号量上的进程
  5. }
  6. P(semaphore s); //消费资源
  7. V(semaphore s); //产生资源
  8. P(semaphore s)
  9. {
  10. s.value--;
  11. if(s.value<0){
  12. sleep(s.queue);
  13. }
  14. }
  15. V(semaphore s){
  16. s.value++;
  17. if(s.value<=0){
  18. wakeup(s.queue);
  19. }
  20. }

L17 信号量的临界保护

:::info 多个进程对于同一个变量修改,容易出错

  • 前一个进程尚未完全将变量修改,即切到另一个进程了,此时出错了
  • 时间片到了,直接切换了
  • ……

错误是因为共享数据竞争造成的错误 ::: :::tips 在写共享变量empty时,阻止其他进程也访问empty ::: image.png

临界区

:::info 一次只允许一个进程进入的那一段代码;读写信号量的代码一定是临界区 :::

  1. //进入区
  2. //临界区
  3. //退出区

基本原则:

  • 互斥进入
  • 有空让进
  • 有限等待

    实现方法1:轮换法

    1. while (turn!=0);
    2. //临界
    3. turn =1;
    4. //-----------
    5. while(turn!=1);
    6. //临界
    7. turn=0;
  • 满足互斥进入

  • 不满足有空让进,进了一个另一个就进不来了

    实现方法2:标记法

    1. //-------------------------
    2. flag[0]=true;
    3. while(flag[1]);
    4. //临界区
    5. flag[0]=false;
    6. //--------------------------
    7. flag[1]=true;
    8. while(flag[0]);
    9. //临界区
    10. flag[1]=false;
    会出现两人都在等待的情况! :::tips 让其中一个人更加勤劳!—非对称的标记 :::

    Peterson算法

    ```cpp flag[0]=true; turn =1; while(flag[1]&&turn==1); //临界 flag[0]=flase; //———————- //———————- flag[1]=true; turn=0; while(flag[0]&&turn==0); // flag[1]=false;
  1. 满足三个原则!<br />turn起到轮换的作用
  2. <a name="Q7uay"></a>
  3. ## 多个进程:面包店算法
  4. :::info
  5. 标记和轮转结合<br />类似取号的想法
  6. - 如何轮转: 每个进程都获得一个序号, 序号最小的进入
  7. - 如何标记: 进程离开时序号为0,不为0 序号即标记
  8. - 面包店: 每个进入商店的客户都获得一个号码,号码最小的先得到服务;号码相 同时,名字靠前的先服务。
  9. :::
  10. ```cpp
  11. choosing[i] = true; num[i] = max(num[0], …, num[n-1])+1;
  12. choosing[i] = false; for(j=0; j<n; j++) { while(choosing[j]);
  13. while ((num[j] != 0) && (num[j], j)<(num[i], i])); }
  14. //临界区
  15. num[i] = 0;

硬件版本:中断法

:::info 核心:阻止调度,阻止中断(只有在中断中才能调用schedule函数,才能切换到了另一进程) :::

  1. cli();//关中断
  2. //临界区
  3. sti();//开中断

:::tips 多CPU情况下不好用 :::

硬件原子指令法

:::info 锁:本质是变量; ::: 指令不能被打断—原子指令;

  1. boolean TestAndSet(boolean &x){
  2. boolean rv=x;
  3. x=true;
  4. return rv;
  5. }

L18 信号量的代码实现

  1. Producer(item){
  2. P(empty);
  3. //...
  4. V(full);
  5. //生成两个信号量
  6. }
  7. typedef struct {
  8. char name[20];int value;//信号量的名字,值
  9. task_struct * queue; //存需要睡眠的进程的队列
  10. } semtable[20];
  11. sys_sem_open(char *name)
  12. {
  13. //在semtable中寻找name对上的;
  14. //没找到则创建;
  15. //返回对应的下标;
  16. }

用户态的程序

  1. main(){
  2. sd=sem_open(“empty”);
  3. for(i=1 to 5)
  4. sem_wait(sd);
  5. write(fd,&i,4);
  6. }
  7. sys_sem_wait(int sd){
  8. cli();
  9. //进入临界区
  10. if(semtable[sd].value-- < 0){
  11. \\设置自己为阻塞;将自己加入semtable[sd].queue中;
  12. schedule();
  13. }
  14. sti();
  15. }

Linux0.11中的实现

:::info 启动磁盘读以后睡眠,等待磁盘读完由磁盘中断将其唤醒, 也是一种同步
:::

  1. bread(int dev,int block){
  2. struct buffer_head * bh;
  3. ll_rw_block(READ,bh);
  4. wait_on_buffer(bh);
  5. }
  6. lock_buffer(buffer_head*bh){
  7. cli();
  8. while(bh->b_lock)//??? b_lock起到信号量的作用
  9. sleep_on(&bh->b_wait);
  10. bh->b_lock = 1;
  11. sti();
  12. }
  13. void sleep_on(struct task_struct **p){
  14. struct task_struct *tmp;
  15. tmp = *p;
  16. *p = current;//这里形成了一种队列,因为当前的指针会存在当前进程对应的PCB中,
  17. //这种隐含的链式关系可以形成队列
  18. current->state = TASK_UNINTERRUPTIBLE;
  19. schedule();
  20. if (tmp)
  21. tmp->state=0;
  22. }

image.png

L19 死锁

:::info 我们将这种多个进程由于互相等待对方持有的 资源而造成的谁都无法执行的情况叫死锁
:::

死锁发生的原因

死锁的4个必要条件

  • 互斥使用(Mutual exclusion)

资源的固有特性,如道口

  • 不可抢占(No preemption)

资源只能自愿放弃,如车开走以后

  • 请求和保持(Hold and wait)

进程必须占有资源,再去申请

  • 循环等待(Circular wait)

在资源分配图中存在一个环路

如何解决

  • 死锁预防 破坏死锁出现的条件
  • 死锁避免 检测每个资源请求,如果造成死锁就拒绝
  • 死锁检测+恢复 检测到死锁出现时,让一些进程回滚,让出资源
  • 死锁忽略 就好像没有出现死锁一样

    银行家算法

    :::info 如果系统中的所有进程存在一个可完成的执行序列P1,…Pn,则称 系统处于安全状态
    ::: image.png

  • 每次申请时假装分配,然后调用银行家算法

  • 代价太大!每次申请都要测

    死锁检测+恢复

    定时检测或者发现资源利用率低才检测