# 2024-06-11,13,20 討論簡記
> [課程期末專題](https://hackmd.io/@sysprog/linux2024-projects)
### 第 17 週測驗 `1`
問題 : 如何避免 [ABA](https://en.wikipedia.org/wiki/ABA_problem) 問題?
Thread~1~ 寫 A 入 stack 後,stack 的top 為 A。
Thread~2~ 搶佔再寫入 B 又寫入 A 入 stack 後。
Thread~1~ 繼續工作,檢查 stack 的 top 值發現仍然是 A ,但其實這個 A 並非 Thread~1~ 一開始寫入的 A 。
#### stack `push` 的操作 :
step 0: Thread~2~ 要 push 變數 `node C`
step 1: Thread~2~ 先看了 stack 的頂端 `head orig`,發現現在頂端為 `node B` ,`aba=1`。
step 2: Thread~2~ 也準備了一個 `head next` 將其 `aba` 設為 2。
step 3: 接下來將 `node C` 的 的 next 指向 `node B` 。
step 4: 再將 `node C` 存到 `head next` 的 node 。
step 5: 再將 `head next` 與 `head orig` 交換即可。
所以 lstack_head 就是像一個套子,其功能是包 `aba` 變數。因此在 step 5 交換套子的時候需檢查,交換的那一刻 是否 `head orig` 的 `aba:1` 也就是 `head next` 的 `aba:2` 編號少一。如果不是則這次在 Thread~2~ step0 到 step5 的過程有其它執行緒將 `head orig` 交換了 。如此一來就可以達到沒有操作時不需要有 lock 。
```
head next
|------------|
| aba : 2 |
| | _________
| | | node C |
| | ---------
|------------| |
|
head orig | next (step 3)
|------------| |
| aba : 1 | |
__________ next| _________ | |
| node A | <---- | node B | <-----|
---------- | --------- |
|------------|
```
#### stack `pop` 的操作 :
step 0: Thread~2~ 要 pop 變數
step 1: Thread~2~ 先看了 stack 的頂端 `head orig`,發現現在頂端為 `node B` ,`aba=1`。
step 2: Thread~2~ 也準備了一個 `head next` 將其 `aba` 設為 2。
step 3: 將 `node A` 放入 `head next`。
step 4: 再將 `head next` 與 `head orig` 交換即可。如此一來新的 `head orig` 也就是 stack 的頂端存的就是 `node A` 。
所以在交換的那一刻也就是 step 4 只要確認 `head orig` 的 `aba:1` 也就是`head next` 的 `aba:2` 編號少一,就可以確保沒有其它執行緒在 step 0 到 step 4 的過程中對 stack 頂端操作。
```
head next
|------------|
| aba : 2 |
| |
| |
| |
|------------|
head orig
|------------|
| aba : 1 |
__________ next| _________ |
| node A | <---- | node B | |
---------- | --------- |
|------------|
```
問題 : `lstack->free` 的作用?
`lstack->free` 這個代表有一個空間可以給執行緒做 push 變數時候使用。
當 Thread~2~ 要 push 先將變數放到 `lstack->free` 這個 node 裡面。所以`node C` 原先就是 free node
---
## 第 17 週測驗 `2`
一種會發生 deadlock 的執行路徑
```
[ Task A ] [ Task B ]
mutex_lock(&mutex1);
mutex_lock(&mutex2);
mutex_lock(&mutex2);
mutex_lock(&mutex1);
```
為何 `does_follow` 可追蹤 lock dependency graph?
follows 紀錄是否發生 directed cycles
follows[i][j] i -> j -> k
follows[j][k]
在測驗題裡面是用深度優先去搜尋是否會有 directed cycles。先去知道 acquire 的 lock 是被哪個 pid 所持有(hold)。接下來看自己所持有(hold) 的 lock 是否被其它 pid acquire。如果有,且該 pid 持有自己 acquire 的 lock 則出現 directed cycles。有 dead lock 發生。
```
my_pid
lock 1 <- pid:x acquire。
lock 2 <- my_pid acquire
```
因此偵測這些 dead lock 的方式會用到離散數學跟演算法的方式,看如何可以快速的找到 directed cycles。但 linux 裡面 [/kernel/locking/lockdep.c](https://github.com/torvalds/linux/blob/master/kernel/locking/lockdep.c) 是用廣度優先的方式璇找 lock dependency,因為它想要快速偵測到 deadlock,一旦偵測到就系統就關閉。
問題 : Linux 是如何實作 lock dependency 的 ?
> weihsinyeh
在 [Linux 核心模組運作原理](https://hackmd.io/@sysprog/linux-kernel-module) 提到的 [BUG_ON](https://kernelnewbies.org/FAQ/BUG) 如果真的偵測到錯誤 kernel 會直接關機,不像 assert 只是不再繼續執行程式,並將程式運行的錯誤印出來。將 `BUG_ON` 打開,就會用 mutex 的 API 來重新包裝真正 lock 實作 e.g., debug_mutex_lock_common 。
在 [kernel/locking/mutex-debug.c](https://github.com/torvalds/linux/blob/master/kernel/locking/mutex-debug.c) 的 debug_mutex_wake_waiter 就像 futex 的功能。
Linux 還有 `WARN_ON` 來 trap 錯誤。
linux 的追蹤不同的 lock 有不同的方法。因為有些 lock 特化出新的功能,像 irq 現在也有新的 spin lock ,但雖然複雜的 lock,本質上追蹤 lock dependency 的方式是相同的。
## [RISC-V 最佳化編譯器實作](https://hackmd.io/@sysprog/H11Da3FQA)
self-hosting
Old compiler (O) ->
newer compiler (written in C)
use old compiler to compile newer compiler
-> binary for newer compiler
-> compile newer compilers
bootstrapping
- stage0 : 用既有的 C 編譯器,得到 x86-64 的執行檔
- stage1 : 用 stage0 得到的 x86-64 執行檔「編譯自己」,得到 Arm 的執行檔
- stage2 : 用 stage1 得到的 Arm 執行檔「編譯自己」
(cross compiler)
如何用 mmap 系統呼叫來實作 malloc
```c
int x = 0, y = 13;
/* bitwise AND */
if (x & y) /* instruction: and -> x 和 y 都會 evaluate */
return 0;
/* logical AND */
if (x && y) /* 只要是 x 是 false (0),y 「不要」 evaluate */
return 0;
```
bitwise AND 的運算元 evaluate 順序未指定;logical AND 則保證為由左至右,如果左運算元為 false 右運算元不會 evaluate。
shecc 要如何驗證 code transformation 是正確的?目前只有看到測試,但是測試不見得涵蓋所有改寫的行為。
> [C-Reduce](https://github.com/csmith-project/creduce) is a tool that takes a large C or C++ program that has a property of interest (such as triggering a compiler bug) and automatically produces a much smaller C/C++ program that has the same property.
defs.h 裡面有原始碼大小上限,如果 shecc 本身超過這個上限不就沒辦法 bootstrap 了?
> 屆時再做調整?
> 我的想法是,為了避免動態配置空間,一樣是先靜態配置,不過分 A, B 兩個空間,大小一樣沿用原本的大小。A 放滿了就放 B,當 parse 到 B 時把 A 清空繼續把原始碼放到 A,交替使用直到 parsing 完畢
> 聽起來像是 ring buffer 的概念,是一種解決方案沒錯。
## [運用並行處理來強化既有的應用場景: Redis](https://hackmd.io/@sysprog/B1W2HKTER)
為什麼要分五種 flavor?
為了要在不同的作業系統或是處理器架構上執行,例如有可能作業系統不支援 `sys_membarrier()` 系統呼叫,所以沒辦法使用 RCU using sys_membarrier() system call (`liburcu`) ,這時就要換另外一種 flavor ,必須要使用 Memory-barrier-based RCU (`liburcu-mb`) 。
---
[全向量視窗系統實作](https://hackmd.io/@sysprog/B13GqdgQ0)
clip region
double buffering
video RAM (DRAM)
## 第 18 週測驗 `1`
`x / 128` ==> `x >> 7`
log_2(128) = 7
* 為何 `#define M ((uint64_t)(UINT64_C(0xFFFFFFFFFFFFFFFF) / (D) + 1))` ?
> 因為計算商數時,需要處理 ${\lceil {2^{64} \over d} \rceil}$ 的數學運算。而若用 C 語言來實作,可以先使用除法運算將其無條件捨去,再加一變成無條件進位,而 $2^{64} -1$ 則是要應對 $d$ 為二的冪時的例外狀況。
Assume 8-bit arith,
n = 217
D = 3
uint8_t x = 0111 1001
uint8_t D = 0000 0011
#define M ((uint16_t)(UINT16_C(0xFFFF) / (D) + 1))
=> `0x5556` or `21846`
uint8_t quotient = ((uint16_t) M * n) >> 8;
=> `1`
### 方法說明
程式碼實作 模三運算(16 位元情境)
```c
#include <stdio.h>
#include <stdint.h>
const uint8_t D = 3;
#define M ((uint16_t)(UINT16_C(0xFFFF) / (D) + 1))
/* compute (n mod d) given precomputed M */
uint8_t quickmod(uint8_t n)
{ uint16_t quotient = ((uint16_t) M * n) >> 16;
return n - quotient * D;
}
int main (){
uint8_t n = 217;
uint8_t result = quickmod(217);
printf("output: %u, %u", M, result);
}
```
```
output: 21846, 1
```
在這個 8 位元的處理器上,我們要計算任意正整數 $N$ 模三之結果, 但是模數會使用到除法運算,為了避免大量時脈週期運算,我們會將整數的模運算簡化為乘法以及位移運算,概念如下所述:
首先模數 $mod$ 可以運算方式為 $mod =$ $n$ $\times$ ${\lfloor {n \over d} \rfloor}$ $\times$ $d$ ,其中 ${\lfloor {n \over d} \rfloor}$ 為商數,我們可以再將商數轉化為 ${\lfloor {n \over d} \rfloor} =$ $n$ $\times$ ${\lceil {{2^{16}} \over d} \rceil}$ $\times$ ${1 \over {2^{16}}}$ ,而其中的 ${\lceil {{2^{16}} \over d} \rceil}$ 就是上面程式中的 $M$ 巨集,除了 $M$ 以外的商數運算已經轉化為乘法以及位移運算。
接著我們來探討 $M$ 的巨集實作,也就是 ${\lceil {2^{16} \over d} \rceil}$ 的數學運算,我們將其轉化為 ${\lfloor {{2^{16} -1} \over d} \rfloor} + 1$ 。若用 C 語言來實作,就是使用除法運算將其無條件捨去,再加一變成無條件進位,而 $2^{16} -1$ 則是要應對 $d$ 為二的冪時的例外狀況。雖然最後還是會使用到除法運算,但是在編譯器最佳化的情況下(使用 constant propagation),預處理階段已經將 $M$ 轉化為常數,因此實際在執行時也不會有除法運算。
---
1, 2, 3, ..., FASTDIV_SAFEMAX (256)
bound > 0U && bound <= FASTDIV_SAFEMAX
short-circuit
pre-compute :
原先的範圍確認需要兩次比較,但是我們可以使用二補數的特性 (自然數 $N$ 之最重要位元 ${MSB}$ 為零 、 負數 ${MSB}$ 為一) , 確認上界與 X 之差以及 X 與下界之差皆大於零,若有一者不符合,則 bitwise OR 的結果會是二補數中的負數,再與零比較作範圍判斷。
```ruby
/* Range check
* For any variable range checking:
* if (x >= minx && x <= maxx) ...
* it is faster to use bit operation:
* if ((signed)((x - minx) | (maxx - x)) >= 0) ...
*/
```
---
[CPU 排程器研究](https://hackmd.io/@sysprog/BJdyxvxX0)
sched_ext 對 Linux 技術社群的影響?
-> Meta
-> Netflix
演化
early Linux sched -> O(n) -> O(1) -> CFS -> EEVDF
(通用的路線) 缺點/限制
特定的 workload <--
一台伺服器通包 vs. 多台主機分工 (microservices)
Redis
Database
=> predict
machine learning
feature extraction