owned this note
owned this note
Published
Linked with GitHub
---
tags: linux2022
---
# 2022q1 Homework4 (quiz4)
contributed by < `sternacht` >
> [作業要求](https://hackmd.io/@sysprog/HkJ-dN_-q)
### 測驗 1
#### 答案
EXP1 = `r | shift | x >> 1`
#### 延伸 - 解釋上述程式碼運作原理
```c
int ceil_log2(uint32_t x)
{
uint32_t r, shift;
x--;
r = (x > 0xFFFF) << 4;
x >>= r;
shift = (x > 0xFF) << 3;
x >>= shift;
r |= shift;
shift = (x > 0xF) << 2;
x >>= shift;
r |= shift;
shift = (x > 0x3) << 1;
x >>= shift;
return (EXP1) + 1;
}
```
本題延伸自 [第三周測驗 7 ](https://hackmd.io/@sysprog/linux2022-quiz3#測驗-7),不同是的這題要計算 $\lceil log_2(x) \rceil$ ,因此在計算前後的方式有些不同,例如最前面有一個 `x--` 的動作,目的是要讓 2 的冪的數得到的值少一位,接著以 fls 計算,並在最後加回來,換言之就是一種減去 offset 概念的使用。
看過第三周測驗 7 的話,我們可以知道這一段程式碼還缺少一小部分,而這個部分全部被整合到 EXP1 裡,若是把 EXP 1 攤開來看,還需要以下幾個步驟
```c
x >>= shift;
// below code is equal to 'return (EXP1) + 1;'
r |= shift;
shift = (x > 0x1);
x >>= shift;
return (r | shift) + 1
```
其中 `x` 已經不會再用到,因此倒數第二行對 `x` 的數值指派可以省略,而 `x` 在經過一連串的右移過後,到第三行的時候數值範圍是從 0 到 3 ,對應二進位就是 `00`, `01`, `10`, `11`,大於 1 的判斷式表示只有後兩個要為 1 ,因此也可以寫成 `x >> 1`,最後整理起來就寫成 `return (r | shift | x >> 1) + 1`
#### 延伸 - 改進程式碼,使其得以處理 x = 0 的狀況,並仍是 branchless
若要改寫成能夠處理 x = 0 的狀況,勢必不能在最前面預先減去 1 ,而不這麼做的話就需要在最後面判斷要不要再補上 1 ,這裡我選擇先將保留 `x` 的值在變數 `_x`,在最後的時候作為判斷用,如下
```c
int ceil_log2(uint32_t x)
{
uint32_t r, shift, _x = x;
r = (x > 0xFFFF) << 4;
x >>= r;
shift = (x > 0xFF) << 3;
x >>= shift;
r |= shift;
shift = (x > 0xF) << 2;
x >>= shift;
r |= shift;
shift = (x > 0x3) << 1;
x >>= shift;
r |= (shift | x >> 1);
return r + (((1UL << r) ^ _x) > 0) - (_x < 1);
}
```
`r` 基本上沒有變,就是去算最高位元的位置,但因為除了 2 的冪以外的數需要再加 1 ,因此這時候就需要用 `_x` 與 `1 << r` 做 ^ 的 bitwise 操作,來看扣掉最高位元之後還有沒有剩下,如果有的話就是需要加 1 ,然而這時候會遇到一個問題是因為數值 0 與 1 做 ^ 操作會有值,導致函式代入 0 的結果會是 1。因此還要再把這個值扣掉,使結果為 0 (雖然log(0)應該為無效輸入)
再進一步想,會需要多加一個減號是因為起初是用 `1UL << r` 這樣的操作,是否有辦法一開始就過濾為 0 狀況呢? 於是又將最後一行改成以下的樣子,當原本的傳入值不為 0 時,才會變成 `1UL << r` ,否則就相當於 `0 << r`
```c
return r + ((((_x > 0) << r) ^ _x) > 0);
```
#### 延伸 - 在 Linux 核心原始程式碼找出 ceil 和 log2 的組合 (類似的形式),並解說應用案例 (提示: 在 Linux 核心排程器就有)
[linux/fair.c](https://github.com/torvalds/linux/blob/master/kernel/sched/fair.c)
---
### 測驗 2
#### 答案
EXP2 = `x & t`
EXP3 = `o += shift`
#### 延伸 - 解釋上述程式碼運作原理,並迴避 while, for, goto 等關鍵字以改寫出功能等價的實作
```c
static inline size_t ffs(unsigned long x)
{
if (x == 0)
return 0;
size_t o = 1;
unsigned long t = ~0UL;
size_t shift = BITS_PER_LONG; // 64
shift >>= 1;
t >>= shift;
while (shift) {
if ((EXP2) == 0) {
x >>= shift;
EXP3;
}
shift >>= 1;
t >>= shift;
}
return o;
}
```
題目提到這是改寫自 [第三周測驗 11 ](https://hackmd.io/@sysprog/linux2022-quiz3#測驗-11)的 fls 函式,這裡則是 ffs 的實作,許多概念上是相同的,都是利用二元搜尋的方式來找到答案,不一樣的地方是, fls 的實作是比較前半部分的 bits 是否為 0 ,來確認最高位元在哪一邊, ffs 則相反,透過 bitmask 的方式來判斷後半部分的 bits 是否為 0 ,來確定最低的 1 位元出現在哪一邊。
概念大致就是這樣,接著看程式碼,shift 預設為 64,接著除以 2 ,這是我們第一次要比較的位元數, `t` 作為 mask,一開始全部的 64 bits 都是 1 ,接著在右移 shift 個 bits,就變成後半為 1 ,前半為 0 的 mask,接著用 mask `t` 與 `x` 用 & 操作做比較,如果結果為 0 ,表示在 x 的後 16bits 中沒有 1 存在,此時把 x 整個右移 32bits ,並將結果加到變數 `o`裡面。
每經過一次迴圈,要比較的位元數就會減半,並且每當後半的位元沒有 1 的時候,就把 `x` 右移,去找前半部分。
若是要迴避 while, for, goto等關鍵字,第一種方式可以參考第三周測驗 11 的寫法,也就是把原本的迴圈打開,改寫如下
```c
static inline unsigned long ffs(unsigned long word)
{
if (!word)
return 0;
int num = 1;
if (!(word & (~0ul >> 32))) {
num += 32;
word >>= 32;
}
if (!(word & (~0ul >> (64 - 16)))) {
num += 16;
word >>= 16;
}
if (!(word & (~0ul >> (64 - 8)))) {
num += 8;
word >>= 8;
}
if (!(word & (~0ul >> (64 - 4)))) {
num += 4;
word >>= 4;
}
if (!(word & (~0ul >> (64 - 2)))) {
num += 2;
word >>= 2;
}
if (!(word & (~0ul >> (64 - 1))))
num += 1;
return num;
}
```
以上 if 判斷式中減法的部分只是闡釋想法,因為都是定值,所以也可以直接寫出數值如 `0xff`, `0xf` 等。另一種想法就是使用 recursive 的方式來改寫,如下
```c
static inline size_t ffs(unsigned long x, size_t shift)
{
if (x == 0)
return 0;
if (shift == 0)
return 1;
size_t o = 0;
unsigned long t = ~0UL >> (64 - shift);
if ((x & t) == 0) {
x >>= shift;
o += shift;
}
shift >>= 1;
return o + ffs(x, shift);
}
```
#### 延伸 - 在 Linux 核心原始程式碼的 include/asm-generic/bitops 目錄找出相關程式碼,並探討應用案例
目錄底下的 ffs 實作方式皆是不使用 while, for, goto ,而用多個 if 寫成的方式,例如
```c
static inline int ffs(int x)
{
int r = 1;
if (!x)
return 0;
if (!(x & 0xffff)) {
x >>= 16;
r += 16;
}
if (!(x & 0xff)) {
x >>= 8;
r += 8;
}
if (!(x & 0xf)) {
x >>= 4;
r += 4;
}
if (!(x & 3)) {
x >>= 2;
r += 2;
}
if (!(x & 1)) {
x >>= 1;
r += 1;
}
return r;
}
```
應用案例如 [linux/sched.h](https://github.com/torvalds/linux/blob/5bfc75d92efd494db37f5c4c173d3639d4772966/include/asm-generic/bitops/sched.h) 裡用 ffs 來判斷地址 b 後的 100bits 內有沒有 1 存在,並且依照不同的 long 長度做調整,與 $O(1)$ schedule 挑選任務的方式頗有相似。
```c
static inline int sched_find_first_bit(const unsigned long *b)
{
#if BITS_PER_LONG == 64
if (b[0])
return __ffs(b[0]);
return __ffs(b[1]) + 64;
#elif BITS_PER_LONG == 32
if (b[0])
return __ffs(b[0]);
if (b[1])
return __ffs(b[1]) + 32;
if (b[2])
return __ffs(b[2]) + 64;
return __ffs(b[3]) + 96;
#else
#error BITS_PER_LONG not defined
#endif
}
#endif /* _ASM_GENERIC_BITOPS_SCHED_H_ */
```
### 測驗 3
#### 答案
EXP4 = `*con = (*con)->next`
EXP5 = `fc->next`
#### 延伸 - 解釋上述程式碼運作原理,並探討這樣區分 foo 和 foo_consumer 的好處
```c
struct foo_consumer {
int (*handler)(struct foo_consumer *self, void *);
struct foo_consumer *next;
};
struct foo {
struct foo_consumer *consumers;
unsigned long flags;
};
#include <stdbool.h>
/*
* For foo @foo, delete the consumer @fc.
* Return true if the @fc is deleted sfccessfully
* or return false.
*/
static bool consumer_del(struct foo *foo, struct foo_consumer *fc)
{
struct foo_consumer **con;
bool ret = false;
for (con = &foo->consumers; *con; EXP4) {
if (*con == fc) {
*con = EXP5;
ret = true;
break;
}
}
return ret;
}
```
題目給的程式碼是要將 foo 裡特定的 foo_consumer 刪除,而看到 foo_consumer 的結構中有一個 next 的指標,可以確定這是一個 single linked-list 的結構,要找到特定節點的方式就是遍尋整個 linked-list。先看迴圈的條件, `con` 的型態是指標的指標,一開始先指向第一個 foo_consumer 的指標,結束條件是 `*con` 為 0 ,也可以說 `con` 指向 NULL ,遞迴則很明顯必須使 `con` 指向下一個 foo_consumer,EXP4 需填入 `*con = (*con)->next`
> `con = &((*con)->next)` 作用相同,但不符合最簡潔的原則
而如果找到要刪除的節點,就將 `con` 的內容,也就是指向 next 的指標,使其指向下一個 foo_consumer ,就完成刪除的動作了。當進行刪除之後,將結果紀錄為 true,若沒有刪除,表示沒有找到要刪除的節點,結果為 false。
接著討論區分成 foo 與 foo_consumer 的好處,首先我聯想到的是 hash table 也有類似的結構,在 hash table 中,會有一個存放 hlist_head 的陣列,指向一個特定 hash 值的開頭(或是 NULL),而真正存放 key 的 hlist_node,則是用 linked-list 的方式接在後面,這樣做的目的是因為 hlist_head(也就是 hash 值)的範圍要是固定的,而不會因為刪除 hlist_node 等因素就使長度改變。
另一個可能的好處是同一個 foo 底下的 foo_consmuer 在進行特定操作時,可能會需要用到一個固定的參數,如果每一個 foo_consumer 都要放同一個參數,就會浪費空間,反之放在 foo 底下就可以供所有 foo_consumer 使用時去呼叫。這是對 foo 底下 flags 的猜想,題目並沒有使用到這個變數。
---
### 測驗 4
#### 答案
EXP6 = `list_del(&t->list)`
EXP7 = `list_del(&t->list)`
EXP8 = `longjmp(sched, 1)`
EXP9 = `longjmp(sched, 1)`
#### 延伸 - 解釋上述程式碼運作原理,舉出上述 coroutine 實作的限制 (注意 stack)
```c
static void task_add(struct list_head *tasklist, jmp_buf env)
{
struct task *t = malloc(sizeof(*t));
memcpy(t->env, env, sizeof(jmp_buf));
INIT_LIST_HEAD(&t->list);
list_add_tail(&t->list, tasklist);
}
```
`task_add` 的目的很簡單,就是把一項待作的任務所記錄的 jmp_buf 放到 list 的最後端,待需要的時候,取出來並跳躍到要執行的地方。
```c
static void task_switch(struct list_head *tasklist)
{
jmp_buf env;
if (!list_empty(tasklist)) {
struct task *t = list_first_entry(tasklist, struct task, list);
EXP6;
memcpy(env, t->env, sizeof(jmp_buf));
free(t);
longjmp(env, 1);
}
}
```
`task_switch` 從 task list 中取出第一個 task (如果 list 不為空),並將其從 list 中刪除,用 `memcpy` 取得該 task 跳躍的目標,並跳躍過去執行。
```c
static void task_join(struct list_head *tasklist)
{
jmp_buf env;
while (!list_empty(tasklist)) {
struct task *t = list_first_entry(tasklist, struct task, list);
EXP7;
memcpy(env, t->env, sizeof(jmp_buf));
free(t);
longjmp(env, 1);
}
}
```
`task_join` 在做的事跟 `task_switch` 很像,不同的是 `task_join` 會一直執行到整個 task list 清空為止,也呼應 fork-join 中 join 的作用。
```c
void schedule(void)
{
static int i;
srand(0xCAFEBABE ^ (uintptr_t) &schedule); /* Thanks to ASLR */
setjmp(sched);
while (ntasks-- > 0) {
int n = rand() % 5;
tasks[i++](&n);
printf("Never reached\n");
}
task_join(&tasklist);
}
void task0(void *arg)
{
jmp_buf env;
static int n;
n = *(int *) arg;
printf("Task 0: n = %d\n", n);
if (setjmp(env) == 0) {
task_add(&tasklist, env);
EXP8;
}
for (int i = 0; i < n; i++) {
if (setjmp(env) == 0) {
task_add(&tasklist, env);
task_switch(&tasklist);
}
printf("Task 0: resume\n");
}
printf("Task 0: complete\n");
longjmp(sched, 1);
}
```
`task0` 與 `task1` 是完全相同的兩個函式,用來模擬兩個不同的 task 在行程排序上的狀況。
```c
int main(void)
{
void (*registered_task[])(void *) = {task0, task1};
tasks = registered_task;
ntasks = ARRAY_SIZE(registered_task);
schedule();
return 0;
}
```
---
### 測驗 5
#### 答案
DECL0 = `char __c[1]`
#### 延伸 - 解釋上述程式碼運作原理,並佐以〈[ACCESS_ONCE() and compiler bugs](https://lwn.net/Articles/624126/)〉及〈[你所不知道的 C 語言:編譯器和最佳化原理篇](https://hackmd.io/@sysprog/c-compiler-optimization)〉進行說明
```c
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#define __READ_ONCE_SIZE \
({ \
switch (size) { \
case 1: \
*(uint8_t *) res = *(volatile uint8_t *) p; \
break; \
case 2: \
*(uint16_t *) res = *(volatile uint16_t *) p; \
break; \
case 4: \
*(uint32_t *) res = *(volatile uint32_t *) p; \
break; \
case 8: \
*(uint64_t *) res = *(volatile uint64_t *) p; \
break; \
default: \
memcpy((void *) res, (const void *) p, size); \
} \
})
static inline void __read_once_size(const volatile void *p, void *res, int size)
{
__READ_ONCE_SIZE;
}
#define READ_ONCE(x) \
({ \
union { \
typeof(x) __val; \
DECL0; \
} __u; \
__read_once_size(&(x), __u.__c, sizeof(x)); \
__u.__val; \
})
```
題目敘述 READ_ONCE 取代 ACCESS_ONCE,可以針對不同長度的變數,將其轉換成對應的 volatile 變數,避免編譯器對該變數做最佳化。
因為之前不了解 union 的用法,因此先查了一下,union 裡存放的所有變數,其開頭的地址皆是一樣的,且一次只有一個結構中的變數能夠被使用,就是最近一次被賦值的變數,舊的則會被新賦的值覆蓋,如果這時候使用舊的變數則是未定義行為,好處是當結構中的變數彼此關聯不深的時候,用 union 可以節省空間,其所用空間是 union 裡最大的那個成員之大小。
回到題目上, `__val` 是我們最終要回傳的變數,而 `__c` (變數名從倒數第二行就可以看出來了,重點是型態)則是用來轉換成對應純量,並給予 volatile 的變數,過程很簡單,就是依照傳入值 `x` 的長度去選擇要用哪一種整數長度的型態,並把 `x` 地址裡的值(就是 `x`)放進去。而根據前面所述, union 的大小是根據最大的成員而定,因此 `__c` 就不能夠比 `__val` 還大,否則空間就浪費掉了,佔用空間最小的型態就是 char,同時我們為了方便,希望 `__c` 是一個指標型態,因此就使用了陣列的形式來寫, `char __c[1]` 可以讓 `__c` 的長度為 1 ,且是一個指標的型態。
> 為何用指標更為方便?
> C 語言傳遞參數時都是複製一份副本給函式,因此直接傳遞的值若是要更改,就必須透過返回值(return)來做更動,但是傳遞指標的話,根據指標指向的地址,我們可以直接修改裡面的內容,而不需透過返回值的方式,同時返回值也可用來做為發生錯誤時儲存錯誤資訊的地方。
再透過 `__read_once_size` 函式,根據 `x` 所佔的空間大小,去讀取記憶體中相對應長度的空間裡的值,最後用到 union 的特性將值轉回到 `__val` 身上
接著討論為何我們需要 `READ_ONCE` 這個巨集,首先我們從 [ACCESS_ONCE()](https://lwn.net/Articles/508991/) 這篇討論中可以知道 ACCESS_ONCE 明確的目標,主要就是賦予變數 volatile 的型態,使編譯器不會在編譯時期對該變數做最佳化,特別是在多執行續的執行環境中,我們需要確保該變數在被讀取的時候,是讀取到最新的值而非舊的值,特別是在確認 mutex lock 的這種狀況下尤其重要。ACCESS_ONCE 最一開始的實作為
```c
#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))
```
乍看之下沒有甚麼問題,就是將變數 `x` 加上 volatile 之後重新指派到 `x`,但問題就出在某一些版本下的 compiler ,在做這個動作的時候,如果遇到的不是純量型態(如 int, short, char, double 等),而是較複雜的型態(如自定義的 struct)時,volatile 便會無效,好巧不巧 mutex lock 就是一個 struct 實作,因此前面所提到的情況便不能用了。最直接的解決辦法就是 ── 不要用會出現問題的 compiler 版本,或是針對結構中純量變數單獨做 volatile 的型態宣告。
而 `READ_ONCE` 則是給出另一種解,強迫去使用純量型態,也就是上面提到的 `__read_once_size` 裡所做的事,如此一來就規避了非純量型態不能使用 volatile 的缺點。