当前位置: 首页 > news >正文

同步与互斥

一、汇编语言简单用法(ARM架构)

汇编是理解锁机制底层实现的基础,尤其在原子操作、硬件独占访问等场景中,需直接通过汇编指令与硬件交互。以下介绍ARM汇编的基础语法与实用场景。

1.1 汇编程序基本结构

一个完整的ARM汇编程序包含数据段(存放初始化数据)、代码段(存放执行指令)和全局标号(供外部调用的入口),格式如下:

    .data               ; 数据段:定义初始化数据
var1: .word 10          ; 32位整数变量var1,值为10
var2: .word 20          ; 32位整数变量var2,值为20
res:  .word 0           ; 用于存放计算结果.text               ; 代码段:存放执行指令.global _start      ; 全局入口点,链接时需指定_start:; 核心逻辑:计算var1 + var2,结果存入resLDR 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:exitSWI 0               ; 触发软中断,执行系统调用

1.2 汇编与C语言的交互

实际开发中很少使用纯汇编,更多是“汇编调用C”或“C调用汇编”,需遵循ARM架构的ATPCS调用约定

  • 参数传递:前4个参数通过R0-R3传递,超过4个则压入栈
  • 返回值:通过R0寄存器返回
  • 函数调用:用BL指令(自动将返回地址存入LR寄存器)
  • 函数返回:用BX LR指令(跳回LR保存的地址)
场景1:汇编调用C函数

汇编文件(call_c.s):负责传递参数并调用C函数

    .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,返回地址存入LRBX LR                ; 返回到C的调用处,R0存返回值

C语言文件(main.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);  // 输出40return 0;
}

编译与运行(需ARM交叉编译工具链):

# 汇编生成目标文件
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调用

    .text.global asm_sub  ; 声明全局函数,供C调用
asm_sub:SUB R0, R0, R1   ; R0 = 被减数(R0) - 减数(R1)BX LR            ; 返回,R0存结果

C语言文件(main.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);  // 输出32return 0;
}

1.3 汇编调试:反汇编查看指令

通过反汇编可验证汇编指令的机器码长度(ARM模式4字节/Thumb模式2字节),命令如下:

# 对可执行文件反汇编,输出到disasm.txt
arm-linux-gnueabihf-objdump -D call_c > disasm.txt

反汇编结果片段(ARM模式):

000083b4 <asm_call_add>:83b4:	e3a0000f 	mov	r0, #15		; 0xf83b8:	e3a01019 	mov	r1, #25		; 0x1983bc:	eb000000 	bl	83c4 <c_add>83c0:	e12fff1e 	bx	lr

可见每条指令占4字节(如e3a0000f为4字节机器码),符合ARM模式特性。

二、内联汇编:C代码中嵌入汇编

在Linux内核锁机制(如原子操作)中,常通过GCC内联汇编直接操作硬件指令(如ARM的独占访问指令),其基本格式如下:

asm volatile ("汇编指令模板"  // 指令占位符%0、%1对应操作数列表: 输出操作数列表  // 汇编->C的变量(格式:"约束"(变量名)): 输入操作数列表  // C->汇编的变量(格式:"约束"(变量名)): 被修改的寄存器/内存  // 告诉编译器:这些资源被汇编修改,需重新加载
);

示例:用内联汇编实现原子自增

ARM架构中,原子自增需通过LDREX(独占加载)和STREX(独占存储)指令保证SMP安全,内联汇编实现如下:

// 原子变量结构体(内核定义)
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结构体封装,核心操作依赖硬件原子指令:

// 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保证原子性:

// 设置指定地址的第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 自旋锁结构体

typedef struct {volatile unsigned int lock;  // 锁状态:0=未持有,1=已持有
#ifdef CONFIG_SMPunsigned int owner;          // SMP系统:记录持有锁的CPU(用于调试)
#endif
} spinlock_t;

4.2 核心实现代码

// 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)中使用自旋锁时,需禁用中断(避免中断打断自旋,导致死锁),内核提供封装函数:

// 保存中断状态并禁用中断,再获取自旋锁
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 信号量结构体

struct semaphore {spinlock_t          lock;       // 保护信号量自身的自旋锁(避免并发修改count)unsigned int        count;      // 可用资源计数:>0表示有资源,=0表示无资源struct list_head    wait_list;  // 等待队列:存放因获取不到锁而睡眠的进程
};

5.2 核心实现代码

// 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_irqrestore(&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函数(获取信号量)
  1. 加自旋锁保护:通过spin_lock_irqsave获取信号量内部的自旋锁,防止多CPU/中断并发修改count和等待队列。
  2. 快速路径(有资源):若count > 0,直接将count减1,释放自旋锁后返回(无需睡眠,性能高)。
  3. 慢速路径(无资源)
    • 调用__down函数,将当前进程状态设为TASK_UNINTERRUPTIBLE(不可被信号唤醒)。
    • 把进程节点加入wait_list等待队列。
    • 释放自旋锁后调用schedule(),主动放弃CPU,进入睡眠状态。
    • 进程被唤醒后,会重新获取自旋锁,再次检查count(避免惊群效应)。
(2)up函数(释放信号量)
  1. 加自旋锁保护:同样通过spin_lock_irqsave保证操作原子性。
  2. 唤醒等待进程:若等待队列wait_list非空,调用__up函数从队列头部取出一个进程,将其状态设为TASK_RUNNING,加入运行队列等待调度。
  3. 更新资源计数:将count加1,释放自旋锁(此时若有新进程调用down,可直接获取资源)。
  4. __up () 函数会从等待队列中取出第一个进程,将其状态设置为 TASK_RUNNING
  5. 被唤醒的进程会被加入到运行队列,等待调度器调度

六、互斥量实现

互斥量是特殊的信号量count固定为1),保证同一时间只有一个进程访问资源,更贴合“独占访问”场景,且比信号量多了所有权、优先级继承等特性。

6.1 互斥量结构体

struct mutex {atomic_t		count;          // 锁状态:1=未锁定,0=已锁定spinlock_t		wait_lock;      // 保护等待队列的自旋锁struct list_head	wait_list;      // 等待队列(存放等待锁的进程)
#ifdef CONFIG_DEBUG_MUTEXESstruct thread_info	*owner;         // 持有锁的线程(调试用,防止非法释放)const char		*name;          // 锁名称(调试用)void			*magic;         // 魔术值(检测野指针)
#endif
};

6.2 核心实现代码

// 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时)
调试支持完善(检测死锁、非法释放)基础(仅检测锁竞争)基础(仅检测计数异常)

七、锁的使用场景:按上下文选择

不同内核上下文(用户态、中断、软中断等)对锁的要求不同,

  1. 用户上下文加锁

    • 适用:信号量或互斥锁
    • 原因:用户态进程可以睡眠,使用睡眠锁可以提高CPU利用率
  2. 用户上下文与Softirqs之间加锁

    • 适用:在访问临界资源前禁止Softirq
    • 方法:使用local_bh_disable()和local_bh_enable()
    • 原因:Softirq运行在中断上下文,不能睡眠,禁用Softirq可以避免竞争
  3. 用户上下文与Tasklet之间加锁

    • 适用:结合自旋锁和local_bh_disable()
    • 原因:Tasklet基于Softirq实现,禁用Softirq可防止Tasklet在用户上下文访问资源时运行
  4. 用户上下文与Timer之间加锁

    • 适用:自旋锁 + 禁用中断
    • 方法:使用spin_lock_irqsave()和spin_unlock_irqrestore()
    • 原因:Timer回调可能在中断上下文执行,需要禁用中断防止竞争
  5. Tasklet与Timer之间加锁

    • 适用:自旋锁
    • 原因:两者都运行在软中断上下文,使用自旋锁可有效同步
  6. Softirq之间加锁

    • 适用:自旋锁
    • 原因:Softirq可能在多个CPU上同时运行,需要自旋锁保证互斥
  7. 硬中断上下文

    • 适用:自旋锁(必须禁用中断)
    • 方法:使用spin_lock_irq()或spin_lock_irqsave()
    • 原因:硬中断上下文不能睡眠,且需要防止中断嵌套导致的竞争

八、内核抢占

内核抢占是指高优先级进程可打断低优先级内核代码(仅当内核不处于临界区时),这是Linux实时性的关键机制,但需锁机制配合避免数据错乱:

  1. 抢占的触发时机:中断返回、系统调用返回、抢占计数清零时。
  2. 锁与抢占的关联
    • 自旋锁:获取时自动将preempt_count(抢占计数)加1,禁用抢占;释放时减1,允许抢占(避免持有锁的进程被打断,导致其他进程自旋等待)。
    • 互斥锁/信号量:获取时会睡眠,主动放弃CPU,本身不影响抢占计数(但睡眠前会释放CPU,允许高优先级进程调度)。
  3. 临界区保护:只要进程持有锁(处于临界区),无论哪种锁,内核都不会允许抢占,确保临界区代码原子执行。

九、同步失败案例

  1. 非原子操作的竞争

    // 错误:判断-修改非原子,可能被进程切换/抢占打断
    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;
    }
    
  2. 多CPU下关闭中断无效

    // 错误:单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);
    
  3. 中断上下文用睡眠锁

    // 错误:中断上下文不能睡眠,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个进程同时访问设备”
http://www.dtcms.com/a/427101.html

相关文章:

  • Java Web搭建商城首页
  • STP生成树(h3c)
  • 深圳汇网网站建设移动互联网时代的到来为很多企业提供了新的商业机会
  • 安卓接入Bigo广告源
  • 安卓Handler+Messenger实现跨应用通讯
  • 公司网站建设完成通知重庆市工程建设交易中心网站
  • 北京网站设计公司hlh成都柚米科技15企业营销型网站系统
  • 德州网站建设招聘帝国网站怎么仿站
  • 15. C++ 类的转换
  • 基于STM32与influxDB的电力监控系统-7
  • python 之 argparse的简单使用
  • 开源 java android app 开发(十七)封库--混淆源码
  • windows显示驱动开发-IddCx 对象
  • 图书馆网站建设的作用广州新建站
  • (27)APS.NET Core8.0 堆栈原理通俗理解
  • SVN 一些命令疑问
  • 精读 C++20 设计模式:行为型设计模式 — 状态机模式
  • 多周期路径约束
  • Webpack配置之path.join、path.resolve和__dirname详解
  • vue打包优化方案都有哪些?
  • Golang 中的字符串:常见错误和最佳实践
  • 花生壳建设网站怎样做网络营销推广
  • 【Rust GUI开发入门】编写一个本地音乐播放器(8. 从文件中提取歌曲元信息)
  • 国内个人网站建设贾汪城乡建设局网站
  • CentOS二进制安装包方式部署K8S集群之系统初始化
  • Spring Boot 缓存集成实践
  • 力扣Hot100--21.合并两个有序链表
  • 网络安全和NLP、CV是并行的应用吗?
  • 如何做好一个企业网站专门做图片的网站
  • 网页设计网站wordpress公告栏插件