热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

面试必备进程同步机制内核自旋锁

进程(线程)间的同步机制是面试时的常见问题,所以准备用一个系列来好好整理下用户态与内核态的各种同步机制。本文就以内核空间的一种基础同步机制—自旋锁开始好了自旋锁是什么自旋锁就是一个

进程(线程)间的同步机制是面试时的常见问题,所以准备用一个系列来好好整理下用户态与内核态的各种同步机制。本文就以内核空间的一种基础同步机制—
自旋锁开始好了

自旋锁是什么

自旋锁就是一个二状态的原子(atomic)变量:

  • unlocked
  • locked

《面试必备进程同步机制--内核自旋锁》

当任务A希望访问被自旋锁保护的临界区(Critical Section),它首先需要这个自旋锁当前处于unlocked状态,然后它会去尝试获取(acquire)这个自旋锁(将这个变量状态修改为locked),

如果在这之后有另一个任务B同样希望去访问这段这段临界区,那么它必须要等到任务A释放(release)掉自旋锁才行,在这之前,任务B会一直等待此处,不段尝试获取(acquire),也就是我们说的自旋在这里。

自旋锁有什么特点

如果被问到这个问题,不少人可能根据上面的定义也能总结出来了:

  • “保护临界区”
  • “一直忙等待,直到锁被其他人释放”
  • “适合用在等待时间很短的场景中”

说错了吗?当然没有!并且这些的确都是自旋锁的特点,那么更多呢 ?

几个基本概念

为什么内核需要引入自旋锁?回答这个问题之前我想先简单引入以下几个基本概念:

UP & SMP

UP表示单处理器,SMP表示对称多处理器(多CPU)。一个处理器就视为一个执行单元,在任何一个时刻,只能运行在一个进程上下文或者中断上下文里。

《面试必备进程同步机制--内核自旋锁》

中断(interrupt)

中断可以发生在任务的指令过程中,如果中断处于使能,会从任务所处的进程上下文切换到中断上下文,在中断上下文中进行所谓的中断处理(ISR)。

《面试必备进程同步机制--内核自旋锁》

内核中使用 local_irq_disable()或者local_irq_save(&flags)来去使能中断。两者的区别是后者会将当前的中断使能状态先保存到flags中。

相反,内核使用local_irq_enale()来无条件的使能中断,而使用local_irq_restore(&flags)来恢复之前的中断状态。

无论是开中断还是关中断的函数都有local前缀, 这表示开关中断的只在当前CPU生效。

内核态抢占(preempt)

抢占,通俗的理解就是内核调度时,高优先级的任务从低优先的任务中抢到CPU的控制权,开始运行,其中又分为用户态抢占内核态抢占, 本文需要关心的是内核态抢占

早期版本(比2.6更早的)的内核还是非抢占式内核,也就是说当高优先级任务就绪时,除非低优先级任务主动放弃CPU(比如阻塞或者主动调用Schedule触发调度),否则高优先级任务是没有机会运行的。

而在此之后,内核可配置为抢占式内核(默认),在一些时机(比如说中断处理结束,返回内核空间时),会触发重新调度,此时高优先级的任务可以抢占原来占用CPU的低优先级任务。

《面试必备进程同步机制--内核自旋锁》

需要特别指出的是,抢占同样需要中断处于打开状态!

void __sched notrace preempt_schedule(void)
{
struct thread_info *ti = current_thread_info();
/*
* If there is a non-zero preempt_count or interrupts are disabled,
* we do not want to preempt the current task. Just return..
*/
if (likely(ti->preempt_count || irqs_disabled()))
return;

上面代码中的 preempt_count表示当前任务是否可被抢占,0表示可以被抢占,而大于0表示不可以。而irqs_disabled用来看中断是否关闭。

内核中使用preemt_disbale()来禁止抢占,使用preempt_enable()来使能可抢占。

单处理器上临界区问题

对于单处理器来说,由于任何一个时刻只会有一个执行单元,因此不存在多个执行单元同时访问临界区的情况。但是依然存在下面的情形需要保护

Case 1 任务上下文抢占

低优先级任务A进入临界区,但此时发生了调度(比如发生了中断, 然后从中断中返回),高优先级任务B开始运行访问临界区。

《面试必备进程同步机制--内核自旋锁》
解决方案:进入临界区前禁止抢占就好了。这样即使发生了中断,中断返回也只能回到任务A.

Case 2 中断上下文抢占

任务A进入临界区,此时发生了中断,中断处理函数中也去访问修改临界区。当中断处理结束时,返回任务A的上下文,但此时临界区已经变了!

《面试必备进程同步机制--内核自旋锁》

解决方案:进入临界区前禁止中断(顺便说一句,这样也顺便禁止了抢占)

Case 3 多处理器上临界区问题

除了单处理器上的问题之外,多处理上还会面临一种需要保护的情形

其他CPU访问

任务A运行在CPU_a上,进入临界区前关闭了中断(本地),而此时运行在CPU_b上的任务B还是可以进入临界区!没有人能限制它

《面试必备进程同步机制--内核自旋锁》

解决方案:任务A进入临界区前持有一个互斥结构,阻止其他CPU上的任务进入临界区,直到任务A退出临界区,释放互斥结构。

这个互斥结构就是自旋锁的来历。所以本质上,自旋锁就是为了针对SMP体系下的同时访问临界区而发明的!

内核中的自旋锁实现

接下来,我们来看一下内核中的自旋锁是如何实现的,我的内核版本是4.4.0

定义

内核使用spinlock结构表示一个自旋锁,如果不开调试信息的话,这个结构就是一个·raw_spinlock·:

typedef struct spinlock {
union {
struct raw_spinlock rlock;
// code omitted
};
} spinlock_t;

raw_spinlock这个结构展开, 可以看到这是一个体系相关的arch_spinlock_t结构

typedef struct raw_spinlock {
arch_spinlock_t raw_lock;
// code omitted
} raw_spinlock_t;

本文只关心常见的x86_64体系来说,这种情况下上述结构可展开为

typedef struct qspinlock {
atomic_t val;
} arch_spinlock_t;

上面的结构是SMP上的定义,对于UParch_spinlock_t就是一个空结构

typedef struct { } arch_spinlock_t;

啊,自旋锁就是一个原子变量(修改这个变量会LOCK总线,因此可以避免多个CPU同时对其进行修改)

API

内核使用spin_lock_init来进行自旋锁的初始化

# define raw_spin_lock_init(lock) \
do { *(lock) = __RAW_SPIN_LOCK_UNLOCKED(lock); } while (0)#define spin_lock_init(_lock) \
do { \
spinlock_check(_lock); \
raw_spin_lock_init(&(_lock)->rlock); \
} while (0)

最终val会设置为0 (对于UP,不存在这个赋值)

内核使用spin_lockspin_lock_irq或者spin_lock_irqsave 完成加锁操作;使用 spin_unlockspin_unlock_irq或者spin_unlock_irqsave完成对应的解锁。

spin_lock / spin_unlock

static inline void spin_lock(spinlock_t *lock)
{
raw_spin_lock(&lock->rlock);
}

对于UP,raw_spin_lock最后会展开为_LOCK

# define __acquire(x) (void)0
#define __LOCK(lock) \
do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)

可以看到,它就是单纯地禁止抢占。这是上面Case 1的解决办法

而对于SMP, raw_spin_lock会展开为

static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
preempt_disable();
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

这里同样会禁止抢占,然后由于spin_acquire在没设置CONFIG_DEBUG_LOCK_ALLOC时是空操作, 所以关键的语句是最后一句,将其展开后是

#define LOCK_CONTENDED(_lock, try, lock) \
lock(_lock)

所以,真正生效的是

static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{
__acquire(lock);
arch_spin_lock(&lock->raw_lock);
}

__acquire并不重要。而arch_spin_lock定义在include/asm-generic/qspinlock.h.这里会检查val,如果当前锁没有被持有(值为0),那么就通过原子操作将其修改为1并返回。

否则就调用queued_spin_lock_slowpath一直自旋。

#define arch_spin_lock(l) queued_spin_lock(l)
static __always_inline void queued_spin_lock(struct qspinlock *lock)
{
u32 val;
val = atomic_cmpxchg(&lock->val, 0, _Q_LOCKED_VAL);
if (likely(val == 0))
return;
queued_spin_lock_slowpath(lock, val);
}

以上就是spin_lock()的实现过程,可以发现除了我们熟知的等待自旋操作之外,它会在之前先调用preempt_disable禁止抢占,不过它并没有禁止中断,也就是说,它可以解决前面说的Case 1Case 3

Case 2还是有问题!

使用这种自旋锁加锁方式时,如果本地CPU发生了中断,在中断上下文中也去获取该自旋锁,这就会导致死锁

因此,使用spin_lock()需要保证知道该锁不会在该CPU的中断中使用(其他CPU的中断没问题)

解锁时成对使用的spin_unlock基本就是加锁的逆向操作,在设置了val重新为0之后,使能抢占。

static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
spin_release(&lock->dep_map, 1, _RET_IP_);
do_raw_spin_unlock(lock);
preempt_enable();
}

spin_lock_irq / spin_unlock_irq

这里我们就只关注SMP的情形了,相比之前的spin_lock中调用__raw_spin_lock, 这里多出的一个操作的就是禁止中断。

static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)
{
local_irq_disable(); // 多了一个中断关闭
preempt_disable();
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

前面说过,实际禁止中断的时候也就不会发生抢占了,那么这里其实使用preemt_disable禁止抢占是个有点多余的动作。

关于这个问题,可以看以下几个连接的讨论
CU上的讨论
Stackoverflow上的回答
linux DOC

对于的解锁操作是spin_unlock_irq会调用__raw_spin_unlock_irq。相比前一种实现方式,多了一个local_irq_enable

static inline void __raw_spin_unlock_irq(raw_spinlock_t *lock)
{
spin_release(&lock->dep_map, 1, _RET_IP_);
do_raw_spin_unlock(lock);
local_irq_enable();
preempt_enable();
}

这种方式也就解决了Case 2

spin_lock_irqsave / spin_unlock_irqsave

spin_lock_irq还有什么遗漏吗?它没有遗漏,但它最后使用local_irq_enable打开了中断,如果进入临界区前中断本来是关闭,那么通过这一进一出,中断竟然变成打开的了!这显然不合适!

因此就有了spin_lock_irqsave和对应的spin_unlock_irqsave.它与上一种的区别就在于加锁时将中断使能状态保存在了flags

static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock)
{
unsigned long flags;
local_irq_save(flags); // 保存中断状态到flags
preempt_disable();
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
do_raw_spin_lock_flags(lock, &flags); return flags;
}

而在对应的解锁调用时,中断状态进行了恢复,这样就保证了在进出临界区前后,中断使能状态是不变的。

static inline void __raw_spin_unlock_irqrestore(raw_spinlock_t *lock,
unsigned long flags)
{
spin_release(&lock->dep_map, 1, _RET_IP_);
do_raw_spin_unlock(lock);
local_irq_restore(flags); // 从 flags 恢复
preempt_enable();
}

总结

  • 内核自旋锁的主要用于SMP系统上的临界区保护,并且在UP系统上也有简化的实现
  • 内核自旋锁与抢占中断的关系密切
  • 内核自旋锁在内核有多个API,实际使用时可以灵活使用。

推荐阅读
  • A题这题贼水,直接暴力就可以了。用个bool数组记录一下,如果某一天,当前剩下的最大的出现了的话,就输出一段。1#include<stdio.h>2intn;3boolvi ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 本文介绍了闭包的定义和运转机制,重点解释了闭包如何能够接触外部函数的作用域中的变量。通过词法作用域的查找规则,闭包可以访问外部函数的作用域。同时还提到了闭包的作用和影响。 ... [详细]
  • 不同优化算法的比较分析及实验验证
    本文介绍了神经网络优化中常用的优化方法,包括学习率调整和梯度估计修正,并通过实验验证了不同优化算法的效果。实验结果表明,Adam算法在综合考虑学习率调整和梯度估计修正方面表现较好。该研究对于优化神经网络的训练过程具有指导意义。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • PG12新增的VACUUM命令的SKIP_LOCKED选项
    PG12版本的VACUUM命令新增了SKIP_LOCKED选项,该选项使得vacuum命令在遇到被lock住的table时可以跳过并被视为成功执行。之前的版本中,vacuum命令会一直处于等待状态。本文还提到了PostgreSQL 12.1版本的相关信息。 ... [详细]
  • 如何使用PLEX播放组播、抓取信号源以及设置路由器
    本文介绍了如何使用PLEX播放组播、抓取信号源以及设置路由器。通过使用xTeve软件和M3U源,用户可以在PLEX上实现直播功能,并且可以自动匹配EPG信息和定时录制节目。同时,本文还提供了从华为itv盒子提取组播地址的方法以及如何在ASUS固件路由器上设置IPTV。在使用PLEX之前,建议先使用VLC测试是否可以正常播放UDPXY转发的iptv流。最后,本文还介绍了docker版xTeve的设置方法。 ... [详细]
  • [oracle@oracle~]$impxxxx/userfile=/usr/local/src/666.dmpfull=ybuffer=40960000Import: ... [详细]
  • 阿里Treebased Deep Match(TDM) 学习笔记及技术发展回顾
    本文介绍了阿里Treebased Deep Match(TDM)的学习笔记,同时回顾了工业界技术发展的几代演进。从基于统计的启发式规则方法到基于内积模型的向量检索方法,再到引入复杂深度学习模型的下一代匹配技术。文章详细解释了基于统计的启发式规则方法和基于内积模型的向量检索方法的原理和应用,并介绍了TDM的背景和优势。最后,文章提到了向量距离和基于向量聚类的索引结构对于加速匹配效率的作用。本文对于理解TDM的学习过程和了解匹配技术的发展具有重要意义。 ... [详细]
  • 基于PgpoolII的PostgreSQL集群安装与配置教程
    本文介绍了基于PgpoolII的PostgreSQL集群的安装与配置教程。Pgpool-II是一个位于PostgreSQL服务器和PostgreSQL数据库客户端之间的中间件,提供了连接池、复制、负载均衡、缓存、看门狗、限制链接等功能,可以用于搭建高可用的PostgreSQL集群。文章详细介绍了通过yum安装Pgpool-II的步骤,并提供了相关的官方参考地址。 ... [详细]
  • Skywalking系列博客1安装单机版 Skywalking的快速安装方法
    本文介绍了如何快速安装单机版的Skywalking,包括下载、环境需求和端口检查等步骤。同时提供了百度盘下载地址和查询端口是否被占用的命令。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • Iamtryingtocreateanarrayofstructinstanceslikethis:我试图创建一个这样的struct实例数组:letinstallers: ... [详细]
  • Gitlab接入公司内部单点登录的安装和配置教程
    本文介绍了如何将公司内部的Gitlab系统接入单点登录服务,并提供了安装和配置的详细教程。通过使用oauth2协议,将原有的各子系统的独立登录统一迁移至单点登录。文章包括Gitlab的安装环境、版本号、编辑配置文件的步骤,并解决了在迁移过程中可能遇到的问题。 ... [详细]
author-avatar
wo缘相聚在空间
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有