同步、互斥的时候我们已经讲了很多 “不对” 的案例
不要笑!🤡 竟是我自己
复习调试理论
理解并发 bugs 的类型、产生原因和预防方法
应用调试理论
在《计算机系统基础》实验中提出 (视频回看)
HIT BAD TRAP
了,最后都是你自己背锅程序:
程序员:犯的是 Fault,看到的是 Failure
调试理论:如果我们能判定任意程序状态的正确性,那么给定一个 Failure,通过二分查找定位到第一个 Error 的状态,导致此状态的代码就是 Fault (bug)。
在实际中的应用:状态机的执行可能很长,但我们只要标记出一些
关键的状态 ,就能帮我们缩小排查问题的范围
实际中的调试:更 “人类友好” 的分段检查
在无法 “准确复现一次执行” 的基础上,“找到第一个 error 的状态” 变难了……
Fault → Error
Error → Failure
调试理论告诉我们:failure point 很重要
RTFM: QEMU Monitor (例子:cpu-reset.c)
log int,cpu_reset,exec
int,cpu_reset
- 帮助我们定位 failureexec
- 帮助我们定位 error (执行流)一种研究流派:实证研究 (empirical study)
一些实证研究
A deadlock is a state in which each member of a group is waiting for another member, including itself, to take action.
Empirical study: MySQL (14/9), Apache (13/4), Mozilla (41/16), OpenOffice (6/2)
出现线程 “互相等待” 的情况 (路口空间 = 资源)
假设你的 spinlock 不小心发生了中断
yield()
void os_run() {
spin_lock(&list_lock);
spin_lock(&xxx);
spin_unlock(&xxx); // ---------+
} // |
// |
void on_interrupt() { // |
spin_lock(&list_lock); // <--+
spin_unlock(&list_lock);
}
void obj_move(int i, int j) {
spin_lock(&lock[i]);
spin_lock(&lock[j]);
arr[i] = NULL;
arr[j] = arr[i];
spin_unlock(&lock[j]);
spin_unlock(&lock[i]);
}
上锁的顺序很重要……
obj_move
本身看起来没有问题obj_move(1, 2)
; obj_move(2, 1)
→ 死锁死锁产生的四个必要条件 (Edward G. Coffman, 1971):
“理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。” ——
Bullshit .破坏每一个条件都是非常困难的 (请阅读教科书)
AA-Deadlock
if (holding(lk)) panic();
ABBA-Deadlock
Textbooks will tell you that if you always lock in the same order, you will never get this kind of deadlock. Practice will tell you that this approach doesn't scale: when I create a new lock, I don't understand enough of the kernel to figure out where in the 5000 lock hierarchy it will fit.
The best locks are encapsulated: they never get exposed in headers, and are never held around calls to non-trivial functions outside the same file. You can read through this code and see that it will never deadlock, because it never tries to grab another lock while it has that one. People using your code don't even need to know you are using a lock.
—— Unreliable Guide To Locking by Rusty Russell
调试公理:未测代码永远是错的
lockdep, since Linux Kernel 2.6.17; also in OpenHarmony!
在涉及同步的时候,情况就复杂得多了……
不上锁不就没有死锁了吗?
回顾我们实现并发控制的工具
忘记上锁——原子性违反 (Atomicity Violation, AV)
忘记同步——顺序违反 (Order Violation, OV)
Empirical study: 在 105 个并发 bug 中 (non-deadlock/deadlock)
“ABA”
有时候上锁也不解决问题
“BA”
程序员:我本地都跑了几万次了,没问题啊
我:在本地测的好好的啊
Empirical study 给了我们更多有趣的发现,指导 bug 的发现/修复:
Lab 1 有一定难度
如果你调试了很久都没有想法,不如用一点系统化的手段。
“跳出问题看问题” 是专业人士应当具备的素质。
做好 Lab1 的测试框架
想糊弄?糊弄不过去的。
来构造 kalloc 的 workload 吧!
void os_run() {
while (1) {
pmm->alloc(random_size());
// Insufficient: pmm->alloc(32);
}
}
kalloc
Online Judge 上的 workload:
struct workload {
int pr[N], sum; // sum = pr[0] + pr[1] + ... pr[N-1]
// roll(0, sum-1) => allocation size
};
static struct workload
wl_typical = {.pr = {10,0,0, 40, 50, 40,30,20,10,4,2,1} },
wl_stress = {.pr = { 1,0,0,400,200,100, 1, 1, 1,1,1,1} },
wl_page = {.pr = {10,0,0, 1, 1, 1, 1, 1, 1,1,1,1} };
static struct workload *workload = &wl_typical;
kfree
并发的 alloc-free 是很多问题的来源。
但同学们一定是懒得去测试的。 —— jyy
Online Judge: 会把分配的结果保存到一个并发数据结构中
stdatomic.h
实现想自己实现个简单的?
基本想法: workload + 在适当的时候插入
delay()
- 这才是 Online Judge 测试的精髓
- allocation-biased/free-biased/mixed
ABBA (deadlock), ABA (AV), BA (OV) 都可以通过这种方式触发
很多程序应该满足的 specification 并没有被检查
- C/C++ 为了性能考虑,就让 undefined behavior 发生
- 并不总是显现 “显著” 的后果
- 给你一种程序好像没问题的错觉
在程序中设计各种检查 (就像 spinlock.c 那样)
检查:不必大费周章
内存分配要求:已分配内存 $S = [\ell_0, r_0) \cup [\ell_1, r_1) \cup \ldots$
// allocation
for (int i = 0; (i + 1) * sizeof(u32) <= size; i++) {
panic_on(((u32 *)ptr)[i] == MAGIC, "double-allocation");
arr[i] = MAGIC;
}
// free
for (int i = 0; (i + 1) * sizeof(u32) <= alloc_size(ptr); i++) {
panic_on(((u32 *)ptr)[i] == 0, "double-free");
arr[i] = 0;
}
Canary (金丝雀) 对一氧化碳非常敏感
Canary
#define MAGIC 0x55555555
#define BOTTOM (STK_SZ / sizeof(u32) - 1)
struct kernel_stack { char data[STK_SZ]; };
void canary_init(struct kernel_stack *s) {
u32 *ptr = (u32 *)s;
for (int i = 0; i < CANARY_SZ; i++)
ptr[BOTTOM - i] = ptr[i] = MAGIC;
}
void canary_check(struct kernel_stack *s) {
u32 *ptr = (u32 *)s;
for (int i = 0; i < CANARY_SZ; i++) {
panic_on(ptr[BOTTOM - i] != MAGIC, "underflow");
panic_on(ptr[i] != MAGIC, "overflow");
}
}
msvc 中 debug mode 的 guard/fence/canary
0xcccccccc
0xcdcdcdcd
0xfdfdfdfd
0xdddddddd
>>> (b'\xcc' * 80).decode('gb2312')
'烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫'
手持两把锟斤拷,口中疾呼烫烫烫
脚踏千朵屯屯屯,笑看万物锘锘锘
(这些曾经可怕的形象原来一直在无形中保护你)
“动态程序分析”
常见的动态分析:检查某些 specification 是否被违反
如果你没用过 lint/sanitizers,根本不能算会编程。
你知道很多变量的
CHECK_INT(waitlist->count, >= 0)
CHECK_INT(pid, < MAX_PROCS)
CHECK_HEAP(ctx->rip)
#define CHECK_INT(x, cond) \
({ panic_on(!((x) cond), "int check fail: " #x " " #cond); })
#define CHECK_HEAP(ptr) \
({ panic_on(!IN_RANGE((ptr), heap)); })
本次课内容与目标
Take-away messages