---
tags: Linux Kernel
---
# 2022q1 Homework4 (quiz4)
contributed by < [steven1lung](https://github.com/steven1lung) >
## 測驗一
### 瞭解程式碼
```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 (r | shift | x >> 1) + 1;
}
```
從判斷是否是大於某個數去右移 `x` 看得出來這是一個二分搜尋法,找出一個數在哪個區域中有最靠近 MSB 的 `1`。找到後就將每次的位移數量 `|` 起來再加一(取 ceiling )就可以得到答案。
> 01101011 檢查有沒有大於 1111,有所以要右移 4 位
> 00000110 檢查有沒有大於 11,有所以要右移 2 位
> 00000001 檢查有沒有大於 1,沒有所以不動
> 答案就是剛剛的 4 | 2 | 1 >> 1 + 1 = 7
> $\lceil log_2(01101011)_2 \rceil =\lceil 6.74 \rceil = 7$
### 在 Linux 核心原始程式碼找出 ceil 和 log2 的組合
## 測驗二
### 瞭解程式碼
```c
#define BITS_PER_BYTE 8
#define BITS_PER_LONG (sizeof(unsigned long) * BITS_PER_BYTE)
#include <stddef.h>
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;
shift >>= 1;
t >>= shift;
while (shift) {
if ((x & t) == 0) {
x >>= shift;
o += shift;
}
shift >>= 1;
t >>= shift;
}
return o;
}
```
這個程式是要從最左邊找出第一個 `1` 的位置,並且回傳在第幾個位元。使用的演算法跟上面那題非常相似,也是用二分搜尋法的思路去找出第一個 `1` 的位置。`x & t` 就是判斷出 `1` 在不在右半邊,如果 `x & t` 回傳 `0` 就代表我們要找的 `1` 在左半邊,所以將 `x` 位移 `shift` 位,並且紀錄目前位移的位元數。如果回傳 `0` 就代表在右半邊,繼續找並且把搜尋的範圍減半。這樣經過六次迭代就可以找出第一個 `1` 所在的位置了。
## 測驗三
### 瞭解程式碼
```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; con = &(*con)->next) {
if (*con == fc) {
*con = fc->next;
ret = true;
break;
}
}
return ret;
}
```
可以看到使用的 `con` 變數是指標的指標,所以在迭代的時候要注意 `con` 變數的更新。這個程式很簡單就看出來是要刪除給定的 `foo_consumer *fc`。
在 `for` 迴圈中每次迭代的更新都應該寫成:`con` 取值(這時候會是一個指向結構體的指標)的下一個結構體再取址。
:::warning
TODO: 用巨集包裝更高階的操作,並參照 [Moving the kernel to modern C](https://lwn.net/Articles/885941/)
:notes: jserv
:::
### 用巨集包裝更高階操作
### [Moving the kernel to modern C](https://lwn.net/Articles/885941/)
這篇討論是從一個[提交的 patch](https://lwn.net/ml/linux-kernel/20220217184829.1991035-1-jakobkoschel@gmail.com/)開始。提交 patch 的作者提到 `list_for_each_entry()` 會在 `pos` 達到 `HEAD` 後結束。但是通常 `HEAD` 不是迭代的結構體,通常會自己獨立在外或是被宣告在其他 type 的結構體裡。所以這時候對迭代結束後的 `HEAD` 使用 `container_of` 來拿 `pos` 的值會是 invalid 而且或造成 type confusion。
提交的 patch 會確保 `pos` 不會指向不合法的 `HEAD`,使用 branchless 邏輯來做到將 `pos` 達到結束迴圈的條件時,將 `pos` 指向 NULL。確保迴圈執行完,不會對 `pos` 進行使用。
但是 Linus Torvalds 不太喜歡這份 patch,雖然在 patch 作者後來多做解釋後,torvalds 也同意「這是一個一般的 bug」。
> Linus Torvalds didn't much like the patch and didn't see how it related to speculative-execution vulnerabilities. After Koschel explained the situation further, though, Torvalds agreed that "this is just a regular bug, plain and simple" and said it should be fixed independently of the larger series.
Torvalds 後來找出問題真正的所在:傳進去的 `pos` 迭代指標必須在迴圈之外進行宣告。
> The whole reason this kind of non-speculative bug can happen is that we historically didn't have C99-style "declare variables in loops". So list_for_each_entry() - and all the other ones - fundamentally always leaks the last HEAD entry out of the loop, simply because we couldn't declare the iterator variable in the loop itself.
最後他也提到或許是時候向前邁進到 [C11](https://en.wikipedia.org/wiki/C11_(C_standard_revision)) 甚至是 [C17](https://en.wikipedia.org/wiki/C17_(C_standard_revision))。
---
## 測驗四
### 瞭解程式碼
```c
#include <setjmp.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "list.h"
struct task {
jmp_buf env;
struct list_head list;
};
static LIST_HEAD(tasklist);
static void (**tasks)(void *);
static int ntasks;
static jmp_buf sched;
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
int main(void)
{
void (*registered_task[])(void *) = {task0, task1};
tasks = registered_task;
ntasks = ARRAY_SIZE(registered_task);
schedule();
return 0;
}
```
從 `main()` 開始讀,看到一開始宣告了一個函數指標陣列,陣列元素分別指向 `task0()` 與 `task1()`。接著將在最上方宣告的函數指標的指標 `**tasks` 指向剛剛的函數指標陣列的開頭。再將 `ntasks` 設成任務的數量,使用 `ARRAY_SIZE()` 巨集來算出陣列元素的數量。最後呼叫 `schedule()` 函式讓排程器挑選任務執行。
```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);
}
```
`schedule()` 函式中,使用了 `srand()` 來達到隨機選擇任務的機制,而其中輸入的種子是將一組數字 `0xCAFEBABE` 去跟 `schedule()` 函式的位址進行 `XOR`。而後面註解提到的 ASLR 全名是 Address Space Layout Randomization,ALSR 會將程式隨機載入記憶體中,防止惡意程式直接跳到特定位址使用函式。
> [Address space layout randomization](https://en.wikipedia.org/wiki/Address_space_layout_randomization)
接著 `setjmp()` 這邊是將現在的資訊儲存起來,像是:stack pointer、instruction pointer、registers 等等,這些資料會被儲存到一個 buffer `jmp_buf`。`jmp_buf` 的定義可以在 [setjmp.h](https://github.com/torvalds/linux/blob/master/arch/powerpc/include/asm/setjmp.h) 裡看到,`jmp_buf` 是一個大小為 23 的 `long` 形態陣列。
```c
#define JMP_BUF_LEN 23
typedef long jmp_buf[JMP_BUF_LEN];
```
接著的 `while` 迴圈就是迭代每一個在 tasks 裡面的任務,隨機指派每個任務要執行的次數。因為 `ntasks` 只有在這個迴圈裡會被遞減,所以只有第一次初始化任務執行次數的時候會執行到此 `while` 迴圈,之後再呼叫 `schedule()` 就會因為 `while` 的判斷而跳過執行,直接執行下一行的 `taskjoin(&tasklist)`。
```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);
list_del(&t->list);
memcpy(env, t->env, sizeof(jmp_buf));
free(t);
longjmp(env, 1);
}
}
```
`taskjoin()` 所做的事情是將 `tasklist` 中所有的 task 都執行完。當 `tasklist` 不是空的,就會將 `tasklist` 第一個節點拿出來執行並且移除。
迴圈最後的 `longjmp()` 就是跳躍到 `env` 所儲存的位址,搭配 `setjmp()` 使用就可以達到 nonlocal 的 goto。
```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);
}
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);
list_del(&t->list);
memcpy(env, t->env, sizeof(jmp_buf));
free(t);
longjmp(env, 1);
}
}
```
剛剛有提到第一次執行 `schedule()` 會初始化各個任務的執行次數,而將任務要放幾個進入 `tasklist` 會在 `task_add()` 中做到。
```c
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);
longjmp(sched, 1);
}
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);
}
void task1(void *arg)
{
jmp_buf env;
static int n;
n = *(int *) arg;
printf("Task 1: n = %d\n", n);
if (setjmp(env) == 0) {
task_add(&tasklist, env);
longjmp(sched, 1);
}
for (int i = 0; i < n; i++) {
if (setjmp(env) == 0) {
task_add(&tasklist, env);
task_switch(&tasklist);
}
printf("Task 1: resume\n");
}
printf("Task 1: complete\n");
longjmp(sched, 1);
}
```
`task0` 跟 `task1` 其實都在做一樣的事情,模仿排程器裡不同任務互相切換執行。舉 `task0` 做例子,輸入的參數就是這個任務要做幾次,也就是要加幾個任務進 `tasklist`。
一開始有一個 `if` 判斷 `setjmp(env)` 回傳的值是不是 `0`,`setjmp` 可以從[官方文件](https://man7.org/linux/man-pages/man3/setjmp.3.html)中看到回傳值會依據 `setjmp()` 是否在一次成功的 `longjmp()` 後呼叫,如果不是就回傳 `0`,先前有呼叫過 `longjmp()` 就會回傳 `longjmp()` 裡的 `val`。
接著會使用 `for` 迴圈將先前設定的執行次數透過 `task_add()` 加入到 `tasklist` 並呼叫 `task_switch()` 將目前狀態處存到 `tasklist` 的尾端,並切換執行。
---
## 測驗五
### 瞭解程式碼
```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; \
char __c[1]; \
} __u; \
__read_once_size(&(x), __u.__c, sizeof(x)); \
__u.__val; \
})
```
`READ_ONCE` 作用是確實讀取該記憶體位址的變數一次,並且不會受編譯器最佳化影響造成實際上運作跟程式碼邏輯不同。
因為要針對 1、2、4、8 或是其他位元組大小的資料進行操作,所以使用到 `union` 的特性,`union` 結構體的位址大小是由最大的成員決定的,並且所有成員的位址都是對齊的。所以我們要考慮到最小的 1 位元組情況,來達到 `union` 結構體的大小是以 `typeof(x)` 決定。
故 `union` 裡的第二個成員要寫成:`char __c[1]` 大小為 1 位元組的成員。在接下來的函式就可以使用 `__c` 指標直接對此 `union` 操作。
### Volatile is unnecessary in Linux Kernel
以 `volatile` 關鍵字宣告的變數會允許變數在執行緒之外被修改,通常會被使用在共享的資料結構。
以下面程式碼為例:
```c
spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);
```
可以看到對共享資料 `shared_data` 進行存取之前,使用了 `spin_lock()` 確保鎖起來的時候不會有其他執行緒對共享變數進行存取,會被擋在外面。那這時候的 `spin_lock()` 也會被編譯器當成是 memory barrier,防止編譯器對這段存取做最佳化。
但是當共享變數被鎖起來時,這時候不會有其他執行緒對共享變數進行存取(都被 lock 擋在外面),使得裡面的共享變數並不是 `volatile`。這樣對共享變數宣告 `volatile` 就會顯得不必要,甚至有可能拖慢速度。
對共享變數宣告 `volatile` 也會防止編譯器對其做最佳化,但在 `spin_lock()` 裡我們已經知道不會有其他人對共享變數進行存取,所以在 critical section 裡的最佳化反而被關掉,所以會拖慢速度。
### [rwonce.h](https://github.com/torvalds/linux/blob/master/include/asm-generic/rwonce.h) 中 `READ_ONCE` `WRITE_ONCE` 的實作與演化
從[最早的 commit](https://github.com/torvalds/linux/commit/e506ea451254ab17e0bf918ca36232fec2a9b10c) 去看,是將 `READ_ONCE` 跟 `WRITE_ONCE` 從 linux/compiler.h 移走,直接寫在一個新檔案 rwonce.h 裡。這樣是為了之後讓不同的架構去定義實作自己的 `READ_ONCE` 巨集。
```c
/*
* Yes, this permits 64-bit accesses on 32-bit architectures. These will
* actually be atomic in some cases (namely Armv7 + LPAE), but for others we
* rely on the access being split into 2x32-bit accesses for a 32-bit quantity
* (e.g. a virtual address) and a strong prevailing wind.
*/
#define compiletime_assert_rwonce_type(t) \
compiletime_assert(__native_word(t) || sizeof(t) == sizeof(long long), \
"Unsupported access size for {READ,WRITE}_ONCE().")
/*
* Use __READ_ONCE() instead of READ_ONCE() if you do not require any
* atomicity or dependency ordering guarantees. Note that this may result
* in tears!
*/
#define __READ_ONCE(x) (*(const volatile __unqual_scalar_typeof(x) *)&(x))
#define __READ_ONCE_SCALAR(x) \
({ \
__unqual_scalar_typeof(x) __x = __READ_ONCE(x); \
smp_read_barrier_depends(); \
(typeof(x))__x; \
})
#define READ_ONCE(x) \
({ \
compiletime_assert_rwonce_type(x); \
__READ_ONCE_SCALAR(x); \
})
```
這時的 `READ_ONCE` 寫法是先確定 (assert) 要讀取的變數 `x` 的`native_word(x)` 或是 `sizeof(x)` 大小等於一個 `long long`。
在 [linux/compiler_types.h](https://github.com/torvalds/linux/blob/master/include/linux/compiler_types.h) 可以看到 `native_word()` 就只是判斷變數的大小是否為 `char`、`short`、`int`、`long`。
```c
#define __native_word(t) \
(sizeof(t) == sizeof(char) || sizeof(t) == sizeof(short) || \
sizeof(t) == sizeof(int) || sizeof(t) == sizeof(long))
```
判斷完成後就是對變數進行一次讀取,會展開 `__READ_ONCE_SCALAR(x)` 這個巨集。巨集開頭的 `__unqual_scalar_typeof()` 是在 compiler_types.h 定義的巨集。
```c
/*
* __unqual_scalar_typeof(x) - Declare an unqualified scalar type, leaving
* non-scalar types unchanged.
*/
/*
* Prefer C11 _Generic for better compile-times and simpler code. Note: 'char'
* is not type-compatible with 'signed char', and we define a separate case.
*/
#define __scalar_type_to_expr_cases(type) \
unsigned type: (unsigned type)0, \
signed type: (signed type)0
#define __unqual_scalar_typeof(x) typeof( \
_Generic((x), \
char: (char)0, \
__scalar_type_to_expr_cases(char), \
__scalar_type_to_expr_cases(short), \
__scalar_type_to_expr_cases(int), \
__scalar_type_to_expr_cases(long), \
__scalar_type_to_expr_cases(long long), \
default: (x)))
```
其中的 `_Generic` 類似 `switch` 用法,會根據編譯時期的形態來選擇展開的值。而 `__unqual_scalar_typeof(x) __x` 做的事情就是新宣告一個變數 `__x` 且變數形態是跟 `x` 一樣。
接著會將 `(*(const volatile __unqual_scalar_typeof(x) *)&(x))` 所讀取到的值賦給 `__x`,這邊做的事情就是將 `&x` 轉換成指標,再對指標取值。