现代操作系统:进程与线程(九)
2.4 Interprocess Communication (IPC) and Coordination/Synchronization
2.4.1 Race Conditions竞争条件
满足以下全部条件时产生竞争条件:
a. 两个进程(或线程)A和B各自将要执行一些(可能不同的)操作;
b. 程序并不能决定哪个进程先执行;
c. 进程A先运行的结果和进程B先运行的结果是不同的。
一个简单的例子:A和B共享初始值为1的变量X, A将要执行X=X+5, B将要执行X=X*2。
A先B后:X = (1 + 5) * 2 = 12;B先A后:X = (1 * 2) + 5 = 7;
换句话说,A和B之间竞争的结果直接决定了X最终的值。
注:a. 可以有两个以上的进程同时参与竞争;b. 有趣的情况是按出现次数最多的进程顺序的执行结果通常是期望的结果,而少数发生的几种排序情况通常带来意外的错误结果;c. 下面的一个例子展示了这个情况。
假设进程A和B都可以访问x=10;
A1: LOAD r1,x B1: LOAD r2,x A2: ADD r1,r1,1 B2: SUB r2,r2,1 A3: STORE r1,x B3: STORE r2,x |
一个进程执行x:=x+1,另一个进程执行x:=x-1;
当两者都运行完毕后的结果应该是x=10;但是x:=x+1并不是原子性的,所以会得到什么样的结果呢?x=9,x=10,x=11都是有可能的。
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h>
int a = 0;
void* threadFunctionAdd(void* params) { int i = 0; for (i = 0; i < 1000000; i++) { a = a + 1; }
pthread_exit(NULL); }
void* threadFunctionMinus(void* params) { int i = 0; for (i = 0; i < 1000000; i++) { a = a - 1; }
pthread_exit(NULL); }
int main() { pthread_t thread1, thread2;
pthread_create(&thread1, NULL, threadFunctionAdd, NULL); pthread_create(&thread2, NULL, threadFunctionMinus, NULL);
pthread_join(thread1, NULL); pthread_join(thread2, NULL);
printf("Finally, variable a = %d\n", a);
return 0; } |
2.4.2 Critical Regions临界区
简而言之,临界区就是不能被多个进程(线程)同时访问的代码段。我们必须避免交叉代码段,这些代码段需要相互独立。也就是说,冲突的部分需要相互排斥。当进程A正在执行它的临界区时,它会排除进程B执行它的临界区。相反,当进程B正在执行它的临界区时,它会排除进程A执行它的临界区。Tanenbaum为关键部分的实现提出了四个要求,其中前三个被每个人采用。
- A. 任何两个进程不能同时在一个临界区内;
- B. 在临界区外运行的进程不能阻塞其他进程;
- C. 不应当对并发执行的CPU的速度和数量进程任何假设;
- D. 不能让进程无限制的等待进入临界区;
- 我(和其他人)不做最后的(公平)要求;我仅仅需要系统作为一个整体进行处理(所以不是所有的进程都被阻塞);
- 具体来说如果一个进程准备进入它的临界区,那么其他的进程也最终会进入这个临界区;
- 我认为满足我的较弱条件而不是Tanenbaum的条件的解决方案是不公平的,但却是正确的,还定义了比Tanenbaum更强的公平条件。
loop forever loop forever "ordinary" code "ordinary" code ENTRY code ENTRY code critical section critical section EXIT code EXIT code "ordinary" code "ordinary" code |
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h>
int a = 0; int wait1 = 1; int wait2 = 1;
pthread_mutex_t lock;
void* threadFunctionAdd(void* params) { wait1 = 0; while (wait2) {}
int i = 0; for (i = 0; i < 1000; i++) { pthread_mutex_lock(&lock); a = a - 1; pthread_mutex_unlock(&lock); }
pthread_exit(NULL); }
void* threadFunctionMinus(void* params) { wait2 = 0; while (wait1) {}
int i = 0; for (i = 0; i < 1000; i++) { pthread_mutex_lock(&lock); a = a + 1; pthread_mutex_unlock(&lock); }
pthread_exit(NULL); }
int main() { pthread_t thread1, thread2;
pthread_mutex_init(&lock, NULL);
pthread_create(&thread1, NULL, threadFunctionAdd, NULL); pthread_create(&thread2, NULL, threadFunctionMinus, NULL);
pthread_join(thread1, NULL); pthread_join(thread2, NULL);
pthread_mutex_destroy(&lock);
printf("Finally, variable a = %d\n", a);
return 0; } |
2.4.3 Mutual exclusion with busy waiting互斥与繁忙等待
这里我们将讨论几种实现互斥的方案,在这些方案中,当一个进程在临界区中更新共享内存时,其他的进程不会进入其临界区。
Disabling Interrupts屏蔽中断
在单处理器系统中,最简单的方法就是使每个进程在刚刚进入临界区后立刻屏蔽所有中断,并在离开临界区前再打开所有中断。当中断被屏蔽后,时钟中断或其他中断的出现均不会使得CPU切换当前正在运行的进程,那么此时这个进程就可以检查和修改共享内存,且不必担心被其余进程介入。
但是这个方案有着很大的问题,显然把屏蔽中断的权力移交给用户进程是很不明智的,如果一个进程屏蔽所有中断后不再打开中断整个系统就会因此终止。而且在多核处理器中屏蔽中断只对当前的CPU生效,其余CPU仍然可以继续访问共享内存。
- 不适用于用户模式的进程;
- 进程阻塞时系统会假死,没有任何响应;
- 不适用于多核处理器,多个CPU;
Software solutions for two processes软件解决方案
Lock Variables锁变量
其思想是,每个进程在进入临界区之前,设置一个变量,将其他进程锁在临界区之外。
Initially: P1wants = P2wants = false
Code for P1 Code for P2
Loop forever { Loop forever { P1wants <-- true ENTRY P2wants <-- true while (P2wants) {} ENTRY while (P1wants) {} critical-section critical-section P1wants <-- false EXIT P2wants <-- false non-critical-section non-critical-section } } |
Explain why this works. But it is wrong! Why? 死锁!
// 死锁
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h>
int a = 0; int wait1 = 0; int wait2 = 0;
void* threadFunctionAdd(void* params) { int i = 0; while (i < 1000) { wait1 = 1; while (wait2) {}
printf("Thread 1 Enter CR!\n"); a += 1;
wait1 = 0; i++; }
pthread_exit(NULL); }
void* threadFunctionMinus(void* params) { int i = 0; while (i < 1000) { wait2 = 1; while (wait1) {}
printf("Thread 2 Enter CR!\n"); a -= 1;
wait2 = 0; i++; }
pthread_exit(NULL); }
int main() { pthread_t thread1, thread2;
pthread_create(&thread1, NULL, threadFunctionAdd, NULL); pthread_create(&thread2, NULL, threadFunctionMinus, NULL);
pthread_join(thread1, NULL); pthread_join(thread2, NULL);
return 0; } |
让我们再试一次。问题是在循环之前设置想让我们卡住。我们把它们的顺序搞错了!
Initially P1wants=P2wants=false
Code for P1 Code for P2
Loop forever { Loop forever { while (P2wants) {} ENTRY while (P1wants) {} P1wants <-- true ENTRY P2wants <-- true critical-section critical-section P1wants <-- false EXIT P2wants <-- false non-critical-section non-critical-section } } |
Explain why this works. But it is wrong again! Why? 根本没锁住……
// 根本没锁住
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h>
int a = 0; int wait1 = 0; int wait2 = 0;
void* threadFunctionAdd(void* params) { int i = 0; while (i < 1000000) { while (wait2) {} wait1 = 1;
a += 1;
wait1 = 0; i++; }
pthread_exit(NULL); }
void* threadFunctionMinus(void* params) { int i = 0; while (i < 1000000) { while (wait1) {} wait2 = 1;
a -= 1;
wait2 = 0; i++; }
pthread_exit(NULL); }
int main() { pthread_t thread1, thread2;
pthread_create(&thread1, NULL, threadFunctionAdd, NULL); pthread_create(&thread2, NULL, threadFunctionMinus, NULL);
pthread_join(thread1, NULL); pthread_join(thread2, NULL);
printf("Finally, a = %d\n", a);
return 0; } |
Initially P1wants=P2wants=false
Code for P1 Code for P2
Loop forever { Loop forever { P1wants <-- true ENTRY while (P1wants) {} while (P2wants) {} ENTRY P2wants <-- true critical-section critical-section P1wants <-- false EXIT P2wants <-- false non-critical-section non-critical-section } } |
Explain why this works. But it is wrong again! Why? 可能锁不住!
// 根本没锁住
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h>
int a = 0; int wait1 = 0; int wait2 = 0;
void* threadFunctionAdd(void* params) { int i = 0; while (i < 1000000) { wait1 = 1; while (wait2) {}
a += 1;
wait1 = 0; i++; }
pthread_exit(NULL); }
void* threadFunctionMinus(void* params) { int i = 0; while (i < 1000000) { while (wait1) {} wait2 = 1;
a -= 1;
wait2 = 0; i++; }
pthread_exit(NULL); }
int main() { pthread_t thread1, thread2;
pthread_create(&thread1, NULL, threadFunctionAdd, NULL); pthread_create(&thread2, NULL, threadFunctionMinus, NULL);
pthread_join(thread1, NULL); pthread_join(thread2, NULL);
printf("Finally, a = %d\n", a);
return 0; } |
Strict Alternation严格轮换法
现在我们试着礼貌点,轮流来。
Initially turn=1
Code for P1 Code for P2
Loop forever { Loop forever { while (turn == 2) {} ENTRY while (turn == 1) {} critical-section critical-section turn = 2 EXIT turn = 1 non-critical-section non-critical-section } } |
这一个强制交替,所以不够普遍。具体来说,它不满足条件三,条件三要求非临界段内的任何过程都不能阻止另一个过程进入临界段。通过交替,如果一个进程处于其非关键部分(Non-Critical Section),那么另一个进程可以一次进入CS,但不能再次进入。
第一个示例违反了规则4(整个系统被阻塞)。第二、三个示例违反了规则1(都在临界部分)。第四个示例违反了规则3 (NCS中的一个进程阻止另一个进程进入其CS)。
// 严格轮换法
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h>
int a = 0; int wait1 = 1;
void* threadFunctionAdd(void* params) { int i = 0; while (i < 1000000) { while (wait1) {}
a += 1;
wait1 = 1; i++; }
pthread_exit(NULL); }
void* threadFunctionMinus(void* params) { int i = 0; while (i < 1000000) { while (!wait1) {}
a -= 1;
wait1 = 0; i++; }
pthread_exit(NULL); }
int main() { pthread_t thread1, thread2;
pthread_create(&thread1, NULL, threadFunctionAdd, NULL); pthread_create(&thread2, NULL, threadFunctionMinus, NULL);
pthread_join(thread1, NULL); pthread_join(thread2, NULL);
printf("Finally, a = %d\n", a);
return 0; } |
The First Correct Solution (Dekker/Peterson) Peterson解法
事实上,我们花了很多年才找到一个正确的解决方案。人们找到了许多早期的解决方案,并发表了一些,但都是错误的。第一个正确的解决方法是由一位名叫Dekker的数学家发现的,他结合了锁变量和警告变量的特性。基本思想是,当有竞争时两个进程轮流执行,但当没有竞争时,需要进入临界区进程可以进入。它非常聪明,但我跳过了它(我在讲授CSCI-GA.2251中的分布式操作系统时涉及到了它)。随后,我们找到了具有更好公平性的算法(例如,没有任务需要等待另一个任务进入CS两次)。
接下来是Peterson的解决方案,它也将锁变量和警告变量结合起来,只有在有竞争时才诉诸武力。当Peterson的算法发表时,看到如此简单的解决方案令人惊讶。事实上,Peterson给出了任意数量过程的解决方案。对于任意数量的进程,该算法满足我们的性质(包括强公平性条件)的证明可以在1990年1月的《操作系统评论》中找到,第18-22页。
Initially P1wants=P2wants=false and turn=1
Code for P1 Code for P2
Loop forever { Loop forever { P1wants <-- true P2wants <-- true turn <-- 2 Competition turn <-- 1 while (P2wants and turn=2) {} while (P1wants and turn=1) {} critical-section critical-section P1wants <-- false P2wants <-- false non-critical-section } non-critical-section } |
// Peterson
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include <unistd.h>
#define TRUE 1 #define FALSE 0 #define N 2
int a = 0; int wants[N]; int turn;
void enter_region(int id) { int other; other = 1 - id; wants[id] = TRUE; turn = id; while (turn == id && wants[other] == TRUE) {} }
void leave_region(int id) { wants[id] = FALSE; }
void* threadFunctionAdd(void* params) { int i = 0; while (i < 100000) { enter_region(1); a += 1; leave_region(1); i++; }
pthread_exit(NULL); }
void* threadFunctionMinus(void* params) { int i = 0; while (i < 100000) { enter_region(0); a -= 1; leave_region(0); i++; }
pthread_exit(NULL); }
int main() { pthread_t thread1, thread2;
pthread_create(&thread1, NULL, threadFunctionAdd, NULL); pthread_create(&thread2, NULL, threadFunctionMinus, NULL);
pthread_join(thread1, NULL); pthread_join(thread2, NULL);
printf("Finally, a = %d\n", a);
return 0; } |
The TSL Instruction (A Hardware Assist: test-and-set) TSL指令
Tanenbaum把这个指令称之为Test-and-Set Lock并且写为TSL。我相信大多数计算机科学家称它为简单的Test-and-Set,然后称它TAS。每个人都同意这个定义TAS(b),其中b是一个布尔变量,原子性的设定b的值并且返回b的OLD值(旧值)。从原子的角度来看,TAS(x)执行的两个操作,即测试x(即返回其旧值)和设置x(即赋予其值true)是不可分割的。具体来说,两个并发TAS(x)操作不可能同时返回false(除非还有另一个并发语句将x设置为false)。
更具体地说,这里有一个“不可能发生”的例子。假设b最初是假的,P1和P2都发出tas(b)。请注意,左列和右列都可以单独发生,但它们不能按所示的顺序交叉。有了TAS,为任意数量的流程实现关键部分就很容易了。每个流程执行。为什么?三个步骤是原子的,不能分割!细化到了指令层面!
Time = 1: P1测试b,发现为False;
Time = 2: P2测试b,发现为False;
Time = 3: P1设置b为True;
Time = 4: P2设置b为True;
Time = 5: TAS(b)返回FALSE
Time = 6: Tab(b)返回FALSE
loop forever { while (TAS(s)) {} ENTRY CS s<--false EXIT NCS } |
enter_region: TSL REGISTER, LOCK 复制锁到寄存器 CMP REGISTER, #0 现在的锁是0吗? JNE enter_region 不是的话就无限循环自己 RET 是的话返回调用者进入临界区 leave_region: MOVE LOCK, #0 在锁中存入0,解锁 RET 返回调用者 |
posted on 2022-01-06 15:01 ThomasZhong 阅读(76) 评论(0) 编辑 收藏 举报