進程概念及應用
我們知道,監聽套接字會有一個等待隊列,里面存放着不同客戶端的連接請求,如果有一百個客戶端,每個客戶端的請求處理是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函數並未阻塞
