contributed by < EricccTaiwan
>
關於鍵盤事件和終端機畫面的處理機制,可參見 mazu-editor 原始程式碼和〈Build Your Own Text Editor〉。
為了理解 simrupt 的模擬 interrupt , 因此查閱了 〈Linux Kernel Development〉 一書,
這本書雖然於 2010 後沒就再進行更新, Linux kernel 停在 v2.6,但其中的概念說明得很清楚,如果要追 code 還是得看最新版本的 kernel
Top Halves Versus Bottom Halves
These two goals—that an interrupt handler execute quickly and perform a large amount of work—clearly conflict with one another. Because of these competing goals, the pro-cessing of interrupts is split into two parts, or halves.The interrupt handler is the top half. The top half is run immediately upon receipt of the interrupt and performs only the work that is time-critical, such as acknowledging receipt of the interrupt or resetting the hardware.Work that can be performed later is deferred until the bottom half. The bottom half runs in the future, at a more convenient time, with all interrupts enabled
在 Linux 核心中,當系統處於正常執行狀態時,若接收到中斷(interrupt),核心會立即暫停當前的執行流程,轉而執行與該中斷相關的處理程式。這段立即執行的程式即為中斷的 Top Half ,其負責處理與硬體即時性相關的工作 (也是 Hard IRQ) ,例如確認中斷來源、清除中斷旗標,或快速收取資料等
在 Linux 核心當中的中斷大致上有數種特性,例如以下兩種
- interrupt handler must execute quickly
- sometimes an interrupt handler must do a large amount of work
有些情況是兩種特性都存在的,但它們看起來互相衝突,該如何解決呢? Linux kernel 採用一種方式叫 deferred interrupts ,將中斷處理延遲,並且將上述兩種特性分別稱為 top half 和 bottom half … 在 top half 只處理一點重要的事然後就將 bottom half 交由之後的 context 處理。
為了避免中斷 handler 耗時過長而影響系統即時性,Linux 將中斷處理流程區分為兩個階段。Top Half 處理緊急且時間敏感的部分 (e.g., 資料存取) ,而剩餘可延後處理的工作 (e.g.,資料處理) ,則交由 Bottom Half 來完成。
softirq
、 tasklet
、 workqueue
、 kernel thread
等機制實作,並允許執行 blocking /可 sleeping 的操作,適合處理耗時、複雜或不可在 IRQ context 進行的邏輯。因此在 simrupt 中主要也可以分成這兩個處理階層
timer_handler()
模擬 interrupt 觸發。process_data()
處理 interrupt 邏輯:
update_simrupt_data()
產生模擬資料。fast_buf_put()
將資料存入中斷專用的快速環形緩衝區 fast_buf
。tasklet_schedule()
排程 Bottom Half 執行後續處理。timer_handler()
結束前 ,會呼叫 local_irq_enable()
新啟用 local interrupt ,模擬 IRQ handler 結束後交還控制權給 kernel 。simrupt_tasklet_func
):
simrupt_tasklet
) 。queue_work(simrupt_workqueue, &work)
simrupt_work_func
):
fast_buf_get()
取出 fast buffer 中的資料。produce_data()
把資料放進 kfifo(供 user-space read 使用)。wake_up_interruptible()
喚醒等待 read 的使用者。timer_init
中 timer_setup
: 初始化 struct timer_list
和指定 callback 函式 timer_handler()
。
當 user 輸入 cat /dev/simrupt
時,會觸發 timer_open
的 mod_timer
起初,繼寬問我 interrupt 不也是一種 process 嗎? 因此針對這個問題,去搜尋了一下答案
首先,什麼是 interrupt ? 在 Quora : How is context switch different than an interrupt? ,留言解釋,
An interrupt is something that causes the CPU to start executing the interrupt service routine [ISR].
interrupt 是一種事件,它會讓 CPU 開始執行對應的 ISR
This makes the synchronous portions of interrupt handling fall outside of the control of the scheduler and is therefore not really considered a process.
同時,interrupt 處理中與 synchronous 相關的部份不受排程器的控制,因此 interrupt 不被視為是一個 process。
這也解釋了在〈Linux Kernel Development〉中給出的以下結論
Interrupt context, on the other hand, is not associated with a process.
也因為 interrupt 不受到排程器控制,因此在 top half 中的操作是 time curcial 的,若不盡快處理完,則 CPU 就會一直卡著,什麼事情也做不了,直到 top half 結束把資源還回來。
Each processor has its own thread that is called ksoftirqd/n where the n is the number of the processor.
我的電腦是 8 核的CPU,透過下方的觀察,也能印證 1 個 CPU 只擁有 1 個特定的 kernel thread 去處理 SoftIRQ ,以及透過 cat /proc/softirqs
觀察每一種 SoftIRQ 類型,在每個 CPU 上被觸發的次數
Tasklets always run on the processor from which they were originally submitted. Workqueues work in the same way, but only by default.
Tasklets 是建立在 SoftIRQ 上,後者是 per-CPU
的機制,因此在 CPU0 上呼叫 tasklet_schedule(&my_tasklet)
,這個 tasklet 只會在 CPU0 上執行; 反之, workqueue 在「預設」的情況下野會在排程它的 CPU 上執行,但可以跨 CPU 、跨 thread 執行。
struct workqueue_struct *alloc_workqueue(const char *fmt, unsigned int flags, int max_active, …)
For a per-cpu workqueue, max_active limits the number of in-flight work items for each CPU. e.g. max_active of 1 indicates that each CPU can be executing at most one work item for the workqueue.
For unbound workqueues,max_active
limits the number of in-flight work items for the whole system. e.g.max_active
of 16 indicates that that there can be at most 16 work items executing for the workqueue in the whole system.
對於 unbound workqueues 來說,max_active
是限制整個系統的 work items 數量,而
struct timer_list
則是運用在 kernel space 當中的 dynamic timers
可以看到這邊的疑惑,為什麼 stringification 需要分兩層做,因此查詢 C99 規格書,
6.10.3.1 Argument substitution
A parameter in the replacement list, unless preceded by a
#
or##
preprocessing token or followed by a##
preprocessing token (see below), is replaced by the corresponding argument after all macros contained therein have been expanded.
當參數與#
或 ##
的操作子 (operator) 搭配時,該參數不會被預先展開,所以才會有了上方兩層轉換的設計,如果只有單層轉換,
此處 FOO
未被展開為 bar
,因 #
操作子阻止了參數的巨集展開。因此透過雙層巨集強至分階段處理,
可以用一個表格來對照,
輸入 | 只用單層巨集 | 用雙層巨集 |
---|---|---|
一般參數 (kxo_state ) |
"kxo_state" |
"kxo_state" |
巨集 (#define FOO bar ) |
"FOO" |
"bar" |
這樣的設計確保能統一處理所有參數類型,因此在 kxo 中,亦能正確的把 kxo_state
轉換成字串 "kxo_state"
進而,最終可以得到,
同時,可以注意到 __stringify(x...)
所接受的參數 x...
形式比較特殊,這其實是 C 語言的一個擴展功能,稱為 Variadic Macros (可變參數巨集),其詳細說明可參考 GCC 官方文件。
The variable argument is completely macro-expanded before it is inserted into the macro expansion, just like an ordinary argument. You may use the ‘#’ and ‘##’ operators to stringize the variable argument or to paste its leading or trailing token with another token.
根據上述文件描述,可變參數如同一般參數一樣,在被插入到巨集擴展體之前會先被完全展開。這也表示我們可以對可變參數使用 #
(字串化) 或 ##
(拼接) 操作子,下方舉例說明其運作方式 :
透過這種設計,即使傳入的參數在展開後包含多個以逗號分隔的部分,__stringify
巨集也能將它們完整地轉換成單一字串,確保了處理的正確性與彈性。
softirqs
tasklets
workqueues
做 mod
(((head) - (tail)) & ((size)-1))
Consumer
static int fast_buf_get(void)
Producer
static int fast_buf_put(unsigned char val)
drive 一定要有這兩條,
因此先去看 simrupt_init 做了什麼事情,
透過 kfifo_alloc
建立一個 circular buffer 給 Bottom-half 使用,
善用 bpftrace 追蹤