[操作系统居家课程讲义]ch06_自旋锁与互斥锁

author: hxy
date: 2020.4.1

上一节我们提出了一个概念,信号量。在上一节的例子里,它就像交通灯、标识符一样,起到通知其它进程的作用。


这节课涉及到一些程序,希望大家可以多思考,理解背后的原理。

1. 原子操作

由于上一节所说的原因,如果用一个程序变量 flag 来控制进程对资源的使用权,由于在硬件实现过程中,一个简单的赋值操作也需要多个硬件指令来完成,而在这期间,有各种原因可能导致进程失去cpu的使用权,从而退出执行状态。

一旦出现这种情况,flag的值就会在多个进程之间变得不稳定,更遑论为需要共享的资源提供指示灯的作用。

因此操作系统为了进程之间能够同步,设置了一种特殊的变量 —— 信号量 , 我们一般用 mutex 这个单词表示。它特殊的地方在于,对于这个变量只能进行规定的几种原子操作。

所谓 原子操作 ,就是“不可中断的一个或一系列操作”。比如检查数值、修改变量值等等均为单一的、不可分割的操作。

因此,信号量的原子操作就意味着这个操作一旦开始执行,那么直到这个操作完成之前就不可能被中断,也就是说这期间cpu不可能让给其他进程使用。这种操作非常特殊,是在操作系统层面的一种系统调用。

信号量的原子操作有哪些呢?根据上一节的讨论,它的主要功能就是指示资源能否使用。这个概念我们可以用公共洗手间的指示灯作为类比。当灯是绿色的时候就说明洗手间资源可以使用,如果有人使用这个洗手间,进门的第一件事是锁门,同时指示灯会变成红色,表示资源正被占用着。此时其他人就只能在门口排队等待。

等待的过程中可以有两种选择:要么一直盯着灯看,直到它变绿;要么可以先玩会儿手机,听到开门的声音再去上厕所。

这两种选择正对应于信号量的两种实现方式:自旋锁互斥锁

之所以称为锁,正是由于前面这个卫生间的例子。因为指示灯在告诉其他人资源暂时不能使用的时候,实际上就等同于对资源上了锁。

2. 自旋锁

如果用卫生间的例子来说,自旋锁意味着,当卫生间被上锁之后,等候的人要一直盯着看(不断检查信号量的状态)。也就是说,在整个过程中,等候的人一直处于忙碌状态,保持着完成上厕所这件事。

这个不断检查的过程用程序实现就是:

while( mutex == False )//如果上了锁
{
  ; //空转,并且不断在上一行检查mutex状态
}

//直到mutex为True才能跳出上面的循环,此时意味着锁已经打开
mutex = False;//立马上锁占用资源
usingResource();//使用资源

mutex = True;//使用完毕后给资源开锁,从而允许其它进程使用

有效语句一共4行,希望大家能够看懂。usingResource()的具体实现取决于具体的进程,比如是一个播放器进程,需要使用内存中的电影资源,此时的usingResource()就被替代为Play()函数了。

在这里面,可以看出对于mutex的操作一共有两种:

  • (1)关锁: mutex = False
  • (2)开锁: mutex = True

其实,在关锁的部分,还应该加上前面的那一部分程序:判断锁的状态。合起来就是尝试关锁。

所以总结起来,实际上的操作应该是这两种:

  • (1)尝试关锁:
//尝试:
while( mutex == False )
{
  ;
}
//关锁:
mutex = False;//立马上锁占用资源
  • (2)开锁:mutex = True

这两个步骤正是将前面的那段代码分成了两个部分的结果。

我们把这两种操作分别取名叫做:P操作(关锁)和V操作(开锁)。请大家记住这两个名字,今后我们就以这这个名字来叙述我们的问题。

此外,在有的操作系统中,这两个操作是以wait函数和signal函数的形式命名的。

这种实现方式有一个很大的问题就是,等待期间我们的进程一直处于空忙状态,所谓的“空忙”,意思是虽然在while循环里什么实际工作都没有做,可是还是要为它分配cpu的时间片。这太浪费宝贵的cpu资源了。

因此就有了第二种实现方式:互斥锁。

3. 互斥锁

实际上大家在等厕所的时候也不会一直盯着厕所看,多半会暂停要上厕所这件 “任务” 去干点别的事,玩玩手机或者聊聊天,等门开了再继续完成上厕所的 “任务” 。

同样的,在进程中,如果大家还记得第四讲中提到的,进程的有一个状态叫做 “阻塞” ,那就是对应着所说的暂停上厕所任务的这种状态。在这种状态下,进程会被加入到 阻塞队列 中。

cpu的时间片只会给就绪队列中的进程按顺序分配,而不会分配给阻塞队列中的进程。也就意味着,在这个过程中,因为得不到所需的资源,进程就被暂停了。

直到进程完成任务所需的资源空出来了,系统才会把进程从阻塞队列放到就绪队列中,等待分配时间片而后继续完成任务。

这种方法实现的P操作和V操作,用程序语言来描述就是:

  • (1)P操作:
if( mutex == False )
{
  block(mutex, L);//将缺少mutex所保护的资源的进程加入到阻塞队列L中
}

计算机中的资源有很多,并发执行的进程也很多,各式各样的进程去请求使用各式各样的资源。遇到需要用mutex保护的共享资源时,都将其加入到阻塞队列中。

实际上的阻塞队列并不是一个单一的队列,它是一系列队列的统称(上面这段代码中的L有很多个),而每一个阻塞队列对应着一种资源。

也就是说,所有申请使用同一种资源的阻塞进程,都放在同一个阻塞队列中。

因此,当某个进程使用完毕某种资源后,它还应该去查找一下这种资源对应的阻塞队列中有没有等待的进程,如果有的话就去 唤醒 队列中的第一个进程,将资源让给它。

这里面可以看出来,阻塞进程的唤醒,是由释放资源的进程实施的。就好比上一个用完洗手间的人出来之后,拍拍你的肩膀说,到你了。

这就是V操作的要做的事:

  • (2)V操作:
//阻塞队列长度大于0,说明有进程在等待这个资源
if(len(L) > 0 )
{
  wakeup(L); //唤醒队首部的进程到就绪队列中去
}
mutex = True;
  • 里面涉及到了两个函数:block和wakeup,都是系统调用,由操作系统提供,用于调度进程(也就是改变进程的状态)。

  • 所以这里其实是操作系统的系统调用再调用它自身的其它系统调用。

  • 即使如此,P操作和V操作依然是不可分割的原子操作。不要着急问这种原子操作怎么实现的,问就是调度和后面要讲到的中断机制。

看起来好像没问题了,但其实这种实现方法有一个缺陷:有的资源并不是唯一的。

举个通俗的例子,比如你的电脑可能连了两台打印机,此时实际上是可以同时运行两个打印进程的。实际上计算机中有许多我们用户感受不是很明显的共享资源,他们往往都不唯一。

为了提高系统的效率,下一讲我们会给大家讲讲互斥锁的更加高级的实现方法。

这一讲的内容主要是锁的实现。当大家理解了锁的原理之后,我们再学习锁的使用。所以如果感觉这部分内容有点吃不透,囫囵吞枣也没有关系。等到下下一节,我们打磨好了工具,开始使用的时候,一切就很明了了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值