執行人: brianlin314
專題解說錄影
提問清單
simrupt 專案名稱由 simulate 和 interrupt 二個單字組合而來,其作用是模擬 IRQ 事件,並展示以下 Linux 核心機制的運用:
相關資訊:
檢查核心版本是在 Linux v5.15+
搭配 Linux Kernel Module Programming Guide 和課程教材,逐一解釋 simrupt 所展示的核心機制,設計對應的實驗來解說。
kfifo 是 linux kernel 中一個 First-In-First-Out 的結構,在 Single Producer Single Consumer 情況中是 safe 的,即不需要額外的 lock 維護,在程式碼中註解中也有提及。
在此專案中有一個 kfifo 資料結構 rx_fifo,用來儲存即將傳到 userspace 的 data。
將 Data 插入到 rx_fifo 中,並檢查寫入的長度與避免過度輸出日誌而影響效能,之所以對 len 進行檢查的原因在於 kfifo_in 所回傳之值,是實際成功插入的數量。
kfifo_in(fifo, buf, n);
kfifo_to_user(fifo, to, len, copied);
kfifo_alloc(fifo, size, gfp_mask);
kfifo_free(fifo);
實現。首先先查閱相關資料,以了解 Circular Buffers。
circular buffer 是一個固定大小的 buffer,其中具有 2 個 indicies:
head index
: the point at which the producer inserts items into the buffer.tail index
: the point at which the consumer finds the next item in the buffer.當 head 和 tail 重疊時,代表當前是空的 buffer,相反的,當 head 比 tail 少 1 時,代表 buffer 是滿的。
當有項目被添加時,head index 會增加,當有項目被移除時,tail index 會被增加,tail 不會超過 head,且當兩者都到達 buffer 的末端時,都必須被設定回 0。也可以透過此方法清除 buffer 中的資料。
Measuring power-of-2 buffers: 讓 buffer 大小維持 2 的冪次方,就可以使用 bitwise 操作去計算 buffer 空間,避免使用較慢的 modulus (divide) 操作。
CIRC_SPACE*()
是 producer 使用的,CIRC_CNT*()
是 consumer 使用的。在 simrupt 中,一個"更快速"的 circular buffer 被拿來儲存即將要放到 kfifo 的資料。
READ_ONCE()
是一個 relaxed-ordering 且保證 atomic 的 memory operation,可以確保在多執行序環境中,讀取到的值是正確的,並保證讀寫操作不會被 compiler 優化。
smp_rmb()
是一個 memory barrier,會防止記憶體讀取指令的重排,確保先讀取索引值後再讀取內容。在 Lockless patterns: relaxed access and partial memory barriers 中提到 smp_rmb()
與 smp_wmb()
的 barrier 效果比 smp_load_acquire()
與 smp_store_release()
還要來的差,但是因為 load-store 之間的排序關係很少有影響,所以開發人員常以 smp_rmb()
和 smp_wmb()
作為 memory barrier 。
fast_buf_get
扮演一個 consumer 的腳色,會從 buffer 中取得資料,並更新 tail index。
fast_buf_put 扮演一個 producer 的腳色,透過 CIRC_SPACE()
判斷 buffer 中是否有剩餘空間,並更新 head index。
函數 process_data 呼叫了 fast_buf_put(update_simrupt_data());
,其中 update_simrupt_data()
會產生 data,這些 data 的範圍在 0x20
到 0x7E
之間,即 ASCII 中的可顯示字元,這些 data 會被放入 circular buffer 中,最後交由 tasklet_schedule 進行排程。
tasklet 是基於 softirq 之上建立的,但最大的差別在於 tasklet 可以動態分配且可以被用在驅動裝置上。
tasklet 可以被 workqueues、timers 或 threaded interrupts 取代,但 kernel 中尚有使用 tasklet 的情況,現在,開發人員正在進行API變更,而 DECLARE_TASKLET_OLD
的存在是為了保持兼容性。
首先會先確保函數在 interrupt context 和 softirq context 中執行,使用 queue_work 將 work 放入 workqueueu 中,並記錄執行時間。
透過上述註解可以得知:
softirq | tasklet | |
---|---|---|
多個在同一個 CPU 執行? | No | No |
相同的可在不同 CPU 執行? | Yes | No |
會在同個 CPU 執行? | Yes | Maybe |
當 tasklet_schedule()
被呼叫時,代表此 tasklet 被允許在 CPU 上執行,詳見 linux/include/linux/interrupt.h
linux/include/linux/workqueue.h
定義兩個 mutex lock,producer_lock、consumer_lock。
get_cpu()
獲取當前 CPU 編號並 disable preemption,最後需要 put_cpu()
重新 enable preemption。
21-23行使用 mutex_lock(&consumer_lock)
鎖住消費者區域,防止其它的任務取得 circular buffer 的資料。
29-31行使用 mutex_lock(&producer_lock)
鎖住生產者區域,防止其它的任務寫入 kfifo buffer。
wake_up_interruptible(&rx_wait)
會換醒 wait queue 上的行程,將其狀態設置為 TASK_RUNNING。
在 workqueue 中執行的 work,可以由 DECLARE_WORK()
或 INIT_WORK()
定義。
DECLARE_WORK(name, void (*func) (void *), void *data)
會在編譯時,靜態地初始化 work。INIT_WORK(struct work_struct *work, woid(*func) (void *), void *data)
在執行時,動態地初始化一個 work。透過 timer_setup()
初始化 timer。
目標是模擬 hard-irq,所以必須確保目前是在 softirq context,欲模擬在 interrupt context 中處理中斷,所以針對該 CPU disable interrupts。
使用 mod_timer 對 timer 進行排程。
Jiffy 是一個非正式術語,表示不具體的非常短暫的時間段,可透過以下公式進行轉換。
在這個函數底下,會進行許多資料結構的初始化,包含:
掛載核心模組。
掛載後,會產生一個裝置檔案/dev/simrupt
,透過以下指令可以看到打印出的資料。
dmesg
顯示核心訊息,加入 --follow
可即時查看。
在 Linux 核心原始程式碼選定規模較小、恰好可展現 irq/softirq/workqueue 的應用案例,需要確保在 Linux v5.15+ 可執行,設計實驗來驗證其行為,並解釋其原理。
下載核心原始程式碼
使用 find、grep 找尋原始程式碼的 irq 應用案例
選定 linux/samples/trace_printk/trace-printk.c 作為應用案例
此案例演示 trace_printk 與 irq_work ,在 Running work in hardware interrupt context 文件中,說明了為了避免在 hardware interrupt context 中執行程式,Linux 中存在很多機制能將 interrupt-driven work 延遲執行,但在某些情況下,仍會需要在 hardware interrupt context 下執行程式,所以新增這個 API。
該 API 預期的使用情境是 non-maskable interrupts,也就是指關鍵的任務,不能被延遲執行,且執行在 hardware interrupt context 下的程式需要被確保不會對系統造成負面影響。
init_irq_work
初始化 irq_work
enqueue irq_work
將 irq_work enqueue 到當前 CPU 上,在 enqueue 之前,會先調用 irq_work_claim() 判斷此 work 是否已經交由另一個處理器進行處理? 即irq_work 的狀態會被宣告為 "claimed",且若 IRQ_WORK_PENDING 被設定,表示不需要觸發 IPI (Inter-Processor Interrupt)。
irq_work_sync
會先確保在此函數執行期間,中斷不會被禁用,若 kernel 配置啟用了 CONFIG_PREEMPT_RT 且該 work 不是 hardware interrupt,或者,不支持 hardware interrupt 的檢測時,會執行 rcuwait_wait_event
等待 irq_work 變成空閒狀態。
總結,irq_work_sync
會對 irq_work 進行同步操作,確保它當前未被使用。
此案例演示在 harware interrupt context 下,使用 trace_printk 輸出 static 與 global 的字串。
先掛載 Debugfs
在 root 權限下,到正確的目錄底下開啟追蹤功能
停止追蹤
查看 ftrace 輸出信息,參照 ftrace.txt
irqs-off
:
d
interrupts are disabled..
otherwise.need-resched
:
N
both TIF_NEED_RESCHED and PREEMPT_NEED_RESCHED is set,n
only TIF_NEED_RESCHED is set,p
only PREEMPT_NEED_RESCHED is set,.
otherwise.hardirq/softirq
:
Z
- NMI occurred inside a hardirqz
- NMI is runningH
- hard irq occurred inside a softirq.h
- hard irq is runnings
- soft irq is running.
- normal context.preempt-depth
:
在 trace 追蹤檔可以觀察到,透過 irq_work 將trace_printk_irq_work
這個函數放在 interrupt context 下執行,且在 interrupt context 下會 disabled interrupt。
workqueue 可以將 work 延後執行,並交由一個 kernel thread 去執行,相對於 tasklet 要在 interrupt context 下執行,workqueue 能在 process context 下執行,且允許重新排程與 sleep,所以 workqueue 可以取代 tasklet。
參考 LKMPG workqueue 實驗,自行設計一個 workqueue-example.c 實驗,該實驗會將 work 與 delayed_work 放到 workqueue 中,並記錄時間,之後,先執行 work,並再延遲5秒後,執行 delayed_work。
schedule_work()
schedule_work_on()
在 Linux 核心原始程式碼選定規模較小、恰好可展現 kfifo 的應用案例,需要確保在 Linux v5.15+ 可執行,設計實驗來驗證其行為,並解釋其原理。
kfifo 是一個 Circular buffer 的資料結構,而 ring-buffer 就是參考 kfifo 所實作的。
kfifo 適合的使用情境,可以在 linux/kfifo.h 中看到:
選定 linux/samples/kfifo/ 作為應用案例,並參考 kfifo-examples 進行實驗。
kfifo_in(&test, &hello, sizeof(hello))
將 struct hello 寫入 kfifo buffer,並用 kfifo_peek_len(&test)
印出 kfifo buffer 下一個 record 的大小。kfifo_in(&test, buf, i + 1)
寫入 kfifo buffer。kfifo_skip(&test)
跳過 kfifo buffer 的第一個值,即跳過 "hello"。kfifo_out_peek(&test, buf, sizeof(buf)
會在不刪除元素情況下,印出 kfifo buffer 的第一個元素。kfifo_len(&test)
印出目前 kfifo buffer 以占用的大小。kfifo_out(&test, buf, sizeof(buf))
逐一比對 kfifo buffer 中的元素是不是和 excepted_result 中的元素一樣。掛載核心模組。
利用 dmesg 查看信息
kfifo_in
與 kfifo_put
將字串 "hello" 與數字 0-9 放入 kfifo buffer。
kfifo_in
: 可一次將 n Bytes 的 object 放到 kfifo buffer 中。kfifo_put
: 與 kfifo_in
相似,只是用來處理要將單一個值放入 kfifo buffer 的情境,若要插入時,buffer 已滿,則會返回 0。kfifo_out
先將 kfifo buffer 前 5 個值拿出,即 "hello"。kfifo_out
將 kfifo buffer 前 2 個值 (0、1) 拿出,再用 kfifo_in
重新將 0、1 放入 kfifo buffer,並用 kfifo_skip
拿出並忽略 buffer 中第一個值。kfifo_get
逐一檢查 buffer 內的值是否與 expected_result 中的值一樣,若一樣,則 test passed。掛載核心模組。
利用 dmesg 查看信息
設計一個 kfifo 的生產者與消費者實驗 - producer-consumer.c,包含一個 producer 與一個 consumer,producer 函數每1秒會將一個值放入 kfifo 中,並從1遞增到10,而consumer 函數每2秒會消耗一個 kfifo 的值。
在 example_init 中,使用 kthread_run
建立兩個 kernel thread,分別是 producer_thread 與 consumer_thread。
在 example_exit 中,會用 kfifo_get
逐一檢查 kfifo 剩餘的值是否與 expected_result 相同。
在 Linux 核心原始程式碼選定規模較小、恰好可展現 memory barrier 的應用案例,需要確保在 Linux v5.15+ 可執行,設計實驗來驗證其行為,並解釋其原理。要特別說明在多核處理器的影響。
Memory barrier 用於控制記憶體存取的順序。在某些情況下,因為編譯器和硬體進行的優化可能導致記憶體的存取順序與開發人員預期的不同。Memory barrier 會影響記憶體存取指令的執行順序與指令完成的時間。
影響編譯器和處理器的記憶體屏障稱為 hardware memory barrier,只影響編譯器的記憶體屏障稱為 software memory barrier。另外,能同時影響讀取和寫入的記憶體屏障稱為 full memory barrier。
還有一類特定於多處理器環境的記憶體屏障。這些記憶體屏障的名稱以 smp
作為前綴。在多處理器系統上,這些屏障是 hardware memory barrier,在單處理器系統上,它們是 software memory barrier。
barrier()
是唯一的 software memory barrier,也是 full memory barrier。Linux kernel 中的所有其他記憶體屏障都是 hardware memory barrier。
mb
/rmb
/wmb
smp_mb
/smp_rmb
/smp_wmb
mb()
/rmb()
/smb()
function on multi-processor systems sequentially, and they are the same as the barrier() function on uni-processor systems.barrier
在本書第 14 章介紹 Scheduling Tasks,有兩種方法執行 tasks,分別是 tasklets 和 work queues。tasklets 能透過 interrupt 以快速且簡單的方式運行單一 function,而 work queues 相對複雜,但適合按順序運行多個 tasks。
雖然 tasklet 易於使用,但是運行於 software interrupt,代表不能 sleep 或存取 user-space。在 linux kernel 中,tasklet 可由 workqueue 取代。
要將 task 交給排程器前,可以使用 workqueue,kernel 會根據 CFS 在 queue 中排程 task。
tasklet | workqueue | |
---|---|---|
Can access user space ? | No | No |
Can sleep ? | No | Yes |
More than one can run on same CPU ? | No | Yes |
Same one can run on multiple CPUs ? | No | Yes |
探討 Linux 核心開發者逐步棄置 tasklet 使用的考量因素。
jserv
當一個 I/O 事件發生時,interrupt 是一個通知 CPU 事件發生的機制,需要 CPU 的回覆,且無論 CPU 是否忙碌。
當 interrupt 發生時,會強制改變 CPU 的處理流程,類似於 context switch,硬體會嘗試儲存原本程式所持有的狀態,此時會切換到 interrupt mode,接著 kernel 會根據對應的 interrupt handler 對事件進行處理,最後會執行 interrupt return,還原到原本程式在中斷發生前執行到的部分。需要注意的是不同的處理器會有不同的處理方式。
取自 COMS W4118: Operating Systems 的 Interrupts in Linux
當正在處理某個 ISR 的時候,此時又有一個新的中斷發生,稱為 Neated Interrupt。
不同的周邊會有很多不同的行為,而系統希望同一時間有更多的中斷能夠執行,這樣就會非常複雜,在早期可以透過 disable interrupt 解決,但在 Linux 中,會希望 disable interrupt 盡可能地少,當存在 disable interrupt 時,再重新 enable interrupt,就會致使延遲提升。
在 linux 中,有時 interrupt handler 需要及時處理,或 interrupt handler 需要處理大量的 works,所以 linux 將其分為兩個部分: top half 與 bottom half。在 top half 中,會執行關鍵的 interrupt,確保其不會受到延遲,同時將一些複雜且相對不重要的 interrupt 延遲到 bottom half 執行,以降低 interrupt 的延遲 。top half 與 bottom half 的最大差別在於,bottom half 在執行時,interrupt enabled,因此 CPU 仍然可以接受中斷請求。
在 linux 中,有3種 deferred interrupts 的方法:
老師在上課中以手機為例,當此刻按下手機電源鍵,會發生中斷並喚醒螢幕,但此中斷不僅僅只包含喚醒螢幕,還需要更新手機上的時間、訊息提醒等; 或透過搖晃手機以搜尋附近女性友人,也是中斷的一種,這些都需要進行模擬。
bottom half 就是 softirq,top half 就是 irq (hardirq)。
softirq 可以重新排程,而 irq 無法。
edge trigger 與 level trigger 皆是用來描述硬體中斷的處理模式。
Difference between interrupt context and process context?
在執行 interrupt handler 或 buttom half 時,kernel 處於 interrupt context,而執行一般 process 時,kernel 的狀態則稱 process context。
softirq 與 tasklet 運行在 interrupt context,而 workqueue 可以 sleep,所以不是運行在 interrupt context。
context | 是否可 sleep | 是否可被 preempt |
---|---|---|
interrupt context | 不可 | 不可 |
process context | 可 | 可 |