談談dpdk應用層包處理程序的多進程和多線程模型選擇時的若干考慮

看到知乎上有個關於linux多進程、多線程的討論:http://www.zhihu.com/question/19903801/answer/14842584

自己項目裏也對這個問題有過很多探討和測試,所以正好開貼整理一下,題目有點長,其實就2點:

1. 多進程模型和多線程模型,這兩種模型在linux上有什麼區別,各有何優缺點?

    這裏僅限於linux平臺,因爲linux平臺跟win平臺關於線程的實現差異很大。

2. 採用intel dpdk做包處理程序,是採用多進程模型好,還是多線程模型好?

 這裏僅限於包處理程序(ips,waf,其他網絡設備引擎),因爲不同應用場景區別也很大。

 

首先知乎裏邊的評論,有個miao網友說的跟我的經驗比較相符,先將其說法貼一下:

"linux使用的1:1的線程模型,在內核中是不區分線程和進程的,都是可運行的任務而已。fork調用clone(最少的共享),pthread_create也是調用clone(最大共享).fork創建會比pthread_create多消耗一點點,因爲要拷貝tables和cow mapping.但是其實差別真的很細微,這些在內核開發者的努力下已經變的很小了。
再來說說contex switch的cost吧。線程的context switch是要比process小一些,因爲線程共享了大部分的memory和tables,當switch的時候這些東西已經在緩存中了。
但是其實差別也很細微。但是在multiprocessor的系統中不共享memory其實是會比共享memory要有一點優勢的,因爲當任務在不同的processor中運行的時候,同步memory帶來的損耗是不可忽視的。"

 

他這裏說了兩點有價值的信息,1  linux裏的線程實現決定,創建、調度、切換線程的開銷跟進程相比,好不了多少。

              2 多核CPU下由於緩存命中率的問題,進程這種天生不共享內存的做法,實際上比線程這種天生共享內存

                的做法,從性能上是有好處的。

這兩點見解跟我們項目實際測試和研究結果是相符合的。下面從幾個方面探討這些問題:

 

1 linux 線程創建方式

linux提供的線程實際上是核外線程,即主要的線程機制是通過應用層面的庫pthread提供的(線程的id分配、線程創建和管理,據說基本實現是pthread庫爲每一個進程維護一個管理線程,單調用 pthread_create等posix API時,調用者與該管理線程通過管道傳遞命令),

核內層面,線程幾乎可以等同於進程。  這裏貼一段從引用1 拷貝的內容:

Linux的線程實現是在覈外進行的,核內提供的是創建進程的接口do_fork()。內核提供了兩個系統調用__clone()和fork(),最終都用不同的參數調用do_fork()核內API。 do_fork() 提供了很多參數,包括CLONE_VM(共享內存空間)、CLONE_FS(共享文件系統信息)、CLONE_FILES(共享文件描述符表)、CLONE_SIGHAND(共享信號句柄表)和CLONE_PID(共享進程ID,僅對核內進程,即0號進程有效)。當使用fork系統調用產生多進程時,內核調用do_fork()不使用任何共享屬性,進程擁有獨立的運行環境。當使用pthread_create()來創建線程時,則最終設置了所有這些屬性來調用__clone(),而這些參數又全部傳給核內的do_fork(),從而創建的”進程”擁有共享的運行環境,只有棧是獨立的,由 __clone()傳入。

         即:Linux下不管是多線程編程還是多進程編程,最終都是用do_fork實現的多進程編程,只是進程創建時的參數不同,從而導致有不同的共享環境。Linux線程在覈內是以輕量級進程的形式存在的,擁有獨立的進程表項,而所有的創建、同步、刪除等操作都在覈外pthread庫中進行。pthread 庫使用一個管理線程(__pthread_manager() ,每個進程獨立且唯一)來管理線程的創建和終止,爲線程分配線程ID,發送線程相關的信號,而主線程pthread_create()) 的調用者則通過管道將請求信息傳給管理線程。

上述內容基本可以這麼表示:

  創建進程= fork ——> do_fork(不使用共享屬性)

      創建線程= pthread_create——>__clone ——> do_fork(共享地址空間(代碼區、數據區)、頁表、文件描述符、信號。。)

這裏其實另外一種多進程創建方式,就是腳本直接啓動多個進程

 

下面再貼一段:

“對於一個進程來說必須有的數據段、代碼段、堆棧段是不是全盤複製呢?對於多進程來說,代碼段是肯定不用複製的,因爲父進程和各子進程的代碼段是相同的,數據段和堆棧段呢?也不一定,因爲在Linux裏廣泛使用的一個技術叫copy-on-write,即寫時拷貝。copy-on-write意味着什麼呢?意味着資源節省,假設有一個變量x在父進程裏存在,當這個父進程創建一個子進程或多個子進程時這個變量x是否複製到了子進程的內存空間呢?不會的,子進程和父進程使用同一個內存空間的變量,但當子進程或父進程要改變變量x的值時就會複製該變量,從而導致父子進程裏的變量值不同。”

這裏我的理解是,剛fork完,子進程和父進程代碼段、頁表等還是共享的,接下去有兩種可能發展方向,1是子進程修改了數據,這時候,代碼段:仍然是共享的,不需要拷貝;堆和靜態數據區: 根據copy-on-wirte機制,不改變值的地方仍然共享,改變值的地方需要重新申請物理頁面並修改值,修改頁表(可能還要拷貝頁表);棧: 不管進程還是線程,都不能共享,都需要創建的時候分配棧區。2是fork之後馬上調用 exec 用新的進程替換,這時候會載入新的代碼段、數據段,構建新的頁表。

對於我們的包處理系統而已,無論怎麼啓動,創建時的性能開銷其實是無所謂的,因爲都是在系統初始化的時候創建。

 

2 調度和切換

由於核內的線程本質就是進程,其調度過程跟進程一樣。切換,不論是進程切換還是線程切換,都需要替換運行環境(內核堆棧,運行時寄存器等),對於內存的切換,內核部分內存是一樣的,用戶空間部分:如果是進程,需要替換頁目錄基址寄存器,如果是線程,不需要替換;總體而言,linux進程和線程的切換,從內存寄存器、內核堆棧寄存器、其他寄存器等的換值開銷應該是差不多的。具體切換代碼參考引用2

但是由於多線程共享地址空間,從一個線程切換到同一個進程上另一個線程運行,頁表,數據區等很多都已經在內存甚至緩存裏,而從一個進程切換到另一個進程,可能由於剛切換進來的進程的頁面被虛擬內存管理模塊替換出去導致的頁面替換開銷,另外還有緩存tlb失效導致的緩存更新開銷,這裏性能有所差別。

 對於我們的包處理系統而已,採用多核架構,主體進程/線程是綁定到不同的物理CPU core上並獨佔的,所以發生調度和切換的情況不多,因而這種影響不是很重要。

3. 地址空間共享相關問題

進程地址空間是獨立的,這意味着,不同進程的內存天生就是不共享的,如果要共享,則需要開發者自己構建共享機制,比如使用IPC。

線程地址空間是共享的,這意味着,同一進程不同線程的內存天生是共享的,如果想要不共享,需要開發者自己實施,比如使用線程本地變量。

進程模型和線程模型,地址空間不共享和共享,會引發以下系列問題:

3.1 進程模型更安全、更健壯、更容易開發

由於一般公司成熟產品不是從無到有一個項目就開發完畢,必然有很多歷史代碼、多項目組合作的代碼,這時候採用多進程模型,

可以有效隔離歷史代碼和當下代碼、不同項目組的代碼,當然,這需要產品本身是可以這麼做的。比如,項目組A開發包處理進程,

項目組B開發包安全檢測功能,兩個功能是兩個進程,這種模型無疑更容易開發和維護

另外,由於天生所以變量都不共享,對開發者要求也比開發多線程要低

3.2 多核下的性能

傳統意義上,一般認爲多線程比多進程性能要高,這其實是有前提的。比如不同線程之間需要頻繁交互大量數據,由於IPC本身的開銷,

如果數據交互非常頻繁且量大,多線程會比多進程性能要高。

對於基於DPDK的多核數據包處理程序而言,由於3個原因,多進程模型更可預見性能高於多線程:

a DPDK提供了基於hugepage的共享內存機制,使得多進程物理地址相同,其虛擬地址也相同,這事實上就跟多線程之間共享地址空間是

一樣的了。即採用DPDK的基礎庫,多進程之間不需要共享部分使用普通內存(libc malloc,靜態區,棧區),相互隔離很安全。需要共享

部分採用dpdk hugepage 內存,通過特殊映射,也能共享虛擬地址。在這片共享內存上交互數據和指針(虛擬地址是一樣的),性能

遠高於利用內核的IPC機制。

b 多核緩存僞共享問題

這個問題在之前帖子裏http://www.cnblogs.com/jiayy/p/3246133.html說過,多核架構一般有3層緩存,緩存命中率是系統整體性能最關鍵是因素之一。緩存命中率有一個致命殺手就是

僞共享現象,多線程由於天生所有內存全部是共享的,所以更容易發生僞共享現象,其任何變量,只要一個CPU核改了,其餘CPU核都產生

一次緩存失效並重新加載。。,而多進程模型,共享部分是有限的且開發者可以精確設計和控制的,其僞共享現象可以得到有效控制。

在項目實際開發中,經常的情況就是多線程性能低於多進程,需要將很大變量改爲線程局部變量,才能讓性能有所提升。

c 同步互斥

其實,無論是多線程還是多進程,都需要面臨同步和互斥,這個不是進程/線程模型決定的,而是業務模型決定的。dpdk 提供了應用層

空間實現的基礎互斥同步接口,包括原子操作、自旋鎖、讀寫鎖等,主要是配合共享內存的訪問,因爲從數據包處理系統來說,基本上

沒有阻塞的概念,所以這種原子操作和忙等待的鎖可以滿足大部分需求,對於需要阻塞的系統,比如應用層協議棧,則還是需要使用內核的

機制,比如信號量等

4 最終採用的模型

最終我們採用的模型是:主體框架是多進程,主進程內部有若干線程用於處理諸如命令接收、文件監控、配置同步、統計數據寫出、

debug數據寫出等功能,包處理的主體流程是多進程的,不同進程之間基礎表項、數據包等數據採用dpdk共享內存,在系統啓動時

靜態映射好,這些關鍵的基礎表項和數據包結構針對緩存做細緻優化,比如對齊內存以避免發生僞共享。由於我們的業務同步和互斥方面

的要求不多,所以只使用了有限的忙等待的鎖和原子操作函數。這種模型實際上也是intel 推薦的模型。當然,選擇多進程模型後,

又有很多需要考慮的東西了,比如是流水線的worker1-worker2-worker3的多進程,還是 master-worker-worker-worker的對稱多進程,這裏頭根據業務邏輯、同步互斥、性能、擴展性、可維護性有很多深入的考慮,這裏就不詳細說了。

http://www.soft-bin.com/html/2010/07/09/%E5%A4%9A%E8%BF%9B%E7%A8%8Bvs%E5%A4%9A%E7%BA%BF%E7%A8%8B%EF%BC%8C%E4%B8%80%E4%B8%AA%E9%95%BF%E6%9C%9F%E7%9A%84%E4%BA%89%E8%AE%BA.html

http://blog.sina.com.cn/s/blog_d9889c5b0101e7x6.html


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章