豫ICP备17040950号-2

操作系统中锁的原理

文章目录
  1. 1. 概述
  2. 2. 锁的本质
  3. 3. 硬件上怎么实现的
  4. 4. 小结
  • 概述

    为了保证数据的一致性,在多线程编程中我们会用到锁,使得在某一时间点,只有一个线程进入临界区代码。虽然不同的语言可能会提供不同的锁接口,但是底层调用的都是操作系统的提供的锁,不同的高级语言只是在操作系统的锁机制基础上进行了些封装而已,要真正理解锁,还是得看操作系统是怎么实现锁的。

  • 锁的本质

    所谓的锁,本质上只是内存中的一个整形数,不同的数值表示不同的状态,比如1表示空闲状态和加锁状态。加锁时,判断锁是否空闲,如果空闲,修改为加锁状态,返回成功,如果已经上锁,返回失败,解锁时,就把锁状态修改为空闲状态。        加锁和解锁看起来都很简单,但是os是怎么保证锁操作本身的原子性呢? 在多核环境中,两个核上的代码同时申请一个锁,两个核同时读取锁变量,同时判断锁是空闲的,再各自修改锁变量为上锁状态,都返回成功,这样两个核同时获取到了锁, 这种情况可能吗? 当然是不可能的,那么os是通过什么手段来保证锁操作本身的原子性的呢?我们可以把上锁的过程具体表示为:

    1
    2
    3
    4
    5
    1.读内存表示锁的变量
    2.判断锁的状态
    3.如果已经加锁,返回失败
    4.把锁设置为上锁状态,
    5.返回成功

上面的每一个步骤都对应一条汇编语句,可以认为这每一步操作都是原子的,什么情况会导致两个线程同时获取到锁?
中断: 当线程A执行完第一步后,发生了中断,os调度线程B,线程B也来加锁并且加锁成功,此时又发生中断,OS调度线程A执行,从第二步开始,也加锁 成功。
多核: 见上面例子。

那么怎么解决呢? 能不能让硬件做一种加锁的原子操作呢? 大名鼎鼎的“test and set”指令就是做这个事情的,该指令将读取内存、判断和设置值作为一个原子操作。单核环境下,锁的操作肯定是原子性了,多核呢?貌似还是不行,因为多个核心他们的锁操作是没有干扰的,都能够同时执行“test and set”,还是会出现两个线程同时获取到锁的情况, 所以硬件提供了锁内存总线的机制,在锁内存总线的状态下执行“test and set”操作就可以保证一个只有一个核执行成功,也就保证了不会存在多线程获取到锁的情况。

  • 硬件上怎么实现的

    前面提到,cpu会通过对总线加锁的手段来解决多核同时获取锁的情况,它到时是怎么实现的呢? 在cpu芯片上有一个HLOCK Pin,可以通过发送指令来操作,将#HLOCK Pin电位拉低,并持续到这条指令执行完毕,从而将总线锁住,这样同一总线上的其他CPU就不能通过总线来访问内存了。最开始这些功能是用来测试cpu的,后来被操作系统实现而封装成各种功能:关键代码段,信号量等。        在加锁的代码编译成汇编后,会有个lock指令前缀:

    Causes the processor’s LOCK# signal to be asserted during execution of the accompanying instruction (turns the instruction into an atomic instruction). In a multiprocessor environment, the LOCK# signal insures that the processor has exclusive use of any shared memory while the signal is asserted.
    lock会使得紧跟在其后的指令变成atomic instruction,暂时的锁一下总线,指令执行完,总线就解锁了。

  • 小结

    在硬件层面,cpu提供了原子操作、锁内存总线等机制,OS根据这几个cpu硬件机制就能够实现锁,在基于锁,就能实现各种各样的同步机制(信号量、消息等等),要理解os提供的各种同步手段,需要先理解os是怎么实现锁的。