美文网首页
Linux内核设计与实现——内核同步方法

Linux内核设计与实现——内核同步方法

作者: leon4ever | 来源:发表于2018-05-02 14:19 被阅读29次

    主要内容

    1. 原子操作
    2. 自旋锁
    3. 读写自旋锁
    4. 信号量
    5. 读写信号量
    6. 互斥锁
    7. 完成变量
    8. 大内核锁
    9. 顺序锁
    10. 禁止抢占
    11. 顺序和屏障

    1. 原子操作

    原子操作可以保证指令以原子的方式执行,不会被打断
    内核提供了对整数,对位的原子操作接口
    特殊的atomic_t类型,32位int的低8位嵌入了一个锁

    2. 自旋锁

    原子操作只能用于临界区只有一个变量的情况,实际应用中,临界区的情况要复杂的多。
    对于复杂的临界区,linux内核中也提供了多种同步方法,自旋锁就是其中一种。

    自旋锁的特点就是当一个线程获取了锁之后,其他试图获取这个锁的线程一直在循环等待获取这个锁,直至锁重新可用。
    由于线程实在一直循环的获取这个锁,所以会造成CPU处理时间的浪费,因此最好将自旋锁用于能很快处理完的临界区。

    注意:

    1. 自旋锁是不可递归的,递归的请求同一个自旋锁会自己锁死自己。
    2. 线程获取自旋锁之前,要禁止当前处理器上的中断。(防止获取锁的线程和中断形成竞争条件,导致死锁)
      比如:当前线程获取自旋锁后,在临界区中被中断处理程序打断,中断处理程序正好也要获取这个锁,
      于是中断处理程序会等待当前线程释放锁,而当前线程也在等待中断执行完后再执行临界区和释放锁的代码。

    中断处理下半部:

    1. 下半部处理和进程上下文共享数据时,由于下半部的处理可以抢占进程上下文的代码,
      所以进程上下文在对共享数据加锁前要禁止下半部的执行,解锁时再允许下半部的执行。
    2. 中断处理程序(上半部)和下半部处理共享数据时,由于中断处理(上半部)可以抢占下半部的执行,
      所以下半部在对共享数据加锁前要禁止中断处理(上半部),解锁时再允许中断的执行。
    3. 同一种tasklet不能同时运行,所以同类tasklet中的共享数据不需要保护。
    4. 不同类tasklet中共享数据时,其中一个tasklet获得锁后,不用禁止其他tasklet的执行,因为同一个处理器上不会有tasklet相互抢占的情况
    5. 同类型或者非同类型的软中断在共享数据时,也不用禁止下半部,因为同一个处理器上不会有软中断互相抢占的情况

    3. 读写自旋锁

    用于生产者消费者类型的读写自旋锁

    读锁之间共享,写锁之间互斥

    4.信号量

    信号量也是一种锁,和自旋锁不同的是,线程获取不到信号量的时候,不会像自旋锁一样循环的去试图获取锁,而是进入睡眠,直至有信号量释放出来时,才会唤醒睡眠的线程,进入临界区执行。

    由于使用信号量时,线程会睡眠,所以等待的过程不会占用CPU时间。所以信号量适用于等待时间较长的临界区。
    信号量消耗的CPU时间的地方在于使线程睡眠和唤醒线程,如果 (使线程睡眠 + 唤醒线程)的CPU时间 > 线程自旋等待的CPU时间,那么可以考虑使用自旋锁。

    信号量有二值信号量和计数信号量2种,其中二值信号量比较常用。

    1. 二值信号量表示信号量只有2个值,即0和1。信号量为1时,表示临界区可用,信号量为0时,表示临界区不可访问。
      二值信号量表面看和自旋锁很相似,区别在于争用自旋锁的线程会一直循环尝试获取自旋锁,
      而争用信号量的线程在信号量为0时,会进入睡眠,信号量可用时再被唤醒。
    2. 计数信号量有个计数值,比如计数值为5,表示同时可以有5个线程访问临界区。

    5. 读写信号量

    读写信号量和信号量之间的关系 与 读写自旋锁和普通自旋锁之间的关系 差不多。

    6. 互斥锁

    互斥体也是一种可以睡眠的锁,相当于二值信号量,只是提供的API更加简单,使用的场景也更严格一些,如下所示:

    1. mutex的计数值只能为1,也就是最多只允许一个线程访问临界区
    2. 在同一个上下文中上锁和解锁
    3. 不能递归的上锁和解锁
    4. 持有个mutex时,进程不能退出
    5. mutex不能在中断或者下半部中使用,也就是mutex只能在进程上下文中使用
    6. mutex只能通过官方API来管理,不能自己写代码操作它

    如何选择?

    在面对互斥体和信号量的选择时,只要满足互斥体的使用场景就尽量优先使用互斥体。
    在面对互斥体和自旋锁的选择时,参见下表:

    需求 建议的加锁方法
    低开销加锁 优先使用自旋锁
    短期锁定 优先使用自旋锁
    长期加锁 优先使用互斥体
    中断上下文中加锁 使用自旋锁
    持有锁需要睡眠 使用互斥体

    7. 完成变量

    如果一个任务要执行一些工作时,另一个任务就会在完成变量上等待。当这个任务完成工作后,会使用完成变量去唤醒在等待的任务。
    实现机制类似于信号量。

    8. 大内核锁

    不再使用,只存在于一些遗留代码

    9. 顺序锁

    为读写共享数据提供了一种简单的实现机制。

    前提到的读写自旋锁和读写信号量,在读锁被获取之后,写锁是不能再被获取的,也就是说,必须等所有的读锁释放后,才能对临界区进行写入操作。
    顺序锁则与之不同,读锁被获取的情况下,写锁仍然可以被获取。
    使用顺序锁的读操作在读之前和读之后都会检查顺序锁的序列值,如果前后值不符,则说明在读的过程中有写的操作发生,那么读操作会重新执行一次,直至读前后的序列值是一样的。

    10. 禁止抢占

    其实使用自旋锁已经可以防止内核抢占了,但是有时候仅仅需要禁止内核抢占,不需要像自旋锁那样连中断都屏蔽掉。

    这时候就需要使用禁止内核抢占的方法了:

    方法 描述
    preempt_disable() 增加抢占计数值,从而禁止内核抢占
    preempt_enable() 减少抢占计算,并当该值降为0时检查和执行被挂起的需调度的任务
    preempt_enable_no_resched() 激活内核抢占但不再检查任何被挂起的需调度的任务
    preempt_count() 返回抢占计数

    11. 顺序和屏障

    一种同步机制(又称栅栏,关卡),用于对一组线程进行协调,所有线程到达一个汇合点后再一起向前推进

    总结

    同步方法总结.png

    相关文章

      网友评论

          本文标题:Linux内核设计与实现——内核同步方法

          本文链接:https://www.haomeiwen.com/subject/kafcrftx.html