同步与互斥

一、汇编语言简单用法(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函数(获取信号量)
  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 互斥量结构体

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

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

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

  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. 非原子操作的竞争

    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;
    }
  2. 多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);
  3. 中断上下文用睡眠锁

    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个进程同时访问设备"
相关推荐
蒙奇D索大2 小时前
【数据结构】图论核心应用:关键路径算法详解——从AOE网到项目管理实战
数据结构·笔记·学习·考研·算法·图论·改行学it
Olrookie3 小时前
若依前后端分离版学习笔记(十八)——页面权限,页签缓存以及图标,字典,参数的使用
vue.js·笔记·学习
半夏知半秋3 小时前
基于skynet框架业务中的gateway实现分析
服务器·开发语言·后端·学习·gateway
Pluchon12 小时前
硅基计划4.0 算法 字符串
java·数据结构·学习·算法
折翅鵬12 小时前
Android 程序员如何系统学习 MQTT
android·学习
~无忧花开~13 小时前
JavaScript学习笔记(十五):ES6模板字符串使用指南
开发语言·前端·javascript·vue.js·学习·es6·js
拾贰_C15 小时前
【pycharm---pytorch】pycharm配置以及pytorch学习
pytorch·学习·pycharm
向阳花开_miemie15 小时前
Android音频学习(二十一)——ALSA简介
学习·音视频
come1123415 小时前
ptyhon 基础语法学习(对比php)
android·学习