# 作業系統- Inter Process Communication
<style>
.red{
color:red;
}
</style>
## Outline
* [IPC方式](#IPC方式)
* [何謂 Race condition](#Race-Condition)
* [Critical Section Design ](#C.S.-設計方法)
* [Message Passing](#Message-Passing)
## IPC方式
* Shared Memory v.s. Message Passing
| 比較項目\方式 |Shared Memory | Message Passing |
| -------- | -------- | -------- |
| 資料量 | 多 | 少 |
| 通訊速度 | 快 | 慢(需System Call 讓 kernel 處理) |
|分散式系統 |不適合->無法擁有共同記憶體空間 (因設備分散各地) | 適合 |
| 互斥存取控制 | 需要 | 不需要 |
***
## Race Condition
**定義:多個Processes 同時存取及處理同一個資料,其結果可能因執行順序而有所不同**
* 例子
<img src="https://blog.cloudxlab.com/wp-content/uploads/2021/04/Screenshot-163-1.png" width="600" heigh="400" alt="race condition">
## How to solve race condition
1. Disable Interrupt
要存取變數時就不讓其他 Process Access
* 在Single Processor System很好用->可以完全避免其他process使用(因為只有一個CPU)
* 而Multiprocessor System較難實現->只關閉一顆CPU不一定能使其他Process不使用此共享變數,但全關processor效能又很差
**最大問題:風險太高(若允許user process 使用->萬一不enable 則全部東西都無法取得CPU的資源)故只開放給 Kernel developer使用**
2. Critical Section Design
將所有共享變數包在臨界區間中(意即一個program可以有很多個C.S.)
**設計主軸:如何設計一個好的進入C.S.的控制碼**
Process需要提出申請才能進入
## C.S.必須滿足的三大性質
1. Mutual Exclusion:
在同一時間點內只能有一個process在臨界區間中
2. Progress:
* 不想進入C.S. 之 Process 不能影響 or 阻擋 or 參與決策使別人不能進入C.S.
* 決策誰可以進入C.S.之時間是有限的,不能發生deadlock
3. Bounded Waiting:
N 個 processes 最多等 N - 1 次就要可以進入C.S.中->No starvation, 要公平對待
**也不能對Process的執行速度有任何假設**
### 當有Process正在C.S.中我們要如何讓其他 Process 卡住?
- Disable Interrupt
- Busy Waiting
使用loop,判斷條件符合就卡住自己
* 優點:若卡住時間<<兩次context switch 則效益比 disable interrupt 高
* 缺點:浪費系統效能(空轉:spinlock)
目前廣泛利用於multicore的系統中
- Non-Busy Waiting
Process go to sleep(waiting state)直到同步事件發生後才會來ready state->兩次context switch
優缺點與busy waiting 相反
## C.S. 設計方法
[1. Software -> Peterson solution](#Software-based-solution)
[2. Hardware supported](#Hardware-Supported)
[3. Semaphore](#Semaphore-號誌)
[4. Monitor](#Monitor)
- 架構圖
<img src = 'https://drive.google.com/uc?export=view&id=1iYW48LL6oUrc3d-L0Eja9_Y3tdDfy0G6' width="600" heigh="400" alt="CS Design architecture">
### Software based solution
#### Peterson Solution
靠純軟體方式來進入C.S.。
**但因為現今電腦架構,processor or compiler會調換指令順序已達最佳效率,所以有可能出錯**
eg. Pi
```
while (true){
flag[i] == True;
turn = j;
while ( flag[j] && turn == j);
/* critical section */
flag[i] == False;
/* remainder section */
}
```
eg. Pj
```
while (true){
flag[j] == True;
turn = i;
while ( flag[i] && turn == i);
/* critical section */
flag[j] == False;
/* remainder section */
}
```
註:先表示你想要,再將執行權(turn)交給對方->順序錯就炸了
所以 compiler 換到這兩行 peterson's solution 就沒用了
互換後就沒有mutual exclusion了
### Hardware Supported
- [Memory Barrier](#Memory-Barrier-又稱-memory-fence)
- [Hardware Instructions](#Hardware-Instructions)
- test&set()
- compare&swap() (CAS指令)
- [Atomic Variables](#Atomic-Variables)
#### Memory Barrier 又稱 memory fence
因為資料被改動時不一定所有 processor 都知道,所以讓所有 processor 都知道的話就不會有 race condition
若此指令被執行->系統將確保所有load store都要按照順序執行
解決亂序執行的問題(out-of-order)
又因為此是一個low level operations 故只提供kernel developer使用,application 開發使用者無法使用
#### Hardware Instructions
- test&set():傳回參數舊值、將值改為True
**特別注意,此為硬體指令,只是利用高階語言模擬幫助理解**
```
boolean test&set(boolean *target){
boolean ret = *target;
*target = True;
return ret; // return 舊值
}
```
但須特別留意,以下此種方法是沒有滿足C.S.的設計條件的
```
//沒有滿足bounded waiting的test&set
boolean lock = false; //共享變數宣告
while True{
while (test&set(&lock)); //若第一個搶到 則return False跳出迴圈 進入C.S.
/* C.S. */
lock = false;
/*R.S. */
}
```
問題出在哪?
等於是把鑰匙(lock)丟在地上看誰先搶到誰就可以進來C.S.
-->解法就是:把要進來的人排隊,即新增一個可以記錄排隊的資料結構
```
//滿足bounded waiting的test&set
//共享變數
boolean lock = false;
boolean waiting[0...n-1]; //initial = False
//區域變數
lock = False;
int j;
// Pi code //
while True{
waiting[i] = True; //Pi想進
key = True;
while (waiting[i] && key){
key = test&set(&lock); // 若是第一個 則取得false lock -> 跳出迴圈到C.S.
}
waiting[i] = False; //不用等了就是你
/* C.S. */
j = (j+1) % n // n為總process數。circular linked list的概念
while ((j != i ) && !waiting[j]) //遍歷一圈
j = (j+1) % n
if (j == i)
lock = False; //代表沒人想進去,鑰匙丟地上
else
waiting[j] = False; //幫Pj解除spinlock
/*R.S. */
}
```
<span class = 'red'>若今移除 C.S. 之後的waiting[i] = False敘述將發生什麼 error ?</span>
-> [*Progress*](#C.S.必須滿足的三大性質) 中的 (1) Pj 不想進入 C.S.但被指定進 (2) 而Pj進入後使 lock = True
造成 deadlock 未來沒有 Process 可以進
假設情況:在 Waiting queue 中 僅 Pi & Pj 想進入 C.S.而 Pi優先執行進入 C.S. 然而在 Pi 完成後並沒有將自身的 waiting[i] 設為False 由 Pj 接續進入 C.S. 待執行完後退出 C.S. 發現 Pi 之 waiting[i] = True 則讓 Pi 進入 C.S.(但實際上 Pi 並沒有要進)
就發生 **不想進去卻進去** **且不讓別人進** 的情況
- compare&set():
**傳回參數舊值、確定*value與expected相同->value = new_value**
**特別注意,此為硬體指令,只是利用高階語言模擬幫助理解**
```
int CAS(int *value, int expected, int new_value){
int temp = *value;
if(*value == expected){ // 與test&set不一樣的地方:要看是不是expected
*value = new_value;
}
return temp; // 回傳舊值
}
```
實際用於C.S.之實例
```
// 此範例一樣未符合bounded waiting 之特性->why?一樣鑰匙掛空中誰搶到是誰的
int lock = 0; // 共享變數,如同test&set中的boolean lock = Fasle;
while (True){
while(CAS(&lock, 0, 1)!= 0); //若lock原先為0->return 0離開迴圈但把它變成1
/* C.S. */
lock = 0;
/* R.S. */
}
```
e.g. 執行流程解釋:
T0 : int lock = 0 // initialize
T1 : Pi拿到CPU執行進入區code,先執行 CAS(&lock, 0, 1) 執行後傳回0,跳出spinlock前往C.S.
T2 : Pj想進 C.S. 則執行 CAS(&lock, 0, 1) 但執行後回傳1,則卡在spinlock中直到 Pi 離開 C.S.
T3 : Pk, Pl, Pm 也想進,但如果依照搶到 CPU 順序會使 bounded waiting 被打破 ->因為可能Pj搶不到,餓死。
**解決方法:建立一個 list 去存類似順序的東西,用circular linked list 去遍歷**
```
// 有符合 bounded waiting 性質的CAS
//共享變數設置
int lock = 0;
boolean waiting[0...n-1] = False
//區域變數(每個 process 獨自擁有)
int key, j;
while (True){
waiting[i] = True;
key = 1;
while(waiting[i] && key == 1){
key = CAS(&lock, 0, 1) // 若沒人在C.S.則value = expected ->return 0跳出spinlock
}
waiting[i] = False; //不用等直接進 C.S.
/* C.S. */
j = (i + 1) % n;
while((j != i) && !waiting[j])
j = (j + 1) % n;
if (j == i)
lock = 0;
else
waiting[j] = False;
/* R.S. */
}
```
#### Atomic Variables
> atomic 意即不可分割 ->代表這些變數在做存取讀取修正都必須一起完成
故在上述 test&set() 及 CAS() 都有使用到的共享變數均須使用 Atomic Variables 去做宣告
->才能實施真正意義的mutual exclusion
**<span class = "red">(本身指令並無提供互斥的機制,只是提供一種工具來設計解決 C.S. 問題)**</span>
使用例子:共享變數
雖然有 Atomic Variables 的協助,但還是不能完全解決race condition
### Mutex lock
即透過一個 boolean variable available去指示 lock 是否 available
也提供兩個 atomic operations:
- acquire() : 取得 lock // 進入 C.S.必須先 acquire
- release() : 釋放 lock // 離開 C.S. 必須 release
// 二元表 lock 值僅能為 0 or 1
故無法統計出有多少 Process 卡在裏面(也可以使用後面定義的 wait()以及 signal())
至於 acquire() & release()要怎麼製作才能保證 atomic 呢?
->用硬體指令 e.g. CAS 來製作 mutex lock
```
// 此地使用 spinlock 求解
boolean Available = True;
Acquire(){
while (! Available); // 無法 available 時 spinlock
available = False;
}
Release(){
available = True; // 將 available 打開供其他 process 使用
}
```
若要使用 CAS 則將 spinlock 裡的判斷式改成 (CAS(available, 0, 1)!= 0) 即可
### Semaphore 號誌
除了 Semaphore OS 還提供 mutex lock 來達到互斥存取(均屬 system call )
原因:hardware-based 太複雜去使用,且無法提供給開發者使用
而 mutex lock 類似一種二元號誌( binary semaphore )
#### Semaphore
Semaphore 是一種方便的工具讓我們進行互斥存取,但也有很多誤用問題需討論
- 定義:semaphore是一個整數變數,除了設定初值以外,只能透過兩個 atomic operations 來存取
```
Wait(S){
while (S<=0); // 卡在這
S--; //消耗一次
}
```
```
Signal(S){
S++; //相當於喚醒 給他補充一次
}
```
以上均 based on atomic operations 否則會 race condition
```
Semaphore mutex = 1 // 1:初值
```
誤用情形:
1. wait() signal() 順序顛倒 ->違反互斥
2. 一直使用 wait() 則會使quota用完 -> deadlock
3. 多個 semaphore 使用容易順序安排不當 -> 互斥及 deadlock 都會炸
使用一個經典同步問題來做範例 - Reader and Writer Problem
但又此問題有兩種類別
1. First 對 reader 有利
2. Second 對 writer 有利
因兩種分析方式雷同 此地挑第一型作討論
#### 第一型之 Reader and Writer
說明:
有一組資料被很多 Reader and Writer 所共享 而分成兩種類型的process
- Reader 僅 read 文件 不會進行 update
- Writer 會進行 read and write
所以 Reader 與 Reader 間可以同時進行,但 Reader and Writer 需要互斥存取,當然 Writer and Writer 之間也要
而 First class 中沒有 reader 會需要一直等 writer -> Writer 會 starvation
```
// 初值設定
Semaphore rw_mutex= 1 ,mutex = 1; //rw 給 rw & ww 互斥條件滿足 mutex為 read_count的互斥存取控制
int read_count = 0 // 統計reader個數 reader來++ 反之--
// Writer Process Code
Writer(){
while(True){
wait(rw_mutex);
/* writing */
signal(rw_mutex);
}
}
// Reader Process Code
Reader(){
while(True){
wait(mutex); // 變更read_count必要步驟
read_count++;
if (read_count == 1) wait(rw_mutex) // 你是第一個reader 偵測有無writer正在寫 有的話等他 沒有的話就換你去讀且阻擋writer
signal(mutex);
/* reading */
wait(mutex);
read_count--;
if (read_count == 0) signal(rw_mutex); // 確定沒有reader 要讀取 就放writer 進來
signal(mutex)
}
}
```
**先前曾經提及的二元號誌與計數型號誌就差異在初值設定可否為0, 1以外之數(也可以負數)**
若此時 semaphore C 為負數 (e.g. -N 則表 N 個process卡在wait( C ))
如何利用 Binary Semaphore 去實現 Counting Semaphore //典型問題, Counting Semaphore都用來控制存取有限的資源
```
// 初值設定
int c; //可以不用是 0, 1 -> 即Counting Semaphore初值
binary_semaphore S1 = 1; // 防止 C race condition
binary_semaphore S2 = 0; // 如果 C<0 就卡住 process
wait(c){
wait(S1); //要使用就先扣quota
C--;
if(C<0){ //代表quota用完了 就等別人救
signal(S1);
wait(S2);
}
else signal(S1); // 可以正常使用就退出對c的互斥存取
}
signal(c){
wait(S1);
C++; //加回quota
if(C<=0){ // -1 + 1 = 0 所以要加等於 代表先前有人卡住
signal(S2);
}
signal(S1);
}<
```
除了此分類外還可以使用 '有無使用 busy-waiting' 來區分
不使用 busy-waiting(spinlock) 即使用system call 讓 process 去睡覺 (sleep()) 而當可以使用時再給他 wakeup()即可
註:以上由 OS 提供
### Monitor
<img src = 'https://i.imgur.com/FMpiMLI.png' alt = 'Monitor'>
- 雖然 Semaphore 好用,但凡數量一多有可能使用不當會造成很多問題,所以我們改用另一種機制來使programmer更方便使用同步工具
- 為一種 ADT 包含三個部分
1. 共享變數宣告區
2. programmer defined function
3. 初值設定區
優點:
- 本身保證互斥存取->同一個時間內僅能讓一個 Process 呼叫 monitor中的某個function 執行
> The monitor construct ensures that only one process at a time is **active** within the monitor.
- 此為高階語言,對 programmer 較為方便使用
#### 變數設置
condition x, y, z...
這個變數只有兩個operations
1. wait(): 不像semaphore的 比較像block()這個 system call
2. signal(): 不像semaphore的 比較像wakeup()這個 system call
而等待的 waiting queue 通常使用 FIFO queue,當然也可以使用 priority queue
e.g. 如果用的是 priority queue 則在wait()中會書寫成 priority依據,像是x.wait(time)
#### Monitor 種類
有三種,而至於會分三種類型是因為以下情境。
假想情境:現在 P 在 Monitor 中,也就是 Active。而 Q 在 waiting queue (in monitor) 中等待 signal,若 P 執行 signal() 則 Q 就會往下執行,但就會變成兩個 Process 同時在 Monitor 中 -> 違反互斥。
**所以,需要解決執行 signal() 的 process 或被 signal() 的 process 誰能夠在 monitor 中執行**
以下均以 P 代表執行 signal() 之 process (救命恩人)
Q 為被 signal() 拯救的 process
- 種類一:P 救完就去睡覺,等 Q 執行完畢再繼續執行 ->overall 最優
Hoare monitor
- 種類二:P 救完但繼續執行到結束或自己發I/O,再讓 Q 執行
但在 P 繼續執行過程很有可能將 Q 所需之資源、可以恢復執行的條件給改變使 Q 無法正常執行 -> Q 不保證可以繼續執行
- 種類三:和一很像,但 P 會立即離開 monitors 來到 entry queue ->效能較一差勁
Concurrent C & Pascal used it.
❖ Type 1 & Type 3 屬於 Signal and wait, Type 2 屬於 Signal and continue
#### Implement Monitor using Semaphore
三大條件必須滿足:
1. 設計需符合上述種類一
2. 互斥性質
3. 須展現wait() and signal()
```
// initialize
Semaphore mutex = 1;
Semaphore next = 0; // 卡住 P
int next_count = 0; // 統計 P 數量
Semaphore x_sem = 0; // 卡住 Q
int x_count = 0; // Q 個數
//about function f
wait(mutex);
/* doing f */
if (next_count >0 ) signal(next); // 接下來有人在等就拯救他,因為他曾經救過你(救命恩人)
else signal(mutex); // 沒人等就放 entry queue的人進來
// 設計 wait()時要站在 Q 的立場
wait(){
x_count++;
if (next_count >0) signal(next);
else signal(mutex);
wait(x_sem);// Q 被卡住
x_count--; // Q 被拯救後才來執行這行,減少 Q 數量
}
//設計 signal()要站在救命恩人(p)的立場
signal(){
next_count++;
signal(x_sem);//先救 Q ->若無 Q 在等待執行這行也沒問題,直接通過
wait(next); // 等 Q 回來救你,P 卡在這
next_count--; // 直到 P 被救 -> P數量 -1
}
```
### Priority Inversion
發生情境:當 high priority process 正在執行時,需要的資源正被 low priority process 把持住,所以需要等 low priority process 執行完將其釋放才能獲得資源,但又因為 low priority 得不到 cpu 執行,故 high priority process 會等很久
解法:先暫時將 high priority 的優先權繼承給 low priority process 讓他趕快做完釋放資源。
## Message Passing
也是一個機制可以使 Process Synchronize 執行,且不需要共同的 Shared memory 空間 -> distributed system 適用
- 步驟:
1. 建立 communciation link
2. 傳輸
3. 傳輸完畢釋放 link
也須有兩個system call 支援:send & receive
- 種類:
- direct 一組 process 一條 link
- synchronous 收送均需指定
- asynchronous 收方指定即可 (like email)
- indirect 共享 mailbox -> 可以有多條 links
direct 缺點:limited modularity
### Synchronization
message passing 可以是blocking and non-blocking
所以有四種組合,若收送均為 blocking -> 稱為 rendezvous
## Reference:
H.Y. operating system