Linux服務器程序規範

Linux服務器程序規範

  • Linux服務器程序一般都是以後臺進程形式運行,後臺進程又稱爲守護進程(daemon),其沒有控制終端,不會意外接收到用戶輸入。守護進程的父進程通常是init進程(PID爲1的進程);
  • Linux服務器程序通常有一套日誌系統,其至少能輸出日誌到文件,有的高級服務器還能輸出日誌到專門的UDP服務器;
  • Linux服務器程序一般以某個專門的非root身份運行;
  • Linux服務器程序通常是可配置的;
  • Linux服務器進程通常會在啓動的時候生成一個PID文件並存入/var/run記錄中,以記錄後臺進程的PID;
  • Linux服務器程序通常需要考慮系統資源和限制,以預測自身能承受多大負荷,比如進程可用文件描述符總數和內存總量等;

用戶信息

在Unix進程中涉及多個用戶ID和用戶組ID,包括如下:

  • 實際用戶ID和實際用戶組ID: 標識我是誰。也就是登錄用戶的uid和gid,比如我的Linux以simon登錄,在Linux運行的所有的命令的實際用戶ID都是simon的uid,實際用戶組ID都是simon的gid(可以用id命令查看)。
  • 有效用戶ID和有效用戶組ID:進程用來決定我們對資源的訪問限制,一般情況下,有效用戶ID等於實際用戶ID,有效用戶組ID等於實際用戶組ID。當設置用戶ID(SUID)位設置,則有效用戶ID等於文件所有者的uid,而不是實際用戶ID;同樣,如果設置了設置-用戶組-ID(SGID),則有效用戶ID等於文件所有者的gid,而不是實際用戶組ID;

有效用戶ID爲root的進程稱爲特權進程;

會話

一些有關聯的進程組可以形成一個會話,下面函數用於創建一個會話:

#include<unistd.h>
pid_t setsid(void);

該函數不能由進程組的首領進程調用,否則產生錯誤。對於非首領進程,調用該函數不僅創建新會話,還有如下作用:

  • 調用進程稱爲會話的首領,此時該進程是新會話的唯一成員;
  • 新建一個進程組,其PGID就是調用進程的PID,調用進程稱爲該組的首領;
  • 調用進程將甩開終端(如果有的話);

服務器程序後臺化

後臺化的步驟:

  • 創建子進程,父進程退出;
  • 在子進程中創建新會話(setsid());
  • 改變當前目錄爲根目錄,防止佔用可卸載的文件系統;
  • 重設文件權限掩碼,防止繼承的文件創建屏蔽字拒絕某些權限;
  • 關閉文件描述符,繼承的打開文件不會用到;
  • 開始執行守護進程核心工作;
  • 守護進程退出處理;
bool daemonize(){]
    pid_t pid = fork();
    if(pid < 0){
        return false;
    }
    else if(pid > 0){
        exit(0);
    }
    //設置文件權限掩碼,當進程創建新文件時,文件的權限將是mode & 0777
    umask(0);

    pid_t sid = setsid();
    if(sid < 0){
        return false;
    } 
    //切換工作目錄
    if(chdir("/") < 0){
        return false;
    }

    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);
    //關閉其他已打開的文件描述符,代碼省略

    //將標準輸入,標準輸出,標準錯誤輸出都重定向到/dev/null文件
    open("/dev/null", O_RDONLY);
    open("/dev/null", O_RDWR);
    open("/dev/null", O_RDWR);

    return true;
}

守護進程詳細說明

事件處理模式

Reactor模式

Reactor:要求主線程只負責監聽文件描述符上是否有事件發生,有的話就立即將該事件通知工作線程,除此之外,主線程不做任何其他實質性工作,讀寫數據,接收新的連接,以及處理客戶請求均在工作線程完成。

這裏寫圖片描述

Proactor模式

與Reactor模式不同,Proactor模式將所有I/O操作都交給主線程和內核來處理,工作線程僅僅負責業務邏輯,因此,Proactor模式更符合服務器編程框架。

使用異步I/O模型(以aio_read和aio_write)實現Proactor模式的工作流程:

1) 主線程調用aio_read函數向內核註冊socket上的讀完成事件,並告訴內核用戶讀緩衝區的位置,以及讀操作完成時如何通知應用程序(以信號爲例);
2) 主線程繼續處理其他邏輯;
3) 當socket上的數據被讀入用戶緩衝區,內核將嚮應用程序發送一個信號,以通知應用程序數據可用;
4)應用程序預定義好的信號處理函數選擇一個工作線程來處理客戶請求,工作線程處理完客戶請求之後,調用aio_write函數向內核註冊socket上的寫完成事件,並告訴內核用戶緩衝區的位置,以及寫操作完成時如何通知應用程序(以信號爲例);
5)主線程繼續處理其他邏輯;
6)當用戶緩衝區的數據被寫入socket之後,內核將嚮應用程序發送一個信號,以通知應用程序數據已經發送完畢;
7)應用程序預定義好的信號處理函數選擇一個工作線程來做善後處理,比如決定是否關閉socket;

這裏寫圖片描述

模擬Proactor模式

使用同步I/O模擬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上寫入服務器處理客戶請求的結果;

流程圖:

這裏寫圖片描述

高效併發模式

併發編程的目的是讓程序“同時”執行多個任務:
如果是計算密集型的,併發編程沒有優勢,反而由任務的切換導致效率降低;
如果程序是I/O密集型的,比如經常讀寫文件,訪問數據庫等,則情況就不同了,由於I/O操作的速度遠沒有CPU計算的速度快,所以讓程序阻塞於I/O操作將浪費大量的CPU時間,如果程序有多個執行進程,則當前被I/O操作所阻塞的執行線程可主動放棄CPU(或由OS調度),並將執行權轉移到其他線程,這樣一來,CPU就可以用來做更加有意義的事情(除非所有線程都同時被I/O操作所阻塞),而不是等待I/O操作完成,因此CPU的利用率顯著提升;

半同步半異步模式

半同步半異步模式中的同步和異步與I/O模式中的同步和異步是完全不同的概念。

  • I/O模型中,同步和異步區分的是內核嚮應用程序通知的何種I/O事件(是就緒事件還是完成事件),以及該由誰來完成I/O操作(是應用程序還是內核);
  • 併發模式中,同步指程序完全按照代碼序列的順序執行,異步值程序的執行需要系統事件來驅動;

在半同步半異步模式中,同步線程用於處理客戶邏輯,異步線程用於處理I/O事件,異步線程監聽到客戶請求後,就將其封裝成請求對象並插入請求隊列中,請求隊列將通知某個工作在同步模式的工作線程讀取並處理該請求對象,具體選擇哪個工作線程來爲新的客戶請求服務,取決於請求隊列的設計;

半同步半反應堆模式:使用模擬的Proactor時間處理模式,主線程完成數據的讀寫,主線程一般會將應用程序數據、任務類型等信息封裝成一個任務對象,將其(或指向任務隊列的指針)插入請求隊列,工作線程從請求隊列中取得任務對象之後,即可直接處理之,無需讀寫操作了。

但有如下缺點:

  • 主線程和工作線程共享請求隊列,主線程往請求隊列中添加任務,或者工作線程從請求隊列中取出任務,都需要對請求隊列加鎖保護,白白消耗CPU時間;
  • 每個工作線程在同一時間只能處理一個客戶請求,如果客戶請求數量較多,而工作線程較少,則請求隊列中將堆積很多任務對象,客戶端的響應速度將越來越慢,如果通過增加工作線程來解決這個問題,則工作線程的切換也將耗費大量CPU時間;

提高服務器性能的建議

影響服務器性能的首要因素是系統的硬件資源,比如CPU的個數、速度、內存的大小等;
從“軟環境”提升服務器的性能,“軟環境”:

  • 系統的軟件資源:操作系統允許用戶打開的最大文件描述符的數量;
  • 服務器程序本身:如何從編程的角度確保服務器的性能?
    • 池、數據複製、上下文切換、鎖

“浪費”服務器的硬件資源,以換取其運行效率,這就是池的概念。

池是一組資源的集合,這組資源在服務器啓動之初就被完全創建好並初始化,稱爲靜態資源分配;

當服務器進入正式運行階段,即開始處理客戶請求的時候,如果它需要相關的資源,可直接從池中獲取,無須動態分配。直接從池中獲取所需資源比動態分配資源的速度快的多,因爲分配系統資源的系統調用時很耗時的。當服務器處理完一個客戶連接之後,可以把相關的連接放入池中,無須執行系統調用釋放資源。

池相當於服務器管理系統資源的應用層設施,避免了服務器對內核的頻繁訪問;

  • 內存池:通常用於socket的接收和發送緩存;
  • 進程池和線程池:可直接從其中獲得一個執行實體,無須動態調用fork或pthread_create等函數創建進程和線程;
  • 連接池:服務器或服務器機羣內部的永久鏈接;

數據複製

高性能服務器應該避免不必要的數據複製,尤其是當數據複製發生在用戶代碼和內核之間的時候。可以使用“零拷貝”函數進行數據的複製,如sendfile等;

用戶代碼內部(不訪問內核)的數據複製也應該避免,可以使用共享內存共享數據而不是使用管道或消息隊列傳遞數據;

上下文切換和鎖

併發程序必須考慮上下文切換的問題,即進程切換或線程切換導致的系統開銷。即使是I/O密集型的服務器,也不應該使用過多的工作線程(或工作進程),否則線程間的切換將佔用大量的CPU時間,服務器真正用處理業務邏輯的CPU時間的比重就顯得不足了。

多線程服務器的一個優點:不同線程可以同時運行在不同的CPU上,當線程的數量不大於CPU的數目時,上下文切換就不是問題了。

併發程序需要考慮的另外一個問題是共享資源的加鎖保護,鎖通常被認爲是導致服務器效率低下的一個因素,因爲由它引入的代碼不僅不處理任何業務邏輯,而且需要訪問內核資源。因此,服務器如果有更好的解決方案,應該避免使用鎖。當服務器必須使用鎖,則可以考慮減小鎖的粒度;

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