天天看點

linux網絡軟中斷softirq底層機制及并發優化

在實際生産系統環境中,我們經常碰到過高的軟中斷導緻CPU的si負載偏高,進而導緻性能伺服器性能出現瓶頸。而這種瓶頸出現的時候往往是在業務高峰期,此時很多優化手段不敢輕易去上,隻能祈禱平穩度過。但是如果能從底層去了解網絡軟中斷,就可以在事前将優化做充足。

軟中斷(softirq)表示可延遲函數的所有種類, linux上使用的軟中斷個數是有限的,linux最多注冊32個,目前使用了10個左右,在include/linux/interrupt.h中定義,如下。

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上,是以軟中斷是可重入函數必須使用自旋鎖保護其資料結構。一個軟中斷不會去搶占另外一個軟中斷。特别适合網絡後半段的處理。

軟中斷通過open_softirq函數(定義在kernel/softirq.c檔案中)來注冊的。open_softirq注冊一個軟中斷處理函數,即在軟中斷向量表softirq_vec數組中添加新的軟中斷處理action函數。

我們可以從start_kernel函數開始,該函數定義在init/main.c中。會調用softirq_init(),該函數會調用open_softirq函數來注冊相關的軟中斷,但是并沒有注冊網絡相關的軟中斷:

  該函數如下:

void __init softirq_init(void)

{

        int cpu;

        for_each_possible_cpu(cpu) {

                per_cpu(tasklet_vec,

cpu).tail =

&per_cpu(tasklet_vec, cpu).head;

per_cpu(tasklet_hi_vec, cpu).tail =

&per_cpu(tasklet_hi_vec, cpu).head;

        }

open_softirq(TASKLET_SOFTIRQ, tasklet_action);

        open_softirq(HI_SOFTIRQ,

tasklet_hi_action);

}      

            那麼網絡相關的軟中斷在哪裡呢?其也是在startup_kernel函數中的中,調用鍊路如下:

            startup_kernel->rest_init->kernel_init->kernel_init_freeable->do_basic_setup();         而do_basic_setup函數會進行驅動設定。會通過調用net_dev_init函數。

net_dev_init函數(定義在net/core/dev.c),最注冊軟中斷,如下:

  open_softirq(NET_TX_SOFTIRQ, net_tx_action); 

open_softirq(NET_RX_SOFTIRQ,

net_rx_action);

定義在:kernel/softirq.c檔案中

              void open_softirq(int nr, void (*action)(struct softirq_action

*))

{              

        softirq_vec[nr].action =

action;

}  

這個就是網絡接收和發送的軟中斷,并關聯兩個函數net_tx_action和net_rx_action。軟中斷由softirq_action結構體表示,

static struct softirq_action

softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

            定義有了,那何時會去調用呢?

這需要回到網卡的中斷函數中(位于驅動代碼中)

在網卡驅動的中斷函數中(如果是e1000,則是e1000_intr函數),其會調用__napi_schedule函數,其調用____napi_schedule,該函數會設定NET_RX_SOFTIRQ。

<b>net/core/dev.c</b>

void __napi_schedule(struct napi_struct *n)

        unsigned long flags;

        local_irq_save(flags);

____napi_schedule(this_cpu_ptr(&amp;softnet_data), n);

        local_irq_restore(flags);

}

/* Called with irq disabled */

static inline void ____napi_schedule(struct softnet_data *sd,

struct napi_struct *napi)

list_add_tail(&amp;napi-&gt;poll_list, &amp;sd-&gt;poll_list);

__raise_softirq_irqoff(NET_RX_SOFTIRQ);

<b>kernel/softirq.c</b><b>檔案</b>

void __raise_softirq_irqoff(unsigned int nr)

        trace_softirq_raise(nr);

        or_softirq_pending(1UL &lt;&lt; nr);

include/linux/interrupt.h檔案中:

#define or_softirq_pending(x)  (local_softirq_pending() |= (x))

arch/ia64/include/asm/hardirq.h檔案中:

#define local_softirq_pending()         (local_cpu_data-&gt;softirq_pending)

arch/ia64/include/asm/processor.h檔案中:

#define local_cpu_data         

(&amp;__ia64_per_cpu_var(ia64_cpu_info))

<b>ia64_cpu_info</b><b>的結構體為</b>cpuinfo_ia64,定義在在檔案<b>arch/ia64/include/asm/processor.h</b><b>中,定義了。其中定義了</b><b>CPU</b><b>類型,硬體</b><b>BUG</b><b>标志,</b><b> CPU</b><b>狀态等。</b><b></b>

struct cpuinfo_ia64 {

        unsigned int softirq_pending;  

………

DECLARE_PER_CPU(struct cpuinfo_ia64, ia64_cpu_info);

<b>而</b>__ia64_per_cpu_var<b>是取變量位址。</b><b></b>

<b>這樣就可以看到,跟軟中斷相關的字段是每個</b><b>CPU</b><b>都有一個</b><b>64</b><b>位</b><b>(32</b><b>位機器就是</b><b>32</b><b>位</b><b>)</b><b>掩碼的字段</b><b></b>

<b>他描述挂起的軟中斷。每一位對應相應的軟中斷。比如</b><b>0</b><b>位代表</b><b>HI_SOFTIRQ.</b>

明白了or_softirq_pending函數設定了CPU中NET_RX_SOFTIRQ<b>,表示軟中斷挂起。</b><b></b>

netif_rx該函數(<b>net/core/dev.c</b>)不特定于網絡驅動程式,主要實作從驅動中擷取包并丢到緩存隊列中,等待其被處理。有些驅動(例如<b>arch/ia64/hp/sim/simeth.c</b>),在中斷函數中調用,

netif_rx, 而netif_rx函數調用enqueue_to_backlog函數,最後也會調用____napi_schedule函數。而e1000驅動則是直接調用了__napi_schedule函數.

NET_RX_SOFTIRQ(<b>include/linux/interrupt.h</b>)标記。

現在系統有挂起的軟中斷了,那麼誰去運作呢?

l   當調用local_bh_enable()函數激活本地CPU的軟中斷時。條件滿足就調用do_softirq()

來處理軟中斷。

l   當do_IRQ()完成硬中斷處理時調用irq_exit()時會喚醒ksoftirq來處理軟中斷。

l   當核心線程ksoftirq/n被喚醒時,處理軟中斷。

以上幾點在不同版本中會略有變化,比如某個函數放被包含在另一個函數裡面了。在不影響大局了解的前提下,暫時不用去關心這個。

先來看下do_IRQ函數,該函數(<b>arch/x86/kernel/irq.c</b><b>檔案</b>)處理普通裝置的中斷。該函數會調用irq_exit()函數。<b>irq_exit</b><b>函數在</b><b>kernel/softirq.c</b><b>檔案中定義,該函數會調用</b>local_softirq_pending(),如果有挂起的軟中斷,就調用invoke_softirq函數,如果ksoftirq在運作就傳回,如果沒有運作就調用wakeup_softirqd喚醒ksoftirq。

執行軟中斷函數do_softirq 參見于kernel/softirq.c檔案,如果有待處理的軟中斷,會調用__do_softirq()函數, 然後執行相應軟中斷處理函數,注冊兩個函數net_tx_action和net_rx_action。

asmlinkage void do_softirq(void)

        __u32 pending;

        unsigned long flags;

        if (in_interrupt())

                return;

        pending =

local_softirq_pending();

        if (pending)

                __do_softirq();

函數中有pending = local_softirq_pending();

用于擷取是否有挂起的軟中斷。

每個CPU下都有一個核心函數程序,他叫做ksoftirq/k,如果是第0個CPU,則程序的名字叫做ksoftirq/0。

            真正的軟中斷處理函數net_rx_action和net_tx_action做什麼呢?

            net_rx_action(net/core/dev.c)用作軟中斷的處理程式,net_rx_action調用裝置的poll方法(預設為process_backlog),process_backlog函數循環處理所有分組。調用__skb_dequeue從等待隊列移除一個套接字緩沖區。

調用__netif_receive_skb(net/core/dev.c)函數,分析分組類型、處理橋接,然後調用deliver_skb(net/core/dev.c),該函數調用packet_type-&gt;func使用特定于分組類型的處理程式。

linux網絡軟中斷softirq底層機制及并發優化

到此我們對軟中斷的整個流程有了清晰的認識,下面開始針對幾個細節進行學習并探究如何在系統中去優化軟中斷并發。

網線收到幀(包處理後為幀)後,會将幀拷貝到網卡内部的FIFO緩沖區(一般現在網卡都支援DMA,如果支援則放到DMA記憶體中),然後觸發硬體中斷。硬體中斷函數屬于網卡驅動,在網卡驅動中實作。

  中斷處理函數會在一個CPU上運作,如果綁定了一個核就在綁定的核上運作。硬中斷處理函數建構sk_buff,把frame從網卡FIFO拷貝到記憶體skb中,然後觸發軟中斷。如果軟中斷不及時處理核心緩存中的幀,也會導緻丢包。這個過程要注意的是,如果網卡中斷時綁定在CPU0上處理硬中斷的,那麼其觸發的軟中斷也是在CPU0上的,因為修改的NET_RX_SOFTIRQ是cpu-per的變量,隻有其上的ksoftirq程序會去讀取及執行。

多隊列網卡由原來的單網卡單隊列變成了現在的單網卡多隊列。通過多隊列網卡驅動的支援,将各個隊列通過中斷綁定到不同的CPU核上,以滿足網卡的需求,這就是多隊列網卡的應用。

是以,加大隊列數量可以優化系統網絡性能,例如10GE的82599網卡,最大可以增加到64個網卡隊列。

RSS (Receive

Side Scaling ) (接收側的縮放) 

把不同的流分散的不同的網卡多列中,就是多隊列的支援,在2.6.36中引入。網卡多隊列的驅動提供了一個核心子產品參數,用來指定硬體隊列個數。每個接收隊列都有一個單獨的IRQ(中斷号),PCIe裝置使用MSI-x來路由每個中斷到CPU,有效隊列的IRQ的映射由/proc/interrupts來指定的。一個終端能被任何一個CPU處理。一些系統預設運作irqbalance來優化中斷(但是在NUMA架構下不太好,不如手動綁定到制定的CPU)。

RPS Receive

Packet Steering (接收端包的控制) 

邏輯上以軟體方式實作RSS,适合于單隊列網卡或者虛拟網卡,把該網卡上的資料流讓多個cpu處理。在netif_rx() 函數和netif_receive_skb()函數中調用get_rps_cpu (定義在net/core/dev.c),來選擇應該執行包的隊列。基于包的位址和端口(有的協定是2元組,有的協定是4元組)來計算hash值。hash值是由硬體來提供的,或者由協定棧來計算的。hash值儲存在skb-&gt;rx_hash中,該值可以作為流的hash值可以被使用在棧的其他任何地方。每一個接收硬體隊列有一個相關的CPU清單,RPS就可以将包放到這個隊列中進行處理,也就是指定了處理的cpu.最終實作把軟中斷的負載均衡到各個cpu。需要配置了才能使用,預設資料包由中斷的CPU來處理的。

對于一個多隊列的系統,如果RSS已經配置了,導緻一個硬體接收隊列已經映射到每一個CPU。那麼RPS就是多餘的和不必要的。如果隻有很少的硬體中斷隊列(比CPU個數少),每個隊列的rps_cpus 指向的CPU清單與這個隊列的中斷CPU共享相同的記憶體域,那RPS将會是有效的。

RFS Receive Flow

Steering (接收端流的控制) :

RPS依靠hash來控制資料包,提供了好的負載平衡,隻是單純把資料包均衡到不同的cpu,如果應用程式所在的cpu和軟中斷處理的cpu不是同一個,那麼對于cpu cache會有影響。

RFS依靠RPS的機制插入資料包到指定CPU隊列,并喚醒該CPU來執行。

資料包并不會直接的通過資料包的hash值被轉發,但是hash值将會作為流查詢表的索引。這個表映射資料流與處理這個流的CPU。流查詢表的每條記錄中所記錄的CPU是上次處理資料流的CPU。如果記錄中沒有CPU,那麼資料包将會使用RPS來處理。多個記錄會指向相同的CPU。

rps_sock_flow_table是一個全局的資料流表,sock_rps_record_flow()來記錄rps_sock_flow_table表中每個資料流表項的CPU号。

RFS使用了第二個資料流表來為每個資料流跟蹤資料包:rps_dev_flow_table被指定到每個裝置的每個硬體接收隊列。

加速RFS

加速RFS需要核心編譯CONFIG_RFS_ACCEL,

需要NIC裝置和驅動都支援。加速RFS是一個硬體加速的負載平衡機制。要啟用加速RFS,網絡協定棧調用ndo_rx_flow_steer驅動函數為資料包通訊理想的硬體隊列,這個隊列比對資料流。當rps_dev_flow_table中的每個流被更新了,網絡協定棧自動調用這個函數。驅動輪流地使用一種裝置特定的方法指定NIC去控制資料包。如果想用RFS并且NIC支援硬體加速,都需要開啟硬體加速RFS。

XPS Transmit

Packet Steering(發送端包的控制)

XPS要求核心編譯了CONFIG_XPS,根據目前處理軟中斷的cpu選擇網卡發包隊列, XPS主要是為了避免cpu由RX隊列的中斷進入到TX隊列的中斷時發生切換,導緻cpu

cache失效損失性能

            最後幾個優化手段:

l   對于開了超線程的系統,一個中斷隻綁定到其中一個。

l   對于一個多隊列的系統,多列隊已經支援。那麼RPS就是多餘、不必要的。如果隻有很少的硬體中斷隊列(比CPU個數少),每個隊列的rps_cpus 指向的CPU清單與這個隊列的中斷CPU共享相同的記憶體域,那RPS将會是有效的。

l   RFS主要是為了避免cpu由核心态進入到使用者态的時候發生切換,導緻cpu cache失效損失性能。

l   不管什麼時候,想用RFS并且NIC支援硬體加速,都開啟硬體加速RFS。

繼續閱讀