Linux 中斷( IRQ / softirq )基礎(chǔ):原理及內(nèi)核實(shí)現(xiàn)
作者:趙亞楠
原文:http://arthurchiao.art/blog/linux-irq-softirq-zh/
來(lái)源:云原生實(shí)驗(yàn)室

中斷(IRQ),尤其是軟中斷(softirq)的重要使用場(chǎng)景之一是網(wǎng)絡(luò)收發(fā)包, 但并未唯一場(chǎng)景。本文整理 IRQ/softirq 的通用基礎(chǔ),這些東西和網(wǎng)絡(luò)收發(fā)包沒(méi)有直接關(guān)系, 雖然整理本文的直接目的是為了更好地理解網(wǎng)絡(luò)收發(fā)包。
什么是中斷?
CPU 通過(guò)時(shí)分復(fù)用來(lái)處理很多任務(wù),這其中包括一些硬件任務(wù),例如磁盤(pán)讀寫(xiě)、鍵盤(pán)輸入,也包括一些軟件任務(wù),例如網(wǎng)絡(luò)包處理。在任意時(shí)刻,一個(gè) CPU 只能處理一個(gè)任務(wù)。當(dāng)某個(gè)硬件或軟件任務(wù)此刻沒(méi)有被執(zhí)行,但它希望 CPU 來(lái)立即處理時(shí),就會(huì)給 CPU 發(fā)送一個(gè)中斷請(qǐng)求 —— 希望 CPU 停下手頭的工作,優(yōu)先服務(wù)“我”。中斷是以事件的方式通知 CPU 的,因此我們??吹?“XX 條件下會(huì)觸發(fā) XX 中斷事件” 的表述。
兩種類(lèi)型:
外部或硬件產(chǎn)生的中斷,例如鍵盤(pán)按鍵。 軟件產(chǎn)生的中斷,異常事件產(chǎn)生的中斷,例如除以零 。
管理中斷的設(shè)備:Advanced Programmable Interrupt Controller(APIC)。
硬中斷
中斷處理流程
中斷隨時(shí)可能發(fā)生,發(fā)生之后必須馬上得到處理。收到中斷事件后的處理流程:
搶占當(dāng)前任務(wù):內(nèi)核必須暫停正在執(zhí)行的進(jìn)程; 執(zhí)行中斷處理函數(shù):找到對(duì)應(yīng)的中斷處理函數(shù),將 CPU 交給它(執(zhí)行); 中斷處理完成之后:第 1 步被搶占的進(jìn)程恢復(fù)執(zhí)行。
Maskable and non-maskable
Maskable interrupts 在 x64_64 上可以用 sti/cli 兩個(gè)指令來(lái)屏蔽(關(guān)閉)和恢復(fù):
static inline void native_irq_disable(void) {
asm volatile("cli": : :"memory"); // 清除 IF 標(biāo)志位
}
static inline void native_irq_enable(void) {
asm volatile("sti": : :"memory"); // 設(shè)置 IF 標(biāo)志位
}
在屏蔽期間,這種類(lèi)型的中斷不會(huì)再觸發(fā)新的中斷事件。大部分 IRQ 都屬于這種類(lèi)型。例子:網(wǎng)卡的收發(fā)包硬件中斷。
Non-maskable interrupts 不可屏蔽,所以在效果上屬于更緊急的類(lèi)型。
問(wèn)題:執(zhí)行足夠快 vs 邏輯比較復(fù)雜
IRQ handler 的兩個(gè)特點(diǎn):
執(zhí)行要非???,否則會(huì)導(dǎo)致事件(和數(shù)據(jù))丟失; 需要做的事情可能非常多,邏輯很復(fù)雜,例如收包
這里就有了內(nèi)在矛盾。
解決方式:延后中斷處理(deferred interrupt handling)
傳統(tǒng)上,解決這個(gè)內(nèi)在矛盾的方式是將中斷處理分為兩部分:
top half bottom half
這種方式稱(chēng)為中斷的推遲處理或延后處理。以前這是唯一的推遲方式,但現(xiàn)在不是了?,F(xiàn)在已經(jīng)是個(gè)通用術(shù)語(yǔ),泛指各種推遲執(zhí)行中斷處理的方式。按這種方式,中斷會(huì)分為兩部分:
第一部分:只進(jìn)行最重要、必須得在硬中斷上下文中執(zhí)行的部分;剩下的處理作為第二部分,放入一個(gè)待處理隊(duì)列; 第二部分:一般是調(diào)度器根據(jù)輕重緩急來(lái)調(diào)度執(zhí)行,不在硬中斷上下文中執(zhí)行。
Linux 中的三種推遲中斷(deferred interrupts):
softirq tasklet workqueue
后面會(huì)具體介紹。
軟中斷
軟中斷子系統(tǒng)
軟中斷是一個(gè)內(nèi)核子系統(tǒng):
1、每個(gè) CPU 上會(huì)初始化一個(gè) ksoftirqd 內(nèi)核線(xiàn)程,負(fù)責(zé)處理各種類(lèi)型的 softirq 中斷事件;
用 cgroup ls 或者 ps -ef 都能看到:
$ systemd-cgls -k | grep softirq # -k: include kernel threads in the output
├─ 12 [ksoftirqd/0]
├─ 19 [ksoftirqd/1]
├─ 24 [ksoftirqd/2]
...
2、軟中斷事件的 handler 提前注冊(cè)到 softirq 子系統(tǒng), 注冊(cè)方式 open_softirq(softirq_id, handler)
例如,注冊(cè)網(wǎng)卡收發(fā)包(RX/TX)軟中斷處理函數(shù):
// net/core/dev.c
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
3、軟中斷占 CPU 的總開(kāi)銷(xiāo):可以用 top 查看,里面 si 字段就是系統(tǒng)的軟中斷開(kāi)銷(xiāo)(第三行倒數(shù)第二個(gè)指標(biāo)):
$ top -n1 | head -n3
top - 18:14:05 up 86 days, 23:45, 2 users, load average: 5.01, 5.56, 6.26
Tasks: 969 total, 2 running, 733 sleeping, 0 stopped, 2 zombie
%Cpu(s): 13.9 us, 3.2 sy, 0.0 ni, 82.7 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st
主處理
smpboot.c 類(lèi)似于一個(gè)事件驅(qū)動(dòng)的循環(huán),里面會(huì)調(diào)度到 ksoftirqd 線(xiàn)程,執(zhí)行 pending 的軟中斷。ksoftirqd 里面會(huì)進(jìn)一步調(diào)用到 __do_softirq,
判斷哪些 softirq 需要處理, 執(zhí)行 softirq handler
避免軟中斷占用過(guò)多 CPU
軟中斷方式的潛在影響:推遲執(zhí)行部分(比如 softirq)可能會(huì)占用較長(zhǎng)的時(shí)間,在這個(gè)時(shí)間段內(nèi), 用戶(hù)空間線(xiàn)程只能等待。反映在 top 里面,就是 si 占比。
不過(guò) softirq 調(diào)度循環(huán)對(duì)此也有改進(jìn),通過(guò) budget 機(jī)制來(lái)避免 softirq 占用過(guò)久的 CPU 時(shí)間。
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
...
restart:
while ((softirq_bit = ffs(pending))) {
...
h->action(h); // 這里面其實(shí)也有機(jī)制,避免 softirq 占用太多 CPU
...
}
...
pending = local_softirq_pending();
if (pending) {
if (time_before(jiffies, end) && !need_resched() && --max_restart) // 避免 softirq 占用太多 CPU
goto restart;
}
...
硬中斷 -> 軟中斷 調(diào)用棧
前面提到,softirq 是一種推遲中斷處理機(jī)制,將 IRQ 的大部分處理邏輯推遲到了這里執(zhí)行。兩條路徑都會(huì)執(zhí)行到 softirq 主處理邏輯 __do_softirq(),
1、CPU 調(diào)度到 ksoftirqd 線(xiàn)程時(shí),會(huì)執(zhí)行到 __do_softirq();
2、每次 IRQ handler 退出時(shí):do_IRQ() -> ...。
do_IRQ() 是內(nèi)核中最主要的 IRQ 處理方式。它執(zhí)行結(jié)束時(shí),會(huì)調(diào)用 exiting_irq(),這會(huì)展開(kāi)成 irq_exit()。后者會(huì)檢查是pending 的 softirq,有的話(huà)就喚醒:
// arch/x86/kernel/irq.c
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
進(jìn)而會(huì)使 CPU 執(zhí)行到 __do_softirq()。
軟中斷觸發(fā)執(zhí)行的步驟
To summarize, each softirq goes through the following stages: 每個(gè)軟中斷會(huì)經(jīng)過(guò)下面幾個(gè)階段:
通過(guò) open_softirq()注冊(cè)軟中斷處理函數(shù);通過(guò) raise_softirq()將一個(gè)軟中斷標(biāo)記為 deferred interrupt,這會(huì)喚醒改軟中斷(但還沒(méi)有開(kāi)始處理);內(nèi)核調(diào)度器調(diào)度到 ksoftirqd內(nèi)核線(xiàn)程時(shí),會(huì)將所有等待處理的 deferred interrupt(也就是 softirq)拿出來(lái),執(zhí)行對(duì)應(yīng)的處理方法(softirq handler);
以收包軟中斷為例, IRQ handler 并不執(zhí)行 NAPI,只是觸發(fā)它,在里面會(huì)執(zhí)行到 raise NET_RX_SOFTIRQ;真正的執(zhí)行在 softirq,里面會(huì)調(diào)用網(wǎng)卡的 poll() 方法收包。IRQ handler 中會(huì)調(diào)用 napi_schedule(),然后啟動(dòng) NAPI poll(),
這里需要注意,雖然 IRQ handler 做的事情非常少,但是接下來(lái)處理這個(gè)包的 softirq 和 IRQ 在同一個(gè) CPU 運(yùn)行。這就是說(shuō),如果大量的包都放到了同一個(gè) RX queue,那雖然 IRQ 的開(kāi)銷(xiāo)可能并不多,但這個(gè) CPU 仍然會(huì)非常繁忙,都花在 softirq 上了。解決方式:RPS。它并不會(huì)降低延遲,只是將包重新分發(fā):RXQ -> CPU。
三種推遲執(zhí)行方式(softirq/tasklet/workqueue)
前面提到,Linux 中的三種推遲中斷執(zhí)行的方式:
softirq tasklet workqueue
其中,
softirq 和 tasklet 依賴(lài)軟中斷子系統(tǒng),運(yùn)行在軟中斷上下文中; workqueue 不依賴(lài)軟中斷子系統(tǒng),運(yùn)行在進(jìn)程上下文中。
softirq
前面已經(jīng)看到, Linux 在每個(gè) CPU 上會(huì)創(chuàng)建一個(gè) ksoftirqd 內(nèi)核線(xiàn)程。
softirqs 是在 Linux 內(nèi)核編譯時(shí)就確定好的,例外網(wǎng)絡(luò)收包對(duì)應(yīng)的 NET_RX_SOFTIRQ 軟中斷。因此是一種靜態(tài)機(jī)制。如果想加一種新 softirq 類(lèi)型,就需要修改并重新編譯內(nèi)核。
內(nèi)部組織
在內(nèi)部是用一個(gè)數(shù)組(或稱(chēng)向量)來(lái)管理的,每個(gè)軟中斷號(hào)對(duì)應(yīng)一個(gè) softirq handler。數(shù)組和注冊(cè):
// kernel/softirq.c
// NR_SOFTIRQS 是 enum softirq type 的最大值,在 5.10 中是 10,見(jiàn)下面
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
void open_softirq(int nr, void (*action)(struct softirq_action *)) {
softirq_vec[nr].action = action;
}
5.10 中所有類(lèi)型的 softirq:
// include/linux/interrupt.h
enum {
HI_SOFTIRQ=0, // tasklet
TIMER_SOFTIRQ, // timer
NET_TX_SOFTIRQ, // networking
NET_RX_SOFTIRQ, // networking
BLOCK_SOFTIRQ, // IO
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ, // tasklet
SCHED_SOFTIRQ, // schedule
HRTIMER_SOFTIRQ, // timer
RCU_SOFTIRQ, // lock
NR_SOFTIRQS
};
也就是在 cat /proc/softirqs 看到的哪些。
$ cat /proc/softirqs
CPU0 CPU1 ... CPU46 CPU47
HI: 2 0 ... 0 1
TIMER: 443727 467971 ... 313696 270110
NET_TX: 57919 65998 ... 42287 54840
NET_RX: 28728 5262341 ... 81106 55244
BLOCK: 261 1564 ... 268986 463918
IRQ_POLL: 0 0 ... 0 0
TASKLET: 98 207 ... 129 122
SCHED: 1854427 1124268 ... 5154804 5332269
HRTIMER: 12224 68926 ... 25497 24272
RCU: 1469356 972856 ... 5961737 5917455
觸發(fā)(喚醒)softirq
void raise_softirq(unsigned int nr) {
local_irq_save(flags); // 關(guān)閉 IRQ
raise_softirq_irqoff(nr); // 喚醒 ksoftirqd 線(xiàn)程(但執(zhí)行不在這里,在 ksoftirqd 線(xiàn)程中)
local_irq_restore(flags); // 打開(kāi) IRQ
}
if (!in_interrupt())
wakeup_softirqd();
static void wakeup_softirqd(void) {
struct task_struct *tsk = __this_cpu_read(ksoftirqd);
if (tsk && tsk->state != TASK_RUNNING)
wake_up_process(tsk);
}
以收包軟中斷為例, IRQ handler 并不執(zhí)行 NAPI,只是觸發(fā)它,在里面會(huì)執(zhí)行到 raise NET_RX_SOFTIRQ;真正的執(zhí)行在 softirq,里面會(huì)調(diào)用網(wǎng)卡的 poll() 方法收包。IRQ handler 中會(huì)調(diào)用 napi_schedule(),然后啟動(dòng) NAPI poll()。
tasklet
如果對(duì)內(nèi)核源碼有一定了解就會(huì)發(fā)現(xiàn),softirq 用到的地方非常少,原因之一就是上面提到的,它是靜態(tài)編譯的, 靠?jī)?nèi)置的 ksoftirqd 線(xiàn)程來(lái)調(diào)度內(nèi)置的那 9 種 softirq。如果想新加一種,就得修改并重新編譯內(nèi)核, 所以開(kāi)發(fā)成本非常高。
實(shí)際上,實(shí)現(xiàn)推遲執(zhí)行的更常用方式 tasklet。它構(gòu)建在 softirq 機(jī)制之上, 具體來(lái)說(shuō)就是使用了上面提到的兩種 softirq:
HI_SOFTIRQTASKLET_SOFTIRQ
換句話(huà)說(shuō),tasklet 是可以在運(yùn)行時(shí)(runtime)創(chuàng)建和初始化的 softirq,
void __init softirq_init(void) {
for_each_possible_cpu(cpu) {
per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head;
}
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
內(nèi)核軟中斷子系統(tǒng)初始化了兩個(gè) per-cpu 變量:
tasklet_vec:普通 tasklet,回調(diào) tasklet_action() tasklet_hi_vec:高優(yōu)先級(jí) tasklet,回調(diào) tasklet_hi_action()
struct tasklet_struct {
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
tasklet 再執(zhí)行針對(duì) list 的循環(huán):
static void tasklet_action(struct softirq_action *a)
{
local_irq_disable();
list = __this_cpu_read(tasklet_vec.head);
__this_cpu_write(tasklet_vec.head, NULL);
__this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head));
local_irq_enable();
while (list) {
if (tasklet_trylock(t)) {
t->func(t->data);
tasklet_unlock(t);
}
...
}
}
tasklet 在內(nèi)核中的使用非常廣泛。不過(guò),后面又出現(xiàn)了第三種方式:workqueue。
workqueue
這也是一種推遲執(zhí)行機(jī)制,與 tasklet 有點(diǎn)類(lèi)似,但也有很大不同。
tasklet 是運(yùn)行在 softirq 上下文中; workqueue 運(yùn)行在內(nèi)核進(jìn)程上下文中;這意味著 wq 不能像 tasklet 那樣是原子的; tasklet 永遠(yuǎn)運(yùn)行在指定 CPU,這是初始化時(shí)就確定了的; workqueue 默認(rèn)行為也是這樣,但是可以通過(guò)配置修改這種行為。
使用場(chǎng)景
// Documentation/core-api/workqueue.rst:
There are many cases where an asynchronous process execution context
is needed and the workqueue (wq) API is the most commonly used
mechanism for such cases.
When such an asynchronous execution context is needed, a work item
describing which function to execute is put on a queue. An
independent thread serves as the asynchronous execution context. The
queue is called workqueue and the thread is called worker.
While there are work items on the workqueue the worker executes the
functions associated with the work items one after the other. When
there is no work item left on the workqueue the worker becomes idle.
When a new work item gets queued, the worker begins executing again.
簡(jiǎn)單來(lái)說(shuō),workqueue 子系統(tǒng)提供了一個(gè)接口,通過(guò)這個(gè)接口可以創(chuàng)建內(nèi)核線(xiàn)程來(lái)處理從其他地方 enqueue 過(guò)來(lái)的任務(wù)。這些內(nèi)核線(xiàn)程就稱(chēng)為 worker threads,內(nèi)置的 per-cpu worker threads:
$ systemd-cgls -k | grep kworker
├─ 5 [kworker/0:0H]
├─ 15 [kworker/1:0H]
├─ 20 [kworker/2:0H]
├─ 25 [kworker/3:0H]
結(jié)構(gòu)體
// include/linux/workqueue.h
struct worker_pool {
spinlock_t lock;
int cpu;
int node;
int id;
unsigned int flags;
struct list_head worklist;
int nr_workers;
...
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
struct lockdep_map lockdep_map;
};
kworker 線(xiàn)程調(diào)度 workqueues,原理與 ksoftirqd 線(xiàn)程調(diào)度 softirqs 一樣。但是我們可以為 workqueue 創(chuàng)建新的線(xiàn)程,而 softirq 則不行。
參考資料
Linux Inside (online book), Interrupts and Interrupt Handling[1]
引用鏈接
Interrupts and Interrupt Handling: https://0xax.gitbooks.io/linux-insides/content/Interrupts/linux-interrupts-9.html
10T 技術(shù)資源大放送!包括但不限于:Linux、虛擬化、容器、云計(jì)算、網(wǎng)絡(luò)、Python、Go 等。在開(kāi)源Linux公眾號(hào)內(nèi)回復(fù)「10T」,即可免費(fèi)獲?。?/span>
shell編程100例(附PDF下載)
IPv6技術(shù)白皮書(shū)(附PDF下載)
Linux主流發(fā)行版本配置IP總結(jié)(Ubuntu、CentOS、Redhat、Suse)
批量安裝Windows系統(tǒng)
無(wú)人值守批量安裝服務(wù)器
運(yùn)維必備的《網(wǎng)絡(luò)端口大全》,看這一份就夠了。
收藏:服務(wù)器和存儲(chǔ)知識(shí)入門(mén)
什么叫SSH?原理詳解,看這一篇就夠了!
Nginx面試40問(wèn)(收藏吃灰)
20 個(gè) Linux 服務(wù)器性能調(diào)優(yōu)技巧
超詳細(xì)!一文帶你了解LVS四層負(fù)載均衡企業(yè)級(jí)實(shí)踐!
收藏 | Linux系統(tǒng)日志位置及包含的日志內(nèi)容介紹
100 道 Linux 常見(jiàn)面試題,建議收藏,慢慢讀~
服務(wù)器12種基本故障+排查方法
IT運(yùn)維管理常用工具大全,讓你成為真正的高手
什么是QoS?
有收獲,點(diǎn)個(gè)在看 
shell編程100例(附PDF下載)
IPv6技術(shù)白皮書(shū)(附PDF下載)
Linux主流發(fā)行版本配置IP總結(jié)(Ubuntu、CentOS、Redhat、Suse)
批量安裝Windows系統(tǒng)
無(wú)人值守批量安裝服務(wù)器
運(yùn)維必備的《網(wǎng)絡(luò)端口大全》,看這一份就夠了。
收藏:服務(wù)器和存儲(chǔ)知識(shí)入門(mén)
什么叫SSH?原理詳解,看這一篇就夠了!
Nginx面試40問(wèn)(收藏吃灰)
20 個(gè) Linux 服務(wù)器性能調(diào)優(yōu)技巧
超詳細(xì)!一文帶你了解LVS四層負(fù)載均衡企業(yè)級(jí)實(shí)踐!
收藏 | Linux系統(tǒng)日志位置及包含的日志內(nèi)容介紹
100 道 Linux 常見(jiàn)面試題,建議收藏,慢慢讀~
服務(wù)器12種基本故障+排查方法
IT運(yùn)維管理常用工具大全,讓你成為真正的高手
什么是QoS?
有收獲,點(diǎn)個(gè)在看

