TCP/IP網絡編程之多進程服務端(一)


進程概念及應用

我們知道,監聽套接字會有一個等待隊列,里面存放着不同客戶端的連接請求,如果有一百個客戶端,每個客戶端的請求處理是0.5s,第一個客戶端當然不會不滿,但第一百個客戶端就會有相當大的意見了。為了要使得所有客戶端都盡可能的滿意,我們應采用並發服務端,使其同時向所有發起請求的客戶端提供服務。而且,網絡程序中數據通信時間比CPU運算時間占比更大,因此,向多個客戶端提供服務是一種有效利用CPU的方式。接下來討論同時向多個客戶端提供服務的並發服務端,下面提出具有代表性的並發服務端實現模型和方法:

  • 多進程服務器:通過創建多個進程提供服務
  • 多路復用服務器:通過捆綁並統一管理I/O對象提供服務
  • 多線程服務器:通過生成與客戶端等量的線程提供服務

先來簡單理解下進程:我們打開電腦一般不會只做一件事,比方單純的瀏覽網站,單純的聊天。一般我們都是幾件事輪流切換着做,我們會在瀏覽網頁時打開音樂播放器播放音樂,還會時不時回復下QQ消息。那么這里就牽扯到三個進程了,一個是瀏覽器進程,一個是播放器進程,還有一個是QQ進程。從操作系統的角度看,進程是程序流的基本單位,若創建多個進程,則操作系統將同時運行。有時一個程序運行過程中也會產生多個進程,像谷歌瀏覽器,打開一個tab頁,實際上就是產生一個新的進程。接下來要創建的多進程服務器就是其中的代表,編寫服務端前,先了解一下通過程序創建進程的方法

CPU核的個數和進程數:擁有兩個運算器的CPU稱為雙核CPU,擁有四個運算器的CPU稱作四核CPU。也就是說,一個CPU可能包含多個運算器(核)。核的個數與可同時運行的進程數相同,相反,若進程數超過核數,進程將分時使用CPU資源。但因CPU運算速度極快,我們會感到所有進程同時運行,當然,核數越多,這種感覺越明顯

進程ID

講解創建進程方法前,先簡要說明下進程ID。無論進程是如何創建的,所有進程都會從操作系統分配得到ID。此ID稱為“進程ID”,其值為大於2的整數,1要分配給操作系統啟動后的(用於協助操作系統)首個進程,因此用於進程無法得到ID為1的進程ID,接下來觀察Linux中正在運行的進程:

# ps au
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root       384  0.0  0.0   1520   208 pts/23   Ss+  Sep04   0:00 /bin/sh -c nginx -g "daemon on;" && uwsgi --ini /data/web/uwsgi.ini
root       438  0.0  0.6 257212 36936 pts/23   Sl+  Sep04   0:03 uwsgi --ini /data/web/uwsgi.ini
root       473  0.0  0.0   1520   208 pts/3    Ss+  Sep21   0:00 /bin/sh -c nginx -g "daemon on;" && uwsgi --ini /data/web/uwsgi.ini
root       513  0.0  0.7 186080 44028 pts/3    S+   Sep21   0:05 uwsgi --ini /data/web/uwsgi.ini
root       555  0.0  0.6 186724 40404 pts/3    Sl+  Sep21   0:00 uwsgi --ini /data/web/uwsgi.ini
root       702  0.0  0.0 110044   696 tty1     Ss+  Aug19   0:00 /sbin/agetty --noclear tty1 linux
root       703  0.0  0.0 110044   732 hvc0     Ss+  Aug19   0:00 /sbin/agetty --keep-baud 115200 38400 9600 hvc0 vt220
root      3025  0.0  0.0   1520    16 pts/1    Ss+  Aug19   0:00 /bin/sh -c nginx -g "daemon on;" && uwsgi --ini /data/web/uwsgi.ini
root      3694  0.0  0.1 242444 10644 pts/1    Sl+  Aug19   0:01 uwsgi --ini /data/web/uwsgi.ini
root      3992  0.0  0.0 102696  1468 pts/7    Ss+  Aug19  10:49 /usr/local/bin/python /usr/local/bin/gunicorn -w 3 -k gevent -b :5001 manage:app
root      4089  0.0  0.0  11636     8 pts/8    Ss+  Aug19   0:00 /bin/sh -c uwsgi --ini /data/code/uwsgi.ini && nginx -g "daemon off;"

  

可以看出,通過ps命令可以查看當前運行的所有進程,該命令同時列出了PID(進程ID),ps命令可通過指定a和u參數u列出所有進程的詳細信息

通過fork函數創建進程

#include<unistd.h>
pid_t fork(void);//成功時返回進程ID,失敗時返回-1

  

fork函數將創建調用的進程副本,也就是說,並非根據完全不同的程序創建進程,而是復制正在運行的、調用fork函數的進程。另外,兩個進程都將執行fork函數調用后的語句(准確地說是在fork函數返回后)。但因為通過同一個進程、復制相同的內存空間,之后的程序流根據fork函數的返回值加以區分。即利用fork函數的如下特點區分程序執行流程:

  • 父進程:fork函數返回子進程ID
  • 子進程:fork函數返回0

此處,“父進程”指原進程,即調用fork函數的主體,而“子進程”是通過父進程調用fork函數復制出的進程。圖1-1展示了調用fork函數后的程序運行流程

圖1-1   fork函數的調用

圖1-1中可以看到,父進程調用fork函數的同時復制出子進程,並分別得到fork函數的返回值。但復制前,父進程全局變量gval增加到11,將局部變量lval的值增加到25。復制完成后根據fork函數的返回類型區分父子進程,父進程將lval加1,但這不會影響子進程的lval的值。同樣,子進程將gval的值加1也不會影響父進程的gval。因為fork函數調用后分成了兩個完全不同的進程,只是二者共享同一代碼塊而已。接下來,我們驗證之前所說的內容

fork.c

#include <stdio.h>
#include <unistd.h>
int gval = 10;

int main(int argc, char *argv[])
{
    pid_t pid;
    int lval = 20;
    gval++, lval += 5;

    pid = fork();
    if (pid == 0) // if Child Process
        gval += 2, lval += 2;
    else // if Parent Process
        gval -= 2, lval -= 2;

    if (pid == 0)
        printf("Child Proc: [%d, %d] \n", gval, lval);
    else
        printf("Parent Proc: [%d, %d] \n", gval, lval);
    return 0;
}

  

  • 第11行:創建子進程,父進程的pid中存有子進程的ID,子進程的pid是0
  • 第12、18行:子進程執行這兩行代碼,因為pid為0
  • 第15、20行:父進程執行這兩行代碼,因為此時pid中存有子進程ID

編譯fork.c並運行

# gcc fork.c -o fork
# ./fork 
Parent Proc: [9, 23] 
Child Proc: [13, 27]

  

從運行結果可以看出,調用fork函數后,父子進程擁有完全獨立的內存結構

進程和僵屍進程

文件操作中,關閉文件和打開文件同等重要。同樣,進程銷毀也和進程創建同等重要。如果未認真對待進程銷毀,它們將變成僵屍進程困擾各位。

僵屍進程

進程完成工作后(執行完main函數中的程序后)應被銷毀,但有時這些進程變成僵屍進程,占用系統中的重要資源。這種狀態下的進程稱作“僵屍進程”,這也是給系統帶來負擔的原因之一。因此,我們應該消滅這種進程

產生僵屍進程的原因

為了防止僵屍進程的產生,先解釋產生僵屍進程的原因。利用如下兩個示例展示調用fork函數產生子進程的終止方式:

  • 傳遞參數並調用exit函數
  • main函數中執行return並返回值

向exit函數傳遞的參數值和main函數的return語句返回的值都會傳遞給操作系統,而操作系統不會銷毀子進程,直到把這些值傳遞給產生該子進程的父進程,處在這種狀態下的進程就是僵屍進程。也就是說,將子進程變成僵屍進程的正是操作系統。既然如此,僵屍進程何時被銷毀呢?其實之前已給出答案:當子進程將返回值傳遞給父進程的時候。那么,如何向父進程傳遞返回值呢?操作系統不會主動把這些值傳遞給父進程,只有父進程主動發起請求(函數調用)時,操作系統才會傳遞該值。換言之,如果父進程未主動要求獲得子進程的結束狀態值,操作系統將一直保存,並讓子進程長時間處於僵屍進程狀態。接下來的示例將創建僵屍進程

zombie.c

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    pid_t pid = fork();

    if (pid == 0) // if Child Process
    {
        puts("Hi I'am a child process");
    }
    else
    {
        printf("Child Process ID: %d \n", pid);
        sleep(30); // Sleep 30 sec.
    }

    if (pid == 0)
        puts("End child process");
    else
        puts("End parent process");
    return 0;
}

  

  • 第14行:輸出子進程ID,可以通過該值查看子進程狀態(是否為僵屍進程)
  • 第15行:父進程暫停30秒,如果父進程終止,處於僵屍進程狀態的子進程將同時銷毀。因此,延緩父進程的執行以驗證僵屍進程

編譯zombie.c並運行

# ./zombie 
Child Process ID: 5507 
Hi I'am a child process
End child process
End parent process

  

程序開始運行,在打印出子進程的進程ID后,會停歇30秒,這個時候我們可以趁機看一下5507進程號所對應的進程狀態

# ps -ef | grep 5507
root      5507  5506  0 11:44 pts/32   00:00:00 [zombie] <defunct>
root      5509 23062  0 11:45 pts/31   00:00:00 grep --color=auto 5507

  

可以看到,5507對應的進程號的狀態為defunct,即為僵屍進程。經過30秒后,隨着父進程的終止,子進程也將銷毀

銷毀僵屍進程1:利用wait函數

如前所述,為了銷毀子進程,父進程應主動請求獲取子進程的返回值,接下來討論下發起請求的具體方法,共有兩種,其中之一就是調用wait函數

#include <sys/wait.h>
pid_t wait(int *statloc);//成功時返回終止的子進程ID,失敗時返回-1

  

調用次函數時如果已有子進程終止,那么子進程終止時傳遞的返回值(exit函數的參數值、main函數的return返回值)將保存到該函數的參數所指的內存空間。但函數參數指向的單元中還包含其他信息,因此需要通過下列宏進行分離

  • WIFEXITED子進程正常終止時返回真(true)
  • WEXITSTATUS返回子進程的返回值

也就是說,向wait函數傳遞變量status的地址時,調用wait函數后應編寫如下代碼 

if (WIFEXITED(status))
{
    puts("Normal termination!");
    printf("Child pass num: %d \n", WEXITSTATUS(status)); //返回值是多少
}

  

根據上述內容編寫示例,此示例中不會再讓子進程編程僵屍進程

wait.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
    int status;
    pid_t pid = fork();

    if (pid == 0)
    {
        return 3;
    }
    else
    {
        printf("Child PID: %d \n", pid);
        pid = fork();
        if (pid == 0)
        {
            exit(7);
        }
        else
        {
            printf("Child PID: %d \n", pid);
            wait(&status);
            if (WIFEXITED(status))
                printf("Child send one: %d \n", WEXITSTATUS(status));

            wait(&status);
            if (WIFEXITED(status))
                printf("Child send two: %d \n", WEXITSTATUS(status));
            sleep(30); // Sleep 30 sec.
        }
    }
    return 0;
}

  

  • 第9、13行:第9行創建的子進程將在第13行通過main函數中的return語句終止
  • 第18、21行:第18行中創建的子進程將在第21行通過調用exit函數終止
  • 第26行:調用wait函數,之前終止的子進程相關信息將保存到status變量,同時相關子進程被完全銷毀
  • 第27、28行:第27行中通過WIFEXITED宏驗證子進程是否正常終止,如果正常退出,則調用WEXITSTATUS宏輸出子進程的返回值
  • 第30~32行:因為之前創建了兩個進程,所以再次調用wait函數和宏
  • 第33行:為暫停父進程終止而插入的代碼,此時可以查看子進程狀態 
# gcc wait.c -o wait
# ./wait 
Child PID: 6862 
Child PID: 6863 
Child send one: 3 
Child send two: 7 

  

在系統中執行ps命令可以發現,並沒有上一個示例中對應PID的進程。這是因為調用了wait函數,完全銷毀了子進程,另外兩個子進程終止時返回3和7傳遞給父進程。這就是通過調用wait函數消滅僵屍進程的方法,調用wait函數時,如果沒有已終止的子進程,那么程序將阻塞直到有子進程終止,因此需謹慎調用該函數

銷毀僵屍進程2:使用waitpid函數

wait函數會引起程序的阻塞,還可以考慮調用waitpid函數,這是防止僵屍進程的第二種方法,也是防止阻塞的方法

#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statloc, int options);//成功時返回終止的子進程ID(或0),失敗時返回-1

  

  • pid:等待終止的目標子進程的ID,若傳遞-1,則與wait函數相同,可以等待任意子進程終止
  • statloc:與wait函數的statloc具有相同意義
  • options:傳遞頭文件sys/wait.h中聲明的常量WNOHANG,即使沒有終止的子進程也不會進入阻塞狀態,而是返回0並退出函數

下面介紹用上述函數的示例,調用waitpid函數,程序不會阻塞

waitpid.c

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
    int status;
    pid_t pid = fork();

    if (pid == 0)
    {
        sleep(15);
        return 24;
    }
    else
    {
        while (!waitpid(-1, &status, WNOHANG))
        {
            sleep(3);
            puts("sleep 3sec.");
        }

        if (WIFEXITED(status))
            printf("Child send %d \n", WEXITSTATUS(status));
    }
    return 0;
}

  

  • 第12行:調用sleep函數推遲子進程的執行,這會導致程序延遲15秒
  • 第17行:while循環調用waitpid函數,向第三個參數傳遞WNOHANG,因此,若之前沒有終止的子進程將返回0

編譯waitpid.c並運行

# gcc waitpid.c -o waitpid
# ./waitpid 
sleep 3sec.
sleep 3sec.
sleep 3sec.
sleep 3sec.
sleep 3sec.
Child send 24 

  

可以看出第20行共執行了五次,另外,也證明waitpid函數並未阻塞

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM