
1. 原子操作原子整形操作typedef struct { int counter; } atomic_t; atomic_t b ATOMIC_INIT(0); //定义并初始化 /* 常用操作函数 */ void atomic_set(atomic_t *v, int i); // 设置值为i int atomic_read(atomic_t *v); // 读取当前值 void atomic_add(int i, atomic_t *v); // 增加i void atomic_sub(int i, atomic_t *v); // 减少i void atomic_inc(atomic_t *v); // 自增1 void atomic_dec(atomic_t *v); // 自减1 int atomic_dec_and_test(atomic_t *v); // 自减并测试是否为0原子位操作—— 位操作为内存操作不依赖于 atomic_t 类型/* 直接在内存地址上操作 */ void set_bit(int nr, volatile unsigned long *addr); // 将addr的第nr位置1 void clear_bit(int nr, volatile unsigned long *addr); // 将addr的第nr位清0 void change_bit(int nr, volatile unsigned long *addr);// 翻转addr的第nr位 int test_bit(int nr, volatile unsigned long *addr); // 测试addr的第nr位 int test_and_set_bit(int nr, volatile unsigned long *addr); // 测试并置位 int test_and_clear_bit(int nr, volatile unsigned long *addr); // 测试并清零2. 自旋锁进程与进程间当使用临界区资源的时候如果资源被其他进程A的自旋锁锁住那么另一个进程B在访问该资源的时候便会不断的查询资源是否被释放因此进程B不会进入休眠而是会随着时间片不断地运行这样会浪费处理器的时间因此自旋锁应用于短时间的轻量级加锁如果需要长时间持有锁则不能用自旋锁。在单核SOC芯片中自旋锁的使用会导致抢占式调度被关闭。因此如果在使用自旋锁的过程中使用了阻塞的代码使得进程进入休眠例如任务A在获取锁后调用阻塞函数进入休眠虽然使用自旋锁会关闭抢占式调度导致调度器无法进行任务切换但主动让出schedule是允许的此时CPU切换到任务B而任务B中也尝试获取锁但因为任务A还没有释放锁因此导致任务B不断查询资源当时间片到达之后由于抢占式调度被关闭调度器无法重新执行任务ACPU一直执行任务B出现死锁。进程与中断间进程中使用锁的时候先关闭中断释放锁后打开中断有相应的函数能够保存中断状态。自旋锁适合在中断中使用因为使用互斥体的进程在申请访问临界资源的时候如果没有申请到则会进入休眠状态释放CPU去执行其他的任务但是由于中断上下文Interrupt Context不是进程它不依赖task_struct因此不能睡眠也不能被调度。这意味着释放CPU之后无法再次回到中断执行剩下的任务导致逻辑混乱。中断下半部Tasklet/Workqueue中锁的使用陷阱1Tasklet 中的自旋锁Tasklet 运行在软中断上下文可被硬件中断抢占且同一个 Tasklet 不会同时在多个 CPU 上运行但不同 Tasklet 可能并行。/* Tasklet 使用自旋锁 —— 必须使用 spin_lock_bh() 版本 */ void tasklet_handler(unsigned long data) { spin_lock_bh(lock); /* 禁止下半部软中断的抢占 */ /* 临界区访问 */ spin_unlock_bh(lock); }陷阱1Tasklet 中如果使用spin_lock_irqsave()会错误地关闭所有硬件中断导致中断响应延迟增大。spin_lock_bh()只关闭软中断包括 Tasklet 和 Timer保留硬件中断响应这更适合下半部场景。陷阱2Tasklet 中绝对不能使用互斥体mutex或信号量semaphore的睡眠版本因为软中断上下文不支持调度。2Workqueue 中的锁使用Workqueue 运行在进程上下文内核线程可以睡眠。/* Workqueue 中使用互斥体 —— 推荐 */ struct mutex lock; void work_handler(struct work_struct *work) { mutex_lock(lock); /* 可能睡眠适合长时间临界区 */ /* 临界区访问 */ mutex_unlock(lock); }陷阱Workqueue 虽然可以睡眠但如果使用自旋锁仍需注意不能在持有自旋锁时调用任何可能睡眠的函数如kmalloc(GFP_KERNEL)、copy_from_user()等否则会触发内核警告甚至系统崩溃。对比总结下半部机制上下文类型推荐锁机制禁止操作Tasklet软中断上下文自旋锁spin_lock_bh睡眠、mutexWorkqueue进程上下文互斥体/信号量持有自旋锁时睡眠/* 示例代码 47.3.2.1 自旋锁使用示例 */ DEFINE_SPINLOCK(lock); /* 定义并初始化一个锁 */ /* 线程 A */ void functionA () { unsigned long flags; /* 中断状态 */ spin_lock_irqsave(lock, flags); /* 获取锁保存中断状态 */ /* 临界区 */ spin_unlock_irqrestore(lock, flags); /* 释放锁恢复中断状态 */ } /* 中断服务函数上半部 */ void irq_handler() { spin_lock(lock); /* 获取锁已在中断上下文无需关中断 */ /* 临界区 */ spin_unlock(lock); /* 释放锁 */ }3. 信号量计数信号量可以控制同一时间访问资源的任务的数量允许多个任务同时访问但数量受限于初始计数值。二值信号量要么0要么1同一时间只能有一个任务访问类似于互斥锁。但信号量不要求持有者释放即一个任务可以 down() 而另一个任务可以 up()这一点与互斥体不同。/* Linux 内核信号量使用示例 */ struct semaphore sem; sema_init(sem, 5); /* 计数信号量初始值5允许5个任务同时访问 */ sema_init(sem, 1); /* 二值信号量初始值1 */ void task_entry() { down(sem); /* 获取信号量可被信号打断 */ // down_interruptible(sem); /* 可被信号中断的版本推荐使用 */ /* 临界区访问 */ up(sem); /* 释放信号量 */ }信号量 vs 互斥体信号量适用于进程上下文且允许在中断上下文释放up而互斥体只能在进程上下文使用且要求获取和释放必须在同一上下文。4. 互斥体相当于值为1的信号量二值信号量但互斥体更严格语义更清晰明确用于互斥访问支持优先级继承以解决优先级反转问题。使用限制只能在进程上下文使用不能用于中断且获取和释放必须在同一进程上下文不能在一个进程获取另一个进程释放。DEFINE_MUTEX(mutex); /* 定义并初始化互斥体 */ void task_a() { mutex_lock(mutex); /* 获取互斥体不可被信号中断会一直等待 */ // mutex_lock_interruptible(mutex); /* 可被信号中断推荐在驱动中使用 */ /* 临界区 */ mutex_unlock(mutex); /* 释放互斥体 */ }锁机制选择总结锁机制上下文限制是否可睡眠适用场景原子操作任意上下文否简单的计数值修改、标志位翻转自旋锁任意上下文否短临界区特别是中断上下文中信号量进程上下文可释放于中断是多资源计数控制或进程间同步互斥体进程上下文是长临界区、互斥访问有优先级继承需求RCU任意上下文否读多写极少的场景