linux2022
延伸第 3 週測驗題的測驗 7
,已知輸入必為大於 0 的數值 (即 x > 0),以下程式碼可計算 ,也就是 ceil 和 log2 的組合並轉型為整數:
整個過程就是對半切看前半部是否有值,也就是 binary search,當剩下最後兩個 bits 時,參考 jim12312321 的化簡前步驟
其中第 2, 3 行可化簡成
因此,EXP1 = r | shift | x >> 1
x--
為何要一開始要 x--
? 假設我們先不考慮 x--
考慮 x--
後
如此一來在最後,取 與取整數步驟的答案就會是對的。另外,當 x = 0
時,x - 1 = 0xFFFFFFFF
,因為 0xFFFFFFFF + 1 = x = 0
,因此這版本的 ceil_log2(0) = 32
複習〈你所不知道的 C 語言: bitwise 操作〉,改寫第 3 週測驗題的測驗 11
裡頭的 fls 函式 (fls 意謂 “find last set”),使其得以計算 Find first set:
假設 sizeof(unsigned long) = 8
,第 15, 16 行得到 shift = 32
, t = 0xFFFFFFFF
,可以看出 t
是 bitmask,因此 EXP2 = x & t
,EXP3 = o += shift
,也就是 binary search 作法
linux/include/asm-generic/bitops/ffs.h
考慮以下改寫自 Linux 核心的程式碼:
EXP4 = con = &(*con)->next
EXP5 = fc->next
可以參考 yaohwang99 的圖,很漂亮
以下嘗試用 lab0 提及的 setjmp 和 longjmp,用以實作〈你所不知道的 C 語言: goto 和流程控制篇〉闡述的 coroutine,參考的程式執行輸出如下:
原始程式碼如下: (檔名 jmp.c
)
EXP6 = list_del(&t->list);
EXP7 = list_del(&t->list);
EXP8 = longjmp(sched, 1)
EXP9 = longjmp(sched, 1)
可參考 C 語言 setjmp 與 longjmp 函數用法教學
首先了解 main()
中的部分程式碼,並理解 function designator
void (*registered_task[])(void *) = {task0, task1}
registered_task
是 an array of function designatorA function designator is an expression that has function type. Except when it is the operand of the sizeof operator or the unary & operator, a function designator with type ‘‘function returning type’’ is converted to an expression that has type ‘‘pointer to function returning type’’. (C99 6.3.2.4)
The unary * operator denotes indirection. If the operand points to a function, the result is a function designator. (C99 6.5.3.2.4)
*
結果都一樣,就跟 一樣不管怎麼對 微分都是 static void (**tasks)(void *) = registered_task
tasks
是 a pointer to pointer to designatorExcept when it is the operand of the sizeof operator or the unary & operator, or is a string literal used to initialize an array, an expression that has type ‘‘array of type’’ is converted to an expression with type ‘‘pointer to type’’ that points to the initial element of the array object and is not an lvalue. (C99 6.3.2.1)
在 assign 完 task0
與 task1
給 registered_task
後進行 schedule()
srand(0xCAFEBABE ^ (uintptr_t) &schedule)
schedule
地址皆不同,因此可用於亂數種設定setjmp(sched)
jmp_buf sched
儲存程式跳躍時所需之資訊
The setjmp() function saves various information about the calling environment (typically, the stack pointer, the instruction pointer, possibly the values of other registers and the signal mask) in the buffer env for later use by longjmp()
while (ntasks-- > 0)
int n = rand() % 5; tasks[i++](&n);
執行 tasks
內的 task0, task1
,迴圈一開始 ntasks = 2
,執行 task0, task1
後 ntasks = 0
,並在之後的 longjmp(sched)
回到 setjmp(sched)
地方,接著執行 task_join
setjmp(env) == 0
setjmp
函數時其傳回值為 0,若是透過 longjmp
跳回這裡時,其傳回值就會是呼叫 longjmp
時所指定的值 (task_join
的 longjmp(env, 1)
會回傳 1)env
透過 task_add
加入到 list 中longjmp(sched, 1)
跳到 schedule(void)
的 setjmp(sched)
task_join
中透過 longjmp(env, 1)
跳到 task0 中的第一個 setjmp(env)
,因為回傳值為 1,因此開始執行 for loopsetjmp(env)
,表示之後的 longjmp(env, 1)
會跳來這task_join
有使用到 list_del
,在這裡就要再次 task_add
,並執行 task_switch
,作用與 task_join
一樣,將此 task
從 list 中移除,接著 longjmp
list_first_entry
longjmp
到對應的 envlongjmp(env, 1)
〈你所不知道的 C 語言:前置處理器應用篇〉已提過若干 Linux 核心的巨集,不難發現這些巨集經歷多次演變。Linux 核心一度有個巨集 ACCESS_ONCE,其作用是確實地讀取所指定記憶體位址的內容值,且限這一次,其原始定義如下:
注意 volatile 關鍵字的使用
ACCESS_ONCE
巨集的使用情境,可參照 Linux v4.0 的kernel/locking/mutex.c
:
然而,如果不使用 ACCESS_ONCE
巨集,程式碼如下:
由於,編譯器偵測到第 8 行的 owner = lock->owner
在迴圈中沒有被更改,所以其最佳化機制可能將第 8 行的程式碼搬出 while 迴圈之外,如此不用每次都實際地讀取 lock->owner
,其程式碼變成:
但問題來了,lock->owner
有可能被其它核心執行緒修改,從而造成資料不一致。因此使用 ACCESS_ONCE
巨集可以防止編譯器做相關最佳化工作,並確保每次都能到實體記憶體位址讀取。其做法便是將某參數暫時性地轉換成具備 volatile
的型態。如此,存取該參數在非引入 ACESS_ONCE
巨集之處 (不具 volatile
特性),仍可享用編譯器最佳化的好處。
在 ACCESS_ONCE() and compiler bugs 則提及 Linux 核心捨棄上述 ACCESS_ONCE
巨集,改為 READ_ONCE
巨集。在 Linux 核心原始程式碼曾存在以下註解:
ACCESS_ONCE will only work on scalar types. For union types, ACCESS_ONCE on a union member will work as long as the size of the member matches the size of the union and the size is smaller than word size.
The major use cases of ACCESS_ONCE used to be
- Mediating communication between process-level code and irq/NMI handlers, all running on the same CPU, and
- Ensuring that the compiler does not fold, spindle, or otherwise mutilate accesses that either do not require ordering or that interact with an explicit memory barrier or atomic instruction that provides the required ordering.
以下是可能的實作:
READ_ONCE
巨集會判斷變數的寬度,針對寬度為 1, 2, 4, 8 位元組的變數,將其轉換為對應的純量 (scalar) 型態並增加 volatile
關鍵字,而對於其他資料寬度的類型,改呼叫 memcpy
來避免編譯器對該變數進行最佳化。
在 ACCESS_ONCE() and compiler bugs 提到 Christian 說 GCC 4.6 與 4.7 版本當變數不是 scalar type 會捨棄 volatile
修飾字
C99 §6.2.5.21 定義了 Arithmetic types 與 pointer types 是 scalar types,而 Array 與 structure types 是 aggregate types.
Note that aggregate type does not include union type because an object with union type can only contain one member at a time
觀察 __read_once_size(&(x), __u.__c, sizeof(x))
,sizeof(x)
的值會是 1, 2, 4, 8, 其他 (>8),透過 *(volatile uint8_t *) p
強制將 p
轉型成 scalar type,避免編譯器出錯
Linus 說到通常不 workaround 編譯器出錯,我們要盡量使程式碼less fragile
__u.__c
與 typeof(x) __val
共用一塊記憶體空間,因為 sizeof(x)
的值會是 1, 2, 4, 8, 其他 (>8),可知最小值是 1,則 DECL0 = uint8_t __c[1]
READ_ONCE
/ WRITE_ONCE
巨集的實作和其演化並行程式設計: Atomics 操作 提到 在 Linux 核心使用 volatile
的場景,絕大部分是誤用,搭配 Why the "volatile" type class should not be used 可以整理如下:
跟著 並行程式設計: Atomics 操作 做實驗,實作 Dekker’s Algorithm
將 flag1
、flag2
、counter
定義為 volatile
的 Global 變數
當 dekker1
要執行時就舉旗,並將 turn
交給 dekker2
,要做的事情就是 counter++
expected_sum = 2 * loop_cnt
主要是我們的預期總和與迴圈次數設定關係
實驗結果如下
觀察以下的組合語言做了什麼事
可以看見 volatile
只保證將變數讀寫至主記憶體,而 CPU 在執行過程中重排,使得該 load 操作讀到「舊值」,從而導致混亂,若要輸出正確,改用 __atomic_thread_fence(__ATOMIC_SEQ_CST)
即可,是 memory barrier 的作法,__ATOMIC_SEQ_CST
要求 thread 間 sequentially consistent:不相關的變數也必須滿足 happens-before
觀察使用 __atomic_thread_fence(__ATOMIC_SEQ_CST)
對應的組合語言輸出