多核心Linux核心路徑最佳化的不二法門之-多核心平台TCP最佳化
0.聲明:0).關於來源
昨天就答應皮鞋廠老闆了,只是昨晚心情太複雜,本文沒有趕出來,今天在飛機上寫下了剩餘的...
1).關於Linux原生代碼
本文假設讀者已經對Linux的TCP實現源碼有了足夠清晰的理解,因此不會大量篇幅分析Linux核心關於TCP的原始碼,比如tcp_v4_rcv的流程之類的。
2).關於我的最佳化代碼
由於涉及到很多複雜的因素,本文不提供完整的可編譯的最佳化源碼(整個源碼並非我一人完成,在未經同夥同意之前,我不敢擅作主張,但是想法是我的,所以我能展示的只是原理以及我負責的那部分代碼)。
3).關於TCP協議
本文不談TCP協議本身以及其細節(細節請參考RFC以及各類論文,比如各種流控,擁控演算法之類的),僅包含的內容是TCP協議之外的架構實現的最佳化。
1.Linux的TCP實現1.1.Linux的TCP實現在協議層面分為兩個部分1).串連握手處理TCP首先會通過三向交握建立一個串連,然後就可以傳輸資料了。TCP規範並沒有指定任何的實現方式,當前的socket規範只是其中一種而已。Linux實現了BSD socket規範。
在Linux中,TCP的串連處理和資料轉送處理在代碼層面是合并在一起的。
2).資料轉送處理這個比較簡單,略。
1.2.Linux的TCP在系統架構方面分為兩個部分1).非強制中斷協議棧處理Linux核心在非強制中斷環境中進行協議棧的處理,在這個處理流程的最上方,會有3個分支:直接將skb複製到使用者緩衝區,簡單將skb排入到prequeue隊列,簡單將skb排入backlog隊列。
2).使用者進程處理Linux的socket API的處理是在使用者進程上下文中進行的。通過1.1節,我們知道由於代碼層面上這些都是合并在一起的,因此一個socket會被各種執行流操作,直觀的考慮,這需要大量鎖的開銷。
1.3.串連處理的總體框圖我給出一個串連處理總體框圖,其中紅線表示發生競爭的地方,而正是這些地方阻止了TCP串連的平行處理,圖示如下:
我來一一解釋這些紅線的意義:
1號紅線:由於使用者進程和協議棧操作的是同一個socket,如果使用者進程正在copy資料包資料,那麼協議棧就要停止同樣的操作,反過來也一樣,因此需要暫時鎖定該socket,然而這種大鎖的開銷過於大,因此Linux核心協議棧的實現採用了一個更加優雅的方式。
協議棧鎖定socket:由於非強制中斷處理協議棧,它可能運行在硬中斷之後的任意上下文,因此不能睡眠,故而必須是一把自旋鎖slock,由socket本身保有,該鎖不僅僅保護和使用者進程之間的競態,也保護不同CPU上對同一個socket協議棧操作之間的競態(很常見,一個偵聽socket上可以同時到達很多串連請求[可悲的是,這些請求不能同時被處理!!])。
使用者進程鎖定socket:使用者進程是可以隨時睡眠的,因此可以採用非自旋鎖xlock來保護多個進程之間的競態,然而同時又為了和核心協議棧操作同一個socket的非強制中斷互斥,因此在擷取xlock之前,首先要擷取該socket的slock,當擷取xlock之後或者暫時沒有獲得xlock要睡眠的時候,將slock釋放掉。相關的邏輯如下:
stack_process{ ... spin_lock(socket->slock); //1 process(skb); spin_unlock(socket->slock); ...}user_process{ ... spin_lock(socket->slock); //2 while(true) { ... spin_unlock(socket->slock); 睡眠; spin_lock(socket->slock); //2 if (佔據xlock成功) { break; } } spin_unlock(socket->slock); ...}
可見,Linux採用了以上的方式很完美的解決了兩類問題,第一類問題是操作socket的執行流之間的同步與互斥,第二類問題時非強制中斷上下文和進程上下文之間的鎖的不同。
在理解了socket鎖定之後,我們來看下backlog這個隊列是幹什麼的。其實很簡單,就是將skb推到當前正佔據socket的那個進程的一個隊列裡面,等到進程完成任務,準備釋放socket佔有權的時候,如果發現該隊列裡面有skb,那麼在其上下文中處理它們。這實際上是一種職責轉移,這個轉移也可以帶來一些最佳化效果,那就是直接在socket所屬的使用者進程上下文處理skb,這樣就避免了一部分cache重新整理。
2號紅線:這條線在1號紅線的解釋中已經涉及了,主要就是上述代碼邏輯中的1和2之間的競爭。這個競爭不是最激烈的,本質上它們屬於縱向的競爭,一個核心態非強制中斷和一個進程上下文之間的競爭,在同一個CPU上,一般而言,這類競爭的機率很低,因為同一個CPU同時只能執行一個執行流,假設此時它在核心態執行非強制中斷,那麼使用者態的進程,它一定在睡眠或者被搶佔,比如在accept中睡眠。
使用者態處理和核心態處理,這種縱向的競爭在單CPU上幾乎不會發生,而使用者態的xlock根本就是為瞭解決使用者進程之間的競爭,核心通過一個backlog在面對這種競爭時轉移了資料包處理職責,事實上在xlock上並不存在競爭,backlog的存在反而帶來了一點最佳化效果。
3號紅線(結合3'號紅線):該紅線為瞭解決多個使用者進程之間的競爭。之所以畫出的是TCP串連處理圖而不是資料轉送處理圖,是因為串連圖更能體現問題,在伺服器端,一個主進程fork出來N多的子進程或者建立多個線程同時在一個繼承下來的socket上accept,這幾乎成了伺服器設計的準則,那麼這多個進程/線程同時到達這個slock的時候,爭搶就會很激烈。再看3'號紅線,我們發現針對同一個socket的accept必須排隊進行。
如果你看Linux TCP的實現,你會發現大量的資料結構操作都沒有用鎖,事實上,進入到那裡的,只有你一人,早在入口就排隊了。
在單CPU上,這不會產生什麼後果,因為單CPU即便在最先進的分時搶佔調度器的協調下,本質上也是一個排隊模型,充其量可以插隊罷了,TCP實現的slock和xlock只是規範了這個排隊規則而已。然而在多CPU上,仔細想想這有必要嗎?
4號紅線:說完了使用者態多個進程/線程同時在同一個socket上accept時的排隊,現在看看核心協議棧的處理,也好不到哪裡去。如果多個CPU同時被串連請求中斷,大量的請求針對同一個偵聽socket,那麼大家就要在4號紅線處排隊!而這個情況在單CPU時幾乎並不存在,根本不可能同時有多個CPU執行到同一個地點...
2.多核CPU架構下Linux TCP串連處理效能瓶頸通過以上針對幾條紅線的描述,到此為止,我已經展示了幾個瓶頸,這些點都是要排隊的點。以上的那個框圖事實上非常好,這些排隊點也無可厚非,因為設計一個系統,就是要自包容解決所有問題,競爭只要存在,就一定要通過排隊來解決它,因此上述的架構根本不用更改,鎖還是留在原處。問題的根本不是鎖的存在,問題的根本在於鎖的不易擷取!
TCP的處理運行在各個CPU上,CPU被當作了一種資源,這是作業系統的核心概念,但是反過來呢?如果把CPU當成服務者本身,而socket當成資源,問題就迎刃而解了。這個思路很重要,我在nf-HiPAC中第一次接觸到了它,原來可以把match和rule顛倒過來玩,然後以這個思想做指導我設計了DxR Pro++,現在還是這個思想,TCP的最佳化依然可以這麼做。
3.Linux TCP串連處理最佳化整體考慮一種串連處理被最佳化後的TCP實現,我只關心從accept API中會返回什麼。只要我能快速擷取一個客戶socket,並且不破壞listen,bind這些API本身,修改其實現是必須的。
3.1.縱向拆分socket我將一個listen socket拆分成了兩個部分,上半部和下半部,上半部對應使用者進程,下半部對應核心協議棧。原始的socket是的樣子:
我的socket被改成了下面的樣子:
3.1.1.消解1號紅線和2號紅線一個socket的上半部和下半部只通過一個accept隊列關聯,事實上即使在上半部正在佔有socket的時候,下半部依然可以繼續處理。
然而,事實證明,在執行accept的進程綁定CPU的情況下,1號紅線的消解並沒有預期的效能提升,而2號紅線的消解帶來的效能提升影響也不大。如果是不綁定CPU,效能提升反而比綁定更好,看來橫向和縱向並不是獨立的兩塊,它們會相互影響。
3.2.橫向拆分socket橫向拆分的思路就是將一個socket下半部拆成多個,每一個CPU上強制綁定一個,類似softirqd每一個CPU上一個一樣,這樣可以消解4號紅線。但是為何不拆解socket的上半部呢?我一開始的想法是讓進程自行決定,後來覺得同樣也要強行綁定一個,至於說什麼使用者進程可以自行解除綁定之類的,加一個層次隱藏掉Per CPU socket即可。此時的使用者進程中的檔案描述符只是指示一個socket描述符,而該描述符真正指向的是nr_cpus個Per CPU socket,如所示:
這樣3號紅線,3'號紅線,4號紅線全部消解。事實上,看到這裡好像一切都大功告成了,但是測試的時候,發現還有兩個問題,接下來我會在描述資料結構的拆解中描述這兩個問題。
3.3.全域Listeners雜湊表由於Listener會複製nr_cpus份到每一個CPU上,故而所有的Listeners會被加入到每一個CPU的本地雜湊表中,這種以空間換無鎖並行是值得的,因為伺服器上並不會出現海量的偵聽服務。
3.4.本地Accept隊列和全域Accept隊列如果系統中每一個CPU上都綁定有accept進程,那麼可以保證所有的串連請求都會被特定的進程處理,然而如果有一個CPU上沒有綁定任何accept進程,那麼被排隊到該CPU的Accept隊列的客戶socket將不會返回給任何進程,從而造成客戶socket餓死。
因此引入一個全域Accept隊列。相關的代碼邏輯如下:
stack_enqueue_socket{ if (本CPU上沒有綁定任何與該Listener相關的使用者進程) { spin_lock(g_table->slock); enqueue_global(g_table, cli_socket); spin_unlock(g_table->slock); } else { local_irq_save enqueue_local(per_cpu(table), cli_socket); local_irq_restore }}user_dequeue_socket_for_accept{ if (當前進程沒有綁定到當前CPU) { spin_lock(g_table->slock); cli_socket = dequeue_global(g_table); spin_unlock(g_table->slock); } else { local_irq_save cli_socket = dequeue_local(per_cpu(table)); local_irq_restore } return cli_socket;}
事實上,可以看到,全域的Accept隊列是專門為那些頑固不化的進程設定的,但是還是可以以一把小鎖的代價換來效能提升,因為鎖的粒度小多了。
3.5.非強制中斷CPU分發問題由於每一個CPU上綁定了一個Listener socket的下半部,並且幾乎所有的資料結構都是本地維護的,CPU正式成了TCP的一部分。因此必須保證一件事,那就是3次握手必須由一個CPU處理,否則就會出錯。而由於如今很多啟動了irqbalance的系統,中斷可能會分發到不同的CPU,比如來自特定用戶端的SYN被CPU0處理,而3次握手中的ACK則被CPU1處理,這就造成了錯誤。為了避免這個局面,底層必須維護資料流和CPU之間的映射。這個可以通過RFS的技術來解決。
當第一個SYN包到達之時,處理這個串連握手過程的CPU就確定了,就是當前的CPU,這個和使用者態的進程“跳”到哪個CPU上沒有任何關係。因此實現比RFS要簡單得多。代碼邏輯如下:
netif_receive_skb{ ... hash = myhash(skb) cpu = get_hash_cpu(hash); if (cpu == -1) { record_cpu_hash(hash, cpu); } else if (current_cpu != cpu) { enqueue_to_backlog(skb, cpu); 此後由IPI觸發中斷,被別的CPU處理 } else { 正常的接收邏輯 } ...}
如下:
3.6.與REUSEPORT以及fastsocket的關係我的這個最佳化版本和REUSEPORT沒有任何關係,REUSEPORT是google的一個patch,非常好用,在我們的一個產品中就用到了這個技術,它可以做到在多個偵聽相同IP地址和連接埠的不同socket之間做到負載平衡,然而這個負載平衡的效果則完全取決於hash演算法。事實上Sina的fastsocket就是在REUSEPORT之上做的最佳化,其主要做了CPU Affinity方面的最佳化,效果很不錯。相似的最佳化還有RPS/RFS patch,然而fastsocket更進一步,它不光延續了RPS/RFS的成果,而且把串連效能瓶頸也解決了,其主要的做法就是採用CPU綁定技術對一個Listen socket在REUSEPORT的基礎上做了橫向拆分,複製了多個Listen socket來reuseport。
fastsocket的做法是使用者進程自己決定是否要綁定CPU,因此哪個CPU處理哪些資料包是通過使用者進程設定的,而我的做法則正好相反,我的最佳化從下到上,我是由第一個SYN包恰好中斷的那個CPU作為其握手過程後續被處理的CPU,和使用者進程的設定無關,即便使用者進程沒有綁定CPU,它也充其量只是從全域Accept隊列小代價取客戶socket而已,而如果綁定了CPU,那幾乎是全線無鎖操作。結合3.5小節,我們看下fastsocket是怎麼獲得對應處理該資料包的CPU的:
netif_receive_skb{ ... socket = inet_lookup(skb); cpu = socket->sk_affinity; if (current_cpu != cpu) { enqueue_to_backlog(skb, cpu); 此後由IPI觸發中斷,被別的CPU處理 } else { 正常的接收邏輯 } ...}
fastsocket有一個查socket表的操作,如果是串連處理,對於綁定CPU的socket,可以在本地表中擷取到。如果你覺得fastsocket在這裡平添了一次尋找,那你就錯了,事實上這是fastsocket的另一個最佳化點,即Direct TCP,也就是說在這個位置就尋找具體的socket,連路由之類的都放進去,後續的所有尋找結果都可以放入,也就可以實現一次尋找,多次使用了。
Direct TCP的處理看似不符合協議棧分層處理的規則,在如此的底層處理四層協議,事實上在某些情況也會因此而付出效能代價:
1).如果有大量的包是轉寄的呢?
2).如果有大量的包是UDP的呢?
3).如果是攻擊包呢?
4).如果這些包是接下來要被Netfilter刷掉的呢?
...
這有點像另一個類似的Direct技術,即socket的busy poll。想獲得甚高的效能,你必須對你的系統的行為足夠理解,比如你知道這台伺服器就是專門處理TCP代理請求的,那麼開啟Direct TCP或者busy poll就是有好處的。
而我認為我的這個最佳化方案是一個更加通用的方案,實際上只有單點微調,且不會涉及別的情況,只要你能保證資料包到達的中斷行為是優雅的,後面的處理都是水到渠成的,完全自動化。就算是面對了上述4種非正常資料包,一個hash計算的代價也很小,且不需要面臨RCU鎖的記憶體屏障導致的刷流水問題。現如今,隨著多隊列網卡以及PCI-E MSI的出現和流行,有很多技術可以保證:
1).同一個元組標識的流的資料包到達後總是中斷同一個CPU;
2).處理不同的流的CPU上的負載非常均衡。
只要做到以上2點,其它的事情就不用管了,對於TCP的串連包,資料包會在同一個CPU一路向上一路無鎖到達本地Accept隊列或者全域Accept隊列,接下來會轉向另外一條同樣寬擁有同樣多車道的高速公路,這條新的公路由進程操縱。
最終,最佳化過的TCP串連處理框圖如下:
PS:我的這個方案不需要修改應用程式,也無須連結任何庫,完全替換核心即可。目前對fastopen並不支援。
對於TCP的非握手包的處理,應該怎麼最佳化呢?
4.Linux TCP資料處理最佳化以前,我們以為TCP典型的C/S處理的服務端是一個一對多的模型,因此listen+accept+fork這種流行的socket編程模型幾乎一致持續到現在。期間衍生出各種MPM技術。這種模型本身就讓人覺得TCP的串連處理就是那個樣子,本身就有一個瓶頸在那裡!事實上,不管是fastsocket,還是我的這個方案,都解決了一對多問題,模型成了多對多的。
那麼對於TCP的資料轉送處理,它該怎麼最佳化呢?要知道,它可不是一對多的模型,它是一個天然的一對一的模型,而TCP又是一個嚴格按序的協議,為了保序,採用並行化處理是一個愚蠢的想法。為了最佳化它,我們先給出可能的瓶頸在哪裡。
4.1.一對一TCP串連的瓶頸分析由於TCP串連中會有大量的資料轉送,因此記憶體拷貝是一個瓶頸,然而本文不關注這個,這個可以通過各種技術解決,比如DMA,零拷貝,分散/聚集IO等,本文關注的是CPU親和力的最佳化。另外,考慮到這種ESTABLISHED狀態的一對一串連的數量還有對其的操作。
考慮持續到來的短串連,大量的並發,我們來看下核心中ESTABLISHED雜湊表的壓力
1).持續的建立串連(握手完成,客戶socket會插入表中),鎖表,排隊;
2).持續的串連釋放,鎖表,排隊;
3).大量的串連同時存在,大量的TIME_WAIT,查詢延遲且連累正常串連;
4).TIME_WAIT socket也面臨持續建立,釋放的問題。
...
如果是長串連,可能會緩解,但病根不除,不絕後患。
由於這個是一對一的對稱模型,TCP的按序性不好做並行化本地操作,因此我也就沒有對其進行橫向拆分,沒了橫向拆分,對於縱向拆分也就沒有了意義,因為勢必會出現多個CPU對隊列的爭搶問題。因此與串連處理的最佳化原則完全不同。
4.2.最佳化一對一TCP串連資料轉送處理部分如何最佳化?目標已經很明確,那就是本地化查詢,本地化插入/刪除,盡量減少RCU的記憶體屏障的影響。
和串連處理不同,我們沒法把CPU當成主角,因為一條串連的處理同一個時刻只能在一個CPU上。因此為了加速處理並且維護cache,建立兩條原則:
1).增加一個ESTABLISHED表的本地cache,該cache可以無鎖操作;
2).在使用者進程等待資料期間,禁止進程遷移。
我的邏輯如下,事實證明真的不錯:
tcp_user_receive/poll{ 0.例行RPS操作 1.設定進程為不可遷移 disable中斷 2.將socket加入當前CPU的本地ESTABLISHED表 (最佳化點1:如果本地表沒有才加入) enable中斷 3.sleep_wait_for_data_or_event 4.讀取資料 disable中斷 5.將socket從本地ESTABLISHED表中摘除 (最佳化點1:不需要摘除) enable中斷 6.清除不可遷移位}tcp_stack_receive{ 1.例行tcp_v4_rcv的操作 2.尋找當前CPU的本地ESTABLISHED表 沒有找到的情況下,尋找系統全域的ESTABLISHED表 3.例行tcp_v4_rcv的操作}
很顯然,上述的邏輯基於以下的判斷:既然進程已經在一個CPU上等待資料了,最好的方式就是將這個進程暫時釘死在這個CPU上,並且告知核心協議棧,將非強制中斷上來的資料包交給這個CPU。釘死這個進程是暫時的,只為接收一輪資料,這個顯然要在task_struct中增加一個flag或者借一個bit,而通知核心協議棧的非強制中斷常式則是通過將該socket加入到本地的ESTABLISHED緩衝雜湊表中實現的。
在進程獲得了一輪資料之後,為了不影響Linux系統全域的域調度行為(Linux的域調度可以很好的做到進程的負載平衡同時又不破壞cache的熱度),需要將釘死的進程釋放,同時注意最佳化點1,到底需不需要將socket從本地表摘除呢?鑒於進程遷移一般都是小機率事件,所以進程在相當大的機率下是不會遷移的,因此不摘除socket可以省下一些mips。
4.3.客戶socket與串連處理完全分開處理一對一的TCP socket資料轉送處理和本文第一部分描述的串連處理是沒有關係的,accept返回之後,由哪個進程/線程處理這個客戶socket,是否綁定它到特定的CPU,這些都不會影響串連處理的過程。
4.4.Linux原生協議棧的例行最佳化-prequeue與backlog事實上,由於涉及到資料拷貝,核心協議棧一直都是傾向於讓使用者進程自己來解析,處理一個skb,而核心要做的只是將skb掛在某個隊列中,這是一個非常常規的最佳化。Linux協議棧中存在兩個隊列,prequeue和backlog,前者是當協議棧發現沒有任何一個使用者進程佔據某個socket的時候,其嘗試將skb掛入一個使用者prequeue隊列,當使用者進程調用recv的時候,自行dequeue並處理之,後者則是當協議棧發現當前socket被某個使用者進程佔據的時候,將skb掛入backlog隊列,待使用者進程釋放該socket的時候,自行dequeue並處理之。
4.5.Linux原生協議棧的例行最佳化-RPS/RFS最佳化這實際上是軟體類比的RSS類似的東西,資料已經不少,其宗旨就是盡量讓協議棧處理的CPU和使用者進程處理的CPU是同一個CPU,我們知道,對於串連處理,由於是一對多的關係,這樣並不可行,然而對於一對一的客戶socket,這樣的效果非常好。
4.6.Linux原生協議棧的例行最佳化-early demuxLinux協議棧期望如果在確認一個資料流的第一個資料包是本地接收的前提下,免除後續路由尋找,只實現socket尋找即可。這是一種典型的一次尋找,短路使用的最佳化方案,類似的還有一次尋找,多方多次使用,類似於我對nf_conntrack所做的那樣。
4.7.Linux原生協議棧的例行最佳化-busy poll顧名思義,就是不等協議棧往上送skb,而是自己直接去底層拿,有點像快遞自提那種。然而由於在底層沒有經過協議棧處理,根本就不確定這個資料包是不是發給自己的,因此這會有一定的風險,但是如果在統計意義上你能確定自己系統的行為,那麼也是可以起到最佳化效果的。
5.總結5.1.關於TCP串連處理的最佳化如果你把一個Listener僅僅看作是一個被核心協議棧處理的socket,那麼你很容易找到很多值得最佳化的點,比如這裡把鎖拆分成粒度更細的,那裡判斷一下搶佔什麼的,然後代碼就會越來越複雜和龐大。但是反過來,如果你把一個Listener看作是一個基礎設施,那事情就簡單多了。基礎設施是服務於客戶的,它有兩個基本的性質:
1).它永遠在那裡
2).它不受客戶的控制
對於一個TCP Listener,難道它不該是一個基礎設施嗎?它應該和CPU是一夥的,作為資源然後為串連請求服務,它本不應該是搶CPU的傢伙,不應該抱有“它如何才能搶到更多的CPU時間,如何才能拽取其它Listener的資源....”這樣的想法去最佳化它。
Linux核心中有很多這樣的例子,比如ksoftirqd/0,ksoftirqd/0/1,像這種kxxxx/n這類核心線程都可以看作是基礎設施,在每一個CPU上提供相同的服務,不干涉別的CPU,永遠都是取Per CPU的管理資料。按照這樣的想法,當需要提供一個TCP服務的時候,很容易想法建立一個類似的基礎設施,它將永遠在那裡,每一個CPU上一個,為新到來的串連提供串連服務,它服務的產品就是一個客戶socket,至於說怎麼交給進程,通過兩類Accept隊列提供一個服務視窗!我們假設一個偵聽進程或者線程掛掉了,這絲毫不會影響TCP Listener基礎設施,它的進程無關性繼續為其它的進程提供客戶socket,直到所有的偵聽進程全部掛掉或者主動退出,而這很容易用引用計數來跟蹤。
在這個最佳化版本中,你可以這樣想,從兩類Accept隊列往下,TCP Listener基礎設施與使用者的偵聽進程無關了,不會受到其影響,即便使用者偵聽進程不斷在CPU間跳躍,綁定,解除綁定,它們影響不到Accept隊列下面的TCP Listener基礎設施,受影響的也只是它們自己從哪個Accept隊列裡擷取客戶socket的問題。下面是一個簡圖:
同樣,由於TCP Listener已經基礎設施化了,向上用Accept隊列隔離了使用者進程socket,向下用中斷調度系統隔離了網卡或者網卡隊列,因此也就沒有了很多最佳化版本中面臨的四種情況:隊列比CPU多,隊列與CPU相等,隊列比CPU少,根本就沒有隊列。
5.2.關於TCP傳輸處理的最佳化本文針對這個方面說的不是很多,很大一部分原因在於這部分並不是核心瓶頸,Linux本身的調度系統已經可以很好的應對進程切換,遷移對cache的影響,因此,你會發現,我做的僅有的最佳化也是針對這兩方面的,同時,增加一個本地ESTABLISHED雜湊緩衝會帶來小量的效能提升,這個想法來自於我增加的nf_conntrack的cache以及slab分級cache。
5.3.與資料包轉寄的統一前段時間搞過一個Linux轉寄效能最佳化,當時引入了一個虛擬輸出隊列(VOQ),解決加速比的問題,為了實現線速pps,轉寄的情況存在網卡頻寬的N加速比問題,因此當時建立轉寄基礎設施的時候是把網卡拉過來搭夥合作,而不是CPU,事實上,如果在多隊列網卡上,這二者是完全一致的,網卡隊列可以綁定到CPU,並且還可以一個CPU專門處理輸入,一個專門處理輸出。
配合Per CPU的skb pool(我舉的是卡車運貨的例子),每一個Listener在每一個CPU上都有一個skb pool,這樣對於本地出發的資料包,可以做到無鎖化的分配skb。
現在針對TCP的情況我們可以把轉寄和本地接收統一在一起了,對於轉寄而言,出口是另一塊網卡,對於本地接收而言,出口是Accept隊列(握手包)或者使用者緩衝區(傳輸包)。如果出口處沒有使用者進程,假設不是放入隊列,而是直接丟棄,那麼Per CPU的Accept隊列真的就是一塊網卡了。