一文搞懂 , Linux内核—— 同步管理(上)

write_sequnlock_irqrestore 会调用 _raw_spin_unlock_irqrestore 么 通过调用链完成底层锁释放和中断恢复。理解这一关系对调试锁争用和优化内核性能至关重要。 阅读详情

因为现代操作系统是多处理器计算的架构,必然更容易遇到多个进程,多个线程访问共享数据的情况,如下图所示:

图中每一种颜色代表一种竞态情况,主要归结为三类:

  1. 进程与进程之间:单核上的抢占,多核上的SMP;
  2. 进程与中断之间:中断又包含了上半部与下半部,中断总是能打断进程的执行流;
  3. 中断与中断之间:外设的中断可以路由到不同的CPU上,它们之间也可能带来竞态;

这时候就需要一种同步机制来保护并发访问的内存数据。本系列文章分为两部分,这一章主要讨论原子操作,自旋锁,信号量和互斥锁

原子操作

原子操作是在执行结束前不可打断的操作,也是最小的执行单位。以 arm 平台为例,原子操作的 API 包括如下:

原子操作通常是内联函数,往往是通过内嵌汇编指令来实现的,如果某个函数本身就是原子的,它往往被定义成一个宏,以下为例。

#define ATOMIC_OP(op, c_op, asm_op)     \
static inline void atomic_##op(int i, atomic_t *v)   \
{         \
 unsigned long tmp;      \
 int result;       \
         \
 prefetchw(&v->counter);      \
 __asm__ __volatile__("@ atomic_" #op "\n"   \
"1: ldrex %0, [%3]\n"      \
" " #asm_op " %0, %0, %4\n"     \
" strex %1, %0, [%3]\n"      \
" teq %1, #0\n"      \
" bne 1b"       \
 : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)  \
 : "r" (&v->counter), "Ir" (i)     \
 : "cc");       \
}

可见原子操作的原子性依赖于 ldrex 与 strex 实现,ldrex 读取数据时会进行独占标记,防止其他内核路径访问,直至调用 strex 完成写入后清除标记。

ldrex 和 strex 指令,是将单纯的更新内存的原子操作分成了两个独立的步骤:

  1. ldrex 用来读取内存中的值,并标记对该段内存的独占访问:
ldrex Rx, [Ry]

读取寄存器 Ry 指向的4字节内存值,将其保存到 Rx 寄存器中,同时标记对 Ry 指向内存区域的独占访问。如果执行 ldrex 指令的时候发现已经被标记为独占访问了,并不会对指令的执行产生影响。

  1. strex 在更新内存数值时,会检查该段内存是否已经被标记为独占访问,并以此来决定是否更新内存中的值:
strex Rx, Ry, [Rz]

如果执行这条指令的时候发现已经被标记为独占访问了,则将寄存器 Ry 中的值更新到寄存器 Rz 指向的内存,并将寄存器 Rx 设置成 0。指令执行成功后,会将独占访问标记位清除。如果执行这条指令的时候发现没有设置独占标记,则不会更新内存,且将寄存器 Rx 的值设置成 1。

ARM 内部的实现如下所示,这里不再赘述。

自旋锁 spin_lock

Linux内核中最常见的锁是自旋锁,自旋锁最多只能被一个可执行线程持有。如果一个线程试图获取一个已被持有的自旋锁,这个线程会进行忙循环——旋转等待(会浪费处理器时间)锁重新可用。自旋锁持有期间不可被抢占。

另一种处理锁争用的方式:让等待线程睡眠,直到锁重新可用时再唤醒它,这样处理器不必循环等待,可以去执行其他代码,但是这会有两次明显的上下文切换的开销,信号量便提供了这种锁机制。

自旋锁的使用接口如下:

以 spin_lock 为例看下它的用法:

DEFINE_SPINLOCK(mr_lock);
spin_lock(&mr_lock);
/* 临界区 */
spin_unlock(&mr_lock);

static inline void arch_spin_lock(arch_spinlock_t *lock)
{
 unsigned int tmp;
 arch_spinlock_t lockval, newval;
 
 asm volatile(
 /* Atomically increment the next ticket. */
 ARM64_LSE_ATOMIC_INSN(
 /* LL/SC */
" prfm pstl1strm, %3\n"
"1: ldaxr %w0, %3\n"
" add %w1, %w0, %w5\n"
" stxr %w2, %w1, %3\n"
" cbnz %w2, 1b\n",
 /* LSE atomics */
" mov %w2, %w5\n"
" ldadda %w2, %w0, %3\n"
 __nops(3)
 )
 
 /* Did we get the lock? */
" eor %w1, %w0, %w0, ror #16\n"
" cbz %w1, 3f\n"
 /*
  * No: spin on the owner. Send a local event to avoid missing an
  * unlock before the exclusive load.
  */
" sevl\n"
"2: wfe\n"
" ldaxrh %w2, %4\n"
" eor %w1, %w2, %w0, lsr #16\n"
" cbnz %w1, 2b\n"
 /* We got the lock. Critical section starts here. */
"3:"
 : "=&r" (lockval), "=&r" (newval), "=&r" (tmp), "+Q" (*lock)
 : "Q" (lock->owner), "I" (1 << TICKET_SHIFT)
 : "memory");
}

static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
 unsigned long tmp;
 
 asm volatile(ARM64_LSE_ATOMIC_INSN(
 /* LL/SC */
 " ldrh %w1, %0\n"
 " add %w1, %w1, #1\n"
 " stlrh %w1, %0",
 /* LSE atomics */
 " mov %w1, #1\n"
 " staddlh %w1, %0\n"
 __nops(1))
 : "=Q" (lock->owner), "=&r" (tmp)
 :
 : "memory");
}

上边的代码中,核心逻辑在于 asm volatile() 内联汇编中,有很多独占的操作指令,只有基于指令的独占操作,才能保证软件上的互斥。把核心逻辑翻译成 C 语言:

可以看出,Linux 中针对每一个 spin_lock 有两个计数。分别是 next 和 owner(初始值为0)。进程 A 申请锁时,会判断 next 和 owner 的值是否相等。如果相等就代表锁可以申请成功,否则原地自旋。直到 owner 和 next 的值相等才会退出自旋。

信号量 Semaphore

信号量是在多线程环境下使用的一种措施,它负责协调各个进程,以保证他们能够正确、合理的使用公共资源。它和 spin_lock 最大的不同之处就是:无法获取信号量的进程可以睡眠,因此会导致系统调度。

信号量的定义如下:

struct semaphore {
 raw_spinlock_t  lock;      //利用自旋锁同步
 unsigned int  count;      //用于资源计数
 struct list_head wait_list; //等待队列
};

信号量在创建时设置一个初始值 count,用于表示当前可用的资源数。一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作为 count - 1。若当前 count 为负数,表明无法获得信号量,该任务必须挂起在该信号量的等待队列等待;若当前 count 为非负数,表示可获得信号量,因而可立刻访问被该信号量保护的共享资源。

当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量是操作 count + 1,如果加一后的 count 为非正数,表明有任务等待,则唤醒所有等待该信号量的任务。

了解了信号量的结构与定义,接下来我们看下常用的信号量接口:

这里我们看下最核心的两个实现 down 和 up。

  • down

down 用于调用者获得信号量,若 count 大于0,说明资源可用,将其减一即可。

void down(struct semaphore *sem)
{
 unsigned long flags;
 
 raw_spin_lock_irqsave(&sem->lock, flags);
 if (likely(sem->count > 0))
  sem->count--;
 else
  __down(sem);
 raw_spin_unlock_irqrestore(&sem->lock, flags);
}
EXPORT_SYMBOL(down);

若 count < 0,调用函数 __down(),将 task 加入等待队列,并进入等待队列,并进入调度循环等待,直至其被 __up 唤醒,或者因超时以被移除等待队列。

static inline int __sched __down_common(struct semaphore *sem, long state,
        long timeout)
{
 struct semaphore_waiter waiter;
 
 list_add_tail(&waiter.list, &sem->wait_list);
 waiter.task = current;
 waiter.up = false;
 
 for (;;) {
  if (signal_pending_state(state, current))
   goto interrupted;
  if (unlikely(timeout <= 0))
   goto timed_out;
  __set_current_state(state);
  raw_spin_unlock_irq(&sem->lock);
  timeout = schedule_timeout(timeout);
  raw_spin_lock_irq(&sem->lock);
  if (waiter.up)
   return 0;
 }
 
 timed_out:
 list_del(&waiter.list);
 return -ETIME;
 
 interrupted:
 list_del(&waiter.list);
 return -EINTR;
}

up

up 用于调用者释放信号量,若 waitlist 为空,说明无等待任务,count + 1,该信号量可用。

void up(struct semaphore *sem)
{
 unsigned long flags;
 
 raw_spin_lock_irqsave(&sem->lock, flags);
 if (likely(list_empty(&sem->wait_list)))
  sem->count++;
 else
  __up(sem);
 raw_spin_unlock_irqrestore(&sem->lock, flags);
}
EXPORT_SYMBOL(up);

若 waitlist 非空,将 task 从等待队列移除,并唤醒该 task,对应 __down 条件。

static noinline void __sched __up(struct semaphore *sem)
{
 struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list,
      struct semaphore_waiter, list);
 list_del(&waiter->list);
 waiter->up = true;
 wake_up_process(waiter->task);
}

互斥锁 mutex

Linux 内核中,还有一种类似信号量的同步机制叫做互斥锁。互斥锁类似于 count 等于 1 的信号量。所以说信号量是在多个进程/线程访问某个公共资源的时候,进行保护的一种机制。而互斥锁是单个进程/线程访问某个公共资源的一种保护,于互斥操作。

互斥锁有一个特殊的地方:只有持锁者才能解锁。如下图所示:

用一句话来讲信号量和互斥锁的区别,就是信号量用于线程的同步,互斥锁用于线程的互斥。

互斥锁的结构体定义:

struct mutex {
 atomic_long_t  owner; //互斥锁的持有者
 spinlock_t  wait_lock; //利用自旋锁同步
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
 struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
 struct list_head wait_list; //等待队列
......
};

其常用的接口如下所示:

 更多Linux内核源码高阶知识请加开发交流Q群篇【318652197】获取,进群免费获取相关资料,免费观看公开课技术分享,入群不亏,快来加入我们吧~ 

学习资料免费领 

学习直通车 

 

Linux驱动开发中并发与竞争学习笔记【上】(2)__raw_spin_unlock_irqrestore失败 你的支持,我的动力;祝各位前程似锦,offer不断,步步高升!!!z获取!!!(备注:嵌入式)**你的支持,我的动力;祝各位前程似锦,offer不断,步步高升!!!更多资料点击此处获qu!! 阅读详情

相关推荐

Sqribble文档流水线:模板驱动的自动化PDF生成系统解析

PDF生成是内容交付的关键环节,其本质是将结构化文本转化为符合印刷与阅读规范的静态出版物。其核心原理在于模板化布局、样式标准化与状态化渲染,技术价值体现在消除人工排版误差、保障跨设备输出一致性、提升非设计人员的内容交付效率。典型应用场景包括企业知识手册编纂、销售工具包快速制作、客户定制化方案生成及培训资料批量输出。本文深入剖析Sqribble这一以Template-Driven为内核的云原生文档流水线系统,揭示其如何通过模块化架构、约束式交互与自动化引擎,在‘内容’与‘成品’之间构建稳定、可复用、低门槛的转

438

并发与同步——spin_lock

spin_lock

qq_42693685的博客 2068

spin_lock_irqsave关中断后,为什么要再禁止抢占呢,不多余吗?

spin_lock_irqsave关中断后,为什么要再禁止抢占呢,不多余吗? static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock) {         unsigned long flags;         local_irq_save(flags);         preempt_

兰宝的专栏 6085

[CPU] 一起内核线程异常占用CPU资源的排查过程

最近有个产品要把内核从4.19升级到 5.10.35版本,产品在内核版本4.19工作正常,升级到5.10.35以后,产品在不接任何外设,不跑应用程序的情况下,发现会有1个核完全被内核异常占用掉,系统启动后,异常现象一直持续。可以看到内核在不停的在 _raw_spin_unlock_irq 与_raw_spin_unlock_irqrestore 之间 频繁调用。现在我们已经确定了该CPU在频繁调用 _raw_spin_unlock_irq 函数,但是我们依然无法判断出具体是那一块的问题。

只有自己觉到悟到的,才是自己的。 3424

一文搞懂Linux内核—— 同步管理(下)

上面讲的自旋锁,信号量和互斥锁的实现,都是使用了原子操作指令。由于原子操作会 lock,当线程在多个 CPU 上争抢进入临界区的时候,都会操作那个在多个 CPU 之间共享的数据 lock。CPU 0 操作了 lock,为了数据的一致性,CPU 0 的操作会导致其他 CPU 的 L1 中的 lock 变成 invalid,在随后的来自其他 CPU 对 lock 的访问会导致 L1 cache miss(更准确的说是communication cache miss),必须从下一个 level 的 cache 中

csbmww的博客 722

Linux内核同步管理--0.概述

Linux内核的同步机制是确保系统高效稳定运行的关键。通过同步与互斥机制,可以有效地避免多个进程并发访问同一临界资源,从而保证系统的数据一致性和正确性。而对于并发与竞态问题,我们需要充分认识其产生的原因和影响,并采取相应的措施加以解决。在实际开发中,需要根据具体的应用场景和性能需求,选择合适的同步机制和解决方法,以确保系统的稳定性和可靠性。

甜航一直在的博客 271

一文搞懂 | Linux 同步管理

一文搞懂 | Linux 同步管理(下) RCU 解决了什么 RCU 是读写锁的高性能版本,它的核心理念是读者访问的同时,写者可以更新访问对象的副本,但写者需要等待所有读者完成访问之后,才能删除老对象。读者没有任何同步开销,而写者的同步开销则取决于使用的写者间同步机制。 RCU 适用于需要频繁的读取数据,而相应修改数据并不多的情景,例如在文件系统中,经常需要查找定位目录,而对目录的修改相对来说并不多,这就是 RCU 发挥作用的最佳场景。 RCU 例子 RCU 常用的接口如下图所示: API

zdy0_2004的专栏 845

一文吃透Linux同步管理,让你的系统稳如泰山

中断是指计算机在执行程序的过程中,当出现某些紧急事件时,暂时停止当前程序的执行,转去处理这些事件,处理完毕后再返回原来的程序继续执行。比如,当有新的数据到达网络接口卡时,会产生一个中断信号,通知 CPU 进行处理。抢占则属于进程调度的概念,Linux 内核从 2.6 版本开始支持抢占调度。通俗地讲,抢占就是一个正在 CPU 上愉快运行的任务(可以是用户态进程,也可以是内核线程)被另一个通常是更高优先级的任务夺去 CPU 执行权。中断和抢占之间有着密切的关系,抢占依赖中断。

深度Linux 749

Linux内核Linux内核同步管理

原子操作是保证指令以原子的方式执行,在执行原子指令的时候不会被打断。比如经典例子,A线程和B线程都尝试对i=1的变量进行+1操作,A进程获取到i=1变量后,被B中断,B获取到i=1后进行+1操作得到i=2然后进行写回,接着A线程从睡眠中恢复,又对i=1进行+1操作,最终得到i=2写回。这很明显和想要的结果不一致针对上述例子,可以使用加锁的方式,例如使用自旋锁来保证i++操作的原子性,但是加锁操作会导致比较大的开销,用在这里有些浪费。Linux内核提供了atomic_t。

weixin_45434953的博客 812

Linux 内核开发学习

123 欢迎使用Markdown编辑器 你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。 新的改变 我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客: 全新的界面设计 ,将会带来全新的写作体验; 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示; 增加

weixin_39273426的博客 1865

一文搞懂 | Linux 同步管理(下)

上面讲的自旋锁,信号量和互斥锁的实现,都是使用了原子操作指令。由于原子操作会 lock,当线程在多个 CPU 上争抢进入临界区的时候,都会操作那个在多个 CPU 之间共享的数据 lock。...

Peter的专栏 607

Linux内核学习——同步管理

u8 pending;u8 locked;#endif。

czw 1003

一文搞懂 | Linux 同步管理(上)

因为现代操作系统是多处理器计算的架构,必然更容易遇到多个进程,多个线程访问共享数据的情况,如下图所示:图中每一种颜色代表一种竞态情况,主要归结为三类:进程与进程之间:单核上的抢占,多核上的...

Peter的专栏 640

Linux内核同步管理--1.原子操作

Linux内核中的原子操作是确保多处理器环境下数据一致性的重要同步机制。原子操作具有不可分割性,在执行过程中不会被中断,且高度依赖硬件架构支持。x86架构通过LOCK指令实现总线锁定,而ARM架构采用LDREX/STREX独占访问指令。Linux提供atomic_t类型和原子操作API(如atomic_inc、atomic_dec等),广泛应用于资源引用计数、并发控制和状态标志等场景。这些机制有效避免了数据竞争,是构建高效同步原语(如自旋锁)的基础,为内核资源管理提供了安全保证。

甜航一直在的博客 1226

Linux内核同步管理--2.内存屏障

Linux内核中的内存屏障机制是确保多核处理器环境下数据一致性与执行顺序性的关键同步原语。内存屏障分为读屏障、写屏障和全屏障三种类型,分别用于阻止指令重排序、强制缓存同步及保证内存操作的可见性。这些屏障通过硬件指令(如x86的lfence/sfence/mfence)和编译器优化控制实现,广泛应用于生产者-消费者模型、设备驱动及自旋锁等同步场景。内存屏障能有效解决多线程编程中因乱序执行导致的数据竞争问题,是构建可靠并发系统的重要基础。

甜航一直在的博客 1319

Linux内核同步管理--3.自旋锁

自旋锁是一种高效的同步机制,通过忙等待避免线程切换开销,适用于锁持有时间极短的场景。本文介绍了自旋锁的基本概念、工作原理及与其他锁机制的区别,分析了其优缺点。重点阐述了自旋锁的实现原理,包括原子操作与内存顺序,并提供了C++代码示例。此外,还探讨了自旋锁在Linux内核中的应用场景及使用注意事项,如避免死锁和禁止睡眠等。最后提出了自适应自旋锁等优化策略,以提高性能。深入理解自旋锁有助于开发者在并发编程中选择合适的同步机制,优化系统性能。

甜航一直在的博客 845

Linux 内核同步管理全解:原理 + 实战 + 考点

Linux内核同步机制详解》文章摘要:本文系统讲解了Linux内核中五种核心同步机制(原子变量、自旋锁、信号量、互斥锁、RCU)的原理与适用场景,通过代码示例对比其特性差异。重点剖析了中断上下文与睡眠场景下的选择策略,并提供共享计数器保护等实战案例。文末总结同步机制的选用指南,并附面试高频问题解析,帮助开发者掌握多核并发环境下的资源保护技术。推荐结合《Yocto项目实战教程》深化嵌入式Linux系统开发能力。(150字)

Interview_TC的博客 541

Linux同步管理资源

并发和竞赛 并发管理是操作系统的核心问题之一,并发有关的bug很容易产生并且很难被发现。 事件序列就是一个竞赛条件,竞赛条件是对数据无控制访问的结果。 并行和管理 避免竞赛条件的方法:使用内核并发原语和基本原则 互斥和信号量 #include <asm/semaphore.h> void sema_init(struct semaphore *sem, int val); //val是原始赋值给semaphore的值。 DECLARE_MUTEX(name); // DE.

ZHANGJNWEI的专栏 242
上一篇: 当Linux内存耗尽时,该如何处理?
下一篇: 一文搞懂 , Linux内核—— 同步管理(下)
csbmww
博客等级 码龄4年 29粉丝 43原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值