哈工大李治军老师操作系统笔记【16】:信号量的代码实现(Learning OS Concepts By Coding Them !)

30 篇文章 22 订阅

0 回顾

  • 原理比较复杂,代码比较简单
  • 信号量非常重要,可以实现多个执行序列之间的同步,互相等待交替执行,合理有序地推进
  • 这种推进不仅在上层应用时需要,实际上在操作系统内部也有大量的这种同步
  • 比如说,一个应用程序进入内核,需要磁盘读写,那么这个磁盘读写需要等待一些时间,比如磁盘是不是忙,这样一个进程就要停下来,同步嘛,首先就是要停下来,其次,当发现磁盘中断的时候,在中断的时候会发现,你磁盘可以不停了,把他唤醒,所以用这个方法,来实现操作系统内部多个执行序列之间的同步
  • 学完本章,有两个意图
  1. 可以写用户态的代码,写信号量的系统调用,来实现上层应用的同步
  2. 明白在操作系统内部也要增加大量代码实现内部多个执行序列的同步

1 代码实现

  • 操作系统内部可以使用信号量
  • 用户程序也可以使用信号量
  • 信号量是在操作系统内部的,其的值包括一个value,且包括一个PCB的队列,因为value是多个进程能看到的,根据value来工作的,所以多个进程都能看到的这个值应该放在内核里,PCB又是内核的数据结构,所以信号量这个数据结构就是在内核里,既然在内核里,就要通过系统调用来获取,大家来共享这个结果

在这里插入图片描述


  • 具体怎么做?
  1. 在内核中定义一个全局数组semtable[20]
  2. 这个数组里面定义了每个信号量的名字
  3. 使用同一个信号量就是使用相同的名字打开这个信号量
  4. 这边生产者用empty,那边消费者也得用empty,所以要打开同一个信号量
  5. 写一个sys_sem_open (char *name) { 在semtable中寻找name对上的; 没找到则创建: 返回对应的下标;},就是为了在表中找到对应的,如果没有就加上
  6. 这样就算是打开了一个信号量了,大家能共同看到这个value值
  7. 大家共同看到这个值,就能根据这个值来确定是否走,是否停,是否阻塞之类的
  8. sem_wait(sd)就是判断空闲的缓冲区
  • 系统调用,首先要申请一个信号量,如此代码sd = sem_open("empty");sem_open就是打开名字为XXX的信号量
    • 这个empty是信号量的名称/标识,如果内核中已经有这个信号量,那么直接返回即可,没有则创建
    • 为什么没有指定信号量的初始值?默认值是多少?

1.1 linux0.11相关


在这里插入图片描述


  • 从linux0.11那里学点东西,也就是来看看linux0.11在没有实现之前讲的信号量方案的情况下,是如何实现进程间同步的
  • 老师说linux 0.11这种也是一种信号量,是用while来实现的,之前讲的是用if来实现的
  • 用户程序发出read系统调用,最终调用bread,读磁盘块,详细细节会在后面讲文件系统的时候讲
  • 读磁盘的过程:首先申请一块内存缓冲区bh用于缓冲数据,bh的结构是buffer_head
  • ll_rw_block(READ, bh),启动磁盘读,具体细节后面讲
  • wait_on_buffer(bh),在缓冲区bh上等待,bh中有类似信号量的数据,bh->b_lock,b_lock这个就是缓冲区定义的信号量,这是buffer对应的lock,这里的b_lock = 1代表上锁,上锁表示没读完,一旦读完会解锁,中断会给其解锁,如果别的进程也想访问这个块,也会被挡在这个锁上,看这个信号量是否为1,为1就睡眠sleep_on,实现信号量机制既可以用while,也可以用if
  • lock_buffer,按照字面意思理解,就是当前进程给buffer上锁
  • 疑问:lock_buffer是谁调用的?磁盘驱动程序?
  • bh->b_lock=1表示上锁,上锁表示没读完,读完后就会解锁,谁来解锁?磁盘读完后的中断会解锁
  • lock_buffer中会使用cli()和sti()开关中断来保护临界区
  • 根据bh->b_lock来决定是否睡眠,while (bh->b_lock) sleep_on(&bh->b_wait);
    • 如果被别人锁住了,就会sleep_on(&bh->b_wait)
    • 为什么要while循环?后面会解释

1.2 sleep_on


在这里插入图片描述


  • 所谓阻塞,就是把自己放在阻塞队列上,然后把状态改为阻塞态

  • current->state置为阻塞状态

  • 然后调用schedule(),会调度切换到别的进程执行,后面被唤醒,再调度切换回来时,会从这里恢复执行

  • if (tmp) temp->state=0;

    • 当前进程被唤醒了,那么队列中的下一个进程也被唤醒
  • tmp=*p; *p=current;

    • 这两条语句非常难,是这个世界上最隐蔽的队列,就是把当前进程放入阻塞队列
  • 解释sleep_on形成的队列

  • **p理解为指向阻塞队列/等待队列队首指针的指针

  • 经过这里的操作,队首指向当前进程的task_struct,头插法,感觉这样就不是队列了???而是一个栈???

  • 按照正常的逻辑,task_struct应该有一个next指针,让当前进程的task_struct->next指向tmp,整个头插法入队操作就完成了

    • 但是根本不需要这么做,这里的tmp局部变量,已经保存在了内核栈中
    • tmp局部变量的作用,就是用于指向队列中下一个进程的task_struct,作用相当于next指针,如下图中的虚线所示
  • 入队的道理都懂了,那么怎么出队?感觉头插法形成的应该是一个栈而不是队列,为什么这里还说是sleep_on形成的队列?

1.3 wake_up

  • 如何从linux0.11的这个队列中唤醒?

  • read_intr是磁盘读完后的中断,其中调用了end_request

  • end_request又调用了unlock_buffer

  • unlock_buffer

    • 首先将bh->b_lock置为0,表示解锁
    • 然后调用wake_up唤醒bh的等待队列中的一个进程
  • wake_up

  • (**p).state=0;唤醒队首的进程,设置为就绪状态,记这个进程为A

  • 设置为就绪状态不是说马上就开始切换到进程A去执行,要实现调度切换,还是要依靠schedule()

  • 所以下一次操作系统执行schedule()的时候,就有可能切换到进程A去执行,进程A从哪里开始恢复执行呢?

    • 回到之前的`sleep on(的代码,就是从这里调用的schedule()内部恢复执行
    • schedule()执行完后,运行if(tmp)tmp->states=0;来唤醒进程A的后一个进程,记这个进程为B
    • 等到真正切换到这个进程B后,又会唤醒它的下一个进程,以此类推
    • 所以我们得到的结论就是,会把队列中的所有进程都唤醒
    • 这也就解释了lock_buffer中,为什么要用到while循环,因为这里会把等待队列中的所有进程唤醒,而接下来只会有一个进程得到锁,剩下的进程需要再次sleep_on
  • 为什么要把所有的进程都唤醒?

    • 前面的方案中,只会唤醒一个进程
    • 但是阻塞队列中,你不能确定剩下的进程的优先级是否更高,所以干脆全部唤醒,让schedule()来决定到底切换给谁
    • 但是我有一个问题:一次schedule()只会切换到一个进程恢复执行,下一次schedule()并不会马上执行,也就不会马上切换到队列中的下一个进程,就算执行了schedu0,切换的进程也不定就是队列中的下一个进程;那么当前进程应该就会马上获得锁,全部唤醒就没有意义了?还是说我这种担心是多余的,schedule()的调用频率非常高,以至于在当前进程去尝试获得锁之前,队列中的全部进程就都已经切换并执行过了,所以这些进程就都在竞争锁了
    • 而且之前插入是头插法,现在仍然是从头部开始唤醒,这明明就是后入先出的栈式结构

在这里插入图片描述


2 总结

很难

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值