• 哈工大李治军老师操作系统笔记【14】:进程同步与信号量(Learning OS Concepts By Coding Them !)


    0 回顾

    • 还是多进程图像
    • 依靠信号量实现进程同步

    1 进程同步

    • 多个进程合作,依靠信号量实现进程同步,推进地合理有序

    在这里插入图片描述

    • 举了公交司机与售票员的例子
    • 进程之间等待信号、发送信号,以此来进行同步
    • 可以看出,司机启动车辆的时候,在等一个信号,那边售票员在在关门的时候,也在等信号,这就体现了进程的同步进制,多个进程的相互合作
    • 你看如果没有同步,没有合作就一直执行下去,有合作就得等一个信号,来进行控制执行,而控制这个执行,关键就是信号,信号就是要告诉你怎么执行
    • 同步就是实现这个合理有序
    • 文档打印,这个例子没讲

    1.1 生产者-消费者问题

    • 共享进程
    • 生产者-消费者就是要共享缓冲区
    • buffer_size就是空间
    • 生产者就是不断往里面放
    • 消费者就是不断从里面取出来
    • 这样就是生产者-消费者之间的进程同步机制
    #define BUFFER_SIZE 10
    typedef struct {...} item;
    item buffer[BUFFER_SIZE];
    int in = out = counter = 0;
    
    • 1
    • 2
    • 3
    • 4
    • 这都是用户态的程序
    • 等待是进程同步的核心
    • 生产者进程当中while (true){while(counter == BUFFER_SIZE) ;... }当生产出来的东西等于buffer_size的时候,就该等待了,不然会把缓冲区给冲了(这里的;表示等待)
    • 所以说,多个进程合作的时候,为了能更好的完成任务,就必须在执行到一定程度的时候进行阻塞,等一个信号(当别的进程执行到一定的时间的时候,产生了某种条件,会给他发一个信号过去),这就是进程同步的基本结构,也就是让进程走走停停,来保证多进程合作的合理有序,所以说,要让多个进程合理有序,要判断在哪些地方停,比如司机,在哪里停车,售票员票卖完了,就可以走。所以先分析在哪里停,在哪里发信号走

    在这里插入图片描述


    • 如图,一个生产者在特定的地方下会调用sleep(),然后另外的进程(消费者)在特定的地方会发wakeup()

    在这里插入图片描述


    • 只发信号还不能解决全部问题
    • 引出信号量
    • 看这里,老师原话,现在是有两个生产者,之前都被sleep()了,然后呢,生产者进程当中,counter-1了,现在buffer_size不满了,所以发送wakeup(),所以p1生产者被唤醒了,之后因为没有阻塞,counter继续–,所以消费者没有必要再发wakeup()命令了,所以p2这个生产者就没有被唤醒
    • 所以就出现了问题,导致p2永远不会被唤醒,单纯依靠counter来进行语义判断是不够的,在这里,不仅需要缓冲区空闲的个数,还需要多少个进程在这里睡眠
    • 所以counter已经无法满足了,这个时候仅仅发信号不行了,还需要一个量来看,根据这个量来看是不是可以发信号,是不是又要发信号

    老师这个例子的总结:总结一下,首先生产者共享缓冲区。现在缓冲区满了,P1加item睡眠,P2也是。然后消费者counter-1,之后正好进入if唤醒P1,消费者之后再循环,counter一直减少无法再进入if。因此没办法再唤醒P2,所以消费者必须知道有几个生产者在睡觉。这样唤醒P1后还要唤醒P2。

    进程调度决定到底该切到哪个进程,进程同步将不同进程间的合作,调度调整进程达到最快时间,这个是进程之间的合作关系

    但是我觉得上面的例子不太好,消费者每消费一个,就可以尝试wkup(生产者),即便这个时候没有生产者在等待也没有关系,反过来也是一样的,wakeup也可以同时唤醒多个吧?即使wakeup.只能唤醒一个,P1唤醒后会再放入一个,那么counter又达到BUFFER_SIZE,消费者在消费一个后,也就可以再唤醒P2了。

    下面的代码还有临界区的问题,不纠结这个例子好不好的问题,那么我们主要关注这一点:counter加到BUFFER_SIZE后,不能再增加了,那么就不知道有多少个生产者在等待;counter)减到C后,不能再减少了,也就不知道有多少个消费者在等待;我们期望增加一个量,可以表示有多少进程在等待主意,这个例子如果要用信号量来改写,需要两个信号量,而不是一个

    1.2 信号量

    • 为什么要引入信号量?看上面的解释
    • 不只是等待信号、发信号?对应睡眠和唤醒
    • 还需要记录更全面的信息
    • 总结一下:只有信号是不能完全通知生产者和消费者自己本身产生和消费了多少,就是buffer里还有多少,这个时候需要一个东西(信号量)来通知对方,这样对方知道了,就可以把正在等待的进程用上了

    在这里插入图片描述


    在这里插入图片描述

    • 总结一下:这个整数什么时候1?什么时候+1?什么时候睡眠?什么时候唤醒?

    • 在这里,-1表示倒欠你一个缓冲区,要睡眠;+1表示还有一个空闲缓冲区

    • 整理说明信号量

    • sem=-x,x表示有多少个进程在等待

    • sem=+x,表示有多少个资源

    • 生产者生产1个资源,如果此时sem<0,则需要唤醒1个进程,同时sem++

    • 消费者想要消费,先sem–,然后若sem<0,说明没有资源,需要睡眠等待

    • 所以接下来,就能通过信号量来看/实现是等待还是唤醒

    • 一个人要使用,操作,申请信号量,看到信号量负的怎么办?等待,负的表示已经欠了,所以你要申请的话,只能等待,信号量减1,再改为睡眠状态;那么消费者看信号量负数,肯定有人在睡眠,就把生产者唤醒,唤醒一个少一个,所以信号量加1,所以可以根据信号量的值,什么时候等,什么时候走

    • 负值的对应的应该是生产者休眠的个数,也就是等待放入缓冲区等待的个数,正值对应的是缓冲区里还可以容纳的个数,就是还可以进来多少个进程到缓冲区,0代表缓冲区正好满

    • 一道例题,答案是B


    在这里插入图片描述


    1.3 信号量的实现

    • 信号量的定义
    • 下面有semaphore结构体和P(semaphore s)的代码,感觉这样会有l临界区的问题,例如两个进程同时调用P操作,那么很可能会导致s.value只减了1
    • sempahore:结构体包含1个int类型的value,用于记录资源个数;还包含1个PCB队列,记录等待在该信号量上的进程
    • 提到了实验五,可以开始做了,linux0.11没有提供信号量,在内核中实现信号量的系统调用
    struct semaphore//定义信号量
    {
    	int va1ue;//记录资源个数,比如刚刚的8
    	PCB *queue;//关联一个阻塞队列
    	//记录等待在该信号量上的进程
    }
    P(semaphore s);
    //消费资源
    V(semaphore s);
    //产生资源
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    P(semaphore s)//生产者
    {
    s.value--;
    if(s.value < 0) {
    sleep(s.queue);}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    V(semaphore s) //消费者
    {
    	s.value++;
        if (s.value <= 0) {
        	wakeup(s.queue);
        }
    }
    
    // 或者
    V(semaphore s) 
    {
        if (s.value < 0) {
        	wakeup(s.queue);
        }
    	s.value++;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 当前value小于0证明有进程在阻塞,可以唤醒
    • 同时对应V,检查当前阻塞队列里,有进程正在阻塞时(value小于0)才唤醒
    • p操作<0说明没有资源可以用了,就睡眠,v操作++说明刚生产一个资源,<=0判断还有没有需要资源的
    • = 0 要不要唤醒?
    • = 0 表示刚刚加完1得0,加之前是-1,那么要不要唤醒?肯定要唤醒
    • 当然,如果是加完之后>0,这时候= 1的时候,之前 = 0的时候,就不用唤醒了
    • 这里的V是唤醒一个被阻塞的进程
    • 当++时,缓冲区有一个位置空出了 ,可以唤醒一个,但阻塞还有多个,所以资源数可以仍为负
    • 对于消费者,负数是没有资源;对于生产者,负数说明有进程在等待队列
    • 两个代码块里的s.value含义不一样的
    • 觉得错了的人,注意P是消费资源的一方,亦即前面说的生产者,不是消费者,V才是消费者产生空闲缓冲

    在这里插入图片描述

    2 总结


    • empty表示剩余缓冲区的数量,执行一次生产者进程,剩余缓冲区的数量减一
    • full表示可消费的产品数量,执行一次消费者进程,可消费数量减一
      在这里插入图片描述

    • 可以把它简单的理解成我们停车场入口立着的那个显示屏,每有一辆车进入停车场显示屏就会显示剩余车位减1,每有一辆车从停车场出去,显示屏上显示的剩余车辆就会加1
    • 从形式上来看,生产者和消费者一致,都是上来就检查某种资源是否有剩余。 生产者其实可以看作消费者,只不过消费的是“剩余空间”
  • 相关阅读:
    Linux下安装部署Redis
    vue3与vue2的区别
    记一次 .NET 某企业内部系统 崩溃分析
    云计算 3月8号 (wordpress的搭建)
    java-net-php-python-jsp乐器销售网站的设计演示录像计算机毕业设计程序
    外包干了10个月,技术退步明显.......
    计算机组成原理期末复习第三章-1(唐朔飞)
    师文汇:OceanBase 4.0 产品核心能力解读
    Jtti:微信小程序怎么连接云主机数据库
    前端必备:Node.js是什么?它有哪些特点和优势?
  • 原文地址:https://blog.csdn.net/weixin_44673253/article/details/126889746