# linux2021: yian02
###### tags: `linux2021`
## 測驗 $\alpha - 1$
在 linux 核心程式碼 (arm) 中,實作[進階加密標準(AES)](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard)時,有使用到 bit rotation。
此加密演算法的四個動作:SubBytes、ShiftRows、MixColumns、AddRoundKey 中的三個操作 (SubBytes、ShiftRows、MixColumns) 可以透過 lookup table 同時完成,這樣的操作會需要 16 個 lookup tables,但這16個 lookup table 中,只有四個需要被儲存起來,其餘的 12 個 lookup tables 都可以透過儲存起來的四個 tables 做 bit rotation 得到,這樣可以節省 75% 的 D-Cache。
程式碼可以見 [linux 核心 AES 程式碼](https://github.com/torvalds/linux/blob/e73f0f0ee7541171d89f2e2491130c7771ba58d3/arch/arm64/crypto/aes-cipher-core.S),其中有使用到在 arm 架構中用來 right rotate 的指令 `ror`。
Reference: [Accelerated AES for the Arm64 Linux kernel](https://www.linaro.org/blog/accelerated-aes-for-the-arm64-linux-kernel/)
## 測驗 $\alpha - 2$
將程式碼輸入 [Compiler Explorer](https://godbolt.org) 並使用 x86-64 gcc 11.1 來編譯程式碼。
#### 觀察不使用最佳化時編譯器的輸出
若完全沒有做編譯器最佳化時,編譯器會輸出每個 function 對應的組合語言,並使用函式呼叫的方式執行相對應的程式碼。
以下程式碼為在不使用編譯器最佳化時輸出的 rotl8 函式。
```c=
rotl8:
push rbp
mov rbp, rsp
mov eax, edi
mov DWORD PTR [rbp-24], esi
mov BYTE PTR [rbp-20], al
mov DWORD PTR [rbp-4], 7
mov eax, DWORD PTR [rbp-4]
and DWORD PTR [rbp-24], eax
movzx edx, BYTE PTR [rbp-20]
mov eax, DWORD PTR [rbp-24]
mov ecx, eax
sal edx, cl
mov eax, edx
mov esi, eax
movzx edx, BYTE PTR [rbp-20]
mov eax, DWORD PTR [rbp-4]
sub eax, DWORD PTR [rbp-24]
add eax, 1
mov ecx, eax
sar edx, cl
mov eax, edx
or eax, esi
pop rbp
ret
```
在上面的程式碼中,編譯器一步一步地做在原始程式碼中寫出來的程式,也沒有發現到我們是在做 bit rotation 而使用 `rotl` 或 `rotr` 指令。
#### 觀察使用最佳化時編譯器的輸出
有使用編譯器最佳化時(只需要使用到最基本的編譯器最佳化 `-O` ),會發現若是在編譯時期就已經可以推論出值的變數,會直接在編譯時期就計算出函數的回傳值,因此不會用到 function call,也不會用到 `rotr` 或 `rotl` 指令。
執行以下的程式碼:
```c
// 8 bits
uint8_t a8 = 0xf0;
uint8_t b8 = rotl8(a8, 2);
printf("%x\n", b8);
```
觀察對應的組合語言輸出:
```c
mov esi, 195
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
```
可以發現編譯器在編譯時期就已經將函式的回傳值計算出來,上面的組合語言基本上只是在將函式回傳值 print 出來而已,因此也不會使用到 `rotl` 或 `rotr`。
不過,若將變數變成編譯器無法在編譯時期就推論出來的值(例如:需要使用者輸入的值),就會發現編譯器能夠將原始的程式碼最佳化成使用 `rotl` 或 `rotr` 指令。
執行以下程式碼:
```c
uint8_t test;
fscanf(stdin,"%d",&test);
test = rotl8(test, 2);
printf("%x\n", test);
```
觀察編譯器最佳化後的輸出(對應到呼叫 `rotl8` 時的輸出):
```c
movzx esi, BYTE PTR [rsp+15]
rol sil, 2
mov BYTE PTR [rsp+15], sil
```
可以發現編譯器使用了 `rol` 指令。
因此,經過編譯器最佳化後的程式碼,如果其變數的數值在編譯時期還無法被編譯器推論出來的話,編譯器就會最佳化成使用 `rol` 或 `ror` ,而不需要使用函式呼叫。
## 測驗 $\beta - 1$
:::warning
power of 2 的翻譯為「2 的冪」
:notes: jserv
:::
該程式碼首先先判斷 alignment 是否為 2 的冪,若不是的話,就用算術運算算出 align_up 的值,若是的話,則可以透過 bitwise operation 算出 align_up 的值。
#### 不是 2 的冪
先將 sz 加上 mask,處理 align_up 的值要大於等於 sz 的問題(先使要被 align 的位數進位,才不會在後面的除法運算中被丟掉),再透過除法和乘法運算計算出其 align_up 的值。
#### 2 的冪
```c
if ((alignment & mask) == 0){ /* power of two? */
return (~mask) & (sz + mask);
}
```
若為 2 的次方,則代表對應到其次方的 bit 會是 1,後面都是 0 ,則減一之後,會使得原本是 1 的位置變為 0,後面的 0 都變為 1。
例如: 8 $\to$ 1000~2~, 8 - 1 = 7 $\to$ 0111~2~
因此對 mask 取 not 後,只要對 sz 做 and ,就可以將 sz 變為 alignment 的倍數。
不過還要先處理 align_up 的值要大於等於 sz 的問題,因此要對 ~mask 做 and 的應該要是 sz + mask,先將該進位的進位完成後,再將不需要的 bits 透過和 ~mask 做 and 運算去除掉,就可以透過 bitwise operation 計算出 2 的次方的 align_up 的值。
## 測驗 $\beta - 2$
在 [linux/include/linux/align.h](https://github.com/torvalds/linux/blob/e73f0f0ee7541171d89f2e2491130c7771ba58d3/include/linux/align.h) 中可以看到以下 macro:
```c
#define ALIGN(x, a) __ALIGN_KERNEL((x), (a))
```
而`__ALIGN_KERNEL((x), (a))` 可以在 [linux/include/uapi/linux/const.h](https://github.com/torvalds/linux/blob/e73f0f0ee7541171d89f2e2491130c7771ba58d3/include/uapi/linux/const.h) 中找到:
```c
#define __ALIGN_KERNEL(x, a) __ALIGN_KERNEL_MASK(x, (typeof(x))(a) - 1)
#define __ALIGN_KERNEL_MASK(x, mask) (((x) + (mask)) & ~(mask))
```
這個 align 的 macro 其中一個用途是用在 page align 上,在 [linux/include/linux/mm.h](https://github.com/torvalds/linux/blob/e73f0f0ee7541171d89f2e2491130c7771ba58d3/include/linux/mm.h) 中:
```c
#define PAGE_ALIGN(addr) ALIGN(addr, PAGE_SIZE)
```
這個 macro 的其中一個用途是在 `make_alloc_exact` 函式中有使用:
```c
static void *make_alloc_exact(unsigned long addr, unsigned int order,
size_t size)
{
if (addr) {
unsigned long alloc_end = addr + (PAGE_SIZE << order);
unsigned long used = addr + PAGE_ALIGN(size);
split_page(virt_to_page((void *)addr), order);
while (used < alloc_end) {
free_page(used);
used += PAGE_SIZE;
}
}
return (void *)addr;
}
```
這個函式會透過對傳入的 `size` 先去算 alignment 後,加上傳入的 `addr` 去計算結束的位置,依照起始和結束的位置去 allocate 最小需要的 physically-contiguous pages 。
而根據該程式碼中的註解提到,在 linux kernel 中有另一個函式 `alloc_pages()` 可以達到差不多的功能,差別在 `alloc_pages()` 只能 allocate memory in power-of-two pages,而 `make_alloc_exact()` 可以只 allocate 最小需要的 page 即可。
## 測驗 $\gamma - 1$
根據 [fork(2)](https://man7.org/linux/man-pages/man2/fork.2.html) 中提到:
> The entire virtual address space of the parent is replicated in the child
而複製過去的 virtual address space 中,也包含了 I/O Buffer,因此已經被 parent process 存進 buffer 但還沒被 flush 出來的值,也會被 child process 接收,在 buffer 滿了或是執行 fflush 時,就會一併被印出來。
假設 `NNN = 12`,則對於每一個被 fork 複製出來的 process 來說,都會印出 12 次的 `-`,只是對於最原始的 parent process 或是在 `i = 0`時即被 fork 出的 child process 來說,這 12 個 `-` 都是自己印出來的。
但對於後面才被 fork 出的 process 來說,以 `i = 11` 時被 fork 出的 child process 為例,這個 process 也會印出 12 次的 `-`,不過前面的 11 次是來自於 parent process 儲存在 buffer 中還未被輸出的,只有最後一次是該 process 自己印出的。
而在 `for` 迴圈中的 `fork` 會使得 process 數量程二的次方增長,迴圈執行過 12 次時總共會有 $2^{12}$ 個 process ,每個 process 又會印出 12 次 `-`,因此印出的 `-` 總數為 $12\times 2^{12}=49152$,符合題目所述。
另外,也可以測試在沒有 I/O Buffer 時會發生的狀況,先計算在沒有 I/O Buffer 時會輸出的 `-` 字元數:
在沒有 I/O Buffer 時,輸出的 `-` 字元數會成等比級數成長,也就是若 `NNN = 12` 時,輸出的字元數會是 $\frac{2\times (2^{12}-1)}{2-1} = 8190$ 個。
將原本程式中的 `printf("-");` 改成 `fprintf(stderr,"-");` ,也就是從會使用 buffer 的 stdout ,改變成輸出至預設沒有 I/O Buffer 的 stderr。
```bash
> ./fork 2> result.txt
> wc -c result.txt
```
將 stderr 輸出的內容導至 result.txt 檔案中,並執行 wc ,可以看到程式顯示 8190 ,符合上面計算的結果。
## 測驗 $\delta - 1$
#### 原理
程式碼在做的事情是同時會有多個 thread 對同一個 concurrent queue 做 push 和 pop 的操作,為了避免 thread 互相衝突(在操作到一半時被別的 thread 更動),因此此 concurrent queue 有兩把鎖: `first_mutex` 和 `last_mutex` ,需要 push 操作的 thread 在操作之前必須要先得到 `last_mutex` 這把鎖才能夠進行操作,並在操作後釋放這把鎖;而 pop 操作的 thread 在操作前需要獲得 `first_mutex` 這把鎖才能夠進行操作,操作後同樣的也要把獲得的鎖釋放出來。
#### 可能的實作缺失
若按照老師在題目程式碼中的寫法,在 push 時會 push 到 queue 的最後一個,因此對於第一個被 push 進 queue 的人來說,他會被 push 到 dummy head 的後面。
但在 pop 時卻是從 queue 的 head 開始 pop,也就是第一個被 pop 出來的會是 dummy head,這樣會造成在程式碼認為 queue 已經空了的時候,裡面存在的不是 dummy head,而是最後一個被 push 進去的 node,這樣可能會造成本來應該要被 free 的 node 還可以被後續程式碼使用,若裡面存放的是重要的資料,則可能會有安全上的疑慮。
## 測驗 $\epsilon - 1$
這個程式碼使用 `mmap` 來實作 memory pool,一開始先透過 `mpool_init` 來初始化 memory pool ,但這時還沒有真正分配出 memory pool 所需要的所有空間,只有把對應到每個 size 的 head 所需要的空間分配出來。
初始化後,可以透過 `mpool_alloc` 來獲得 memory pool 中的空間,在此函式中,會先透過傳入的引數 `sz` 來找到對應大小的 memory pool,找到後就會檢查該 memory pool 是否已經有分配空間,若沒有的話在此時才會真正分配空間給對應 size 的 memory pool,而分配空間是透過呼叫 `mpool_new_pool`,透過傳入此函式的引數可以看出來,一次分配的 memory pool 總共的大小就是一個 page。
在 `mpool_new_pool` 函式中,會呼叫 `mmap` 來獲得記憶體空間,再將獲得的空間依照傳入的 `sz` 引數初始化成 linked list 的形式,假設傳入的 `sz` 是 64 ,則代表此 memory pool 中一個區塊就是 64 bytes,因此將獲得空間的 offset = 0 的位置所儲存的資訊變為 offset = 64 的位置,依此類推,就可以將獲得的連續空間變成類似 linked list 的結構,來方便之後得 allocate 和 repool 操作。
而若是呼叫 `mpool_alloc` 時發現對應大小的 memory pool 沒有可用空間時,就會呼叫 `mpool_new_pool` 獲得一個新的 memory pool ,再呼叫 `add_pool` ,將新的 memory pool 串到原本的 memory pool 後方,就可以使用其配置的空間。
而從 memory pool 分配空間出去之前,會先將 `hs` 中儲存的相對應大小的 head 換成要分配出去的下一個,也就是儲存在要分配出去的空間中的「下一個人的記憶體位置」。透過 memory pool 分配出來的空間中所儲存的資訊,就會從原本儲存「下一個人的記憶體位置」變為儲存「真正的資料」。
而 `mpool_repool` 函數在做的事情,就是回收分配出去的 memory pool 空間,透過將回收的空間中所儲存的資訊換成 `hs` 中的第一個人所在的「記憶體位置」,並將 `hs` 中對應大小的 head 換成要回收的那個空間所在的位置,就完成了將要回收的空間串回 memory pool 的 linked list 中的操作。
## 測驗 $\epsilon - 1$
在老師題目的程式碼中, `mpool_repool` 函數,不管要回收的 memory pool 的大小是多少,都會將其回收到 size 最小的那個 memory pool 中,這樣會造成空間的浪費,因為大的 memory pool 回收後只能當作最小的來使用。
可以透過在 `mpool_repool` 函式中,先尋找對應大小的 memory pool,再將 memory pool 歸還至對應大小的 pool 中,來解決這個問題。
改寫後的 `mpool_repool` 如下:
```c
void mpool_repool(mpool *mp, void *p, int sz)
{
int i = 0, curr_sz;
if (sz > mp->max_pool)
{
if (munmap(p, sz) == -1)
fprintf(stderr, "Failed to unmap %d bytes at %p\n", sz, p);
return;
}
// looking for the appropriate hs entry
long szceil = 0;
for (i = 0, curr_sz = mp->min_pool;; i++, curr_sz *= 2)
{
if (curr_sz > sz)
{
szceil = curr_sz;
break;
}
}
assert(szceil > 0);
void **ip = (void **)p;
// YYY;
*ip = mp->hs[i];
assert(ip);
mp->hs[i] = ip;
}
```