Linux高性能服務器編程 第八章(高性能服務器程序框架)

轉載自:http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=22906954&id=4425122,感謝作者。

8  高性能服務器程序框架


服務器解構爲三個主要模塊:
IO處理單元。四種IO模型和兩種高效事件處理模式。
邏輯單元。兩種高效併發模式。
存儲單元。(暫不討論)


IO模型:
阻塞IO
非阻塞IO
IO複用//程序阻塞於IO複用系統調用,但可同時監聽多個IO事件。
SIGIO信號//信號觸發讀寫就緒事件,用戶程序執行讀寫操作,程序沒有阻塞階段
異步IO//內核執行讀寫操作並觸發讀寫完成事件。程序沒有阻塞階段


兩種高效的事件處理模式:
服務器通常要處理三類事件:IO事件、信號事件、定時事件。
同步IO模型通常用於實現Reactor模式,異步IO模型用於實現Proactor模式。


Reactor模式:
同步IO模型(以epoll_wait爲例)實現的Reactor模式的工作流程:
1、主線程往epoll內核事件表中註冊socket上的讀就緒事件。
2、主線程調用epoll_wait等待socket上有數據可讀。
3、當socket上有數據可讀時,epoll_wait通知主線程。主線程則將socket可讀事件放入請求隊列。
4、睡眠在請求隊列上的工作線程被喚醒,它從socket讀取數據,並處理客戶請求,然後往epoll內核事件表中註冊該socket上的寫就緒事件。
5、主線程調用epoll_wait等待socket可寫。
6、當socket可寫時,epoll_wait通知主線程。主線程將socket可寫事件放入請求隊列。
7、睡眠在請求隊列上的某個工作線程被喚醒,它往socket上寫入服務器處理客戶請求的結果。


工作線程從隊列中取出事件後,將根據事件是可讀或可寫執行讀寫數據和處理請求的操作。因此,在Reactor模式中,沒必要區分所謂的“讀工作線程”和“寫工作線程”。




Proactor模式:
與Reactor模式不同,Proactor模式將所有IO操作都交給主線程和內核來處理,工作線程僅僅負責業務邏輯。
(以aio_read和aio_write爲例)工作流程:
1、主線程調用aio_read函數向內核註冊socket上的讀完成事件,並告訴內核用戶讀緩衝區的位置,以及讀操作完成時如何通知應用程序(這裏以信號爲例,詳情sigevent的man手冊)
2、主線程繼續處理其他邏輯。
3、當socket上的數據被讀入用戶緩衝區後,內核將嚮應用程序發送一個信號,以通知應用程序數據已經可用。
4、應用程序預先定義好的信號處理函數選擇一個工作線程來處理客戶請求。工作線程處理完客戶請求之後,調用aio_write函數向內核註冊socket上的寫完成事件,並告訴內核用戶寫緩衝區位置,以及寫操作完成時如何通知應用程序(仍以信號爲例)
5、主線程繼續處理其他邏輯。
6、當用戶緩衝區的數據被寫入socket之後,內核將嚮應用程序發送一個信號,以通知應用程序數據已經發送完畢。
7、應用程序預先定義好的信號處理函數選擇一個工作線程來做善後處理,比如決定是否關閉socket.




使用同步IO模型(仍然以epoll_wait爲例)模擬出的Proactor模式的工作流程如下:
1、主線程往epoll內核事件表中註冊socket上的讀就緒事件。
2、主線程調用epoll_wait等待socket上有數據可讀。
3、當socket上有數據可讀時,epoll_wait通知主線程。主線程從socket循環讀取數據,知道沒有更多數據可讀,然後將讀取到的數據封裝成一個請求對象並插入到請求隊列。
4、睡眠在請求隊列上的某個工作線程被喚醒,它獲得請求對象並處理客戶請求,然後網epoll內核事件表中註冊socket上的寫就緒事件。
5、主線程調用epoll_wait等待socket可寫。
6、當socket可寫時,epoll_wait通知主線程。主線程網socket上寫入服務器處理客戶端請求的結果。




兩種高效的併發模式:
半同步半異步模式:

這裏的“同步”和“異步”和前面的IO的“同步”“異步”是完全不同的概念。在IO模型中,“同步”和“異步”區分的是內核嚮應用程序通知的是何種IO事件(是就緒事件還是完成事件),以及該由誰來完成IO讀寫(是應用程序還是內核)。在併發模式中,“同步”指的是程序完全按照代碼序列的順序執行;“異步”指的是程序的執行需要由系統事件來驅動。常見的系統事件包括中斷、信號等。
顯然異步線程的執行效率高,實時性強,是很多嵌入式系統採用的模型。但編寫異步方式執行的程序相對複雜,難於調試和擴展,而且不適合於大量的併發。而同步線程則相反,它雖然效率相對較低,實時性較差,但邏輯簡單。
在半同步半異步模式中,同步線程用於處理客戶邏輯,異步線程用於處理IO事件。異步線程監聽到客戶請求後,就將其封裝成請求對象並插入到請求隊列中。請求隊列將通知某個工作在同步模式的工作線程來讀取並處理該請求對象。




半同步半異步模式存在如下缺點:
1、主線程和工作線程共享請求隊列。
2、每一個工作線程在同一時間只能處理一個客戶請求。
高效模式:每個工作線程都能同時處理多個客戶連接。
主線程只管理監聽socket,連接socket由工作線程來管理。當有新的連接到來時,主線程就接受之並將新返回的連接socket派發給某個工作線程,此後該socket上的任何IO操作都由被選中的工作線程來處理,直到客戶端關閉連接。主線程向工作線程派發socket的最簡單的方式,是往它和工作線程之間的管道里寫數據。工作線程檢測到管道里有數據可讀時,就分析是否是一個新的客戶連接請求到來。如果是,則把該新socket上的讀寫事件註冊到自己的epoll內核事件表中。
每個線程(主線程和工作線程)都維持自己的事件循環,它們各自獨立的監聽不同的事件。因此在這種模式中,每個線程都工作在異步模式,所以它並非嚴格意義上的半同步半異步模式。



領導者追隨者模式:
是多個工作線程輪流獲得事件源集合,輪流監聽、分發並處理事件的一種模式。在任意時間點,程序都僅有一個領導者線程,它負責監聽IO事件。而其他線程都是追隨者,它們休眠在線程池中等待成爲新的領導者。當前的領導者如果檢測到IO事件,首先要從線程池中推選出新的領導者線程,然後處理IO事件。此時,新的領導者等待新的IO事件,而原來的領導者則處理IO事件,二者實現了併發。
包含如下幾個組件:句柄集(HandleSet)、線程集(ThreadSet)、事件處理器(EventHandler)、具體的事件處理器(ConcreteEventHandler)。




提高服務器性能的其他建議:


池:


服務器硬件資源相對“充裕”,那麼提高服務器性能的一個很直接的方法就是以空間換時間,即“浪費”服務器的硬件資源,以換取其運行效率。這就是池(pool)的概念。池是一種資源的集合,這組資源在服務器啓動之初就被完全創建並初始化,這稱爲靜態資源分配。速度要快得多,因爲分配系統資源的系統調用都是很耗時的。當服務器處理完一個客戶連接後,可以把相關的資源放回池中,無須執行系統調用來釋放資源。從最終效果來看,池相當於服務器管理系統資源的應用層設施,它避免了服務器對內核的頻繁訪問。


按照資源類型分類:
內存池:通常用於socket的接收緩存和發送緩存。
進程池、線程池:併發編程常用“伎倆”。
連接池:常用於服務器或服務器集羣的內部永久連接。




數據複製:


應該避免不必要的數據複製,尤其當數據複製發生在用戶代碼和內核之間的時候。如果內核可以直接處理從socket或者文件讀入的數據,則應用程序就沒有必要將這些數據從內核緩衝區複製到應用程序緩衝區。如ftp服務器,服務器只需檢測目標文件是否存在,以及客戶是否有讀取權限,而不用關心文件具體內容。就可以使用“零拷貝”sendfile來直接將其發送給客戶。
此外,用戶代碼內部(不訪問內核)的數據複製也是應該避免的。如兩個工作進程之間要傳遞大量的數據時,我們就應該考慮使用共享內存來在它們之間直接共享這些數據,而不是使用管道或者消息隊列來傳遞。




上下文切換和鎖:
併發程序必須考慮上下文切換(context switch)的問題,即進程線程切換導致的系統開銷。即使是IO密集型的服務器,也不應該使用過多的工作線程(或進程,下同),否則切換將佔用大量CPU時間,服務器真正用於業務邏輯的CPU時間比重就顯得不足了。因此爲每個客戶連接都建立一個服務器線程的模型不可取。之前描述的半同步半異步模型是一個比較合理的解決方案,它允許一個線程同時處理多個客戶連接。此外,多線程服務器的一個優點是不同的線程可以同時運行在不同的cpu上。當線程數量不大於cpu的數目時,上下文切換就不是問題了。
併發程序需要考慮的另一個問題是共享資源的枷鎖保護。鎖通常被認爲是導致服務器效率低下的一個因素,因爲由它引入的代碼不僅不處理任何業務邏輯,而且需要訪問內核資源。因此,服務器如果有更好的解決方案,就應該避免使用鎖。如果服務器必須使用鎖,則可以考慮減小鎖的粒度,比如使用讀寫鎖。當所有工作線程都只讀取一塊共享內存的內容時,讀寫鎖並不會增加系統的額外開銷。只有當其中一個工作線程需要寫這塊內存時,系統才必須去鎖住這塊區域。

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