contributed by < steven1lung >
從判斷是否是大於某個數去右移 x
看得出來這是一個二分搜尋法,找出一個數在哪個區域中有最靠近 MSB 的 1
。找到後就將每次的位移數量 |
起來再加一(取 ceiling )就可以得到答案。
01101011 檢查有沒有大於 1111,有所以要右移 4 位
00000110 檢查有沒有大於 11,有所以要右移 2 位
00000001 檢查有沒有大於 1,沒有所以不動
答案就是剛剛的 4 | 2 | 1 >> 1 + 1 = 7
這個程式是要從最左邊找出第一個 1
的位置,並且回傳在第幾個位元。使用的演算法跟上面那題非常相似,也是用二分搜尋法的思路去找出第一個 1
的位置。x & t
就是判斷出 1
在不在右半邊,如果 x & t
回傳 0
就代表我們要找的 1
在左半邊,所以將 x
位移 shift
位,並且紀錄目前位移的位元數。如果回傳 0
就代表在右半邊,繼續找並且把搜尋的範圍減半。這樣經過六次迭代就可以找出第一個 1
所在的位置了。
可以看到使用的 con
變數是指標的指標,所以在迭代的時候要注意 con
變數的更新。這個程式很簡單就看出來是要刪除給定的 foo_consumer *fc
。
在 for
迴圈中每次迭代的更新都應該寫成:con
取值(這時候會是一個指向結構體的指標)的下一個結構體再取址。
TODO: 用巨集包裝更高階的操作,並參照 Moving the kernel to modern C
這篇討論是從一個提交的 patch開始。提交 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.
從 main()
開始讀,看到一開始宣告了一個函數指標陣列,陣列元素分別指向 task0()
與 task1()
。接著將在最上方宣告的函數指標的指標 **tasks
指向剛剛的函數指標陣列的開頭。再將 ntasks
設成任務的數量,使用 ARRAY_SIZE()
巨集來算出陣列元素的數量。最後呼叫 schedule()
函式讓排程器挑選任務執行。
schedule()
函式中,使用了 srand()
來達到隨機選擇任務的機制,而其中輸入的種子是將一組數字 0xCAFEBABE
去跟 schedule()
函式的位址進行 XOR
。而後面註解提到的 ASLR 全名是 Address Space Layout Randomization,ALSR 會將程式隨機載入記憶體中,防止惡意程式直接跳到特定位址使用函式。
接著 setjmp()
這邊是將現在的資訊儲存起來,像是:stack pointer、instruction pointer、registers 等等,這些資料會被儲存到一個 buffer jmp_buf
。jmp_buf
的定義可以在 setjmp.h 裡看到,jmp_buf
是一個大小為 23 的 long
形態陣列。
接著的 while
迴圈就是迭代每一個在 tasks 裡面的任務,隨機指派每個任務要執行的次數。因為 ntasks
只有在這個迴圈裡會被遞減,所以只有第一次初始化任務執行次數的時候會執行到此 while
迴圈,之後再呼叫 schedule()
就會因為 while
的判斷而跳過執行,直接執行下一行的 taskjoin(&tasklist)
。
taskjoin()
所做的事情是將 tasklist
中所有的 task 都執行完。當 tasklist
不是空的,就會將 tasklist
第一個節點拿出來執行並且移除。
迴圈最後的 longjmp()
就是跳躍到 env
所儲存的位址,搭配 setjmp()
使用就可以達到 nonlocal 的 goto。
剛剛有提到第一次執行 schedule()
會初始化各個任務的執行次數,而將任務要放幾個進入 tasklist
會在 task_add()
中做到。
task0
跟 task1
其實都在做一樣的事情,模仿排程器裡不同任務互相切換執行。舉 task0
做例子,輸入的參數就是這個任務要做幾次,也就是要加幾個任務進 tasklist
。
一開始有一個 if
判斷 setjmp(env)
回傳的值是不是 0
,setjmp
可以從官方文件中看到回傳值會依據 setjmp()
是否在一次成功的 longjmp()
後呼叫,如果不是就回傳 0
,先前有呼叫過 longjmp()
就會回傳 longjmp()
裡的 val
。
接著會使用 for
迴圈將先前設定的執行次數透過 task_add()
加入到 tasklist
並呼叫 task_switch()
將目前狀態處存到 tasklist
的尾端,並切換執行。
READ_ONCE
作用是確實讀取該記憶體位址的變數一次,並且不會受編譯器最佳化影響造成實際上運作跟程式碼邏輯不同。
因為要針對 1、2、4、8 或是其他位元組大小的資料進行操作,所以使用到 union
的特性,union
結構體的位址大小是由最大的成員決定的,並且所有成員的位址都是對齊的。所以我們要考慮到最小的 1 位元組情況,來達到 union
結構體的大小是以 typeof(x)
決定。
故 union
裡的第二個成員要寫成:char __c[1]
大小為 1 位元組的成員。在接下來的函式就可以使用 __c
指標直接對此 union
操作。
以 volatile
關鍵字宣告的變數會允許變數在執行緒之外被修改,通常會被使用在共享的資料結構。
以下面程式碼為例:
可以看到對共享資料 shared_data
進行存取之前,使用了 spin_lock()
確保鎖起來的時候不會有其他執行緒對共享變數進行存取,會被擋在外面。那這時候的 spin_lock()
也會被編譯器當成是 memory barrier,防止編譯器對這段存取做最佳化。
但是當共享變數被鎖起來時,這時候不會有其他執行緒對共享變數進行存取(都被 lock 擋在外面),使得裡面的共享變數並不是 volatile
。這樣對共享變數宣告 volatile
就會顯得不必要,甚至有可能拖慢速度。
對共享變數宣告 volatile
也會防止編譯器對其做最佳化,但在 spin_lock()
裡我們已經知道不會有其他人對共享變數進行存取,所以在 critical section 裡的最佳化反而被關掉,所以會拖慢速度。
READ_ONCE
WRITE_ONCE
的實作與演化從最早的 commit 去看,是將 READ_ONCE
跟 WRITE_ONCE
從 linux/compiler.h 移走,直接寫在一個新檔案 rwonce.h 裡。這樣是為了之後讓不同的架構去定義實作自己的 READ_ONCE
巨集。
這時的 READ_ONCE
寫法是先確定 (assert) 要讀取的變數 x
的native_word(x)
或是 sizeof(x)
大小等於一個 long long
。
在 linux/compiler_types.h 可以看到 native_word()
就只是判斷變數的大小是否為 char
、short
、int
、long
。
判斷完成後就是對變數進行一次讀取,會展開 __READ_ONCE_SCALAR(x)
這個巨集。巨集開頭的 __unqual_scalar_typeof()
是在 compiler_types.h 定義的巨集。
其中的 _Generic
類似 switch
用法,會根據編譯時期的形態來選擇展開的值。而 __unqual_scalar_typeof(x) __x
做的事情就是新宣告一個變數 __x
且變數形態是跟 x
一樣。
接著會將 (*(const volatile __unqual_scalar_typeof(x) *)&(x))
所讀取到的值賦給 __x
,這邊做的事情就是將 &x
轉換成指標,再對指標取值。