这一讲我们来介绍信号量和管程
信号量和管程
都是操作系统当中
提供的两种同步方法
那最开始我们说
因为我们在系统里呢
为了提高系统的并发性
所以我们引入了多进程和多线程
多线程的引入导致了
资源的共享使用和竞争
我们就会需要去
寻找解决的办法
这就是我们这里要同步
同步呢是为了协调多个进程
对共享数据的访问
我们把这个问题抽象出来
就变成是在任何一个时刻
那我们只能有一个线程
在执行临界区的代码
解决的办法呢
我们前面有这样几种说
我们可以基于底层的硬件实现
也可以在上面呢
有高层的抽象编程方法
那我们今天讲的信号量和管程
都是属于编程方法的两种
这是我们要最终解决的问题
我们需要协调各个线程
对临界资源的访问
然后我们的做法呢有这样几种
首先呢我们在最底层
希望来解决这个问题
你比如说禁止中断
那我也有 做原子操作
把读和修改这两个
变成一个原子操作
比如说我们TS指令
然后我们也有呢
完全基于最基本的读写操作
来构造出来的软件同步办法
这几类办法呢
我们把它都抽象成一个接口
说 就是我们这里前面说的锁
那用锁来申请临界区的进入
和在临界区完成之后那退出释放
好 这个机制能比较好地抽象
而它的实现呢
是依靠这底下这三种办法
这三种办法呢各有各的适用场合
然后有一个统一的锁机制之后
我们在上面能实现临界区的访问
今天我们要说的信号量呢
实际上就是跟锁机制
在同一个层次上的一种编程方法
它的做法是在操作系统的控制下
来做这件事情
信号量是操作系统提供的一种
协调共享资源访问的方法
那这种方法呢
它和我们前面讲到的
软件同步方法有什么区别呢
软件同步方法呢
实际上我们讨论的是平等的
线程之间的一种协调机制
那也就是说我如果用线程
通过软件方法来协调
谁进入临界区的时候
这些线程之间的位置是完全平等的
但是我们这里讲到的信号量呢
实际上它是由操作系统来负责管理的
操作系统作为管理者
它高于我们的进程
这做一个类比
就相当于是说我们在球场上踢球
一种是说两个球队一块踢
没有裁判
好 那这时候
我们有一套共同的规则
好 那这时候大家一起来踢
在这个过程当中呢
如果说有问题 我们照规则来
但实际上这时候呢
大家对规则理解
总是会有各种各样偏差
于是我们引入一个裁判
那裁判就是说
只要我们俩有分歧的时候
找到一个仲裁
由裁判说了算
那我们在这儿呢
实际上就是由操作系统
来做这个裁判
也就说两个进程协调
谁能使用临界区资源
由操作系统说了算
有了一个仲裁者
或者一个管理者之后呢
这种协调会变得比原来容易
好 在这种情况下
我们用一个信号量
来表示一类资源
信号量的取值呢
就是它的资源的数目
这种做法呢是最早
由一个荷兰科学家
叫Dijkstra 他提出的
那这个名字呢
我们在前面的数据结构里头
计算机科学里头
有很多他做出的贡献
这种办法呢在早期的操作系统当中
是最主要的同步机制
那目前呢在我们实际系统当中
这种用的相对来说比较少
但是它的重要性呢仍然是非常高的
它具体怎么做呢
信号量是一种抽象的数据类型
这个数据类型里头有三个内容
一个是一个整型变量
这个变量的取值呢
你可以理解为
你要共享的资源的数目
然后由两个原子操作
那这两个原子操作呢一个叫P操作
它是申请资源的时候要用的
我申请使用一个资源
那我把这个计数呢减一
也就相当于可用的资源数变少了
好 那这时候呢
就可能有一种情况是说
我来申请资源的时候
已经没有资源了
那这时候我就减成一个负数了
减成负数之后呢
我就需要等待另外的线程
用完这个资源之后
它释放了我才能用
好 那这时候呢它就进入等待状态
如果说不是这样 它是大于0的
也就相当于我拿到了相应资源
那我就可以使用了
好 这个P的出处呢是荷兰语
那最早这个科学家他做出来的
我们后续就延续这个提法了
然后反过来呢
我用完资源之后
我释放的时候一个V操作
V操作就是把计数加一
如果说你占用的是
由1变成0的那个资源
后面的线程再来的时候
那它就已经是负数了 它在等待
好 那这时候你加一之后
如果说它仍然是小于等于0的
那么这时候就说明
有另外一个线程
在等这个资源来使用
它目前处于等待状态
好 那这时候呢
我就唤醒相应的等待线程
好 这个V呢
也是跟P一样的
它来自于荷兰语
好 有了这两个之后
我们看这个它怎么来用的
我们做这样一个类比
也就说我假定有一个车站
车站有两个站台
一个站台呢可以停一列火车
进行上下乘客 或者说货物的装卸
好 我们在这儿呢把它抽象成
一个有两个资源的信号量
它做法什么样子呢
在这儿我们看到
这是车站的两个站台
好 没有任何站台被使用的时候
这时候任何一个火车
都是可以进来的
好 那这时候呢有一列火车
直接进到一个站台
然后这时候仍然有资源呢
这时候我这儿还是绿灯
好 等我把这资源占满了
我们通常情况下理解
这时候就有红灯
后面再来火车的话
它就停到这儿了
好 那这时候呢说再来火车
那实际上这时候呢 我就相当于
这地方的P操作减1再减1
这时候由2就减成0了
好 那这时候呢
再来第三辆的时候
它就出去等着了
等第一辆火车装卸完毕 它离开
好 那这时候加1
加1之后发现那
我这里头后面还有一个等着的
那这时候呢它就可以进来了
好 然后这时候呢两个都占用了
那我们在信号灯上看着就是红灯了
那我们实际上信号量的作用呢
就是类似于这个车站的控制流程
好 那我们说信号量它是
由一个整型变量加两个操作
那么这个整型变量呢
加上前面这个操作呢是
P操作和V操作对它进行保护
好这种保护之后呢
实际上我们就能做到
你对信号量中的整型变量的修改
你只能通过PV操作来完成
操作系统呢来保证
这个PV操作的原子性
那实际上这是我们前面说到的
操作系统在执行它的代码的时候
它的优先级高于我们
进程的用户代码
所以在这时候呢
它能保证它在执行过程当中
不受应用进程的代码执行的干扰
好 那这样就能保证它的原子性了
好 那这时候在执行过程当中呢
P操作有可能由于没有资源
而进入等待状态
V操作呢只会释放资源
处于等待状态的进程呢
变成是就绪状态
好 那这时候说
我们用信号量实现的同步
那这时候它能够做到公平吗
在我们实际用的时候呢
通常情况下假设它是公平的
在实际的系统当中呢
这个公平呢也会有些偏差的
那我们在这里说到的公平是指
我线程不会无限期的等待下去
它一定会结束
这是由于操作系统
在这里起的作用
那实际上我们在实际系统用的时候
这个P操作呢它也会说
在后面有一个等的最长时限的参数
那超时之后它直接错误返回
好 然后在这儿信号量的等待队列呢
我们是按先进先出排队
这一条呢实际上
在我们前面说到的锁机制里
自旋锁它实际上是做不到的
为什么呢 原因在于
我们自旋锁是需要占用CPU
随时随地去查
有可能临界区的使用者
退出的时候它刚改完
下一个进入者是谁去查
那它就能进去
如果说你运气不好
正好是这个资源变成有效
你去查的时候在你之前
就有一个人已经查过了
好 那么这时候呢
你就没有办法按照你
等待的这个顺序来做这一条
好 那我们看信号量的实现
我们把它定义成一个数据类型
好那这里头呢有一个变量 sem(semaphore)
然后有一个等待队列q
那这些呢都是在操作系统内核里头的
然后它定义两个操作
P操作和V操作
那在这里干些啥呢
进来之后它是信号量的变量减1
然后 如果你减完之后的值是小于0
那么这时候呢就说明我没有资源了
减到零是可以的
好 那它把这个线程呢
放到等待队列里头
并且呢阻塞
好 那这时候呢切换
好 等到我申请到了
那我就没有进入这个堵塞
我就直接进到临界区里去执行
那执行的过程当中呢
用完了退出的时候作为啥呢
信号量当中的变量加1
好 如果说加一之后还小于0
那就说实际上前面有人在等着
好 那这时候呢
我就从对应的等待队列里头
把相应的线程放到就绪队列里头
这和我们前面讲的
进程的状态的变迁
这样就结合起来了
然后它结束 它就干别的去了
那另一个 就可以进到临界区里头
好 这是我们说到信号量的实现
从这儿来讲好像这个东西很简单
和我们前面区别在哪
就是在于这一段代码
执行的原子性
它是由操作系统来保护的
为啥我们前面做不到这样一点呢
我这里头 减一和判断
那我们在前面
讨论软件方法的时候
最大的问题就在于
我减完一之后和这个判断这俩
中间可能会中断
好 由于这个中断
我们就有无数的麻烦
好 那这个时候现在有
操作系统在里头做保护了
好 我这两个代码
它的执行不会被打断
我们就会发现
这个地方 我变成一个原子之后
我的日子就好过多了
好 那这是我们说
信号量的基本原理
下面我们会去
用实际的例子
来说明它的使用方法