網卡接到一個幀後,協議棧的處理代碼是怎麼被調起執行的?
對於內核處理網卡接受到的幀的流程我是這樣理解的:網卡接到一個幀,網卡向處理器發出中斷信號,處理器中斷正在執行的程序轉而執行網卡中斷處理程序,將幀拷貝到內核空間,把要執行的下半部程序做上標記,然後打開中斷,接著返回。
這裡的疑問是,那麼對應的下半部程序雖然被標記了,但是怎麼執行的呢?是誰在什麼時候調起執行的呢?
整個協議棧處理網路數據的過程還是比較需要時間的,所以不可能一直在中斷上下文中執行。查看的資料中都提到了放在下半部中執行,可好像都沒說下半部是怎麼被執行的。另外,中斷下半部是在進程上下文中執行的嗎?
我將我剛在文章中回答貼在此處以供交流。之所以建議你公開提問,是為了能得到更多相關專業的人的解釋和探討,我作為一個非計算機網路方面的人士,以僅一家之言回答網路相關的問題,怕對你有所誤導。下面是我的回答,也希望你可以多多參考別人的回答,以及更多專業方面資料的解釋。自己去閱讀代碼和文檔當然是最有效的學習手段,我們每個人的說辭有時都有我們的時間和空間上的侷限性。
什麼是中斷和軟中斷
首先我們要對中斷和軟中斷有個起碼的認識,否則下面的內容就不用看了。如果已經認識的人可跳過此段。
中斷,一般指硬體中斷,多由系統自身或與之鏈接的外設(如鍵盤、滑鼠、網卡等)產生。中斷首先是處理器提供的一種響應外設請求的機制,是處理器硬體支持的特性。一個外設通過產生一種電信號通知中斷控制器,中斷控制器再向處理器發送相應的信號。處理器檢測到了這個信號後就會打斷自己當前正在做的工作,轉而去處理這次中斷(所以才叫中斷)。當然在轉去處理中斷和中斷返回時都有保護現場和返回現場的操作,這裡不贅述,我在下面問題的回答裏解釋過一些,可以參考:
為什麼系統調用時要把一些寄存器保存到內核棧又從內核棧恢復??www.zhihu.com當然更推薦去閱讀更正規的書籍資料來瞭解中斷的進入和返回是如何保護和恢復現場的。不同的設備會對應不同的中斷號,不同的中斷也會有不同的中斷處理函數,中斷處理函數一般在設備驅動註冊時一同註冊,這樣一來哪個設備有了事件就能產生對應的中斷,並找到對應的中斷處理程序來執行了。所以一個硬體中斷的大致過程描述是下面這樣(非絕對,依具體情況而定,意會一下即可):
+---------+ 產生中斷 +----------+ 通知 +-----+
| 硬體設備 | -------------&> | 中斷控制器 | -------&> | CPU |
+---------+ +----------+ +-----+
|
V
[中斷內核]
|
V
[是否存在中斷處理程序?] & irq_exit ----&> 恢復現場 .....
那軟中斷又是什麼呢?我們知道在中斷處理時CPU沒法處理其它事物,對於網卡來說,如果每次網卡收包時中斷的時間都過長,那很可能造成丟包的可能性。當然我們不能完全避免丟包的可能性,以太包的傳輸是沒有100%保證的,所以網路纔有協議棧,通過高層的協議來保證連續數據傳輸的數據完整性(比如在協議發現丟包時要求重傳)。但是即使有協議保證,那我們也不能肆無忌憚的使用中斷,中斷的時間越短越好,儘快放開處理器,讓它可以去響應下次中斷甚至進行調度工作。基於這樣的考慮,我們將中斷分成了上下兩部分,上半部分就是上面說的中斷部分,需要快速及時響應,同時需要越快結束越好。而下半部分就是完成一些可以推後執行的工作。對於網卡收包來說,網卡收到數據包,通知內核數據包到了,中斷處理將數據包存入內存這些都是急切需要完成的工作,放到上半部完成。而解析處理數據包的工作則可以放到下半部去執行。
軟中斷就是下半部使用的一種機制,它通過軟體模仿硬體中斷的處理過程,但是和硬體沒有關係,單純的通過軟體達到一種非同步處理的方式。其它下半部的處理機制還包括tasklet,工作隊列等。依據所處理的場合不同,選擇不同的機制,網卡收包一般使用軟中斷。對應NET_RX_SOFTIRQ這個軟中斷,軟中斷的類型如下:
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
為了代碼可查詢的準確性,我下面的代碼均出自upstream mainline Linux v5.8-rc4,如果其他人在閱讀時發現有對不上的函數,請檢查自己的內核版本,或者根據理解自行「修補」偏差。
網卡收包的中斷過程
- 註冊網卡中斷
這其實是一個大問題,我上面說過我不是網路這方面的專業人士,所以不想在此越俎代庖班門弄斧,我下面只針對收包時從中斷到軟中斷的過程進行一定的論述,不再向後續深入講解。
一個網卡收到數據包,它首先要做的事情就是通知處理器進行中斷處理,這一點我們在上面簡述中斷過程的時候說過了。不同的外設有不同的中斷和中斷處理函數,所以要研究網卡的中斷我們得以一個具體的網卡驅動為例,比如e1000。其模塊初始化函數就是:
static int __init e1000_init_module(void)
{
int ret;
pr_info("%s - version %s
", e1000_driver_string, e1000_driver_version);
pr_info("%s
", e1000_copyright);
ret = pci_register_driver(e1000_driver);
...
...
return ret;
}
其中e1000_driver這個結構體是一個關鍵,它的賦值如下:
static struct pci_driver e1000_driver = {
.name = e1000_driver_name,
.id_table = e1000_pci_tbl,
.probe = e1000_probe,
.remove = e1000_remove,
.driver = {
.pm = e1000_pm_ops,
},
.shutdown = e1000_shutdown,
.err_handler = e1000_err_handler
};
其中很主要的一個方法就是.probe方法,也就是e1000_probe():
/**
* e1000_probe - Device Initialization Routine
* @pdev: PCI device information struct
* @ent: entry in e1000_pci_tbl
*
* Returns 0 on success, negative on failure
*
* e1000_probe initializes an adapter identified by a pci_dev structure.
* The OS initialization, configuring of the adapter private structure,
* and a hardware reset occur.
**/
static int e1000_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
...
...
netdev-&>netdev_ops = e1000_netdev_ops;
e1000_set_ethtool_ops(netdev);
...
...
}
這個函數很長,我們不都列出來,這是e1000主要的初始化函數,即使從注釋都能看出來。我們留意其註冊了netdev的netdev_ops,用的是e1000_netdev_ops這個結構體:
static const struct net_device_ops e1000_netdev_ops = {
.ndo_open = e1000_open,
.ndo_stop = e1000_close,
.ndo_start_xmit = e1000_xmit_frame,
.ndo_set_rx_mode = e1000_set_rx_mode,
.ndo_set_mac_address = e1000_set_mac,
.ndo_tx_timeout = e1000_tx_timeout,
...
...
};
這個e1000的方法集裏有一個重要的方法,e1000_open,我們要說的中斷的註冊就從這裡開始:
/**
* e1000_open - Called when a network interface is made active
* @netdev: network interface device structure
*
* Returns 0 on success, negative value on failure
*
* The open entry point is called when a network interface is made
* active by the system (IFF_UP). At this point all resources needed
* for transmit and receive operations are allocated, the interrupt
* handler is registered with the OS, the watchdog task is started,
* and the stack is notified that the interface is ready.
**/
int e1000_open(struct net_device *netdev)
{
struct e1000_adapter *adapter = netdev_priv(netdev);
struct e1000_hw *hw = adapter-&>hw;
...
...
err = e1000_request_irq(adapter);
...
}
e1000在這裡註冊了中斷:
static int e1000_request_irq(struct e1000_adapter *adapter)
{
struct net_device *netdev = adapter-&>netdev;
irq_handler_t handler = e1000_intr;
int irq_flags = IRQF_SHARED;
int err;
err = request_irq(adapter-&>pdev-&>irq, handler, irq_flags, netdev-&>name,
...
...
}
如上所示,這個被註冊的中斷處理函數,也就是handler,就是e1000_intr()。我們不展開這個中斷處理函數看了,我們知道中斷處理函數在這裡被註冊了,在網路包來的時候會觸發這個中斷函數。
- 註冊軟中斷
上面我們看到了網卡硬中斷的註冊,我們下面看一下軟中斷處理的註冊。我們在一開始提到了網卡收包時使用的軟中斷是NET_RX_SOFTIRQ,我們就在內核中查找這個關鍵字,看看這個註冊的位置在哪。踏破鐵鞋無覓處,得來全不費工夫,原來這個註冊的位置在這裡:
/*
* Initialize the DEV module. At boot time this walks the device list and
* unhooks any devices that fail to initialise (normally hardware not
* present) and leaves us with a valid list of present and active devices.
*
*/
...
static int __init net_dev_init(void)
{
...
...
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
...
...
}
open_softirq()函數就是註冊軟中斷用的函數,向它指定軟中斷號NET_RX_SOFTIRQ和軟中斷處理函數 net_rx_action()就可以完成註冊了。
- 從硬中斷到軟中斷
現在一個網路包來了,會產生中斷,會執行do_IRQ。關於do_IRQ的實現有很多,不同硬體對中斷的處理都會有所不同,但一個基本的執行思路就是:
void __irq_entry do_IRQ(unsigned int irq) |do_IRQ[98] void do_IRQ(struct pt_regs *regs, int irq)
{ |
irq_enter(); |*** arch/sh/kernel/irq.c: |do_IRQ[185] asmlinkage __irq_entry int do_IRQ(unsigned int irq, struct pt_r
generic_handle_irq(irq); |egs *regs)
irq_exit(); |
}
我們沒必要都展開,讓我們專註我們的問題。do_IRQ會執行上面e1000_intr這個中斷處理函數,這個中斷處理是屬於上半部的處理,在do_IRQ的結尾會調用irq_exit(),這是軟中斷和中斷銜接的一個地方。我們重點說一下這裡。
void irq_exit(void)
{
__irq_exit_rcu();
rcu_irq_exit();
/* must be last! */
lockdep_hardirq_exit();
}
static inline void __irq_exit_rcu(void)
{
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
local_irq_disable();
#else
lockdep_assert_irqs_disabled();
#endif
account_irq_exit_time(current);
preempt_count_sub(HARDIRQ_OFFSET);
if (!in_interrupt() local_softirq_pending())
invoke_softirq();
tick_irq_exit();
}
在irq_exit()的第一步就是一個local_irq_disable(),也就是說禁止了中斷,不再響應中斷。因為下面要處理所有標記為要處理的軟中斷,關中斷是因為後面要清除這些軟中斷,將CPU軟中斷的點陣圖中置位的位清零,這需要關中斷,防止其它進程對點陣圖的修改造成幹擾。
然後preempt_count_sub(HARDIRQ_OFFSET),硬中斷的計數減1,表示當前的硬中斷到這裡就結束了。但是如果當前的中斷是嵌套在其它中斷裏的話,這次減1後不會計數清0,如果當前只有這一個中斷的話,這次減1後計數會清0。注意這很重要。
因為接下來一步判斷!in_interrupt() local_softirq_pending(),第一個!in_interrupt()就是通過計數來判斷當前是否還處於中斷上下文中,如果當前還有為完成的中斷,則直接退出當前中斷。後半部的執行在後續適當的時機再進行,這個「適當的時機」比如ksoftirqd守護進程的調度,或者下次中斷到此正好不在中斷上下文的時候等情況。
我們現在假設當前中斷結束後沒有其它中斷了,也就是不在中斷上下文了,且當前CPU有等待處理的軟中斷,即local_softirq_pending()也為真。那麼執行invoke_softirq()。
static inline void invoke_softirq(void)
{
if (ksoftirqd_running(local_softirq_pending()))
return;
if (!force_irqthreads) {
#ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK
/*
* We can safely execute softirq on the current stack if
* it is the irq stack, because it should be near empty
* at this stage.
*/
__do_softirq();
#else
/*
* Otherwise, irq_exit() is called on the task stack that can
* be potentially deep already. So call softirq in its own stack
* to prevent from any overrun.
*/
do_softirq_own_stack();
#endif
} else {
wakeup_softirqd();
}
}
這個函數的邏輯很簡單,首先如果ksoftirqd正在被執行,那麼我們不想處理被pending的軟中斷,交給ksoftirqd線程來處理,這裡直接退出。
如果ksoftirqd沒有正在運行,那麼判斷force_irqthreads,也就是判斷是否配置了CONFIG_IRQ_FORCED_THREADING,是否要求強制將軟中斷處理都交給ksoftirqd線程。因為這裡明顯要在中斷處理退出的最後階段處理軟中斷,但是也可以讓ksoftirqd來後續處理。如果設置了force_irqthreads,則不再執行__do_softirq(),轉而執行wakeup_softirqd()來喚醒ksoftirqd線程,將其加入可運行隊列,然後退出。
如果沒有設置force_irqthreads,那麼就執行__do_softirq():
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
...
...
pending = local_softirq_pending();
account_irq_enter_time(current);
__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
in_hardirq = lockdep_softirq_start();
restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0);
local_irq_enable();
h = softirq_vec;
while ((softirq_bit = ffs(pending))) {
...
...
}
if (__this_cpu_read(ksoftirqd) == current)
rcu_softirq_qs();
local_irq_disable();
pending = local_softirq_pending();
if (pending) {
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);
}
注意在函數開始時就先執行了一個__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET),表示當前要處理軟中斷了,在這種情況下是不允許睡眠的,也就是不能進程調度。這點很重要,也很容易混淆,加上前面我們說的irq_exit()開頭的local_irq_disable(),所以當前處在一個既禁止硬中斷,又禁止軟中斷,不能睡眠不能調度的狀態。很多人就容易將這種狀態歸類為「中斷上下文」,我個人認為是不對的。從上面in_interrupt函數的定義來看,是否處於中斷上下文和preempt_count對於中斷的計數有關:
#define irq_count() (preempt_count() (HARDIRQ_MASK | SOFTIRQ_MASK
| NMI_MASK))
#define in_interrupt() (irq_count())
和是否禁止了中斷沒有直接的關係。雖然中斷上下文應該不允許睡眠和調度,但是不能睡眠和調度的時候不等於in_interrupt,比如spin_lock的時候也是不能睡眠的(這是目前我個人觀點)。但是很多程序員之所以容易一概而論,是因為對於內核程序員來講,判斷自己所編程的位置是否可以睡眠和調度是最被關心的,所以禁用了中斷後不能調度和睡眠就很容易被歸類為在中斷上下文,實際上我個人認為這應該算一個誤解,或者說是「變相擴展」後的說辭。一切還要看我們對中斷上下文這個概念的界定,如果像in_interrupt那樣界定,那關不關中斷和是否處於中斷上下文就沒有直接的關係。
下面在__do_softirq開始處理軟中斷(執行每一個待處理的軟中斷的action)前還有一個很關鍵的地方,就是local_irq_enable(),這就打開了硬體中斷,然後後面的軟中斷處理可以在允許中斷的情況下執行。注意這時候__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET)仍然有效,睡眠仍然是不允許的。
到這裡我們可以看到,內核是盡量做到能允許中斷就盡量允許,能允許調度就盡量允許,因為無情的禁止是對CPU資源最大的浪費,也是對外設中斷的不負責。否則長期處于禁止中斷的情況下,網卡大量丟包將是難免的,而這也將是制約成網卡實際速率的瓶頸。
結語
因為提問者一直在糾結網卡收包時軟中斷和中斷的關係,以及軟中斷到底是不是在中斷上下文這種問題。所以我圍繞此重點講述,更多其它的內容就不涉及了。再次強調我不是這方面的專業人士,上述內容僅憑我個人短時間的代碼閱讀理解,純屬越俎代庖,如有網路方面的專業人士路過看到有明顯疏漏的地方還請不吝指出。
內容來自SJTU,IPADS OS-16-Network