對於內核處理網卡接受到的幀的流程我是這樣理解的:網卡接到一個幀,網卡向處理器發出中斷信號,處理器中斷正在執行的程序轉而執行網卡中斷處理程序,將幀拷貝到內核空間,把要執行的下半部程序做上標記,然後打開中斷,接著返回。

這裡的疑問是,那麼對應的下半部程序雖然被標記了,但是怎麼執行的呢?是誰在什麼時候調起執行的呢?

整個協議棧處理網路數據的過程還是比較需要時間的,所以不可能一直在中斷上下文中執行。查看的資料中都提到了放在下半部中執行,可好像都沒說下半部是怎麼被執行的。另外,中斷下半部是在進程上下文中執行的嗎?


我將我剛在文章中回答貼在此處以供交流。之所以建議你公開提問,是為了能得到更多相關專業的人的解釋和探討,我作為一個非計算機網路方面的人士,以僅一家之言回答網路相關的問題,怕對你有所誤導。下面是我的回答,也希望你可以多多參考別人的回答,以及更多專業方面資料的解釋。自己去閱讀代碼和文檔當然是最有效的學習手段,我們每個人的說辭有時都有我們的時間和空間上的侷限性。

什麼是中斷和軟中斷

首先我們要對中斷和軟中斷有個起碼的認識,否則下面的內容就不用看了。如果已經認識的人可跳過此段。

中斷,一般指硬體中斷,多由系統自身或與之鏈接的外設(如鍵盤、滑鼠、網卡等)產生。中斷首先是處理器提供的一種響應外設請求的機制,是處理器硬體支持的特性。一個外設通過產生一種電信號通知中斷控制器,中斷控制器再向處理器發送相應的信號。處理器檢測到了這個信號後就會打斷自己當前正在做的工作,轉而去處理這次中斷(所以才叫中斷)。當然在轉去處理中斷和中斷返回時都有保護現場和返回現場的操作,這裡不贅述,我在下面問題的回答裏解釋過一些,可以參考:

為什麼系統調用時要把一些寄存器保存到內核棧又從內核棧恢復??

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

Linux全流程

既然要講,那就把一個包的整個包生都說了算了

觸發中斷

  • 在非虛擬化環境下,網卡通過DMA將packet寫入內核的rx_ring環形隊列緩衝區,並觸發中斷。
  • 如果在虛擬化環境下,VMM配置GIC ITS (Interrupt Translation Service) ,建立物理中斷與虛擬中斷的映射完成中斷虛擬化使得網卡能直接向VM發出中斷,同時通過IO虛擬化,網卡通過IOMMU將packet直接寫入虛擬機內核的rx_ring

Top Half

  • CPU在收到中斷之後,調用網卡ISR也就是所謂的中斷handler
  • 分配sk_buf併入input_pkt_queue(如果隊列已滿則丟棄)
  • 發出一個軟中斷NET_RX_SOFTIRQ,軟中斷可以被調度例如通過tasklet

Bottom Half

  • sk_buf從input_pkt_queue傳入process_queue,根據協議類型調用網路層協議的handler
  • ip_rcv執行包頭檢查,ip_router_input()進行路由,決定本機/轉發/丟棄
  • tcp_v4_rcv執行包頭檢查,tcp_v4_lookup查詢對應的socket和connection,如果正常,tcp_prequeue將skb放進socket接收隊列
  • socket隨即喚醒所在的進程

Kqueue

因為epoll沒有論文,就說說kqueue是怎麼做的吧,kqueue會根據socket綁定的knote鏈表(每個監聽的kqueue都可能創建一個knote),將knote通過反向指針獲得kqueue,將knote加入kqueue的就緒隊列末尾。如果此時恰好有進程正在監聽的話,將會喚醒進程,kqueue會被掃描,並從就緒隊列處獲得所有的event,從而瞭解已經就緒的所有socket。

https://zhuanlan.zhihu.com/p/157431765?

zhuanlan.zhihu.com圖標
  • 喚醒的進程調用socket recv系統調用,如果是TCP則調用tcp_recvmsg從sk_buffer拷貝數據

Batch

netif_receive_skb_list()

Linux的NAPI還會繼續延遲軟中斷的處理,等待其積累足夠的skb後進行輪詢,一次性處理所有的skb。

SKB

skb並不是直接存儲報文,而是存儲指針,指針只需要移動,就能完成解包,而本身的報文並不需要修改。上一層的協議棧會在處理當前層的同時設置好下一層的頭指針,並且移動data指針。與此同時,skb本身是雙向鏈表實現的隊列。qlen為鏈表元素長度,lock為添加元素時的鎖。


談到指針的用法,這裡舉個做OS lab時印象深刻的奇淫巧技,也是C的指針變態的地方

#define list_entry(ptr, type, field)
container_of(ptr, type, field)
#define container_of(ptr, type, field)
((type *)((void *)(ptr) - (u64)((((type *)(0))-&>field))))

(u64)((((type *)(0))-&>field))))指的是field在結構體type中的偏移量,通過減去這個偏移量我們就能找出某個對象所在上級type對象的地址,也就是container。

一般來說,我們都會使用下面這樣的方式,讓鏈表節點去包裹數據。

struct page_list_node {
struct page* p;
struct page_list_node *prev;
struct page_list_node *next;
};

但是,通過指針操作,卻可以讓數據去包裹鏈表節點

struct list_head {
struct list_head *prev;
struct list_head *next;
};

struct page{
struct list_head list_node;
}

在僅僅知道鏈表節點的情況下,藉助成員偏移量即可知道容器對象的位置並取出

list_entry(somenode,struct page,list_node);

感謝評論區分享的侵入性容器概念。這個東西是linux的經典宏,同時也是C++ boost的改進。

侵入性的好處在於,元素本身就是iterator,僅僅需要元素本身就能夠從容器中刪除或遷移到別的容器,不需要關注存在於哪個容器中。

數據結構就是數據結構,和數據分離考慮。因此不需要為各種鏈表分別按照數據類型創建list類型,只需要解釋的時候附帶類型就可。


硬體中斷分上下2部分,上半部分只通知有中斷髮生,設置標記,就會打開中斷。下半部分叫軟中斷。

第一次硬體中斷,會觸發軟中斷。

軟中斷是開中斷的,因此軟中斷運行過程中會發生硬體中斷。這時就屬於硬體中斷重入。重入之後不會執行新的軟中斷,因為第一次的時候軟中斷已經在執行了。

重入的硬體中斷處理會馬上退出。

而第一次進入的軟中斷在處理完之後,會繼續判斷是否有軟中斷髮生(即發生了硬中斷重入),如果有會繼續執行。

但是軟中斷不會一直執行,有個時間判斷,超時了就會終止執行,啟動一個內核線程來繼續執行軟中斷。

比如當前是用戶線程A,發生硬體中斷,那麼軟中斷執行的時候,當前線程還是A,但是執行時間久了之後,會卡住系統,因為軟中斷是除了硬體中斷之外優先順序最高的,軟中斷執行過程中,整個系統就調度就停止了。為了避免卡住系統,處理時間久了之後(2ms),就會啟動一個內核線程B(tasklet),專門處理軟中斷,而A線程會繼續,從內核態退出。

軟中斷之後有的就會結束這次硬體中斷邏輯。有的還會啟動workqueue繼續處理。比如網路協議棧,除了從設備讀寫之外,還要繼續處理協議內容。因為軟中斷優先順序太高,不能執行太長時間,需要長時間處理的,會再分級別。不要緊的部分放到workqueue線程去處理。


STM32是通過中斷,就和其他匯流排外設一樣。別的硬體我不知道


推薦閱讀:
相關文章