contributed by < hsuedw
>
延伸第 3 週測驗題的測驗 7
,已知輸入必為大於 0
的數值 (即 x > 0),以下程式碼可計算 ,也就是 ceil 和 log2 的組合並轉型為整數:
請補完,使其行為符合預期。作答規範:
EXP1
應以最簡潔的形式撰寫,且符合作業一排版規範 (近似 Linux 核心程式碼排版風格)EXP1
為表示式,限制使用 ^
, &
, |
, <<
, >>
這幾個運算子,可能會出現常數EXP1
不該包含小括號 (即 (
和 )
)r
, shift
, x
EXP1
不可包含 ,
和 ;
這樣的分隔符號 (separator)EXP1
: r | shift | x >> 1
為了說明方便,將題目的程式碼改寫如下。程式長得跟題目有點不一樣,但效果是等價的。這個程式的運做原理與第 3 週測驗題的測驗 7
如出一轍,事實上就是 binary search。請參閱其說明。
讀完第 3 周測驗題第 7 題的說明後,請將注意力集中在第 22~26 行。因為本周的這一題就是要把這幾行濃縮成一行,並且取代第 28 行的變數 r
。
如果可以理解下面兩點,就可以理解為什麼 EXP1
要實作為 r | shift | x >> 1
。
EXP1
中的 shift
就是第 22 行的 shift
。EXP1
中的 x >> 1
就是第 26 行的 shift
。7
是計算 。而本題是計算 。所以 ceil_log2()
在回傳時要加 1
。x = 0
的狀況,並仍是 branchless複習〈你所不知道的 C 語言: bitwise 操作〉,改寫第 3 週測驗題的測驗 11 裡頭的 fls 函式 (fls 意謂 “find last set”),使其得以計算 Find first set:
請補完,使其行為符合預期。作答規範:
EXP2
和 EXP3
應以最簡潔的形式撰寫,且符合作業一排版規範 (近似 Linux 核心程式碼排版風格)EXP2
和 EXP3
限制使用 ^, &, |, <<=, >>=, +=, -= 這幾個運算子,可能會出現常數EXP2
和 EXP3
不該包含小括號 (即 (
和 )
)x
, o
, t
, shift
,也就是說,你應該寫 x ^ t
而非 t ^ x
EXP2
和 EXP3
不可包含 , 和 ; 這樣的分隔符號 (separator)EXP2
: x & t
EXP3
: o += shift
程式碼運作原理
為了說明方便,將程式碼補齊後,如下所示:
o
, t
, shift
初值如下
o
= 1
(o
就是 output,也就是 first set bit 的位置)t
= 0xffffffffffffffff
(t
的功能就是 bit mask)shift
= 64
t
的值變為 0x00000000ffffffff
, shift
的值變為 32
。
while
迴圈後,判斷 first set bit 在左半邊 (bit 63~32) 還是右半邊 (bit 31~0)。while
迴圈後,
x
的 bit 31~0 皆為 0。所以, first set bit 在 bit 63~32 之間。
x
左移 shift = 32
個 bits。o
累加 shift
的值,也就是 32
。shifh = 16
, t = 0x000000000000ffff
。
o
中。迴避 while, for, goto 等關鍵字以改寫出功能等價的實作
ffs(linux/blob/master/include/asm-generic/bitops/ffs.h)
實作在 linux/blob/master/include/asm-generic/bitops/ffs.h 中。
考慮以下改寫自 Linux 核心的程式碼:
請補完,使 consumer_del
行為符合註解規範。作答規範:
EXP4
和 EXP5
應以最簡潔的形式撰寫,且符合作業一排版規範 (近似 Linux 核心程式碼排版風格)EXP4
和 EXP5
都包含指標操作 (如 ->
)EXP4
: con = &(*con)->next
EXP5
: fc->next
或 (*con)->next
上述程式碼運作原理
為了說明方便,將程式碼補齊後,還原如下:
foo
指標,即 consumer_del()
的第一個參數,就是指向單向鏈結串列的第一個節點。fc
指標,即 consumer_del()
的第二個參數,就是指向單向鏈結串列要被移除的節點。con
變數是指標的指標。其作用就是被用來在單向鏈結串列中逐一尋訪個節點,以找尋所要被移除的節點。
for
迴圈的迭帶過程中,con
內存著節點中 next
指標的記憶體位址。con
內存著 fc
前一個節點的 next
的記憶體位址。所以只要把 con
內的值設定為 fc->next
就可以把 fc
所指向的節點自單向鏈結串列中移除。這就是第 25 行 (EXP5
) 的作用。探討這樣區分 foo 和 foo_consumer 的好處
這樣的實作的精神與 struct list_head
一樣。就是將鏈結串列抽象化。把鏈結串列的實作與節點資料結構的實作分離。當要尋訪鏈結串列中的各個節點時,就沿著 next
指標一路往下走。當找到目標節點時,可用 container_of
這樣的巨集找到目標節點的起始位址,進而存取目標節點的其他 member。
最明顯的好處就是所有對單向鏈結操作的程式碼 (如新增、刪除節點) 可被所有的單向鏈結串列共用。不需要為每一種資料結構重複實作類似的程式碼 (如新增、刪除節點)。
以下嘗試用 lab0 提及的 setjmp 和 longjmp,用以實作〈你所不知道的 C 語言: goto 和流程控制篇〉闡述的 coroutine,參考的程式執行輸出如下:
原始程式碼如下: (檔名 jmp.c
)
請補完,使行為符合註解規範。作答規範:
EXP6
, EXP7
, EXP8
, EXP9
應以最簡潔的形式撰寫,且符合作業一排版規範 (近似 Linux 核心程式碼排版風格)EXP6
和 EXP7
都包含 List API 的使用EXP8
和 EXP9
應有函式呼叫EXP6
: list_del(&t->list)
EXP7
: list_del(&t->list)
EXP8
: longjmp(sched, 1)
EXP9
: longjmp(sched, 1)
〈你所不知道的 C 語言:前置處理器應用篇〉已提過若干 Linux 核心的巨集,不難發現這些巨集經歷多次演變。Linux 核心一度有個巨集 ACCESS_ONCE
,其作用是確實地讀取所指定記憶體位址的內容值,且限這一次,其原始定義如下:
注意 volatile 關鍵字的使用
然而,如果不使用 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
來避免編譯器對該變數進行最佳化。
請補完程式碼,使其運作符合預期。書寫規範:
符合一致的程式碼風格,使用最精簡的方式撰寫
DECL0
為變數宣告,不該存在 ;
和 ,
分隔字元
DECL0
: unsigned char __c[1]
因為在 union
中,所有的 member 共用記憶體空間,起始位址相同。所以對其中一個 member 進行讀寫,等同於對其他 member 進行讀寫。
將 __c
宣告為只有一個 unsigned char
元素的陣列,有下列目的:
__val
在 union
中佔用相同的記憶體空間。且第一個元素的起始位址與 __val
相同。__c
就是 __val
的位址。對 __c
這個記憶體位址讀寫,就是對 __val
所佔的記憶體空間做讀寫。又因為 __read_once_size()
的第二個參數的型別是 void *
,也就是個指標,所以將 __c
宣告為陣列正好可以滿足這個需要。剛好可以把 __c
這個記憶體位址傳給 res
這個指標。__c
宣告為 unsigned char
的陣列? 由前面的描述可知我們只是想要對 __val
所佔的記憶體空間做讀寫。但題目的程式碼有沒有對 __val
做取址的動作。我們只是單純的利用 union
的特性與 __c
這個陣列名稱(也就是 __val
的記憶體位址)對 __val
做讀寫。所以,陣列 __c
只需要佔用最小空間。這就是為什麼要將 __c
宣告為 unsigned char
陣列,且只有一個元素的原因。事實上,我覺得也可以將 __c
宣告為其他型別的陣列,且有多個元素。只是比較佔用記憶體空間而已。2022 年 Linux 核心設計第 4 週隨堂測驗 (A)
2022 年 Linux 核心設計第 4 週隨堂測驗 (B)
2022 年 Linux 核心實作第 4 週隨堂測驗 ©