自旋锁spin_lock和raw_spin_lock

本文不打算详细探究spin_lock的详细实现机制,只是最近对raw_spin_lock的出现比较困扰,搞不清楚什么时候用spin_lock,什么时候用raw_spin_lock,因此有了这篇文章。


/*****************************************************************************************************/
声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!
/*****************************************************************************************************/

1.  临界区(Critical Section)

我们知道,临界区是指某个代码区间,在该区间中需要访问某些共享的数据对象,又或者是总线,硬件寄存器等,通常这段代码区间的范围要控制在尽可能小的范围内。临界区内需要对这些数据对象和硬件对象的访问进行保护,保证在退出临界区前不会被临界区外的代码对这些对象进行修改。出现以下几种情形时,我们需要使用临界区进行保护:
  • (1)  在可以抢占(preemption)的系统中,两个线程同时访问同一个对象;
  • (2)  线程和中断同时访问同一个对象;
  • (3)  在多核系统中(SMP),可能两个CPU可能同时访问同一个对象;

2.  自旋锁(spin_lock)

针对单处理器系统,对第一种情况,只要临时关闭系统抢占即可,我们可以使用以下方法:
preempt_disable();
.....
// 访问共享对象的代码
......
preempt_enable();

同样地,针对单处理器系统,第二种情况,只要临时关闭中断即可,我们可以使用以下方法:

local_irq_disable();
......
// 访问共享对象的代码
......
local_irq_enable();

那么,针对多处理器的系统,以上的方法还成立么?答案很显然:不成立。

对于第一种情况,虽然抢占被禁止了,可是另一个CPU上还有线程在运行,如果这个线程也正好要访问该共享对象,上面的代码段显然是无能为力了。

对于第二种情况,虽然本地CPU的中断被禁止了,可是另一个CPU依然可以产生中断,如果他的中断服务程序也正好要访问该共享对象,上面的代码段也一样无法对共享对象进行保护。

实际上,在linux中,上面所说的三种情况都可以用自旋锁(spin_lock)解决。基本的自旋锁函数对是:
  • spin_lock(spinlock_t *lock);
  • spin_unlock(spinlock_t *lock);
对于单处理器系统,在不打开调试选项时,spinlock_t实际上是一个空结构,把上面两个函数展开后,实际上就只是调用preempt_disable()和preempt_enable(),对于单处理器系统,关掉抢占后,其它线程不能被调度运行,所以并不需要做额外的工作,除非中断的到来,不过内核提供了另外的变种函数来处理中断的问题。

对于多处理器系统,spinlock_t实际上等效于内存单元中的一个整数,内核保证spin_lock系列函数对该整数进行原子操作,除了调用preempt_disable()和preempt_enable()防止线程被抢占外,还必须对spinlock_t上锁,这样,如果另一个CPU的代码要使用该临界区对象,就必须进行自旋等待。

对于中断和普通线程都要访问的对象,内核提供了另外两套变种函数:
  • spin_lock_irq(spinlock_t *lock);
  • spin_unlock_irq(spinlock_t *lock);
和:
  • spin_lock_irqsave(lock, flags);
  • spin_lock_irqrestore(lock, flags);
我们可以按以下原则使用上面的三对变种函数(宏):
  • 如果只是在普通线程之间同时访问共享对象,使用spin_lock()/spin_unlock();
  • 如果是在中断和普通线程之间同时访问共享对象,并且确信退出临界区后要打开中断,使用spin_lock_irq()/spin_unlock_irq();
  • 如果是在中断和普通线程之间同时访问共享对象,并且退出临界区后要保持中断的状态,使用spin_lock_irqsave()/spin_unlock_irqrestore();
其实变种还不止这几个,还有read_lock_xxx/write_lock_xxx、spin_lock_bh/spin_unlock_bh、spin_trylock_xxx等等。但常用的就上面几种。

3.  raw_spin_lock

在2.6.33之后的版本,内核加入了raw_spin_lock系列,使用方法和spin_lock系列一模一样,只是参数有spinlock_t变为了raw_spinlock_t。而且在内核的主线版本中,spin_lock系列只是简单地调用了raw_spin_lock系列的函数,但内核的代码却是有的地方使用spin_lock,有的地方使用raw_spin_lock。是不是很奇怪?要解答这个问题,我们要回到2004年,MontaVista Software, Inc的开发人员在邮件列表中提出来一个Real-Time Linux Kernel的模型,旨在提升Linux的实时性,之后Ingo Molnar很快在他的一个项目中实现了这个模型,并最终产生了一个Real-Time preemption的patch。
该模型允许在临界区中被抢占,而且申请临界区的操作可以导致进程休眠等待,这将导致自旋锁的机制被修改,由原来的整数原子操作变更为信号量操作。当时内核中已经有大约10000处使用了自旋锁的代码,直接修改spin_lock将会导致这个patch过于庞大,于是,他们决定只修改哪些真正不允许抢占和休眠的地方,而这些地方只有100多处,这些地方改为使用raw_spin_lock,但是,因为原来的内核中已经有raw_spin_lock这一名字空间,用于代表体系相关的原子操作的实现,于是linus本人建议:
  • 把原来的raw_spin_lock改为arch_spin_lock;
  • 把原来的spin_lock改为raw_spin_lock;
  • 实现一个新的spin_lock;
写到这里不知大家明白了没?对于2.6.33和之后的版本,我的理解是:
  • 尽可能使用spin_lock;
  • 绝对不允许被抢占和休眠的地方,使用raw_spin_lock,否则使用spin_lock;
  • 如果你的临界区足够小,使用raw_spin_lock;
对于没有打上Linux-RT(实时Linux)的patch的系统,spin_lock只是简单地调用raw_spin_lock,实际上他们是完全一样的,如果打上这个patch之后,spin_lock会使用信号量完成临界区的保护工作,带来的好处是同一个CPU可以有多个临界区同时工作,而原有的体系因为禁止抢占的原因,一旦进入临界区,其他临界区就无法运行,新的体系在允许使用同一个临界区的其他进程进行休眠等待,而不是强占着CPU进行自旋操作。写这篇文章的时候,内核的版本已经是3.3了,主线版本还没有合并Linux-RT的内容,说不定哪天就会合并进来,也为了你的代码可以兼容Linux-RT,最好坚持上面三个原则。

<think>好的,我需要解释用户提供的汇编指令`do_raw_read_lock rsbpls %0, %1, #0`。首先,用户之前询问过`STREXPL`,所以可能对ARM汇编有一定的了解。现在这个指令看起来也是ARM架构的,但具体含义需要拆解。 首先,注意到`rsbpls`是一个操作码,可能由多个部分组成。ARM的条件码指令后缀需要分析。`rsb`是逆向减法(Reverse Subtract),`pl`是条件码(条件执行),`s`可能表示更新状态标志。所以`rsbpls`可能分解为`rsb`操作,条件为`pl`(N标志为0,即结果非负),并更新状态标志。 接下来,操作数部分是`%0, %1, #0`。在嵌入式汇编中,`%0``%1`通常指代寄存器占位符,具体对应由编译器分配。这里的第三个操作数是立即数0。 然后,整个指令的功能可能是将`%1`的值进行逆向减法,即`0 - %1`,然后根据条件`pl`(非负)执行,并更新标志。这可能用于某种锁机制,比如自旋锁的读取锁获取。 需要验证`rsbpls`的合法性。查阅ARM指令集手册,`rsb`确实存在,条件码标志更新后缀组合是允许的,如`rsbpls`。因此,这条指令的作用可能是计算`%0 = 0 - %1`,并且在结果非负时更新状态标志,常用于循环中判断是否继续尝试获取锁。 另外,用户提供的指令在上下文中可能出现在`do_raw_read_lock`函数中,用于原子地减少锁计数器或检查锁状态。结合之前的`STREXPL`讨论,可能涉及多线程环境下的锁操作,使用条件执行状态标志来确保原子性。 最后,需要确保解释清晰,结构符合用户之前的回答格式,分点说明指令的组成、条件码作用、操作数含义以及典型应用场景,帮助用户逐步理解。</think>--- ### **1. 指令拆解** 指令 `do_raw_read_lock rsbpls %0, %1, #0` 是一条 **ARM 汇编指令**,主要用于实现**自旋锁Spin Lock)的读锁获取**。 - **核心操作码**:`rsbpls` - **操作数**:`%0, %1, #0`(`%0` `%1` 是寄存器占位符,由编译器分配) - **上下文**:通常出现在内核或并发编程中,用于原子操作或锁机制。 --- ### **2. 指令组成解析** #### **(1) 操作码 `rsbpls`** - **rsb**:逆向减法(Reverse Subtract),功能为 `目标寄存器 = 操作数2 - 操作数1`。 - **pl**:条件码后缀,表示指令仅在 **N 标志(Negative Flag)为 0**(即前一条指令结果非负)时执行。 - **s**:更新状态标志(CPSR 寄存器中的 N/Z/C/V 标志)。 完整含义: - **逆向减法**:`%0 = #0 - %1` - **条件执行**:仅在 `N=0` 时执行。 - **更新标志**:根据结果更新状态寄存器。 #### **(2) 操作数 `%0, %1, #0`** - `%0`:目标寄存器,存储计算结果(例如锁状态)。 - `%1`:源寄存器(通常是锁计数器的地址或当前值)。 - `#0`:立即数 0,用于逆向减法运算。 --- ### **3. 功能与逻辑** #### **(1) 核心功能** 通过 **逆向减法** **条件判断**,实现以下逻辑: - **计算锁状态**:`%0 = 0 - %1`(等价于 `%0 = -%1`)。 - **条件检查**: - 若前一步操作结果非负(`N=0`),执行减法并更新标志。 - 否则跳过指令。 - **状态更新**:根据结果设置标志位(如 Z 标志表示结果是否为 0)。 #### **(2) 典型应用场景** 在 **自旋锁的读锁获取** 中: 1. **锁计数器**:`%1` 可能表示当前锁的读计数(例如,`0` 表示无锁,正数表示读锁被持有)。 2. **逆向减法**:`%0 = -%1`,用于生成新的锁状态。 3. **条件判断**:若结果非负(即原锁计数 `%1 ≤ 0`),表示可安全获取读锁。 --- ### **4. 示例代码逻辑** 假设在内核中实现读锁: ```assembly do_raw_read_lock: rsbpls r0, r1, #0 ; r0 = 0 - r1,仅在 N=0 时执行并更新标志 bmi lock_failed ; 若 N=1(结果为负),跳转到失败处理 strex r2, r0, [lock_addr] ; 尝试原子更新锁状态 cmp r2, #0 ; 检查是否成功 bne retry ; 失败则重试 ``` - **关键步骤**: 1. 通过 `rsbpls` 计算新锁状态。 2. 若结果合法(非负),尝试原子写回内存(`strex`)。 3. 失败时重试(自旋等待)。 --- ### **5. 条件码(PL)的作用** - **N 标志的来源**: - 由前一条指令(如 `ldrex` 加载锁状态)的结果设置。 - 若锁计数 `%1 ≥ 0`,则 `N=0` → 允许执行 `rsbpls`。 - **逻辑意义**: - 若锁未被写锁独占(读计数非负),则允许继续操作。 - 否则(`N=1`),直接跳转到错误处理。 --- ### **6. 与其他指令的协作** - **LDREX/STREX**:用于原子加载存储锁状态。 - **条件分支(如 BNE/BMI)**:根据标志位控制执行流。 - **内存屏障指令**:确保内存访问顺序性(如 `dmb`)。 --- ### **总结** `rsbpls %0, %1, #0` 是一条 **ARM 条件逆向减法指令**,结合 `PL`(非负条件) `s`(更新标志),用于: 1. 原子地生成锁的新状态(`%0 = -%1`)。 2. 通过条件执行避免竞争条件下的无效操作。 3. 常见于自旋锁实现中,保证读锁获取的原子性安全性。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值