Linux 內核網絡收包角度—淺入中斷 -2-
上文 Linux 內核網絡收包角度——淺入中斷 (1) 主要從針對 e1000 網卡收包開始,分析了硬中斷註冊、處理過程,網卡收包後硬中斷的過程非常簡潔,處理過程比較簡單,**主****要是將網卡的 napi->poll_list 加入到 CPU 的 softnet_data->poll_list 裏,然後發起軟中斷,就結束了,下面的就交給軟中斷進行數據包的接收處理,所以收包任務最大程度地交給軟中斷處理,最大程度簡化硬中斷處理。**總結以上描述中,上篇博文的具體細節步驟:
-
內核啓動網卡,爲網卡分配 (Ring Buffer, 內核通過網卡驅動將 DMA 內存地址信息寫入網卡寄存器,使得網卡獲得 DMA 內存信息),並通過 request_irq 內核 APi 註冊硬中斷服務例程 e1000_intr
-
網卡收到數據包,通過 DMA 將數據包寫入內存
-
網卡觸發硬中斷,通過 CPU 接收數據
-
CPU 中斷當前的進程,跳轉到異常向量表的中斷異常向量處
-
保存現場
-
調用 irq_handler
-
對於 ARM64,irq_handler 將調用 gic_handle_irq
-
gic_handle_irq 首先讀取中斷寄存器得到硬件中斷號,調用 handle_domain_irq 函數
-
handle_domain_irq->irq_enter 進入中斷上下文
-
handle_domain_irq->irq_find_mapping 通過硬件中斷號獲取 IRQ Number
-
handle_domain_irq->generic_handle_irq 進入中斷通用層處理
-
generic_handle_irq->irq_to_desc 通過 IRQ Number 獲取對應的中斷描述符
-
generic_handle_irq->generic_handle_irq_desc->__handle_irq_event_percpu: 遍歷中斷描述符中的 action 鏈表,依次執行每個 action 中回調函數 action->handler,對應 e1000 網卡驅動,執行 e1000_intr 中斷服務例程
-
e1000_intr->ew32(IMC, ~0) 禁止網卡中斷,避免頻繁硬中斷,降低內核的工作效率
-
e1000_intr->napi_schedule 激活 NAPI, napi_struct.poll_list 掛在 softnet_data.poll_list 上, 方便後面軟中斷調用 napi_struct.poll 獲取網卡數據。然後設置 NET_RX_SOFTIRQ 軟中斷標識位。
-
handle_domain_irq->irq_exit():退出中斷上下文,觸發軟中斷執行收包流程。
-
恢復現場
上文 Linux 內核網絡收包角度——淺入中斷 (1) 重點在於通過網卡收包分析了硬中斷,本文從網絡收包角度分析和學習軟中****斷過程。
軟中斷的種類、定義 (Kernel 4.15)
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
numbering. Sigh! */
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
}
以上是內核默認定義的幾種軟中斷,硬中斷是在 CPU 每個指令週期後,會去判斷是否有硬中斷產生,根據硬中斷號找到對應的 IRQ Number,執行對應的中斷服務例程,而軟中斷會有內核線程,輪詢一組標誌位,如果標誌位有值,那去這個標誌位對應的軟中斷向量表,找到中斷處理函數執行。
軟中斷的註冊
全局的軟中斷向量數組,即軟中斷描述符表
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
const char * const softirq_to_name[NR_SOFTIRQS] = {
"HI", "TIMER", "NET_TX", "NET_RX", "BLOCK", "IRQ_POLL",
"TASKLET", "SCHED", "HRTIMER", "RCU"
}
數組中的內容就是軟中斷觸發時的鉤子函數:
struct softirq_action
{
void (*action)(struct softirq_action *);
}
如網絡收發包的軟中斷處理函數在網絡子系統初始化時註冊的:
static int __init net_dev_init(void)
{
......
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
......
}
註冊軟中斷回調函數:
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
軟中斷在內核中的整體如下所示,pending 爲軟中斷標誌位
其中標誌位在內核中使用 irq_cpustat 來表示:
typedef struct {
unsigned int __softirq_pending;
#ifdef CONFIG_SMP
unsigned int ipi_irqs[NR_IPI];
#endif
} ____cacheline_aligned irq_cpustat_t;
}
其中__softirq_pending 爲軟中斷標誌位,該類型爲 int 類型,最多支持 32 位軟中斷,ipi_rrq 位 cpu 與 cpu 之間的中斷。
在 Linux 內核中,定義 NR_CPUS 個該結構,即每個 CPU 有一個 32bit 的位圖,來維護本 cpu 上的軟中斷是否激活
irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;
軟中斷的激活
在上文 Linux 內核網絡收包角度——淺入中斷 (1) 中,硬中斷的中斷服務例程 e1000_intr 執行:
e1000_intr->__napi_schedule:
void __napi_schedule(struct napi_struct *n)
{
unsigned long flags;
local_irq_save(flags);//禁用中斷
____napi_schedule(this_cpu_ptr(&softnet_data), n);
local_irq_restore(flags);//恢復中斷
}
__napi_schedule(this_cpu_ptr(&softnet_data),n):
static inline void ____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi)
{
list_add_tail(&napi->poll_list, &sd->poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
最終調用__raise_soft_irqoff 設置軟中斷標誌位,nr 爲 NET_RX_SOFTIRQ
void __raise_softirq_irqoff(unsigned int nr)
{
trace_softirq_raise(nr);
or_softirq_pending(1UL << nr);
}
可以看到在執行__raise_soft_irqoff 函數前,在__napi_schedule 中,先是通過 local_irq_save(flags) 禁用了中斷,因爲置位位圖是一個競爭操作,所有硬中斷都可以做,所以要保證關中斷的情況下完成,等 pending 置位結束後調用 local_irq_restore(flags) 恢復中斷。
內核線程 ksoftirqd 是如何監測到軟中斷髮生的:
static void run_ksoftirqd(unsigned int cpu)
{
local_irq_disable();
if (local_softirq_pending()) {
/*
* We can safely run softirq on inline stack, as we are not deep
* in the task stack here.
*/
__do_softirq();
local_irq_enable();
cond_resched_rcu_qs();
return;
}
local_irq_enable();
}
上面函數先關中斷查看本 cpu 的 pending 置位情況,如果有則進行進一步軟中斷處理。
軟中斷的入口函數__do_softirq:
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
......
pending = local_softirq_pending(); //將32位的pending集合取出來
account_irq_enter_time(current);
__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
in_hardirq = lockdep_softirq_start();
restart:
set_softirq_pending(0);//pending清空
local_irq_enable(); //使能中斷
h = softirq_vec; //軟中斷處理向量表,裏面是軟中斷處理函數指針
while ((softirq_bit = ffs(pending))) { //獲取最高位置1的位數
unsigned int vec_nr;
int prev_count;
h += softirq_bit - 1;
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++;
pending >>= softirq_bit;
}
rcu_bh_qs();
local_irq_disable();//關中斷
pending = local_softirq_pending(); //重新獲取pending集合(開中斷到關中斷的過程中可能有新的硬中斷到來,這時候需要重新處理一遍)
if (pending) { //如果pengding中又有軟中斷置位
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;
wakeup_softirqd(); //喚醒軟中斷處理線程處理
}
lockdep_softirq_end(in_hardirq);
account_irq_exit_time(current);
__local_bh_enable(SOFTIRQ_OFFSET);
WARN_ON_ONCE(in_interrupt());
current_restore_flags(old_flags, PF_MEMALLOC);
}
對於數據包接收 h->action() 將執行:網絡子系統註冊的的回調函數 net_rx_action:
static int __init net_dev_init(void)
{
......
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
......
}
關於 net_rx_action 涉及到軟中斷處理中進一步使用 NAPI 機制收包,在接下來的文章中進行分析和學習。
參考:
https://zhuanlan.zhihu.com/p/363225092
https://zhuanlan.zhihu.com/p/363225717
https://www.i4k.xyz/article/weixin_38878510/109024909
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/H_7pq8Ea3nCAEJ3GjAsj0g