使用進程池實現高併發服務器

近期在將《Linux高性能服務器編程》的代碼整理出一個實驗板服務器demo,陸陸續續將知識點梳理出來,本文主要實現進程池。react

進程池的必要性
算法

爲何要使用進程池/線程池?cause:編程

一、動態建立進程/線程比較耗費時間,會致使比較慢的客戶響應;數組

二、動態申請的進程/線程一般只用來爲一條鏈接服務,這會致使系統產生大量的進程/線程,進程切換會消耗大量CPU時間;服務器

三、動態建立的子進程是當前進程的完整鏡像。所以當前進程必須謹慎管理其分配的fd、socket和堆內存等系統資源,不然子進程複製這些資源後會致使系統可用資源降低,從而影響服務器性能。微信

進程池實現的併發服務器能夠解決上述問題。進程池是由服務器預先建立的一組子進程,全部子進程都運行相同的代碼,而且具備相同的屬性,好比優先級、PGID(進程組)等,沒有打開沒必要要的文件描述符,也不會使用大塊的堆內存。當有新的客戶端鏈接到來時,主進程經過均衡算法從進程池選擇某一子進程來服務,其代價比臨時建立進程小得多。至於如何選擇子進程,有兩種方式:網絡

一、隨機/輪流選擇空閒子進程併發

二、主進程和全部子進程共享一個工做隊列,子進程都睡眠在該隊列上,當有新鏈接到來時喚醒某一個子進程。異步

選擇好子進程後,主進程還須要經過某種機制通知目標子進程有新任務須要處理,並傳遞必要的數據。最簡單的方法是父子進程預先創建好管道。對於線程池而言就不須要管道了,由於父子線程自己就是共享全局數據的。socket

綜上進程池的實現方式爲:

                         

 

處理多客戶

使用進程池處理併發鏈接須要考慮的一個問題是:listen socket和connect socket事件是否都由主進程管理。

服務器程序一般須要處理三類事件:IO事件、信號signal、定時事件,同步IO模型一般用於實現reactor模式,異步IO模型用於實現proactor模式。結合同步異步IO模型,能夠設計出下面兩種服務器事件處理模式:

半同步/半反應堆模式

半同步/半異步模式

IO複用模型的具體介紹參考UNIX網絡編程第六章,這裏不展開。

半同步/半異步模式

第一種模式由主進程統一管理兩種socket,由於主從進程共享請求隊列,進程對隊列的任何操做都須要加鎖,影響CPU性能。第二種模式主進程管理全部監聽socket,子進程分別管理本身的鏈接socket。子進程能夠本身調用accept獲取TCP鏈接隊列,而主進程只須要通知就行,具體通知實現方式:

在兩個進程之間創建一個Unix域socket做爲消息傳遞的通道(使用 socketpair 函數),而後父進程調用 sendmsg 向通道發送一個特殊的消息,內核將對這個消息作特殊處理,從而將消息傳遞到子進程,子進程調用recvmsg 從通道接收消息。

此外,在設計進程池時還須要考慮同一個客戶端的屢次請求是否能夠複用一個TCP鏈接(http層面添加keepalive字段便可),若是客戶請求是無狀態的,那麼服務器能夠實現使用不一樣子進程來爲該客戶的不一樣請求進行服務:

若是客戶任務是有上下文狀態的(好比購物車,總不能刷新一次就被清空了),那麼只能一致用同一個子進程來處理該客戶的請求。使用epoll的EPOLLONESHOT特性能夠確保客戶鏈接在整個生命週期僅被一個進程處理。

對於註冊了EPOLLONESHOT事件的socket,操做系統最多觸發一次其註冊的事件,且只觸發一次。所以當子進程處理該socket上的事件時,其餘進程不可能操做該socket。

具體實現

定義一個描述子進程的類process,包含子進程的PID,以及父子進程通訊的管道pipe;進程池使用單體模式,保證程序僅建立一個進程池實例,這是程序正確處理信號的必要條件:

下面是父進程啓動後建立的數據,包括:處理信號的管道、承載子進程信息的數組、進程標識和epoll事件表,並進行了初始化。所以調用fork生成子進程後會繼承這些數據,子進程會將本身的數據保存。

父進程的執行流程爲:

關於SIGNAL信號的更多知識,參考UNIX環境高級編程第十章:

進程池中父子進程使用信號實現通訊的實現以下:

子進程實現

子進程的數據以下:

子進程處理流程以下:

在使用進程池時,監聽socket通常由main函數建立,在退出後須要關閉該socket。下期講解如何使用該進程池實現一個簡單的CGI服務器。


本文分享自微信公衆號 - 機械猿(on_ourway)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索