目的: 檢驗學員對 bitops 及記憶體配置的認知
作答表單: 測驗 1
作答表單: 測驗 2
作答表單: 測驗 3
1
以下程式碼建構在 C 語言基本型態之上,打造更高精密度 (precision) 的整數運算,定義如下:
其記憶體管理相關的操作如下:
接著準備基本操作:
基本的乘法運算:
除法和餘數運算:
最大公因數運算:
測試程式碼:
假設記憶體配置都會成功,已知不會遇到任何 abort
或 assert
錯誤,依據〈最大公因數特性和實作考量〉及〈Linux 核心原始程式碼的整數除法〉,補完程式碼並滿足以下規範:
;
:
或 ?
字元ceil_div
的第一個參數為 n
,第二個參數為 d
,若要表示二者的加法操作,書寫為 n+d
,而非 d+n
U
, UL
或 ULL
延伸問題:
2
SIMD within a register (SWAR) 是軟體最佳化技巧之一,以下展示 SWAR 運用於 64 位元微處理器架構,原本判斷 2 個 32 位元寬度的整數是否都是奇數 (odd),可能會這樣撰寫:
但我們可先組合 (compound) 2 個 32 位元寬度的整數為 1 個 64 位元整數,再運用特製的 bitmask,從而減少運算量:
測試程式:
延伸閱讀: SIMD and SWAR Techniques
在 Linux 核心原始程式碼中,lib/string.c 具備 memchr 的實作:
這段程式會逐字元去檢查輸入的字串,直至與 c
相符。C99 規格書 6.5.2.4 提到:
The result of the postfix ++ operator is the value of the operand. After the result is obtained, the value of the operand is incremented. (That is, the value 1 of the appropriate type is added to it.) See the discussions of additive operators and compound assignment for information on constraints, types, and conversions and the effects of operations on pointers.
測試程式:
預期執行結果:
利用上述 SIMD within a register (SWAR) 的技巧,我們可改寫為以下 memchr_opt
函式:
留意巨集 DETECT_NULL
: 首先會先將輸入字串跟 mask
做 XOR 運算,若輸入字串中有任何字元與要搜尋的字元相符,進行 XOR 後就會出現 00000000
(全為 0 的位元組)。接著可看到這個巨集:
這段巨集會依據 long
型態是否為 32 位元或 64 位元,做出相對應的位元運算。
(X) - (0x01010101)
:
若以每個位元組(byte)來看,當
X
的任一個位元組為0
,那麼和0x01010101
相減後,對應的位元組就會變成0xFF
。
和上一步結果再跟 ~X
做 AND 運算:
當
X
的某個位元組為0
,於是~X
中對應的位元組也會是0xFF
。
舉例來說,若 A
和B
都是 32 位元的 long,其中 A = 0x00FF00FF
,B = 0x12345678
。 特意把 A
的第一與第三個位元組設為 0
:
A - 0x01010101 = 0xFFFEFFFE
0xFFFEFFFE & ~A = 0xFF00FF00
接著看 B
(它沒有任何位元組為 0):
B - 0x01010101 = 0x11335577
0x11335577 & ~B = 0x01030107
最後這些結果再和 0x80808080
做 bitwise AND 運算。可把 1 個位元組分成左右各 4 個 bits 看,若原先確實有包含 0 byte,那它在最高的一組 4 個 bits 會有 1
。示意圖如下:
程式先透過 XOR 檢查目標字元是否存在(其中一個位元組為 0),如果存在,它就會觸發對應的條件判斷並立即 break
(表示目標字元已找到)。
請補完程式碼,使上述 memchr_opt
的實作符合 memchr 行為,作答規範:
延伸問題:
memchr_opt
,設計實驗來觀察隨著字串長度和特定 pattern 變化的效能影響下一步:貢獻程式碼到 Linux 核心
Phoronix 報導
代表行程接收到 signal 後會產生什麼行為,並且每個行程有各自的 signal disposition ,基本的預設行為有下列幾種
行程可以透過 sigaction(2) 改變 signal disposition ,有以下幾種行為可以選擇
如果利用 signal handler 來處理該 signal ,預設是將該 signal handler 指向的函式的函式資訊框 (function frame,簡稱 fp) 加在原本行程的堆疊當中,也可用 signalstack(2) 使該函式資訊框使用其他的堆疊。
Linux 核心在每次轉移回使用者模式時,都會檢查是否有正在等待的 signal 並檢查是否有對應的 signal handler,如果有的話會進行以下動作
sigprocmask()
來建立在 act->sa_mask
當中的 signal 都會被加到該執行緒的 signal mask 當中。sigreturn()
,恢復執行緒的狀態回到呼叫 signal handler 之前,然後 Linux 核心把控制權轉回給使用者模式,繼續執行 signal handler 被呼叫之前的指令。如果 signal handler 因為 siglongjmp()
或使用 execve()
啟動新的程式而沒有回傳,則不會回傳 signal trampoline 也不會呼叫 sigreturn()
,此時這是開發者的責任要去恢復 signal mask 的狀態。
以 Linux 核心的角度來看,它不知道 signal handler 跟其他使用者層級的程式碼之間的差別,自然也不知道目前執行的是誰,所有 signal handler 需要的相關資訊都儲存在使用者層級的暫存器和堆疊中,所以巢狀呼叫的 signal handler 數量上限是被使用者層級的堆疊大小所限制。
sigaction() 系統呼叫可以改變行程接收到特定 signal 時的行為。
signum
是用來指定該 signal 的數字(不能指定 SIGKILL
和 SIGSTOP
)。
struct sigaction
定義如下
特別注意到 sa_restorer
並非是給使用者使用的,詳見 sigreturn(2) 。
如上節所說的, sa_handler
是行程接收到 signum
這個 signal 時會採取的行為
SIG_DFL
代表採取預設行為SIG_IGN
代表忽略該 signalsa_flags
中設定 SA_SIGINFO
,則 sa_sigaction
就指向處理 signum
的 signal-hadling functionsignal handler 的函式定義如下
以下解說參數
sig
: 觸發此 handler function 的 signal number (和 signum
相同)info
: 指向 siginfo_t
的指標,該結構體包含 signal 的詳細資訊。詳見 sigaction(2) 。ucontext
: a pointer to a ucontext_t
structure, cast to void *
。此指標指向的結構體包含了 Linux 核心 存在 userspace stack 中的 signal context information 。通常此參數不會被使用到,詳見 getcontext(3) 。sa_mask
則代表在 signal handler 執行期間會被阻塞著的 signals ,並且觸發 signal handler 的 signal 本身也會被阻塞。
sa_flags
代表改變該 signal 行為的標籤集合,有數種標籤詳見 sigaction(2)
在 泛 Sytem V 環境當中,我們可以透過以下幾種結構體和函式來控制行程當中不同執行緒的 user-level context switch 。
mcontext_t
: machine-dependent 且不開放給使用者操作。ucontext_t
: 定義如下uc_link
: 指向在目前 context 被終止後會接續執行的 context 。uc_sigmask
: 被此 context 阻塞的 signals 集合。uc_stack
: 此 context 使用的堆疊空間。uc_mcontext
: 被儲存的 context 的 machine-specific 表示法,包含 calling thread's machine registers 。getcontext()
會將 ucp
指向的結構體初始化並指向目前執行的 context 。
setcontext()
會繼續執行 ucp
指向的 user context ,此 context 有三種方式獲取
getcontext()
makecontext()
最早實作 user-level context switch 此機制的方式是透過 setjmp()
/longjmp()
,不過這樣的方式沒有定義如何處理 signal context ,之後 sigsetjmp()
/siglongjmp()
讓我們可以處理 signal context 。當 signal 觸發某個 signal handler 時,目前的 user context 會被暫存而 Linux 核心 會為 signal handler 建立一個新的 user context 。我們不該使用 longjmp()
函式來離開 signal handler ,這樣的行為是未定義的,取而代之應該使用 siglongjmp()
或 setcontext()
。
注意到除非我們自己設置額外儲存裝置進行記錄,不然我們無從得知 getcontext()
的回傳行為是從首次呼叫 getcontext()
還是透過 setcontext()
來回傳。因為 register 會被重新恢復,所以利用 register variable 來記錄是無效的。
3
以下程式碼實作並行程式設計: 排程器原理提及的使用者層級的執行緒 (user-level thread; ULT,也可稱為 green thread),原理是透過 SIGRTMIN signal 來模擬作業系統的 timer 中斷,設定的時間間隔為 10 ms,於是每 10 ms 觸發模擬的 timer 中斷,帶動 ULT 排程器,實作 round-robin 排程策略。留意到 trampoline 的使用:
Trampolines (sometimes referred to as indirect jump vectors) are memory locations holding addresses pointing to interrupt service routines, I/O routines, etc. Execution jumps into the trampoline and then immediately jumps out, or bounces, hence the term trampoline.
留意 getcontext 及 makecontext 的使用。
採 round-robin 排程。
標頭檔: (檔名: coro.h
)
使用範例: (檔名 prime.c
)
執行輸出:
使用範例: (檔名 yield.c
)
參考執行輸出:
coro.c
原始程式碼 (部分遮蔽)
作答規範:
ENV_
開頭coro_
開頭參考資訊: