第八章 高性能服務器編程框架
這一章主要介紹服務器的三個主要模塊: I/O處理單元、邏輯單元、存儲單元。另外服務器的模型有:C/S模型和P2P模型。雖然服務器模型比較多,但是其核心框架都一樣,只是在於邏輯處理方面。如下圖簡單的介紹一台服務器或服務器機群模型的基本框架:
1、I/O模型
I/O處理單元:I/O處理單元是服務器管理客戶連接的模塊。主要是等待並受理新的客戶連接接收客戶數據,將服務器響應數據返回給客戶端。
邏輯單元:就是一個個進程或者線程。用於處理客戶數據,將結果傳遞給I/O處理單元或者直接發送給客戶端。服務器中通常由多個邏輯單元,以實現多個客戶端任務的並行處理。
網絡存儲單元:網絡存儲單元用於存儲數據庫、緩存以及文件。
請求隊列:請求隊列是各個單元之間的通信方式的抽象。請求隊列通常被實現為池的一部分。請求隊列是各台服務器之間預先建立的靜態的永久的TCP連接。
I/O模型 有阻塞I/O模型和 非阻塞I/O模型。因為socket在創建的時候默認是阻塞的,在創建socket的時候講第二個參數設置為SOCK_NONBLOCK標志,或通過fcntl系統調用 F_SETFL 命令,將其設置為非阻塞的。對於阻塞I/O執行的系統調用會因為無法立即完成而被操作系統掛起,直到等待事情發生為止。比如:1)客戶端通過connect向服務器端發起連接,connect將發送同步報文段給服務器然后等待服務器返回確認報文段。2)如果服務器確認報文段沒有立即到達客戶端,則connect調用將被掛起,直到客戶端收到確認報文段喚醒connect調用。在socket的基礎API中,可能被阻塞的系統調用包括 accept send recv 和 connect.
對於非阻塞I/O總是需要和其他I/O通知機制一起使用,如果不和其他通知機制一起使用還是阻塞的。比如:I/O復用 和 SIGIO 信號等 另行去處理I/O,處理是異步的。 在非阻塞I/O執行系統調用總是立即返回一個(通知事件結果)。不管事件是否已經發生都會返回。所以對於非阻塞I/O就需要根據errno來區分成功還是失敗的情況。事件返回的結果類型有(再來一次 EAGAIN) (期望阻塞 EWOULDBLOCK) (在處理中 EINPROGRESS) 。
I/O復用是最常用的 I/O通知機制。例如:應用程序通過I/O復用函數向內核注冊一組事件,內核通過I/O復用函數把其中就緒的事件通知給應用程序。Linux上常用的I/O復用函數有 select pull epoll_wait. 它們能提高程序效率的原因在於它們具有同時監聽多個I/O事件能力。
理論上來說,阻塞I/O和 I/O復用 以及信號驅動I/O都是屬於同步I/O模型。因為I/O的讀寫操作都是在I/O事件發生之后由應用程序完成的。而異步I/O的讀寫都是立即返回的,不論是否阻塞,因為真正的讀寫操作已經由內核完成了。也就是說:同步I/O模型要求用戶代碼自行執行I/O操作,將數據從內核緩存區讀入用戶緩沖區,或將數據從用戶緩沖區寫入內核緩沖區。而異步I/O機制則有內核來執行I/O操作,數據在內核緩沖區和用戶緩沖區之間的移動是由內核在“后台”完成的。
可以總結 為同步I/O像應用程序通知的是I/O就緒事件、而異步I/O向應用程序通知的是I/O完成事件。
I/O模型對比
1)阻塞I/O : 程序阻塞於讀寫函數;
2)I/O復用: 程序阻塞於I/O復用系統調用,但可同時監聽多個I/O事件。對I/O本身的讀寫操作是非阻塞的。
3)SIGIO信號: 信號觸發讀寫就緒事件,用戶陳谷執行讀寫操作。程序沒有阻塞階段。
4)異步I/O: 內核執行讀寫操作並觸發讀寫完成事件。程序沒有阻塞階段。
2、Reactor 和 Proactor 事件處理模式
事件處理的兩種模式:Reactor 和 Proactor 通常服務器程序需要處理三類事件:I/O事件、信號、定時事件。同步I/O模型通常使用 Reactor模式,異步I/O模式用Proactor處理。也可以通過同步I/O模擬出 Proactor模式;
Reactor模式:她只要求主線程(i/o處理單元)監聽文件描述上是否有事件發生,有就立即通知通知工作線程(邏輯單元)處理任務。所有的讀寫處理數據都在線程上執行。使用同步I/O模型(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上寫入服務器處理客戶請求的結果。這是一個環形的操作,如下圖:
Proactor模式:她是將I/O操作都交給主線程和內核處理。工作線程僅負責業務邏輯。使用異步I/O(aio_read和 aio_write為例) 實現Proactor模式。1)主線程調用aio_read函數向內核注冊socket上的讀完成事件,並告訴內核用戶讀緩沖區的位置,以及讀操作完成時如何通知應用程序。2)主線程繼續處理其他邏輯。3)當socket上的數據被讀入用戶緩沖區后,內核將向應用程序發送一個信號,已通知應用程序數據已可用。4)應用程序預先定義好信號處理函數選擇一個工作線程來處理客戶請求。工作線程處理完客戶請求之后,調用aio_write函數向內核注冊socket上的寫完成事件,並告訴內核用戶寫緩沖區的位置,以及寫操作完成時如何通知應用程序。5)主線程繼續處理其他邏輯。6)當用戶緩沖區的數據被寫入socket之后,內核將向應用程序發送一個信號,以通知應用程序數據已經發送完畢。7)應用程序預先定義好的信號處理函數選擇一個工作線程來做善后處理,決定是否關閉socket.如下圖:
3、池
池有很多種,常見的有 內存池、進程池、線程池、連接池。池是在服務器啟動時預先初始化創建好的長連接。已達到空間換時間的概念提高效率。當然逾期初始化好的數據對它的大小就難以把控了,當然也可以動態擴容。