LINUX內核網絡中的軟中斷KSOFTIRQD
1. 前言
之前分享過Linux內核網絡數據包的接收過程,當執行到網卡通過硬件中斷(IRQ)通知CPU,告訴它有數據來了,CPU會根據中斷表,調用已經注冊的中斷函數,這個中斷函數會調到驅動程序(NIC Driver)中相應的函數。驅動先禁用網卡的中斷,表示驅動程序已經知道內存中有數據了,告訴網卡下次再收到數據包直接寫內存就可以了,不要再通知CPU了,這樣可以提高效率,避免CPU不停的被中斷。
由于硬中斷處理程序執行的過程中不能被中斷,所以如果它執行時間過長,會導致CPU沒法響應其它硬件的中斷,于是內核引入軟中斷,這樣可以將硬中斷處理函數中耗時的部分移到軟中斷處理函數里面來慢慢處理。內核中的ksoftirqd進程專門負責軟中斷的處理,當它收到軟中斷后,就會調用相應軟中斷所對應的處理函數,網卡驅動模塊拋出的軟中斷,ksoftirqd會調用網絡模塊的net_rx_action函數。
那么接下來,我們先宏觀上回顧一下數據包接收的過程,以了解軟中斷在此過程中的位置,然后介紹一下內核中的軟中斷。
2. 數據包接收宏觀過程
加載網卡驅動,初始化 數據包從外部網絡進入網卡 網卡(通過DMA)將包拷貝到內核內存中的ring buffer 產生硬件中斷,通知系統收到了一個包 驅動調用 NAPI ,如果輪詢(poll)還沒有開始,就開始輪詢 ksoftirqd軟中斷調用 NAPI 的poll函數從ring buffer收包(poll 函數是網卡驅動在初始化階段注冊的;每個cpu上都運行著一個ksoftirqd進程,在系統啟動期間就注冊了) ring buffer里面對應的內存區域解除映射(unmapped) 如果 packet steering 功能打開,或者網卡有多隊列,網卡收到的數據包會被分發到多個cpu 數據包從隊列進入協議層 協議層處理數據包 數據包從協議層進入相應 socket 的接收隊列
3. 軟中斷
內核的軟中斷系統是一種在硬中斷處理上下文(驅動中)之外執行代碼的機制。硬中斷處理函數(handler)執行時,會屏蔽部分或全部(新的)硬中斷。中斷被屏蔽的時間越長,丟失事件的可能性也就越大。所以,所有耗時的操作都應該從硬中斷處理邏輯中剝離出來,硬中斷因此能盡可能快地執行,然后再重新打開硬中斷。
內核中也有其他機制將耗時操作轉移出去,不過對于網絡棧,我們接下來只看軟中斷ksoftirqd。可以把軟中斷系統想象成一系列內核線程(每個 CPU 一個),這些線程執行針對不同事件注冊的處理函數(handler),內核子系統(比如網絡)能通過 open_softirq() 注冊軟中斷處理函數。通過 top 命令,會注意到 ksoftirqd/0 這個內核線程,其表示這個軟中斷線程跑在 CPU 0 上,如下圖所示。
4. ksoftirqd
軟中斷對分擔硬中斷的工作量至關重要,因此軟中斷線程在內核啟動的很早階段就 spawn 出來了。
kernel/softirq.c 中對 ksoftirqd 系統進行了初始化:
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);
看到注冊了兩個回調函數:ksoftirqd_should_run 和 run_ksoftirqd。這兩個函數都會從 kernel/smpboot.c 里調用,作為事件處理循環的一部分。
kernel/smpboot.c 里面的代碼首先調用 ksoftirqd_should_run 判斷是否有 pending 的軟中斷,如果有,就執行 run_ksoftirqd,后者做一些 bookeeping 工作,然后調用 __do_softirq。
__do_softirq 做的幾件事情:
-
判斷哪個 softirq 被 pending -
計算 softirq 時間,用于統計 -
更新 softirq 執行相關的統計數據 -
執行 pending softirq 的處理函數
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();?//獲取當前CPU的軟中斷寄存器__softirq_pending值到局部變量pending。
????account_irq_enter_time(current);
????__local_bh_disable_ip(_RET_IP_,?SOFTIRQ_OFFSET);?//增加preempt_count中的softirq域計數,表明當前在軟中斷上下文中。
????in_hardirq?=?lockdep_softirq_start();
restart:
????/*?Reset?the?pending?bitmask?before?enabling?irqs?*/
????set_softirq_pending(0);?//清除軟中斷寄存器__softirq_pending。
????local_irq_enable();?//打開本地中斷
????h?=?softirq_vec;?//指向softirq_vec第一個元素,即軟中斷HI_SOFTIRQ對應的處理函數。
????while?((softirq_bit?=?ffs(pending)))?{?//ffs()找到pending中第一個置位的比特位,返回值是第一個為1的位序號。這里的位是從低位開始,這也和優先級相吻合,低位優先得到執行。如果沒有則返回0,退出循環。
????????unsigned?int?vec_nr;
????????int?prev_count;
????????h?+=?softirq_bit?-?1;?//根據sofrirq_bit找到對應的軟中斷描述符,即軟中斷處理函數。
????????vec_nr?=?h?-?softirq_vec;?//軟中斷序號
????????prev_count?=?preempt_count();
????????kstat_incr_softirqs_this_cpu(vec_nr);
????????trace_softirq_entry(vec_nr);
????????h->action(h);?//執行對應軟中斷函數
????????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遞增,指向下一個軟中斷
????????pending?>>=?softirq_bit;?//pending右移softirq_bit位
????}
????rcu_bh_qs();
????local_irq_disable();?//關閉本地中斷
????pending?=?local_softirq_pending();?//再次檢查是否有軟中斷產生,在上一次檢查至此這段時間有新軟中斷產生。
????if?(pending)?{
????????if?(time_before(jiffies,?end)?&&?!need_resched()?&&?max_restart)?//再次觸發軟中斷執行的三個條件:1.軟中斷處理時間不超過2jiffies,200Hz的系統對應10ms;2.當前沒有有進程需要調度,即!need_resched();3.這種循環不超過10次。
????????????goto?restart;
????????wakeup_softirqd();?//如果上面的條件不滿足,則喚醒ksoftirq內核線程來處理軟中斷。
????}
????lockdep_softirq_end(in_hardirq);
????account_irq_exit_time(current);
????__local_bh_enable(SOFTIRQ_OFFSET);?//減少preempt_count的softirq域計數,和前面增加計數呼應。表示這段代碼處于軟中斷上下文。
????WARN_ON_ONCE(in_interrupt());
????tsk_restore_flags(current,?old_flags,?PF_MEMALLOC);
}
查看 CPU 利用率時,si 字段對應的就是 softirq,度量(從硬中斷轉移過來的)軟中斷的 CPU 使用量。
5. 監測
軟中斷的信息可以從 /proc/softirqs 讀取:
6. 總結
中斷是一種異步的事件處理機制,用來提高系統的并發處理能力。中斷事件發生,會觸發執行中斷處理程序,而中斷處理程序被分為上半部和下半部這兩個部分。上半部對應硬中斷,用來快速處理中斷;下半部對應軟中斷,用來異步處理上半部未完成的工作。Linux 中的軟中斷包括網絡收發、定時、調度、RCU 鎖等各種類型,我們可以查看 proc 文件系統中的 /proc/softirqs ,觀察軟中斷的運行情況。在 Linux 中,每個 CPU 都對應一個軟中斷內核線程,名字是 ksoftirqd/CPU 編號。當軟中斷事件的頻率過高時,內核線程也會因為 CPU 使用率過高而導致軟中斷處理不及時,進而引發網絡收發延遲、調度緩慢等性能問題。
參考資料:
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/
(版權歸原作者所有,侵刪)