---
# System prepended metadata

title: 'linux2021: yian02'
tags: [linux2021]

---

# 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;
}
```


