LINUX內(nèi)核網(wǎng)絡(luò)中的軟中斷KSOFTIRQD
1. 前言
之前分享過(guò)Linux內(nèi)核網(wǎng)絡(luò)數(shù)據(jù)包的接收過(guò)程,當(dāng)執(zhí)行到網(wǎng)卡通過(guò)硬件中斷(IRQ)通知CPU,告訴它有數(shù)據(jù)來(lái)了,CPU會(huì)根據(jù)中斷表,調(diào)用已經(jīng)注冊(cè)的中斷函數(shù),這個(gè)中斷函數(shù)會(huì)調(diào)到驅(qū)動(dòng)程序(NIC Driver)中相應(yīng)的函數(shù)。驅(qū)動(dòng)先禁用網(wǎng)卡的中斷,表示驅(qū)動(dòng)程序已經(jīng)知道內(nèi)存中有數(shù)據(jù)了,告訴網(wǎng)卡下次再收到數(shù)據(jù)包直接寫內(nèi)存就可以了,不要再通知CPU了,這樣可以提高效率,避免CPU不停的被中斷。
由于硬中斷處理程序執(zhí)行的過(guò)程中不能被中斷,所以如果它執(zhí)行時(shí)間過(guò)長(zhǎng),會(huì)導(dǎo)致CPU沒(méi)法響應(yīng)其它硬件的中斷,于是內(nèi)核引入軟中斷,這樣可以將硬中斷處理函數(shù)中耗時(shí)的部分移到軟中斷處理函數(shù)里面來(lái)慢慢處理。內(nèi)核中的ksoftirqd進(jìn)程專門負(fù)責(zé)軟中斷的處理,當(dāng)它收到軟中斷后,就會(huì)調(diào)用相應(yīng)軟中斷所對(duì)應(yīng)的處理函數(shù),網(wǎng)卡驅(qū)動(dòng)模塊拋出的軟中斷,ksoftirqd會(huì)調(diào)用網(wǎng)絡(luò)模塊的net_rx_action函數(shù)。
那么接下來(lái),我們先宏觀上回顧一下數(shù)據(jù)包接收的過(guò)程,以了解軟中斷在此過(guò)程中的位置,然后介紹一下內(nèi)核中的軟中斷。
2. 數(shù)據(jù)包接收宏觀過(guò)程
加載網(wǎng)卡驅(qū)動(dòng),初始化 數(shù)據(jù)包從外部網(wǎng)絡(luò)進(jìn)入網(wǎng)卡 網(wǎng)卡(通過(guò)DMA)將包拷貝到內(nèi)核內(nèi)存中的ring buffer 產(chǎn)生硬件中斷,通知系統(tǒng)收到了一個(gè)包 驅(qū)動(dòng)調(diào)用 NAPI ,如果輪詢(poll)還沒(méi)有開(kāi)始,就開(kāi)始輪詢 ksoftirqd軟中斷調(diào)用 NAPI 的poll函數(shù)從ring buffer收包(poll 函數(shù)是網(wǎng)卡驅(qū)動(dòng)在初始化階段注冊(cè)的;每個(gè)cpu上都運(yùn)行著一個(gè)ksoftirqd進(jìn)程,在系統(tǒng)啟動(dòng)期間就注冊(cè)了) ring buffer里面對(duì)應(yīng)的內(nèi)存區(qū)域解除映射(unmapped) 如果 packet steering 功能打開(kāi),或者網(wǎng)卡有多隊(duì)列,網(wǎng)卡收到的數(shù)據(jù)包會(huì)被分發(fā)到多個(gè)cpu 數(shù)據(jù)包從隊(duì)列進(jìn)入?yún)f(xié)議層 協(xié)議層處理數(shù)據(jù)包 數(shù)據(jù)包從協(xié)議層進(jìn)入相應(yīng) socket 的接收隊(duì)列
3. 軟中斷
內(nèi)核的軟中斷系統(tǒng)是一種在硬中斷處理上下文(驅(qū)動(dòng)中)之外執(zhí)行代碼的機(jī)制。硬中斷處理函數(shù)(handler)執(zhí)行時(shí),會(huì)屏蔽部分或全部(新的)硬中斷。中斷被屏蔽的時(shí)間越長(zhǎng),丟失事件的可能性也就越大。所以,所有耗時(shí)的操作都應(yīng)該從硬中斷處理邏輯中剝離出來(lái),硬中斷因此能盡可能快地執(zhí)行,然后再重新打開(kāi)硬中斷。
內(nèi)核中也有其他機(jī)制將耗時(shí)操作轉(zhuǎn)移出去,不過(guò)對(duì)于網(wǎng)絡(luò)棧,我們接下來(lái)只看軟中斷ksoftirqd??梢园衍浿袛嘞到y(tǒng)想象成一系列內(nèi)核線程(每個(gè) CPU 一個(gè)),這些線程執(zhí)行針對(duì)不同事件注冊(cè)的處理函數(shù)(handler),內(nèi)核子系統(tǒng)(比如網(wǎng)絡(luò))能通過(guò) open_softirq() 注冊(cè)軟中斷處理函數(shù)。通過(guò) top 命令,會(huì)注意到 ksoftirqd/0 這個(gè)內(nèi)核線程,其表示這個(gè)軟中斷線程跑在 CPU 0 上,如下圖所示。

4. ksoftirqd
軟中斷對(duì)分擔(dān)硬中斷的工作量至關(guān)重要,因此軟中斷線程在內(nèi)核啟動(dòng)的很早階段就 spawn 出來(lái)了。
kernel/softirq.c 中對(duì) ksoftirqd 系統(tǒng)進(jìn)行了初始化:
static?struct?smp_hotplug_thread?softirq_threads?=?{
??????.store??????????????=?&ksoftirqd,
??????.thread_should_run??=?ksoftirqd_should_run,
??????.thread_fn??????????=?run_ksoftirqd,
??????.thread_comm????????=?"ksoftirqd/%u",
};
static?__init?int?spawn_ksoftirqd(void)
{
??????register_cpu_notifier(&cpu_nfb);
??????BUG_ON(smpboot_register_percpu_thread(&softirq_threads));
??????return?0;
}
early_initcall(spawn_ksoftirqd);
看到注冊(cè)了兩個(gè)回調(diào)函數(shù):ksoftirqd_should_run 和 run_ksoftirqd。這兩個(gè)函數(shù)都會(huì)從 kernel/smpboot.c 里調(diào)用,作為事件處理循環(huán)的一部分。
kernel/smpboot.c 里面的代碼首先調(diào)用 ksoftirqd_should_run 判斷是否有 pending 的軟中斷,如果有,就執(zhí)行 run_ksoftirqd,后者做一些 bookeeping 工作,然后調(diào)用 __do_softirq。
__do_softirq 做的幾件事情:
-
判斷哪個(gè) softirq 被 pending -
計(jì)算 softirq 時(shí)間,用于統(tǒng)計(jì) -
更新 softirq 執(zhí)行相關(guān)的統(tǒng)計(jì)數(shù)據(jù) -
執(zhí)行 pending softirq 的處理函數(shù)
asmlinkage?__visible?void?__do_softirq(void)
{
????unsigned?long?end?=?jiffies?+?MAX_SOFTIRQ_TIME;
????unsigned?long?old_flags?=?current->flags;
????int?max_restart?=?MAX_SOFTIRQ_RESTART;
????struct?softirq_action?*h;
????bool?in_hardirq;
????__u32?pending;
????int?softirq_bit;
????/*
?????*?Mask?out?PF_MEMALLOC?s?current?task?context?is?borrowed?for?the
?????*?softirq.?A?softirq?handled?such?as?network?RX?might?set?PF_MEMALLOC
?????*?again?if?the?socket?is?related?to?swap
?????*/
????current->flags?&=?~PF_MEMALLOC;
????pending?=?local_softirq_pending();?//獲取當(dāng)前CPU的軟中斷寄存器__softirq_pending值到局部變量pending。
????account_irq_enter_time(current);
????__local_bh_disable_ip(_RET_IP_,?SOFTIRQ_OFFSET);?//增加preempt_count中的softirq域計(jì)數(shù),表明當(dāng)前在軟中斷上下文中。
????in_hardirq?=?lockdep_softirq_start();
restart:
????/*?Reset?the?pending?bitmask?before?enabling?irqs?*/
????set_softirq_pending(0);?//清除軟中斷寄存器__softirq_pending。
????local_irq_enable();?//打開(kāi)本地中斷
????h?=?softirq_vec;?//指向softirq_vec第一個(gè)元素,即軟中斷HI_SOFTIRQ對(duì)應(yīng)的處理函數(shù)。
????while?((softirq_bit?=?ffs(pending)))?{?//ffs()找到pending中第一個(gè)置位的比特位,返回值是第一個(gè)為1的位序號(hào)。這里的位是從低位開(kāi)始,這也和優(yōu)先級(jí)相吻合,低位優(yōu)先得到執(zhí)行。如果沒(méi)有則返回0,退出循環(huán)。
????????unsigned?int?vec_nr;
????????int?prev_count;
????????h?+=?softirq_bit?-?1;?//根據(jù)sofrirq_bit找到對(duì)應(yīng)的軟中斷描述符,即軟中斷處理函數(shù)。
????????vec_nr?=?h?-?softirq_vec;?//軟中斷序號(hào)
????????prev_count?=?preempt_count();
????????kstat_incr_softirqs_this_cpu(vec_nr);
????????trace_softirq_entry(vec_nr);
????????h->action(h);?//執(zhí)行對(duì)應(yīng)軟中斷函數(shù)
????????trace_softirq_exit(vec_nr);
????????if?(unlikely(prev_count?!=?preempt_count()))?{
????????????pr_err("huh,?entered?softirq?%u?%s?%p?with?preempt_count?%08x,?exited?with?%08x?\n",
???????????????????vec_nr,?softirq_to_name[vec_nr],?h->action,
???????????????????prev_count,?preempt_count());
????????????preempt_count_set(prev_count);
????????}
????????h++;?//h遞增,指向下一個(gè)軟中斷
????????pending?>>=?softirq_bit;?//pending右移softirq_bit位
????}
????rcu_bh_qs();
????local_irq_disable();?//關(guān)閉本地中斷
????pending?=?local_softirq_pending();?//再次檢查是否有軟中斷產(chǎn)生,在上一次檢查至此這段時(shí)間有新軟中斷產(chǎn)生。
????if?(pending)?{
????????if?(time_before(jiffies,?end)?&&?!need_resched()?&&?max_restart)?//再次觸發(fā)軟中斷執(zhí)行的三個(gè)條件:1.軟中斷處理時(shí)間不超過(guò)2jiffies,200Hz的系統(tǒng)對(duì)應(yīng)10ms;2.當(dāng)前沒(méi)有有進(jìn)程需要調(diào)度,即!need_resched();3.這種循環(huán)不超過(guò)10次。
????????????goto?restart;
????????wakeup_softirqd();?//如果上面的條件不滿足,則喚醒ksoftirq內(nèi)核線程來(lái)處理軟中斷。
????}
????lockdep_softirq_end(in_hardirq);
????account_irq_exit_time(current);
????__local_bh_enable(SOFTIRQ_OFFSET);?//減少preempt_count的softirq域計(jì)數(shù),和前面增加計(jì)數(shù)呼應(yīng)。表示這段代碼處于軟中斷上下文。
????WARN_ON_ONCE(in_interrupt());
????tsk_restore_flags(current,?old_flags,?PF_MEMALLOC);
}
查看 CPU 利用率時(shí),si 字段對(duì)應(yīng)的就是 softirq,度量(從硬中斷轉(zhuǎn)移過(guò)來(lái)的)軟中斷的 CPU 使用量。

5. 監(jiān)測(cè)
軟中斷的信息可以從 /proc/softirqs 讀?。?/p>

6. 總結(jié)
中斷是一種異步的事件處理機(jī)制,用來(lái)提高系統(tǒng)的并發(fā)處理能力。中斷事件發(fā)生,會(huì)觸發(fā)執(zhí)行中斷處理程序,而中斷處理程序被分為上半部和下半部這兩個(gè)部分。上半部對(duì)應(yīng)硬中斷,用來(lái)快速處理中斷;下半部對(duì)應(yīng)軟中斷,用來(lái)異步處理上半部未完成的工作。Linux 中的軟中斷包括網(wǎng)絡(luò)收發(fā)、定時(shí)、調(diào)度、RCU 鎖等各種類型,我們可以查看 proc 文件系統(tǒng)中的 /proc/softirqs ,觀察軟中斷的運(yùn)行情況。在 Linux 中,每個(gè) CPU 都對(duì)應(yīng)一個(gè)軟中斷內(nèi)核線程,名字是 ksoftirqd/CPU 編號(hào)。當(dāng)軟中斷事件的頻率過(guò)高時(shí),內(nèi)核線程也會(huì)因?yàn)?CPU 使用率過(guò)高而導(dǎo)致軟中斷處理不及時(shí),進(jìn)而引發(fā)網(wǎng)絡(luò)收發(fā)延遲、調(diào)度緩慢等性能問(wèn)題。
參考資料:
https://blog.packagecloud.io/eng/2016/06/22/monitoring-tuning-linux-networking-stack-receiving-data/
https://www.cnblogs.com/luoahong/p/10815283.html
鏈接:http://kerneltravel.net/blog/2020/ksoftirqd_ljr/
(版權(quán)歸原作者所有,侵刪)