contributed by < Risheng1128
>
1
延伸問題:
memchr_opt
,設計實驗來觀察隨著字串長度和特定 pattern 變化的效能影響以下為分析的完整程式碼
memchr_opt
接著將程式碼拆開來一一解釋
根據巨集函式 UNALIGNED
可以發現其目的是判斷資料的地址是否對齊 8
位元組,因此上面的迴圈是要判斷 src
所指到的地址是否對齊 8
位元組,如果是則不會進迴圈,否則會進迴圈
判斷字串的長度是否有大於 LBLOCKSZE
,這邊的 LBLOCKSZE
為 long
的大小
建立 mask
,結果為一個 unsigned long
的大小,且每個位元組都是 d
的數值
接著是題目的要求
使用巨集函式 DETECT_CHAR
搜尋是否有目標字元出現,如果有的話利用 __builtin_ctzl
計算是位於第幾個位元組,最後回傳移動後的地址
而最後的情況則是如果原本的字串長度小於 8 ,或是找到長度小於 8 後還沒找到目標字元,就會執行上述程式碼,直接一個一個找
Linux 核心原本版本位於 lib/string.c ,以下為原始碼實作
很明顯, Linux 核心原始碼的 memchr
是使用一個一個找字元的方法
接著參考同學 arthurchang09 的開發紀錄,採用類似的方法進行測量
core 0
執行程式-O0
1000
的字元陣列,每個元素初始化為字元 0
,目標字元 4
會逐一放在第 0 個字元、第 1 個字元直到第 999 字元為 \0
clock_gettime
測量找到目標字元的時間以下為測試程式碼
time.c
並且在檔案 eliminate.py
進行極端值的刪除
eliminate.py
最後的測試結果
可以發現我們所使用的 mem_opt
比 Linux 核心的 memchr
快了非常多
參考 Repeat String Operation Prefix 及 Scan String
REPNE SCAS m8
: Find AL
, starting at ES:[(E)DI]
or [RDI]
SCASB
: Compare AL
with byte at ES:(E)DI
or RDI
then set status flags從上述的組合語言,可以大致推論實作方法為一個一個字元比較,直到找到目標為止,和 Linux 核心無平台的方法相似
接著可以開始測量 x86-64 實作的效能,首先編譯產生以下錯誤
將 movl
及 decl
分別改成 movq
及 decq
即可,接著為測量結果,使用的方法和上面的方式一樣,以下為測量結果
可以看到測量的結果為 opt
> x86-64
> origin
參考 Arm A64 Instruction Set Architecture 得到上述的分析結果,發現在 ARM64 的實作和小考的題目相似都是使用 SWAR 的手法解決
line 38
對應小考的變數 mask
差在 ARM64 使用 mul
指令,而小考則是用迴圈加上 bitwise 實作line 40 ~ 49
開始則是使用 SWAR 的方法,一樣是使用 8 byte 進行計算,接著稍微分析和小考的差別
line 49
對應到小考最後的迴圈,當字串長度不足 8 時就會執行該迴圈,並使用一個一個字串判斷line 57
則是使用 SWAR 找到字元後如何取得字元地址的實作,對應到小考所要求的部份,兩者主要差別在於 ARM64 是使用指令 clz
計算 Leading zero ,而小考的部份我是使用函式 __builtin_ctzl
計算 Trailing zero2
延伸問題:
git log include/linux/kfifo.h lib/kfifo.c
並觀察修改記錄
spin_unlock_irqrestore
的使用lfring
移植到 Linux 核心,並提供對應的測試及效能評比程式DDD
: idx++
KKK
: mask + 1
TTT
: &lfr->ring[tail & mask].idx, __ATOMIC_RELAXED
HHH
: head + actual
參考 6.55 Built-in Functions for Memory Model Aware Atomic Operations 得到以下
__ATOMIC_RELAXED
: Implies no inter-thread ordering constraints.__ATOMIC_CONSUME
: This is currently implemented using the stronger __ATOMIC_ACQUIRE
memory order because of a deficiency in C++11’s semantics for memory_order_consume.__ATOMIC_ACQUIRE
: Creates an inter-thread happens-before constraint from the release (or stronger) semantic store to this acquire load. Can prevent hoisting of code to before the operation.__ATOMIC_RELEASE
: Creates an inter-thread happens-before constraint to acquire (or stronger) semantic loads that read from this release store. Can prevent sinking of code to after the operation.__ATOMIC_ACQ_REL
: Combines the effects of both __ATOMIC_ACQUIRE
and __ATOMIC_RELEASE
.__ATOMIC_SEQ_CST
: Enforces total ordering with all other __ATOMIC_SEQ_CST
operations.根據上述程式碼,可以得知程式碼測試是由 MPMC → MPSC → SPMC → SPSC 的順序測試
從上述的宣告,可以畫出以下連續記憶體的結構,如下圖所示
參考 Ring Library , enqueue 分成兩種作法,分別針對 Single Producer 和 Multiple Producers
以下為函式 lfring_enqueue
執行 single producer 的部份,搭配 7.5.1. Single Producer Enqueue 可以畫出實作流程圖
主要流程
tail
更新分析上述程式碼 line 8
開始的迴圈,假設一開始狀態為下圖
接著將資料複製到 tail
所在的空間
接著 tail
移動到下一個陣列位置
重複上述的過程直到一共執行 actual
次
以下為函式 lfring_enqueue
在 Multiple producers 的情況所執行的程式碼
和 single producer 不同的地方在於, multiple producers 考慮到不同執行緒互相影響時要怎麼應對,以下使用流程圖表示,假設有兩個執行續 (Thread1 及 Thread2) 同時要執行 enqueue
假設 Thread1 先成功加入資料,因此會更新共享資料的 lfr->tail
,如下圖所示
此時根據上述程式的 line 29
,當 Thread2 要進行加入資料的動作時,會發現 slot
已經不是最一開始讀取到的 slot
,因此會將 slot
複製到 old.pp
並重新再執行一次 enqueue ,如下圖所示
lf_compare_exchange
這邊著重在上述 inline assembly 的實作,參考 6.36. Constraints for asmOperands 、How to specify register constraints on the Intel x86_64 register r8 to r15 in GCC inline assembly? 及 6.47 How to Use Inline Assembly Language in C Code
上述的 inline assembly 可以理解為:
lock cmpxchg16b [var]
(參考 Compare and Exchange Bytes)
setz [ret]
(參考 X86-assembly/Instructions/setz)
Simple Constraints
m
: A memory operand is allowed, with any kind of address that the machine supports in general. Note that the letter used for the general memory constraint can be re-defined by a back end using the TARGET_MEM_CONSTRAINT
macro.q
: For x86-64 it is equivalent to r
class. (for 8-bit instructions that do not use upper halves)a
: The a
register.b
: The b
register.c
: The c
register.d
: The d
register.Constraint Modifier Characters
=
: Means that this operand is written to by this instruction: the previous value is discarded and replaced by new data.+
: Means that this operand is both read and written by the instruction.clobber arguments
cc
: The cc
clobber indicates that the assembler code modifies the flags register. On some machines, GCC represents the condition codes as a specific hardware register; cc
serves to name this register. On other machines, condition code handling is different, and specifying cc
has no effect. But it is valid no matter what the target.memory
: The memory
clobber tells the compiler that the assembly code performs memory reads or writes to items other than those listed in the input and output operands (for example, accessing the memory pointed to by one of the input parameters). To ensure memory contains correct values, GCC may need to flush specific register values to memory before executing the asm. Further, the compiler does not assume that any values read from memory before an asm remain unchanged after that asm; it reloads them as needed. Using the "memory" clobber effectively forms a read/write memory barrier for the compiler.看了以上所有的參考連結後,可以開始分析程式的邏輯
和 enqueue 相同, dequeue 也分成兩種作法,分別針對 Single Consumer 和 Multiple Consumers
以下為函式 lfring_dequeue
執行 single consumer 所執行的程式碼,參考 7.5.2. Single Consumer Dequeue ,畫出流程圖
主要流程
head
更新從上述程式碼 line 14
開始分析流程,假設初始狀態如以下所示
往後移動 lfr->head
一共 actual
個資料,假設 actual = 1
在分析函式 lfring_dequeue
時,發現一個特別的函式,如以下所示
首先了解為什麼這邊需要 memory barrier ,參考 從硬體觀點了解 memory barrier 的實作和效果 有很清楚的解釋為什麼需要 memory barrier
這邊直接提出結論
因此可以得知這邊使用 memory barrier 是為了確保 queue 資料的讀取以及最後 head
的更新要保持正確的順序
接著參考 Working of __asm__ __volatile__ ("" : : : "memory")
,可以得知這行 inline assembly 產生的影響
以下為函式 lfring_dequeue
在 multiple consumers 的情況下執行的程式碼
迴圈的內容和 single consumer 程式碼相同,但重點在於 line 23
的 while
判斷,可以用下面的流程圖表示,假設一開始有兩個執行序 (thread1 及 thread2) 都做 dequeue 的動作,並且即將讀取相同的資料
接著假設 thread1 的執行比較快,並且取得了資料,如下圖所示,而此時會因為 line 23
的關係,整個共享資料的 lfr->head
被修改了
因此當 thread2 執行到 line 23
時,會發現 lfr->head
已經和原本的 head
不同,因此接下來的步驟變成將 lfr->head
更新到 head
,並重新執行一次取資料的動作
最後再重新取一次資料,因此可以得知 thread1 得到 obj1
, thread2 得到 obj2
研讀ing…
3
延伸問題:
dont_trace
核心模組掛載 dont_trace
核心模組時,會執行函式 dont_trace_init
首先使用函式 create_workqueue
建立 workqueue ,並且使用函式 queue_delayed_work
使在 workqueue 執行的工作延遲 JIFFIES_DELAY
個 jiffy ,而 linux 核心所指的 jiffy 則是指觸發計時中斷所間隔的時間
接著有一個特別的參數 dont_trace_task
,可以發現模組使用巨集 DECLARE_DELAYED_WORK
建立延時工作任務
而巨集 DECLARE_DELAYED_WORK
可以從 include/linux/workqueue.h 找到,以下是相關實作
雖然沒有很清楚整體的實作,但是可以稍微得知巨集 DECLARE_DELAYED_WORK
對模組的影響
struct delayed_work dont_trace_task
作為工作儲存在 workqueueperiodic_routine
設定為工作會執行的函式dont_trace
核心模組掛載 dont_trace
核心模組後,會不斷執行函式 periodic_routine
,正是題目要填補的程式碼,以下為回答的結果
首先判斷變數 load
是否為真,如果為真則直接進入函式 check
,主要是用來判斷是否有程式正在被監控,接著就是使用函式 schedule_delayed_work
重新將任務放回 workqueue
接著蠻好奇這個核心模組是如何知道其他程式是否被監控的,從函式 check
開始研究
首先是巨集 for_each_process
,從名子可以知道是用來走訪所有程式,其實作位於 include/linux/sched/signal.h ,很明顯是從第一個程式 init_task
一路走訪
在討論函式 is_tracer
之前,先研究結構 struct task_struct
成員 ptraced
的功能,根據題目敘述
Once any process starts “ptracing” another, the tracee gets added into ptraced that’s located in task_struct, which is simply a linked list that contains all the tracees that the process is “ptracing”.
當有任何的程式開始追蹤其他的程式,此時管理被追蹤程式的 task_struct
成員 ptraced
就會被接到追蹤者上,可以觀察 include/linux/sched.h 裡 ptraced
的定義
很明顯可以看到 ptraced
是一個 linked list ,所有被追蹤的程式都會由這個 linked list 所管理
接著分析函式 is_tracer
,藉由走訪整個 ptraced
的 linked list 判斷該程式是否追蹤其他的程式
函式 kill_tracee
則是當發現有程式正在追蹤其他程式時,使用函式 kill_task
將上述提到的 linked list 所含有的程式中止
而函式 kill_task
則是送出 SIGKILL
將目標程式中止