0%

LDD3 并发与竞态

并发与竞态

​ 什么是并发呢?什么是竞态

​ 并发就是同时存在多条能独立推进、且彼此不可预期对方进度的执行路径,这些路径对同一份状态拥有读写机会,从而带来“谁先谁后”的不确定性。这个不确定性可能会导致同一个时间对同一个资源的访问,这就是竞态。并发的价值在于“让事情看起来同时进行”,代价是“必须主动管理这些时间线之间的交叉点”,否则就会出现“字被盖掉”的竞态。并发是能力,竞态是 bug。

​ 为了处理这个bug,我们就有保护这个资源,让他在一个时刻只能被一个人所用,当有人在拥有这个资源的使用权时,其他人不能使用,这就是锁。

​ 我们要建立临界区: 即在任意给定时刻,代码只能被一个线程执行。

  • 信号量

用计算机语言说:
信号量 = 一个整型计数器 + 两条原子操作
• P / wait / down:计数器减 1;若结果 < 0,则阻塞(去睡觉)。
• V / signal / up:计数器加 1;若结果 ≤ 0,则唤醒一个等待者。

计数器可以为任意非负初值,表示“可用资源数”。
N = 1 时退化为互斥锁(一次只能一个人)。
N > 1 时称为计数信号量,允许多个并发使用者。

操作信号量必须是原子的,加减判断的过程不能被打断。当资源暂时不可得时,线程可以睡眠(与自旋锁不同)。

在当前6.10+的内核中,信号量是这样实现的:

1
2
3
4
5
6
/* include/linux/semaphore.h */
struct semaphore {
raw_spinlock_t lock; /* 保护内部字段的原子自旋锁 */
unsigned int count; /* 当前可用资源数(负值表示等待者个数) */
struct list_head wait_list; /* 等待队列,链表元素是 struct semaphore_waiter */
};

想用信号量,肯定要先初始化的啦。分为静态初始化和动态运行期初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <linux/semaphore.h>

//静态
/* 宏函数 声明并初始化一个计数信号量*/
DECLARE_SEMAPHORE_GENERIC(my_sem, 3);
/* 计数初值 = 1,相当于二值锁 */
static DEFINE_SEMAPHORE(my_sem);
/* 或者自己指定初值 */
static struct semaphore my_sem = __SEMAPHORE_INITIALIZER(my_sem, 4);


//动态
struct semaphore my_sem;
sema_init(&my_sem, 3); /* 允许 3 个并发持有者 */

初始化值后就是要用,自然要先了解一下用的逻辑:

核心算法:睡眠/唤醒计数器

(1) 获取(down)
- 如果 count > 0,直接减 1 后返回。
- 否则将当前任务封装成 semaphore_waiter 插入 wait_list,设置状态为 TASK_UNINTERRUPTIBLE,然后 schedule() 睡眠。
- 被唤醒后再次检查 count,成功则减 1 退出,否则继续睡眠。

(2) 释放(up)
- 原子地将 count 加 1。
- 如果加 1 后仍 ≤0,说明有等待者,从 wait_list 摘取第一个 waiter,将其唤醒(wake_up_process)。

具体流程:

• 初始 count = 3
down → 2 → 1 → 0,都直接返回;
第 4 次 down 时 count = 0,于是把任务挂起并把 count 设为 -1;
第 5 次 down 时 count = -1,再挂一个任务并把 count 设为 -2;
……
此时 count == -n 就说明有 n 个等待者。

• up() 时把 count++:
如果加完后 count 仍是负数(≤0),说明还有人在睡觉,于是唤醒链表头的一个任务;
被唤醒的任务重新尝试 down(),把 count 再减 1——于是 count 从 -1 变 0,或从 0 变 1,依此类推。

api:

接口 说明
void down(struct semaphore *sem) 不可中断睡眠,直到获得
int down_interruptible(struct semaphore *sem) 可中断版本,返回 -EINTR
int down_killable(struct semaphore *sem) 可被致命信号打断
int down_trylock(struct semaphore *sem) 不睡眠,立即返回 0/-EAGAIN
void up(struct semaphore *sem) 释放资源,唤醒等待者

烦了,信号量的知识看linux内核设计与实现的笔记去。

这里只讲咋用。

-------------本文结束感谢您的阅读-------------