主講人: jserv / 課程討論區: 2025 年系統軟體課程
返回「Linux 核心設計」課程進度表Image Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
simrupt 專案名稱由 simulate 和 interrupt 二個單字組合而來,其作用是模擬 IRQ 事件,並展示以下 Linux 核心機制的運用:
Linux Kernel 提供兩種類型的計時器,分別是 dynamic timer 和 interval timers ,前者用在 kernel space 當中而後者用在 user space 當中。 struct timer_list
則是運用在 kernel space 當中的 dynamic timers ( 參見 include/linux/timer_types.h )。
在 kernel/time/timer.c 當中定義以下函式,注意到每個 cpu 都有自己的 timer 。
我們可以利用 $ sudo cat /proc/timer_list
來觀察 CPU timer_list 資訊。
Interrupts and Interrupt Handling
「中斷」(interrupt)是指處理器接收到來自硬體或軟體的訊號,該訊號表示某事件已發生,此類事件統稱中斷事件。所有可能的中斷事件都會被賦予一個唯一的中斷號。
當處理器接收到中斷訊號時,會根據中斷號查詢中斷向量表 (Interrupt Vector Table)找到並執行對應的程式。負責處理中斷事件的程式稱為「中斷處理程序」(interrupt handler),屬於核心層級的函式。
節錄自 CS:APP 第八章
與一般核心函式 (kernel function) 相比,中斷處理程序無法進入休眠狀態(無法排程),因此其執行時間應盡可能縮短,以避免長時間佔用處理器資源,影響系統整體效能。然而,中斷事件的處理過程往往涉及大量工作。
為有效分散負載並提升系統即時性,Linux 採用「先上車,後補票」的機制,將中斷處理流程劃分為兩個階段—「中斷處理」及「其後續」,也就是:Top half
(上車) 與 Bottom half
(補票)
Top Half
包括:
這些任務通常在屏蔽其他中斷的狀態下執行,需快速完成,以減少對系統其他部分的影響。
實作後續的處理(Bottom half
) 有以下三種機制:
延伸閱讀: Linux 核心設計: 中斷處理和現代架構考量
透過稱為 ksoftirqd
的 kernel thread 來達成,每個 CPU 都有一個這樣的 thread ,可透過以下命令觀察
我們可以在 kernel/softirq.c 看到以下定義,分別有 softirq_vec, ksoftirqd, softirq_to_name
,每個 CPU 都有自己的 ksoftirqd
kernel thread ,而這些 kernel thread 也有各自的 softirq_vec
,分別對應到 softirq_to_name
所對應的種類。
利用以下命令觀察
被延遲的 interrupt 會被放到對應的欄位當中,透過 raise_softirq
來觸發, wakeup_softirqd
則是會觸發當前 CPU 的 ksoftirqd
kernel thread 。
tasklets 在 Linux kernel 當中的實作位在 /include/linux/interrupt.h
它是實作在 softirq 上,另一種延遲中斷處理的機制,它依賴以下兩種 softirqs
TASKLET_SOFTIRQ
HI_SOFTIRQ
同一種類型的 tasklets 不能同時在多個處理器上運作,從以上 tasklet_struct 的定義來看,可以剖析它的實作包括
Linux 核心利用以下兩個函式來標示 tasklet 為 ready to run
tasklet_schedule()
tasklet_hi_schedule()
這兩個函式實作相近,差別在優先權,第一個函式所標註的 tasklet 優先權最低。
workqueue 和 tasklet 的概念類似,但依舊有差別, tasklet 透過 software interrupt context 來執行,而 workqueue 當中的 work items 則是透過 kernel process ,這代表 work item 的執行不像 tasklet 一樣是 atomic 的 (換言之,整個 tasklet 的函式只能執行在最初被分配到的 CPU 上)。
Kernel 會建立稱為 worker threads
的 kernel threads 來處理 work items ,我們可以透過以下命令來觀察這些 kernel threads 。
queue_work()
函式則是可以幫我們把 work item 放置到 workqueue 當中。
kfifo 是 Linux 核心裡頭 First-In-First-Out (FIFO) 的結構,在 Single Producer Single Consumer (SPSC) 情況中是 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 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 重疊時,代表目前是空的緩衝區。相反的,當 head 比 tail 少 1 時,代表緩衝區是滿的。
當有項目被添加時,head index 會增加,當有項目被移除時,tail index 會被增加,tail 不會超過 head,且當兩者都到達緩衝區的末端時,都必須被設定回 0。也可以藉由此方法清除緩衝區中的資料。
Measuring power-of-2 buffers: 讓緩衝區大小維持 2 的冪,就可以使用 bitwise 操作去計算緩衝區空間,避免使用較慢的 modulus (divide) 操作。
CIRC_SPACE*()
被 producer 使用,CIRC_CNT*()
是 consumer 所用。
在 simrupt 中,一個「更快」的 circular buffer 被拿來儲存即將要放到 kfifo 的資料。
READ_ONCE()
是個 relaxed-ordering 且保證 atomic 的 memory operation,可以確保在多執行緒環境中,讀取到的值是正確的,並保證讀寫操作不會被編譯器最佳化所影響。
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 的角色,會從緩衝區中取得資料,並更新 tail index。
fast_buf_put 扮演一個 producer 的角色,藉由 CIRC_SPACE()
判斷 buffer 中是否有剩餘空間,並更新 head index。
process_data 函式呼叫 fast_buf_put(update_simrupt_data());
,其中 update_simrupt_data()
會產生資料,這些資料的範圍在 0x20
到 0x7E
之間,即 ASCII 中的可顯示字元,這些資料會被放入 circular buffer 中,最後交由 tasklet_schedule
進行排程。
tasklet 是基於 softirq 之上建立的,但最大的差別在於 tasklet 可以動態配置且可以被用在驅動裝置上。
tasklet 可以被 workqueues, timers 或 threaded interrupts 取代,但 kernel 中尚有使用 tasklet 的情況,Linux 核心開發者已著手 API 變更,而 DECLARE_TASKLET_OLD
的存在是顧及相容性。
首先會先確保函式在 interrupt context 和 softirq context 中執行,使用 queue_work 將 work 放入 workqueue 中,並記錄執行時間。
藉由上述註解可以得知:
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。
24-26行使用 mutex_lock(&consumer_lock)
鎖住消費者區域,防止其它的任務取得 circular buffer 的資料。
32-34行使用 mutex_lock(&producer_lock)
鎖住生產者區域,防止其它的任務寫入 kfifo buffer。
wake_up_interruptible(&rx_wait)
會喚醒 wait queue 上的行程,將其狀態設置為 TASK_RUNNING。
在 workqueue 中執行的 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
來存取和控制該設備
掛載核心模組。
掛載後,會產生一個裝置檔案/dev/simrupt
,藉由以下命令可見到輸出的資料。
參考輸出: (可能會有異)
dmesg
顯示核心訊息,加入 --follow
可即時查看。
參考輸出:
kfifo 是一個 Circular buffer 的資料結構,而 ring-buffer 就是參考 kfifo 所實作。
在 simrupt_init 會先配置 buffer,使其具備一個 PAGE 的空間。fast_buf.buf = vmalloc(PAGE_SIZE);
將 buffer 的虛擬記憶體位址存在 fast_buf.buf
。
主執行緒會將更新的字元放入 buffer 中,而每個 worker thread 則是使用函式 fast_buf_get()
從 buffer 取出資料後,藉由 produce_data()
放到 kfifo。
kfifo 適合的使用情境,可以在 linux/kfifo.h 中看到:
選定 linux/samples/kfifo/ 作為應用案例,並參考 kfifo-examples 進行實驗。
record-example.c
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 查看核心訊息
bytestream-example.c
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,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 相同。