NAPI

NAPI 是 Linux 網路堆疊使用的事件處理機制。NAPI 這個名稱不再特指任何事物[1]

在基本操作中,裝置透過中斷通知主機新事件。主機隨後排程一個 NAPI 例項來處理這些事件。裝置也可以透過 NAPI 輪詢事件,而無需首先接收中斷(忙輪詢)。

NAPI 處理通常發生在軟體中斷上下文中,但也有選項可以使用單獨的核心執行緒進行 NAPI 處理。

總而言之,NAPI 抽象了驅動程式對事件(資料包接收 Rx 和傳送 Tx)處理的上下文和配置。

驅動程式API

NAPI 最重要的兩個元素是 `struct napi_struct` 結構體及其關聯的輪詢方法。`struct napi_struct` 儲存 NAPI 例項的狀態,而該方法是驅動程式特定的事件處理程式。該方法通常會釋放已傳送的 Tx 資料包並處理新接收的 Rx 資料包。

控制API

netif_napi_add()netif_napi_del() 用於向系統新增/刪除 NAPI 例項。這些例項附加到作為引數傳遞的網絡卡裝置 (netdevice) 上(並在網絡卡設備註銷時自動刪除)。例項在停用狀態下新增。

napi_enable()napi_disable() 管理停用狀態。停用的 NAPI 無法被排程,並且其輪詢方法保證不會被呼叫。napi_disable() 會等待 NAPI 例項的所有權被釋放。

這些控制 API 不是冪等的。控制 API 呼叫對於資料路徑 API 的併發使用是安全的,但錯誤的控制 API 呼叫序列可能導致崩潰、死鎖或競態條件。例如,連續多次呼叫 napi_disable() 將導致死鎖。

資料路徑 API

napi_schedule() 是排程 NAPI 輪詢的基本方法。驅動程式應在其中斷處理程式中呼叫此函式(更多資訊請參見排程和中斷遮蔽)。成功呼叫 napi_schedule() 將獲得 NAPI 例項的所有權。

隨後,在 NAPI 被排程後,將呼叫驅動程式的輪詢方法來處理事件/資料包。該方法接受一個 budget 引數——驅動程式可以處理任意數量的 Tx 資料包的完成,但只能處理最多 budget 數量的 Rx 資料包。Rx 處理通常昂貴得多。

換句話說,對於 Rx 處理,budget 引數限制了驅動程式在一次輪詢中可以處理的資料包數量。當 budget 為 0 時,無法使用像頁面池 (page pool) 或 XDP 這樣的 Rx 特定 API。skb Tx 處理應無論 budget 值如何都發生,但如果該引數為 0,驅動程式則無法呼叫任何 XDP(或頁面池)API。

警告

如果核心程式碼僅嘗試處理 skb Tx 完成而沒有 Rx 或 XDP 資料包,則 budget 引數可能為 0。

輪詢方法返回已完成的工作量。如果驅動程式仍有未完成的工作(例如 budget 已用盡),輪詢方法應精確返回 budget。在這種情況下,NAPI 例項將被再次服務/輪詢(無需重新排程)。

如果事件處理已完成(所有未完成的資料包都已處理),輪詢方法應在返回之前呼叫 napi_complete_done()napi_complete_done() 釋放該例項的所有權。

警告

必須仔細處理完成所有事件並恰好用盡 budget 的情況。沒有辦法將這種(罕見)情況報告給堆疊,因此驅動程式必須要麼不呼叫 napi_complete_done() 並等待再次被呼叫,要麼返回 budget - 1

如果 budget 為 0,則絕不應呼叫 napi_complete_done()

呼叫序列

驅動程式不應假定呼叫的確切順序。輪詢方法可能在驅動程式未排程例項的情況下被呼叫(除非例項被停用)。同樣,即使 napi_schedule() 成功,也無法保證輪詢方法一定會被呼叫(例如,如果例項被停用)。

正如控制API部分所述——napi_disable() 及後續對輪詢方法的呼叫僅等待例項所有權被釋放,而不是等待輪詢方法退出。這意味著驅動程式在呼叫 napi_complete_done() 後應避免訪問任何資料結構。

排程和中斷遮蔽

驅動程式在排程 NAPI 例項後應保持中斷被遮蔽——直到 NAPI 輪詢完成,任何進一步的中斷都是不必要的。

必須顯式遮蔽中斷的驅動程式(與裝置自動遮蔽中斷不同)應使用 napi_schedule_prep()__napi_schedule() 呼叫。

if (napi_schedule_prep(&v->napi)) {
    mydrv_mask_rxtx_irq(v->idx);
    /* schedule after masking to avoid races */
    __napi_schedule(&v->napi);
}

只有在成功呼叫 napi_complete_done() 之後,中斷才應被解除遮蔽。

if (budget && napi_complete_done(&v->napi, work_done)) {
  mydrv_unmask_rxtx_irq(v->idx);
  return min(work_done, budget - 1);
}

napi_schedule_irqoff()napi_schedule() 的一個變體,它利用了在中斷 (IRQ) 上下文中呼叫所提供的保證(無需遮蔽中斷)。如果中斷是執行緒化的(例如啟用了 PREEMPT_RT),napi_schedule_irqoff() 將退回到 napi_schedule()

例項到佇列的對映

現代裝置每個介面有多個 NAPI 例項(struct napi_struct)。對於例項如何對映到佇列和中斷沒有嚴格要求。NAPI 主要是一種輪詢/處理抽象,沒有特定的面向使用者語義。儘管如此,大多數網路裝置最終都會以相當相似的方式使用 NAPI。

NAPI 例項通常與中斷和佇列對(佇列對是單個 Rx 佇列和單個 Tx 佇列的集合)呈 1:1:1 對應關係。

在較少見的情況下,一個 NAPI 例項可能用於多個佇列,或者 Rx 和 Tx 佇列可以在單個核心上由單獨的 NAPI 例項提供服務。然而,無論佇列分配如何,NAPI 例項和中斷之間通常仍然存在 1:1 的對映。

值得注意的是,ethtool API 使用“通道”術語,其中每個通道可以是 rxtxcombined。目前尚不清楚通道的具體構成;推薦的解釋是將通道理解為服務給定型別佇列的中斷/NAPI。例如,配置為 1 個 rx、1 個 tx 和 1 個 combined 通道,預計將使用 3 箇中斷、2 個 Rx 佇列和 2 個 Tx 佇列。

NAPI 持久配置

驅動程式經常動態分配和釋放 NAPI 例項。這導致每次重新分配 NAPI 例項時,NAPI 相關的使用者配置都會丟失。netif_napi_add_config() API 透過將每個 NAPI 例項與基於驅動程式定義的索引值(例如佇列號)的持久 NAPI 配置關聯起來,從而防止了這種配置丟失。

使用此 API 可以實現持久的 NAPI ID(以及其他設定),這對於使用 SO_INCOMING_NAPI_ID 的使用者空間程式可能很有益。其他 NAPI 配置設定請參見以下章節。

驅動程式應儘可能嘗試使用 netif_napi_add_config()

使用者API

使用者與 NAPI 的互動取決於 NAPI 例項 ID。例項 ID 僅透過 SO_INCOMING_NAPI_ID 套接字選項對使用者可見。

使用者可以使用 netlink 查詢裝置或裝置佇列的 NAPI ID。這可以在使用者應用程式中透過程式設計方式完成,或者使用核心原始碼樹中包含的指令碼:tools/net/ynl/pyynl/cli.py

例如,使用該指令碼轉儲裝置的所有佇列(這將揭示每個佇列的 NAPI ID)。

$ kernel-source/tools/net/ynl/pyynl/cli.py \
          --spec Documentation/netlink/specs/netdev.yaml \
          --dump queue-get \
          --json='{"ifindex": 2}'

有關可用操作和屬性的更多詳細資訊,請參見 Documentation/netlink/specs/netdev.yaml

軟體中斷合併

NAPI 預設不執行任何顯式事件合併。在大多數情況下,批處理是由於裝置進行的中斷合併 (IRQ coalescing) 而發生的。在某些情況下,軟體合併是有幫助的。

NAPI 可以配置為在所有資料包處理完畢後,啟動一個重新輪詢計時器,而不是立即解除硬體中斷的遮蔽。網絡卡裝置 (netdevice) 的 gro_flush_timeout sysfs 配置被重用以控制計時器的延遲,而 napi_defer_hard_irqs 則控制在 NAPI 放棄並恢復使用硬體中斷之前,連續空輪詢的次數。

上述引數也可以透過 netlink 經由 netdev-genl 進行每 NAPI 例項的設定。當與 netlink 配合使用並按每 NAPI 例項配置時,上述引數使用連字元而不是下劃線:gro-flush-timeoutnapi-defer-hard-irqs

每 NAPI 例項配置可以在使用者應用程式中透過程式設計方式完成,或者使用核心原始碼樹中包含的指令碼:tools/net/ynl/pyynl/cli.py

例如,使用該指令碼

$ kernel-source/tools/net/ynl/pyynl/cli.py \
          --spec Documentation/netlink/specs/netdev.yaml \
          --do napi-set \
          --json='{"id": 345,
                   "defer-hard-irqs": 111,
                   "gro-flush-timeout": 11111}'

類似地,引數 irq-suspend-timeout 可以透過 netlink 經由 netdev-genl 設定。該值沒有全域性的 sysfs 引數。

irq-suspend-timeout 用於確定應用程式可以完全暫停中斷 (IRQs) 的時長。它與 SO_PREFER_BUSY_POLL 結合使用,後者可以透過 EPIOCSPARAMS ioctl 在每 epoll 上下文的基礎上設定。

忙輪詢

忙輪詢允許使用者程序在裝置中斷觸發之前檢查傳入的資料包。與任何忙輪詢一樣,它以 CPU 週期為代價換取更低的延遲(NAPI 忙輪詢的生產用途不為人所熟知)。

忙輪詢可以透過在選定的套接字上設定 SO_BUSY_POLL,或者使用全域性的 net.core.busy_pollnet.core.busy_read sysctls 來啟用。還存在一個用於 NAPI 忙輪詢的 io_uring API。

基於 epoll 的忙輪詢

可以直接從對 epoll_wait 的呼叫中觸發資料包處理。為了使用此功能,使用者應用程式必須確保新增到 epoll 上下文的所有檔案描述符都具有相同的 NAPI ID。

如果應用程式使用專用的接受器執行緒,則可以透過 SO_INCOMING_NAPI_ID 獲取傳入連線的 NAPI ID,然後將該檔案描述符分發給工作執行緒。工作執行緒會將該檔案描述符新增到其 epoll 上下文中。這將確保每個工作執行緒都有一個包含相同 NAPI ID 檔案描述符的 epoll 上下文。

或者,如果應用程式使用 SO_REUSEPORT,則可以插入 bpf 或 ebpf 程式,將傳入連線分發給執行緒,從而使每個執行緒僅獲得具有相同 NAPI ID 的傳入連線。必須注意仔細處理系統可能具有多個網絡卡 (NIC) 的情況。

為了啟用忙輪詢,有兩種選擇:

  1. /proc/sys/net/core/busy_poll 可以設定為一個微秒 (useconds) 時間,以進行忙迴圈等待事件。這是一個系統範圍的設定,將導致所有基於 epoll 的應用程式在呼叫 epoll_wait 時進行忙輪詢。這可能不理想,因為許多應用程式可能不需要忙輪詢。

  2. 使用較新核心的應用程式可以在 epoll 上下文檔案描述符上發出 ioctl,以設定 (EPIOCSPARAMS) 或獲取 (EPIOCGPARAMS) struct epoll_params: 結構體,使用者程式可以定義如下:

struct epoll_params {
    uint32_t busy_poll_usecs;
    uint16_t busy_poll_budget;
    uint8_t prefer_busy_poll;

    /* pad the struct to a multiple of 64bits */
    uint8_t __pad;
};

中斷緩解

雖然忙輪詢應由低延遲應用程式使用,但類似的機制也可用於中斷 (IRQ) 緩解。

非常高的每秒請求量應用程式(特別是路由/轉發應用程式以及使用 AF_XDP 套接字的應用程式)可能不希望在完成處理請求或一批資料包之前被中斷。

此類應用程式可以向核心承諾,它們將定期執行忙輪詢操作,並且驅動程式應使裝置中斷 (IRQs) 永久遮蔽。此模式透過使用 SO_PREFER_BUSY_POLL 套接字選項啟用。為了避免系統異常行為,如果 gro_flush_timeout 經過且沒有任何忙輪詢呼叫,則該承諾將被撤銷。對於基於 epoll 的忙輪詢應用程式,可以將 struct epoll_paramsprefer_busy_poll 欄位設定為 1,併發出 EPIOCSPARAMS ioctl 以啟用此模式。更多詳細資訊請參見上一節。

NAPI 用於忙輪詢的預算低於預設值(考慮到正常忙輪詢的低延遲意圖,這是有道理的)。然而,中斷緩解 (IRQ mitigation) 的情況並非如此,因此可以使用 SO_BUSY_POLL_BUDGET 套接字選項調整預算。對於基於 epoll 的忙輪詢應用程式,可以在 struct epoll_params 中將 busy_poll_budget 欄位調整到所需值,並使用 EPIOCSPARAMS ioctl 在特定的 epoll 上下文中設定。更多詳細資訊請參見上一節。

值得注意的是,為 gro_flush_timeout 選擇一個較大的值將延遲中斷 (IRQs),以實現更好的批處理,但在系統未完全負載時會引入延遲。為 gro_flush_timeout 選擇一個較小的值可能會導致嘗試透過裝置中斷和軟中斷處理進行忙輪詢的使用者應用程式受到干擾。應仔細權衡這些利弊來選擇此值。基於 epoll 的忙輪詢應用程式可以透過為 maxevents 選擇適當的值來緩解使用者處理的量。

使用者可能希望考慮另一種方法——中斷暫停 (IRQ suspension),以幫助應對這些權衡。

中斷暫停

中斷暫停是一種機制,其中裝置中斷 (IRQs) 在 epoll 觸發 NAPI 資料包處理時被遮蔽。

當應用程式呼叫 epoll_wait 成功檢索事件時,核心將延遲中斷暫停計時器。如果核心在忙輪詢期間未檢索到任何事件(例如,因為網路流量水平下降),則中斷暫停將被停用,並啟用上述的中斷緩解策略。

這允許使用者平衡 CPU 消耗與網路處理效率。

要使用此機制:

  1. 每個 NAPI 例項的配置引數 irq-suspend-timeout 應設定為應用程式可以暫停其中斷 (IRQs) 的最長時間(以納秒為單位)。這可以透過 netlink 完成,如上所述。此超時用作一種安全機制,以在應用程式停滯時重新啟動中斷驅動程式的處理。應選擇此值,使其涵蓋使用者應用程式從其對 epoll_wait 的呼叫中處理資料所需的時間,並注意應用程式可以透過在呼叫 epoll_wait 時設定 max_events 來控制檢索的資料量。

  2. sysfs 引數或每個 NAPI 例項的配置引數 gro_flush_timeoutnapi_defer_hard_irqs 可以設定為較低的值。它們將在忙輪詢未發現數據後用於延遲中斷 (IRQs)。

  3. prefer_busy_poll 標誌必須設定為 true。這可以使用 EPIOCSPARAMS ioctl 完成,如上所述。

  4. 應用程式使用 epoll 來觸發 NAPI 資料包處理,如上所述。

如上所述,只要後續對 epoll_wait 的呼叫向用戶空間返回事件,irq-suspend-timeout 就會被延遲,並且中斷 (IRQs) 將被停用。這允許應用程式在不受干擾的情況下處理資料。

一旦對 epoll_wait 的呼叫沒有找到事件,中斷暫停 (IRQ suspension) 將自動停用,並且 gro_flush_timeoutnapi_defer_hard_irqs 緩解機制將接管。

預計 irq-suspend-timeout 將設定為遠大於 gro_flush_timeout 的值,因為 irq-suspend-timeout 應在一次使用者空間處理週期內暫停中斷 (IRQs)。

雖然使用 napi_defer_hard_irqsgro_flush_timeout 來使用中斷暫停 (IRQ suspension) 並非嚴格必要,但強烈建議使用它們。

中斷暫停 (IRQ suspension) 會導致系統在輪詢模式和中斷驅動的資料包傳輸之間交替。在忙碌期間,irq-suspend-timeout 會覆蓋 gro_flush_timeout 並使系統保持忙輪詢,但當 epoll 未發現事件時,gro_flush_timeoutnapi_defer_hard_irqs 的設定將決定下一步。

網路處理和資料包傳輸本質上有三種可能的迴圈:

  1. 硬中斷 (hardirq) -> 軟中斷 (softirq) -> NAPI 輪詢;基本中斷傳輸

  2. 計時器 -> 軟中斷 (softirq) -> NAPI 輪詢;延遲中斷處理

  3. epoll -> 忙輪詢 -> NAPI 輪詢;忙迴圈

如果設定了 gro_flush_timeoutnapi_defer_hard_irqs,迴圈 2 可以從迴圈 1 接管控制。

如果設定了 gro_flush_timeoutnapi_defer_hard_irqs,則迴圈 2 和迴圈 3 會互相“爭奪”控制權。

在忙碌期間,irq-suspend-timeout 在迴圈 2 中用作計時器,這本質上使網路處理傾向於迴圈 3。

如果未設定 gro_flush_timeoutnapi_defer_hard_irqs,則迴圈 3 無法從迴圈 1 接管控制。

因此,建議設定 gro_flush_timeoutnapi_defer_hard_irqs,因為否則設定 irq-suspend-timeout 可能沒有任何可察覺的效果。

執行緒化 NAPI

執行緒化 NAPI 是一種操作模式,它使用專用的核心執行緒而不是軟體中斷 (IRQ) 上下文進行 NAPI 處理。該配置是按網絡卡裝置 (netdevice) 進行的,並將影響該裝置的所有 NAPI 例項。每個 NAPI 例項將生成一個單獨的執行緒(命名為 napi/${ifc-name}-${napi-id})。

建議將每個核心執行緒繫結到單個 CPU,與處理中斷的 CPU 相同。請注意,中斷 (IRQs) 和 NAPI 例項之間的對映可能並非簡單直觀(並且取決於驅動程式)。NAPI 例項 ID 將以與核心執行緒的程序 ID 相反的順序分配。

執行緒化 NAPI 透過向網絡卡裝置 (netdev) 的 sysfs 目錄中的 threaded 檔案寫入 0/1 來控制。

腳註