一、汇编语言简单用法(ARM架构)
汇编是理解锁机制底层实现的基础,尤其在原子操作、硬件独占访问等场景中,需直接通过汇编指令与硬件交互。以下介绍ARM汇编的基础语法与实用场景。
1.1 汇编程序基本结构
一个完整的ARM汇编程序包含数据段 (存放初始化数据)、代码段 (存放执行指令)和全局标号(供外部调用的入口),格式如下:
asm
.data ; 数据段:定义初始化数据
var1: .word 10 ; 32位整数变量var1,值为10
var2: .word 20 ; 32位整数变量var2,值为20
res: .word 0 ; 用于存放计算结果
.text ; 代码段:存放执行指令
.global _start ; 全局入口点,链接时需指定
_start:
; 核心逻辑:计算var1 + var2,结果存入res
LDR R0, =var1 ; R0 = var1的内存地址
LDR R1, [R0] ; R1 = var1的值(10),从地址取值
LDR R0, =var2 ; R0 = var2的内存地址
LDR R2, [R0] ; R2 = var2的值(20)
ADD R3, R1, R2 ; R3 = R1 + R2(30)
LDR R0, =res ; R0 = res的内存地址
STR R3, [R0] ; 将R3的值(30)存入res地址
; Linux系统调用:程序退出(避免死循环)
MOV R7, #1 ; 系统调用号1:exit
SWI 0 ; 触发软中断,执行系统调用
1.2 汇编与C语言的交互
实际开发中很少使用纯汇编,更多是"汇编调用C"或"C调用汇编",需遵循ARM架构的ATPCS调用约定:
- 参数传递:前4个参数通过
R0-R3
传递,超过4个则压入栈 - 返回值:通过
R0
寄存器返回 - 函数调用:用
BL
指令(自动将返回地址存入LR
寄存器) - 函数返回:用
BX LR
指令(跳回LR
保存的地址)
场景1:汇编调用C函数
汇编文件(call_c.s):负责传递参数并调用C函数
asm
.text
.global asm_call_add ; 声明全局函数,供C调用
asm_call_add:
MOV R0, #15 ; R0 = 第一个参数(15)
MOV R1, #25 ; R1 = 第二个参数(25)
BL c_add ; 调用C函数c_add,返回地址存入LR
BX LR ; 返回到C的调用处,R0存返回值
C语言文件(main.c):定义被调用的C函数并测试
c
#include <stdio.h>
// 声明汇编函数(告诉编译器:该函数在汇编中实现)
extern int asm_call_add(void);
// 被汇编调用的C函数:实现两数相加
int c_add(int a, int b) {
return a + b;
}
int main() {
int sum = asm_call_add(); // 调用汇编函数
printf("15 + 25 = %d\n", sum); // 输出40
return 0;
}
编译与运行(需ARM交叉编译工具链):
bash
# 汇编生成目标文件
arm-linux-gnueabihf-as call_c.s -o call_c.o
# 链接C文件与汇编目标文件,生成可执行程序
arm-linux-gnueabihf-gcc call_c.o main.c -o call_c
# 运行
./call_c
场景2:C语言调用汇编函数
汇编文件(asm_sub.s):实现减法逻辑,供C调用
asm
.text
.global asm_sub ; 声明全局函数,供C调用
asm_sub:
SUB R0, R0, R1 ; R0 = 被减数(R0) - 减数(R1)
BX LR ; 返回,R0存结果
C语言文件(main.c):调用汇编函数
c
#include <stdio.h>
// 声明汇编函数
extern int asm_sub(int a, int b);
int main() {
int res = asm_sub(50, 18); // 调用汇编实现的减法
printf("50 - 18 = %d\n", res); // 输出32
return 0;
}
1.3 汇编调试:反汇编查看指令
通过反汇编可验证汇编指令的机器码长度(ARM模式4字节/Thumb模式2字节),命令如下:
bash
# 对可执行文件反汇编,输出到disasm.txt
arm-linux-gnueabihf-objdump -D call_c > disasm.txt
反汇编结果片段(ARM模式):
000083b4 <asm_call_add>:
83b4: e3a0000f mov r0, #15 ; 0xf
83b8: e3a01019 mov r1, #25 ; 0x19
83bc: eb000000 bl 83c4 <c_add>
83c0: e12fff1e bx lr
可见每条指令占4字节(如e3a0000f
为4字节机器码),符合ARM模式特性。
二、内联汇编:C代码中嵌入汇编
在Linux内核锁机制(如原子操作)中,常通过GCC内联汇编直接操作硬件指令(如ARM的独占访问指令),其基本格式如下:
c
asm volatile (
"汇编指令模板" // 指令占位符%0、%1对应操作数列表
: 输出操作数列表 // 汇编->C的变量(格式:"约束"(变量名))
: 输入操作数列表 // C->汇编的变量(格式:"约束"(变量名))
: 被修改的寄存器/内存 // 告诉编译器:这些资源被汇编修改,需重新加载
);
示例:用内联汇编实现原子自增
ARM架构中,原子自增需通过LDREX
(独占加载)和STREX
(独占存储)指令保证SMP安全,内联汇编实现如下:
c
// 原子变量结构体(内核定义)
typedef struct {
volatile int counter;
} atomic_t;
// 原子自增函数(内联汇编实现)
static inline void atomic_inc(atomic_t *v) {
asm volatile (
"ldrex r0, [%0]\n" // 独占加载:将v->counter加载到r0
"add r0, r0, #1\n" // 自增:r0 = r0 + 1
"strex r1, r0, [%0]\n"// 独占存储:r0的值存回v->counter,结果存r1(0=成功)
"cmp r1, #0\n" // 检查存储是否成功
"bne atomic_inc\n" // 失败则重试(重新执行原子操作)
: // 无输出操作数
: "r" (&v->counter) // 输入操作数:%0 = v->counter的地址("r"表示用寄存器传递)
: "r0", "r1", "memory"// 被修改的资源:r0、r1寄存器,内存(避免编译器优化)
);
}
三、原子操作实现
原子操作是所有锁机制的基础,保证"读取-修改-写入"过程不可分割,分为原子变量操作 和原子位操作。
3.1 原子变量实现
Linux内核中原子变量通过atomic_t
结构体封装,核心操作依赖硬件原子指令:
c
// 1. 读取原子变量(单条指令,天然原子)
#define atomic_read(v) ((v)->counter)
// 2. 设置原子变量(单条指令,天然原子)
#define atomic_set(v, i) (((v)->counter) = (i))
// 3. 原子自增(SMP安全版本,依赖LDREX/STREX)
static inline void atomic_inc(atomic_t *v) {
unsigned long tmp;
asm volatile("@ atomic_inc\n"
"1: ldrex %0, [%1]\n" // 独占加载v->counter到tmp(%0)
" add %0, %0, #1\n" // tmp = tmp + 1
" strex %0, %0, [%1]\n" // 独占存储tmp回v->counter,结果存tmp
" teq %0, #0\n" // 检查是否成功(tmp=0表示成功)
" bne 1b" // 失败则跳回1处重试
: "=&r" (tmp) // 输出操作数:%0 = tmp("=&r"表示独占寄存器)
: "r" (&v->counter) // 输入操作数:%1 = v->counter的地址
: "cc"); // 被修改的状态寄存器(cc=条件码)
}
3.2 原子位操作
原子位操作直接操作内存中的单个比特位(如设备状态标志),核心是通过LDREX/STREX
保证原子性:
c
// 设置指定地址的第nr位(置1)
static inline void set_bit(int nr, volatile unsigned long *addr) {
unsigned long mask = 1UL << (nr % BITS_PER_LONG); // 位掩码(BITS_PER_LONG=32/64)
unsigned long *p = ((unsigned long *)addr) + (nr / BITS_PER_LONG); // 目标字节地址
asm volatile("@ set_bit\n"
"1: ldrex %0, [%2]\n" // 独占加载p地址的值到mask(%0)
" orr %0, %0, %1\n" // 按位或:mask(%0) |= 位掩码(%1)
" strex %0, %0, [%2]\n"// 独占存储结果回p地址
" teq %0, #0\n" // 检查是否成功
" bne 1b" // 失败重试
: "=&r" (mask) // 输出操作数:%0 = mask
: "r" (mask), "r" (p) // 输入操作数:%1 = 位掩码,%2 = 目标地址p
: "cc"); // 条件码寄存器
}
四、自旋锁实现
自旋锁适用于短临界区(如中断上下文),当无法获取锁时,会循环"自旋"等待(不睡眠,避免上下文切换开销)。
4.1 自旋锁结构体
c
typedef struct {
volatile unsigned int lock; // 锁状态:0=未持有,1=已持有
#ifdef CONFIG_SMP
unsigned int owner; // SMP系统:记录持有锁的CPU(用于调试)
#endif
} spinlock_t;
4.2 核心实现代码
c
// 1. 初始化自旋锁
#define SPIN_LOCK_UNLOCKED { .lock = 0 }
#define spin_lock_init(lock) do { *(lock) = (spinlock_t)SPIN_LOCK_UNLOCKED; } while (0)
// 2. 获取自旋锁(SMP安全)
static inline void spin_lock(spinlock_t *lock) {
unsigned long tmp;
asm volatile("@ spin_lock\n"
"1: ldrex %0, [%1]\n" // 独占加载锁状态到tmp(%0)
" teq %0, #0\n" // 检查锁是否未持有(tmp == 0)
" strexeq %0, %2, [%1]\n" // 若未持有,尝试将1写入锁地址(%2=1)
" teqeq %0, #0\n" // 检查写入是否成功(tmp=0表示成功)
" bne 1b" // 失败则跳回1处自旋重试
: "=&r" (tmp) // 输出操作数:%0 = tmp
: "r" (&lock->lock), "r" (1) // 输入操作数:%1=锁地址,%2=1
: "cc"); // 条件码寄存器
}
// 3. 释放自旋锁
static inline void spin_unlock(spinlock_t *lock) {
asm volatile("@ spin_unlock\n"
" str %1, [%0]" // 直接将0写入锁地址,释放锁(单条指令,原子)
: // 无输出操作数
: "r" (&lock->lock), "r" (0) // 输入操作数:%0=锁地址,%1=0
: "cc"); // 条件码寄存器
}
4.3 自旋锁在UP与SMP系统的差异
特性 | UP系统(单CPU) | SMP系统(多CPU) |
---|---|---|
核心问题 | 防止内核抢占(单CPU无跨核竞争) | 防止跨核竞争 + 内核抢占 |
实现依赖 | 禁用抢占(preempt_disable() ) |
硬件原子指令(如ARM的LDREX/STREX) |
锁状态判断 | 无需独占访问,直接检查lock 变量 |
需通过独占指令保证"检查-修改"原子性 |
持有者记录 | 无需记录(单CPU只有一个执行流) | 需记录owner (当前持有锁的CPU),用于调试 |
4.4 中断安全的自旋锁
中断上下文(如硬中断、Softirq)中使用自旋锁时,需禁用中断(避免中断打断自旋,导致死锁),内核提供封装函数:
c
// 保存中断状态并禁用中断,再获取自旋锁
static inline unsigned long spin_lock_irqsave(spinlock_t *lock) {
unsigned long flags;
local_irq_save(flags); // 保存中断状态到flags,禁用中断
spin_lock(lock); // 获取自旋锁
return flags;
}
// 释放自旋锁,恢复中断状态
static inline void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) {
spin_unlock(lock); // 释放自旋锁
local_irq_restore(flags); // 恢复中断状态
}
五、信号量实现
信号量适用于长临界区 (如用户上下文),支持多个进程同时访问资源(通过count
计数控制),无法获取锁时会睡眠(释放CPU)。
5.1 信号量结构体
c
struct semaphore {
spinlock_t lock; // 保护信号量自身的自旋锁(避免并发修改count)
unsigned int count; // 可用资源计数:>0表示有资源,=0表示无资源
struct list_head wait_list; // 等待队列:存放因获取不到锁而睡眠的进程
};
5.2 核心实现代码
c
// 1. 初始化信号量(count=初始资源数)
static inline void sema_init(struct semaphore *sem, int val) {
static struct lock_class_key __key;
*sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}
// 2. 获取信号量(P操作):若count>0则减1,否则睡眠
void down(struct semaphore *sem) {
unsigned long flags;
// 先获取保护信号量的自旋锁(禁用中断,避免并发修改)
spin_lock_irqsave(&sem->lock, flags);
if (sem->count > 0) {
sem->count--; // 有可用资源,直接获取(count减1)
spin_unlock_irqrestore(&sem->lock, flags);
} else {
// 无资源,将当前进程加入等待队列并睡眠
__down(sem);
spin_unlock_irq
restore(&sem->lock, flags);
}
}
// 3. 释放信号量(V操作):count加1,唤醒等待队列中的进程
void up(struct semaphore *sem) {
unsigned long flags;
spin_lock_irqsave(&sem->lock, flags);
if (!list_empty(&sem->wait_list)) {
// 等待队列非空,唤醒第一个睡眠进程
__up(sem);
}
sem->count++; // 释放资源,增加可用计数
spin_unlock_irqrestore(&sem->lock, flags);
}
5.3 down与up函数的实现细节
(1)down函数(获取信号量)
- 加自旋锁保护 :通过
spin_lock_irqsave
获取信号量内部的自旋锁,防止多CPU/中断并发修改count
和等待队列。 - 快速路径(有资源) :若
count > 0
,直接将count
减1,释放自旋锁后返回(无需睡眠,性能高)。 - 慢速路径(无资源) :
- 调用
__down
函数,将当前进程状态设为TASK_UNINTERRUPTIBLE
(不可被信号唤醒)。 - 把进程节点加入
wait_list
等待队列。 - 释放自旋锁后调用
schedule()
,主动放弃CPU,进入睡眠状态。 - 进程被唤醒后,会重新获取自旋锁,再次检查
count
(避免惊群效应)。
- 调用
(2)up函数(释放信号量)
- 加自旋锁保护 :同样通过
spin_lock_irqsave
保证操作原子性。 - 唤醒等待进程 :若等待队列
wait_list
非空,调用__up
函数从队列头部取出一个进程,将其状态设为TASK_RUNNING
,加入运行队列等待调度。 - 更新资源计数 :将
count
加1,释放自旋锁(此时若有新进程调用down
,可直接获取资源)。 - __up () 函数会从等待队列中取出第一个进程,将其状态设置为 TASK_RUNNING
- 被唤醒的进程会被加入到运行队列,等待调度器调度
六、互斥量实现
互斥量是特殊的信号量 (count
固定为1),保证同一时间只有一个进程访问资源,更贴合"独占访问"场景,且比信号量多了所有权、优先级继承等特性。
6.1 互斥量结构体
c
struct mutex {
atomic_t count; // 锁状态:1=未锁定,0=已锁定
spinlock_t wait_lock; // 保护等待队列的自旋锁
struct list_head wait_list; // 等待队列(存放等待锁的进程)
#ifdef CONFIG_DEBUG_MUTEXES
struct thread_info *owner; // 持有锁的线程(调试用,防止非法释放)
const char *name; // 锁名称(调试用)
void *magic; // 魔术值(检测野指针)
#endif
};
6.2 核心实现代码
c
// 1. 初始化互斥量(count=1,未锁定状态)
#define mutex_init(mutex) \
do { \
static struct lock_class_key __key; \
__mutex_init((mutex), #mutex, &__key); \
} while (0)
// 2. 获取互斥锁:成功则count=0,失败则睡眠
void __sched mutex_lock(struct mutex *lock) {
might_sleep(); // 提示编译器:此函数可能睡眠,不能在中断上下文调用
// 快速路径:尝试直接获取锁(原子减1,若返回0表示成功)
if (atomic_dec_if_positive(&lock->count) == 0)
return;
// 慢速路径:获取失败,加入等待队列睡眠
__mutex_lock_slowpath(lock);
}
// 3. 释放互斥锁:count恢复为1,唤醒等待进程
void __sched mutex_unlock(struct mutex *lock) {
// 快速路径:直接释放锁(原子加1,若返回1表示无等待进程)
if (atomic_inc_return(&lock->count) == 1)
return;
// 慢速路径:有等待进程,唤醒队列中的第一个进程
__mutex_unlock_slowpath(lock);
}
6.3 互斥锁相较于自旋锁、信号量的核心特点
对比维度 | 互斥锁(Mutex) | 自旋锁(Spinlock) | 信号量(Semaphore) |
---|---|---|---|
适用场景 | 用户上下文、长临界区 | 中断/软中断上下文、短临界区 | 用户上下文、长临界区(支持多进程共享) |
资源竞争处理 | 睡眠(释放CPU) | 自旋(占用CPU循环等待) | 睡眠(释放CPU) |
所有权 | 有(只有持有者能释放) | 无(任意进程可释放,易误操作) | 无(任意进程可释放) |
优先级继承 | 支持(避免优先级反转) | 不支持 | 不支持 |
递归加锁 | 禁止(同一线程多次加锁会死锁) | 禁止(默认禁止,需特殊版本) | 允许(count>1时) |
调试支持 | 完善(检测死锁、非法释放) | 基础(仅检测锁竞争) | 基础(仅检测计数异常) |
七、锁的使用场景:按上下文选择
不同内核上下文(用户态、中断、软中断等)对锁的要求不同,
-
用户上下文加锁:
- 适用:信号量或互斥锁
- 原因:用户态进程可以睡眠,使用睡眠锁可以提高CPU利用率
-
用户上下文与Softirqs之间加锁:
- 适用:在访问临界资源前禁止Softirq
- 方法:使用local_bh_disable()和local_bh_enable()
- 原因:Softirq运行在中断上下文,不能睡眠,禁用Softirq可以避免竞争
-
用户上下文与Tasklet之间加锁:
- 适用:结合自旋锁和local_bh_disable()
- 原因:Tasklet基于Softirq实现,禁用Softirq可防止Tasklet在用户上下文访问资源时运行
-
用户上下文与Timer之间加锁:
- 适用:自旋锁 + 禁用中断
- 方法:使用spin_lock_irqsave()和spin_unlock_irqrestore()
- 原因:Timer回调可能在中断上下文执行,需要禁用中断防止竞争
-
Tasklet与Timer之间加锁:
- 适用:自旋锁
- 原因:两者都运行在软中断上下文,使用自旋锁可有效同步
-
Softirq之间加锁:
- 适用:自旋锁
- 原因:Softirq可能在多个CPU上同时运行,需要自旋锁保证互斥
-
硬中断上下文:
- 适用:自旋锁(必须禁用中断)
- 方法:使用spin_lock_irq()或spin_lock_irqsave()
- 原因:硬中断上下文不能睡眠,且需要防止中断嵌套导致的竞争
八、内核抢占
内核抢占是指高优先级进程可打断低优先级内核代码(仅当内核不处于临界区时),这是Linux实时性的关键机制,但需锁机制配合避免数据错乱:
- 抢占的触发时机:中断返回、系统调用返回、抢占计数清零时。
- 锁与抢占的关联 :
- 自旋锁:获取时自动将
preempt_count
(抢占计数)加1,禁用抢占;释放时减1,允许抢占(避免持有锁的进程被打断,导致其他进程自旋等待)。 - 互斥锁/信号量:获取时会睡眠,主动放弃CPU,本身不影响抢占计数(但睡眠前会释放CPU,允许高优先级进程调度)。
- 自旋锁:获取时自动将
- 临界区保护:只要进程持有锁(处于临界区),无论哪种锁,内核都不会允许抢占,确保临界区代码原子执行。
九、同步失败案例
-
非原子操作的竞争:
c// 错误:判断-修改非原子,可能被进程切换/抢占打断 if (dev->is_open == 0) { dev->is_open = 1; // 此处可能被抢占,导致多个进程同时打开设备 return 0; } // 正确:用原子操作(如atomic_dec_and_test) if (atomic_dec_and_test(&dev->open_count)) { return 0; }
-
多CPU下关闭中断无效:
c// 错误:单CPU有效,多CPU下其他核心仍可修改dev->is_open local_irq_disable(); if (dev->is_open == 0) dev->is_open = 1; local_irq_enable(); // 正确:用自旋锁(支持多CPU) spin_lock_irqsave(&dev->lock, flags); if (dev->is_open == 0) dev->is_open = 1; spin_unlock_irqrestore(&dev->lock, flags);
-
中断上下文用睡眠锁:
c// 错误:中断上下文不能睡眠,mutex_lock会导致内核崩溃 void irq_handler(int irq, void *dev_id) { mutex_lock(&dev->lock); // 致命错误! } // 正确:用自旋锁 void irq_handler(int irq, void *dev_id) { spin_lock_irqsave(&dev->lock, flags); // 临界区操作 spin_unlock_irqrestore(&dev->lock, flags); }
十、总结:锁机制选择指南
需求场景 | 优先选择的机制 | 核心考量 |
---|---|---|
简单计数器/标志位(无等待) | 原子操作(atomic_t) | 无上下文切换开销,性能最高 |
中断/软中断上下文、短临界区 | 自旋锁 | 不能睡眠,且临界区短,自旋开销小于上下文切换开销 |
用户上下文、独占访问 | 互斥锁 | 支持所有权、优先级继承,避免误操作和优先级反转 |
用户上下文、多进程共享资源 | 信号量(count>1) | 需控制访问进程数量,如"最多3个进程同时访问设备" |