第 10 章 多進(jìn)程服務(wù)器端
10.1 進(jìn)程概念及應(yīng)用
并發(fā)服務(wù)端的實(shí)現(xiàn)方法:
????????通過改進(jìn)服務(wù)端,使其同時(shí)向所有發(fā)起請求的客戶端提供服務(wù),以提高平均滿意度。而且,網(wǎng)絡(luò)程序中數(shù)據(jù)通信時(shí)間比 CPU 運(yùn)算時(shí)間占比更大,因此,向多個(gè)客戶端提供服務(wù)是一種有效的利用 CPU 的方式。接下來討論同時(shí)向多個(gè)客戶端提供服務(wù)的并發(fā)服務(wù)器端。下面列出的是具有代表性的并發(fā)服務(wù)端的實(shí)現(xiàn)模型和方法:
- 多進(jìn)程服務(wù)器:通過創(chuàng)建多個(gè)進(jìn)程提供服務(wù)
- 多路復(fù)用服務(wù)器:通過捆綁并統(tǒng)一管理 I/O 對象提供服務(wù)
- 多線程服務(wù)器:通過生成與客戶端等量的線程提供服務(wù)
第一種方法:多進(jìn)程服務(wù)器
?理解進(jìn)程:
????????進(jìn)程的定義如下:
占用內(nèi)存空間的正在運(yùn)行的程序。
????????假如你下載了一個(gè)游戲到電腦上,此時(shí)的游戲不是進(jìn)程,而是程序。只有當(dāng)游戲被加載到主內(nèi)存并進(jìn)入運(yùn)行狀態(tài),這是才可稱為進(jìn)程。
進(jìn)程 ID:
????????無論進(jìn)程是如何創(chuàng)建的,所有的進(jìn)程都會(huì)被操作系統(tǒng)分配一個(gè) ID。此 ID 被稱為「進(jìn)程ID」,其值為大于 2 的整數(shù)。1 要分配給操作系統(tǒng)啟動(dòng)后的(用于協(xié)助操作系統(tǒng))首個(gè)進(jìn)程,因此用戶無法得到 ID 值為 1 。接下來輸入以下命令來觀察在 Linux 中運(yùn)行的進(jìn)程:
ps au
?????????通過上面的命令可查看當(dāng)前運(yùn)行的所有進(jìn)程。需要注意的是,該命令同時(shí)列出了 PID(進(jìn)程ID)。參數(shù) a 和 u列出了所有進(jìn)程的詳細(xì)信息。
通過調(diào)用 fork 函數(shù)創(chuàng)建進(jìn)程:
????????創(chuàng)建進(jìn)程的方式很多,此處只介紹用于創(chuàng)建多進(jìn)程服務(wù)端的 fork 函數(shù):
#include <unistd.h>
pid_t fork(void);
// 成功時(shí)返回進(jìn)程ID,失敗時(shí)返回 -1
????????fork 函數(shù)將創(chuàng)建調(diào)用的進(jìn)程副本。也就是說,并非根據(jù)完全不同的程序創(chuàng)建進(jìn)程,而是復(fù)制正在運(yùn)行的、調(diào)用 fork 函數(shù)的進(jìn)程。另外,兩個(gè)進(jìn)程都執(zhí)行 fork 函數(shù)調(diào)用后的語句(準(zhǔn)確的說是在 fork 函數(shù)返回后)。但因?yàn)槭峭ㄟ^同一個(gè)進(jìn)程、復(fù)制相同的內(nèi)存空間,之后的程序流要根據(jù) fork 函數(shù)的返回值加以區(qū)分。即利用 fork 函數(shù)的如下特點(diǎn)區(qū)分程序執(zhí)行流程。
- 父進(jìn)程:fork 函數(shù)返回子進(jìn)程 ID
- 子進(jìn)程:fork 函數(shù)返回 0
????????此處,「父進(jìn)程」(Parent Process)指原進(jìn)程,即調(diào)用 fork 函數(shù)的主體,而「子進(jìn)程」(Child Process)是通過父進(jìn)程調(diào)用 fork 函數(shù)復(fù)制出的進(jìn)程。接下來是調(diào)用 fork 函數(shù)后的程序運(yùn)行流程。如圖所示:????????
?????????從圖中可以看出,父進(jìn)程調(diào)用 fork 函數(shù)的同時(shí)復(fù)制出子進(jìn)程,并分別得到 fork 函數(shù)的返回值。但復(fù)制前,父進(jìn)程將全局變量 gval 增加到 11,將局部變量 lval 的值增加到 25,因此在這種狀態(tài)下完成進(jìn)程復(fù)制。復(fù)制完成后根據(jù) fork 函數(shù)的返回類型區(qū)分父子進(jìn)程。父進(jìn)程的 lval 的值增加 1 ,但這不會(huì)影響子進(jìn)程的 lval 值。同樣子進(jìn)程將 gval 的值增加 1 也不會(huì)影響到父進(jìn)程的 gval 。因?yàn)?fork 函數(shù)調(diào)用后分成了完全不同的進(jìn)程,只是二者共享同一段代碼而已。接下來給出一個(gè)例子:
#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)
gval += 2, lval += 2;
else
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;
}
運(yùn)行結(jié)果:
????????可以看出,當(dāng)執(zhí)行了 fork 函數(shù)之后,此后就相當(dāng)于有了兩個(gè)程序在執(zhí)行代碼,對于父進(jìn)程來說,fork 函數(shù)返回的是子進(jìn)程的ID,對于子進(jìn)程來說,fork 函數(shù)返回 0。所以這兩個(gè)變量,父進(jìn)程進(jìn)行了 +2 操作 ,而子進(jìn)程進(jìn)行了 -2 操作。?
10.2 進(jìn)程和僵尸進(jìn)程
????????文件操作中,關(guān)閉文件和打開文件同等重要。同樣,進(jìn)程銷毀和進(jìn)程創(chuàng)建也同等重要。如果未認(rèn)真對待進(jìn)程銷毀,他們將變成僵尸進(jìn)程。????????
僵尸(Zombie)進(jìn)程:
????????進(jìn)程的工作完成后(執(zhí)行完 main 函數(shù)中的程序后)應(yīng)被銷毀,但有時(shí)這些進(jìn)程將變成僵尸進(jìn)程,占用系統(tǒng)中的重要資源。這種狀態(tài)下的進(jìn)程稱作「僵尸進(jìn)程」,這也是給系統(tǒng)帶來負(fù)擔(dān)的原因之一。
僵尸進(jìn)程是當(dāng)子進(jìn)程比父進(jìn)程先結(jié)束,而父進(jìn)程又沒有回收子進(jìn)程,釋放子進(jìn)程占用的資源,此時(shí)子進(jìn)程將成為一個(gè)僵尸進(jìn)程。如果父進(jìn)程先退出 ,子進(jìn)程被init接管,子進(jìn)程退出后init會(huì)回收其占用的相關(guān)資源
產(chǎn)生僵尸進(jìn)程的原因:
????????為了防止僵尸進(jìn)程產(chǎn)生,先解釋產(chǎn)生僵尸進(jìn)程的原因。利用如下兩個(gè)示例展示調(diào)用 fork 函數(shù)產(chǎn)生子進(jìn)程的終止方式。
- 傳遞參數(shù)并調(diào)用 exit() 函數(shù)
- main 函數(shù)中執(zhí)行 return 語句并返回值
????????向 exit 函數(shù)傳遞的參數(shù)值和 main 函數(shù)的 return 語句返回的值都會(huì)傳遞給操作系統(tǒng)。而操作系統(tǒng)不會(huì)銷毀子進(jìn)程,直到把這些值傳遞給產(chǎn)生該子進(jìn)程的父進(jìn)程。處在這種狀態(tài)下的進(jìn)程就是僵尸進(jìn)程。也就是說將子進(jìn)程變成僵尸進(jìn)程的正是操作系統(tǒng)。既然如此,僵尸進(jìn)程何時(shí)被銷毀呢?
應(yīng)該向創(chuàng)建子進(jìn)程的父進(jìn)程傳遞子進(jìn)程的 exit 參數(shù)值或 return 語句的返回值。
如何向父進(jìn)程傳遞這些值呢?操作系統(tǒng)不會(huì)主動(dòng)把這些值傳遞給父進(jìn)程。只有父進(jìn)程主動(dòng)發(fā)起請求(函數(shù)調(diào)用)的時(shí)候,操作系統(tǒng)才會(huì)傳遞該值。換言之,如果父進(jìn)程未主動(dòng)要求獲得子進(jìn)程結(jié)束狀態(tài)值,操作系統(tǒng)將一直保存,并讓子進(jìn)程長時(shí)間處于僵尸進(jìn)程狀態(tài)。接下來的示例是創(chuàng)建僵尸進(jìn)程:????????
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
pid_t pid = fork(); // 創(chuàng)建一個(gè)新的子進(jìn)程
if (pid == 0) // 子進(jìn)程執(zhí)行代碼
{
puts("Hi, I am a child Process"); // 輸出一條消息:"Hi, I am a child Process"
}
else // 父進(jìn)程執(zhí)行代碼
{
printf("Child Process ID: %d \n", pid); // 輸出子進(jìn)程的進(jìn)程ID(PID)
sleep(30); // 父進(jìn)程睡眠30秒
}
// 以下代碼父子進(jìn)程共同執(zhí)行
if (pid == 0) // 子進(jìn)程將執(zhí)行這部分代碼
puts("End child process"); // 輸出一條消息:"End child process"
else // 父進(jìn)程將執(zhí)行這部分代碼
puts("End parent process"); // 輸出一條消息:"End parent process"
return 0;
}
運(yùn)行結(jié)果:
?????????通過?ps au
?命令可以看出,子進(jìn)程仍然存在,并沒有被銷毀,僵尸進(jìn)程在這里顯示為?Z+。
30秒后,紅框里面的兩個(gè)進(jìn)程會(huì)同時(shí)被銷毀。
銷毀僵尸進(jìn)程 1:利用 wait 函數(shù)
為了銷毀子進(jìn)程,父進(jìn)程應(yīng)該主動(dòng)請求獲取子進(jìn)程的返回值。下面是發(fā)起請求的具體方法。一共有兩種:
#include <sys/wait.h>
pid_t wait(int *statloc);
/*
成功時(shí)返回終止的子進(jìn)程 ID ,失敗時(shí)返回 -1
*/
????????調(diào)用此函數(shù)時(shí)如果已有子進(jìn)程終止,那么子進(jìn)程終止時(shí)傳遞的返回值(exit 函數(shù)的參數(shù)返回值,main 函數(shù)的 return 返回值)將保存到該函數(shù)的參數(shù)所指的內(nèi)存空間。但函數(shù)參數(shù)指向的單元中還包含其他信息,因此需要用下列宏進(jìn)行分離:
- WIFEXITED 子進(jìn)程正常終止時(shí)返回「真」
- WEXITSTATUS 返回子進(jìn)程時(shí)的返回值
也就是說,向 wait 函數(shù)傳遞變量 status 的地址時(shí),調(diào)用 wait 函數(shù)后應(yīng)編寫如下代碼:
if (WIFEXITED(status))
{
puts("Normal termination");
printf("Child pass num: %d", WEXITSTATUS(status));
}
????????如下示例不會(huì)讓子進(jìn)程變成僵尸進(jìn)程:
#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(); // 創(chuàng)建第一個(gè)子進(jìn)程
if (pid == 0) // 子進(jìn)程執(zhí)行代碼
{
return 3; // 子進(jìn)程返回值為3,表示子進(jìn)程正常終止
}
else // 父進(jìn)程執(zhí)行代碼
{
printf("Child PID: %d \n", pid); // 輸出第一個(gè)子進(jìn)程的進(jìn)程ID(PID)
pid = fork(); // 在父進(jìn)程中創(chuàng)建第二個(gè)子進(jìn)程
if (pid == 0) // 第二個(gè)子進(jìn)程執(zhí)行代碼
{
exit(7); // 第二個(gè)子進(jìn)程以退出碼7正常終止
}
else // 父進(jìn)程執(zhí)行代碼
{
printf("Child PID: %d \n", pid); // 輸出第二個(gè)子進(jìn)程的進(jìn)程ID(PID)
wait(&status); // 等待第一個(gè)子進(jìn)程終止并處理其退出狀態(tài)
if (WIFEXITED(status)) // 驗(yàn)證第一個(gè)子進(jìn)程是否正常終止
printf("Child send one: %d \n", WEXITSTATUS(status)); // 輸出第一個(gè)子進(jìn)程的返回值
wait(&status); // 等待第二個(gè)子進(jìn)程終止并處理其退出狀態(tài)
if (WIFEXITED(status)) // 驗(yàn)證第二個(gè)子進(jìn)程是否正常終止
printf("Child send two: %d \n", WEXITSTATUS(status)); // 輸出第二個(gè)子進(jìn)程的返回值
sleep(30); // 父進(jìn)程睡眠30秒
}
}
return 0;
}
運(yùn)行結(jié)果:
????????此時(shí),系統(tǒng)中并沒有上述 PID 對應(yīng)的進(jìn)程,這是因?yàn)檎{(diào)用了 wait 函數(shù),完全銷毀了該子進(jìn)程。另外兩個(gè)子進(jìn)程返回時(shí)返回的 3 和 7 傳遞到了父進(jìn)程。
????????這就是通過 wait 函數(shù)消滅僵尸進(jìn)程的方法,值得注意的是:調(diào)用 wait 函數(shù)時(shí),如果沒有已經(jīng)終止的子進(jìn)程,那么程序?qū)⒆枞˙locking)直到有子進(jìn)程終止,因此要謹(jǐn)慎調(diào)用該函數(shù)。
銷毀僵尸進(jìn)程 2:使用 waitpid 函數(shù)
????????wait 函數(shù)會(huì)引起程序阻塞,還可以考慮調(diào)用 waitpid 函數(shù)。這是防止僵尸進(jìn)程的第二種方法,也是防止阻塞的方法:
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statloc, int options);
/*
成功時(shí)返回終止的子進(jìn)程ID 或 0 ,失敗時(shí)返回 -1
pid: 等待終止的目標(biāo)子進(jìn)程的ID,若傳 -1,則與 wait 函數(shù)相同,可以等待任意子進(jìn)程終止
statloc: 與 wait 函數(shù)的 statloc 參數(shù)具有相同含義
options: 傳遞頭文件 sys/wait.h 聲明的常量 WNOHANG ,即使沒有終止的子進(jìn)程也不會(huì)進(jìn)入阻塞狀態(tài),而是返回 0 退出函數(shù)。
*/
?waitpid 的使用示例:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int status;
pid_t pid = fork(); // 創(chuàng)建子進(jìn)程
if (pid == 0) // 子進(jìn)程執(zhí)行代碼
{
sleep(15); // 用 sleep 推遲子進(jìn)程的執(zhí)行,讓其睡眠15秒
return 24; // 子進(jìn)程返回值為24,表示子進(jìn)程正常終止
}
else // 父進(jìn)程執(zhí)行代碼
{
// 使用 waitpid 傳遞參數(shù) WNOHANG,這樣在沒有終止的子進(jìn)程時(shí),waitpid 立即返回,不會(huì)阻塞
// 循環(huán)等待子進(jìn)程終止
while (!waitpid(-1, &status, WNOHANG))
{
sleep(3); // 父進(jìn)程睡眠3秒
puts("sleep 3 sec.");
}
if (WIFEXITED(status)) // 驗(yàn)證子進(jìn)程是否正常終止
printf("Child send %d \n", WEXITSTATUS(status)); // 輸出子進(jìn)程的返回值
}
return 0;
}
運(yùn)行結(jié)果:
????????可以看出來,在 while 循環(huán)中正好執(zhí)行了 5 次。這也證明了 waitpid 函數(shù)并沒有阻塞 。
10.3 信號(hào)處理
????????我們已經(jīng)知道了進(jìn)程的創(chuàng)建及銷毀的辦法,但是還有一個(gè)問題沒有解決:子進(jìn)程究竟何時(shí)終止?調(diào)用 waitpid 函數(shù)后要無休止的等待嗎?
向操作系統(tǒng)求助:
????????子進(jìn)程終止的識(shí)別主題是操作系統(tǒng),因此,若操作系統(tǒng)能把子進(jìn)程結(jié)束的信息告訴正忙于工作的父進(jìn)程,將有助于構(gòu)建更高效的程序。
????????為了實(shí)現(xiàn)上述的功能,引入信號(hào)處理機(jī)制(Signal Handing)。此處「信號(hào)」是在特定事件發(fā)生時(shí)由操作系統(tǒng)向進(jìn)程發(fā)送的消息。另外,為了響應(yīng)該消息,執(zhí)行與消息相關(guān)的自定義操作的過程被稱為「處理」或「信號(hào)處理」。
信號(hào)與 signal 函數(shù):
????????下面進(jìn)程和操作系統(tǒng)的對話可以幫助理解信號(hào)處理。
進(jìn)程:操作系統(tǒng),如果我之前創(chuàng)建的子進(jìn)程終止,就幫我調(diào)用 zombie_handler 函數(shù)。
操作系統(tǒng):好的,如果你的子進(jìn)程終止,我就幫你調(diào)用 zombie_handler 函數(shù),你先把函數(shù)要執(zhí)行的語句寫好。????????
????????上述的對話,相當(dāng)于「注冊信號(hào)」的過程。即進(jìn)程發(fā)現(xiàn)自己的子進(jìn)程結(jié)束時(shí),請求操作系統(tǒng)調(diào)用的特定函數(shù)。該請求可以通過如下函數(shù)調(diào)用完成:?
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
/*
為了在產(chǎn)生信號(hào)時(shí)調(diào)用,返回之前注冊的函數(shù)指針
函數(shù)名: signal
參數(shù):int signo,void(*func)(int)
返回類型:參數(shù)類型為int型,返回 void 型函數(shù)指針
*/
????????調(diào)用上述函數(shù)時(shí),第一個(gè)參數(shù)為特殊情況信息,第二個(gè)參數(shù)為特殊情況下將要調(diào)用的函數(shù)的地址值(指針)。發(fā)生第一個(gè)參數(shù)代表的情況時(shí),調(diào)用第二個(gè)參數(shù)所指的函數(shù)。下面給出可以在 signal 函數(shù)中注冊的部分特殊情況和對應(yīng)的函數(shù)。
- SIGALRM:已到通過調(diào)用 alarm 函數(shù)注冊時(shí)間
- SIGINT:輸入 ctrl+c
- SIGCHLD:子進(jìn)程終止
????????接下來編寫調(diào)用 signal 函數(shù)的語句完成如下請求:子進(jìn)程終止則調(diào)用 mychild 函數(shù)。
????????此時(shí) mychild 函數(shù)的參數(shù)應(yīng)為 int ,返回值類型應(yīng)為 void 。只有這樣才能成為 signal 函數(shù)的第二個(gè)參數(shù)。另外,常數(shù) SIGCHLD 定義了子進(jìn)程終止的情況,應(yīng)成為 signal 函數(shù)的第一個(gè)參數(shù)。也就是說,signal 函數(shù)調(diào)用語句如下:
signal(SIGCHLD , mychild);
接下來編寫 signal 函數(shù)的調(diào)用語句,分別完成如下兩個(gè)請求:
- 已到通過 alarm 函數(shù)注冊時(shí)間,請調(diào)用 timeout 函數(shù)
- 輸入 ctrl+c 時(shí)調(diào)用 keycontrol 函數(shù)
代表這 2 種情況的常數(shù)分別為 SIGALRM 和 SIGINT ,因此按如下方式調(diào)用 signal 函數(shù):
signal(SIGALRM , timeout);
signal(SIGINT , keycontrol);
????????以上就是信號(hào)注冊過程。注冊好信號(hào)之后,發(fā)生注冊信號(hào)時(shí)(注冊的情況發(fā)生時(shí)),操作系統(tǒng)將調(diào)用該信號(hào)對應(yīng)的函數(shù)。先介紹 alarm 函數(shù):
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
// 返回0或以秒為單位的距 SIGALRM 信號(hào)發(fā)生所剩時(shí)間
????????如果調(diào)用該函數(shù)的同時(shí)向它傳遞一個(gè)正整型參數(shù),相應(yīng)時(shí)間后(以秒為單位)將產(chǎn)生 SIGALRM 信號(hào)。若向該函數(shù)傳遞為 0 ,則之前對 SIGALRM 信號(hào)的預(yù)約將取消。如果通過改函數(shù)預(yù)約信號(hào)后未指定該信號(hào)對應(yīng)的處理函數(shù),則(通過調(diào)用 signal 函數(shù))終止進(jìn)程,不做任何處理。
? ? ? ? 示例代碼:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void timeout(int sig) //信號(hào)處理器
{
if (sig == SIGALRM)
puts("Time out!");
alarm(2); //為了每隔 2 秒重復(fù)產(chǎn)生 SIGALRM 信號(hào),在信號(hào)處理器中調(diào)用 alarm 函數(shù)
}
void keycontrol(int sig) //信號(hào)處理器
{
if (sig == SIGINT)
puts("CTRL+C pressed");
}
int main(int argc, char *argv[])
{
int i;
signal(SIGALRM, timeout); //注冊信號(hào)及相應(yīng)處理器
signal(SIGINT, keycontrol);
alarm(2); //預(yù)約 2 秒后發(fā)生 SIGALRM 信號(hào)
for (i = 0; i < 3; i++)
{
puts("wait...");
sleep(100);
}
return 0;
}
? ? ? ? 運(yùn)行結(jié)果:
? ? ? ? 第一次結(jié)果是沒有任何輸入的運(yùn)行結(jié)果 。 第二次是連續(xù)鍵入3次?ctrl+c 的結(jié)果。發(fā)生信號(hào)時(shí)將喚醒由于調(diào)用 sleep 函數(shù)而進(jìn)入阻塞狀態(tài)的進(jìn)程。
????????調(diào)用函數(shù)的主體的確是操作系統(tǒng),但是進(jìn)程處于睡眠狀態(tài)時(shí)無法調(diào)用函數(shù),因此,產(chǎn)生信號(hào)時(shí),為了調(diào)用信號(hào)處理器,將喚醒由于調(diào)用 sleep 函數(shù)而進(jìn)入阻塞狀態(tài)的進(jìn)程。而且,進(jìn)程一旦被喚醒,就不會(huì)再進(jìn)入睡眠狀態(tài)。即使還未到 sleep 中規(guī)定的時(shí)間也是如此。所以上述示例運(yùn)行不到 10 秒后就會(huì)結(jié)束,連續(xù)輸入 CTRL+C 可能連一秒都不到。
利用 sigaction 函數(shù)進(jìn)行信號(hào)處理:
????????前面所學(xué)的內(nèi)容可以防止僵尸進(jìn)程,還有一個(gè)函數(shù),叫做 sigaction 函數(shù),他類似于 signal 函數(shù),而且可以完全代替后者,也更穩(wěn)定。之所以穩(wěn)定,是因?yàn)椋?strong>signal 函數(shù)在 Unix 系列的不同操作系統(tǒng)可能存在區(qū)別,但 sigaction 函數(shù)完全相同。
????????實(shí)際上現(xiàn)在很少用 signal 函數(shù)編寫程序,他只是為了保持對舊程序的兼容,下面介紹 sigaction 函數(shù),只講解可以替換 signal 函數(shù)的功能:
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
/*
成功時(shí)返回 0 ,失敗時(shí)返回 -1
act: 對于第一個(gè)參數(shù)的信號(hào)處理函數(shù)(信號(hào)處理器)信息。
oldact: 通過此參數(shù)獲取之前注冊的信號(hào)處理函數(shù)指針,若不需要?jiǎng)t傳遞 0
*/
????????聲明并初始化 sigaction 結(jié)構(gòu)體變量以調(diào)用上述函數(shù),該結(jié)構(gòu)體定義如下:?
struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
};
????????此結(jié)構(gòu)體的成員 sa_handler 保存信號(hào)處理的函數(shù)指針值(地址值)。sa_mask 和 sa_flags 的所有位初始化 0 即可。這 2 個(gè)成員用于指定信號(hào)相關(guān)的選項(xiàng)和特性,而我們的目的主要是防止產(chǎn)生僵尸進(jìn)程,故省略。
????????下面的示例是關(guān)于 sigaction 函數(shù)的使用方法:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void timeout(int sig)
{
if (sig == SIGALRM)
puts("Time out!");
alarm(2);
}
int main(int argc, char *argv[])
{
int i;
struct sigaction act;
act.sa_handler = timeout; //保存函數(shù)指針
sigemptyset(&act.sa_mask); //將 sa_mask 成員的所有位初始化成0
act.sa_flags = 0; //sa_flags 同樣初始化成 0
sigaction(SIGALRM, &act, 0); //注冊 SIGALRM 信號(hào)的處理器。
alarm(2); //2 秒后發(fā)生 SIGALRM 信號(hào)
for (int i = 0; i < 3; i++)
{
puts("wait...");
sleep(100);
}
return 0;
}
? ? ? ? 運(yùn)行結(jié)果:
?????????可以發(fā)現(xiàn),結(jié)果和之前用 signal 函數(shù)的結(jié)果沒有什么區(qū)別。
利用信號(hào)處理技術(shù)消滅僵尸進(jìn)程:
????????下面利用子進(jìn)程終止時(shí)產(chǎn)生 SIGCHLD 信號(hào)這一點(diǎn),來用信號(hào)處理來消滅僵尸進(jìn)程??匆韵麓a:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void read_childproc(int sig)
{
int status;
pid_t id = waitpid(-1, &status, WNOHANG);
if (WIFEXITED(status))
{
printf("Removed proc id: %d \n", id); //子進(jìn)程的 pid
printf("Child send: %d \n", WEXITSTATUS(status)); //子進(jìn)程的返回值
}
}
int main(int argc, char *argv[])
{
pid_t pid;
struct sigaction act;
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, 0);
pid = fork();
if (pid == 0) //子進(jìn)程執(zhí)行階段
{
puts("Hi I'm child process");
sleep(10);
return 12;
}
else //父進(jìn)程執(zhí)行階段
{
printf("Child proc id: %d\n", pid);
pid = fork();
if (pid == 0)
{
puts("Hi! I'm child process");
sleep(10);
exit(24);
}
else
{
int i;
printf("Child proc id: %d \n", pid);
for (i = 0; i < 5; i++)
{
puts("wait");
sleep(5);
}
}
}
return 0;
}
????????運(yùn)行結(jié)果:
????????代碼中創(chuàng)建了兩個(gè)子進(jìn)程,父進(jìn)程在創(chuàng)建子進(jìn)程后會(huì)輸出子進(jìn)程的信息,然后進(jìn)入等待狀態(tài),睡眠5秒,循環(huán)輸出"wait"。而子進(jìn)程在輸出自身信息后,睡眠10秒后返回不同的返回值。父進(jìn)程在循環(huán)等待期間,若有子進(jìn)程終止,則信號(hào)處理函數(shù) read_childproc
會(huì)被調(diào)用,輸出子進(jìn)程的PID和返回值。?
10.4 基于多任務(wù)的并發(fā)服務(wù)器
基于進(jìn)程的并發(fā)服務(wù)器模型:
????????之前的回聲服務(wù)器每次只能同事向 1 個(gè)客戶端提供服務(wù)。因此,需要擴(kuò)展回聲服務(wù)器,使其可以同時(shí)向多個(gè)客戶端提供服務(wù)。下圖是基于多進(jìn)程的回聲服務(wù)器的模型:
????????從圖中可以看出,每當(dāng)有客戶端請求時(shí)(連接請求),回聲服務(wù)器都創(chuàng)建子進(jìn)程以提供服務(wù)。如果請求的客戶端有 5 個(gè),則將創(chuàng)建 5 個(gè)子進(jìn)程來提供服務(wù),為了完成這些任務(wù),需要經(jīng)過如下過程:
- 第一階段:回聲服務(wù)器端(父進(jìn)程)通過調(diào)用 accept 函數(shù)受理連接請求
- 第二階段:此時(shí)獲取的套接字文件描述符創(chuàng)建并傳遞給子進(jìn)程
- 第三階段:進(jìn)程利用傳遞來的文件描述符提供服務(wù)
實(shí)現(xiàn)并發(fā)服務(wù)器:
????????
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
void read_childproc(int sig);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
pid_t pid;
struct sigaction act;
socklen_t adr_sz;
int str_len, state;
char buf[BUF_SIZE];
if (argc != 2)
{
printf("Usgae : %s <port>\n", argv[0]);
exit(1);
}
act.sa_handler = read_childproc; //防止僵尸進(jìn)程
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
state = sigaction(SIGCHLD, &act, 0); //注冊信號(hào)處理器,把成功的返回值給 state
serv_sock = socket(PF_INET, SOCK_STREAM, 0); //創(chuàng)建服務(wù)端套接字
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) //分配IP地址和端口號(hào)
error_handling("bind() error");
if (listen(serv_sock, 5) == -1) //進(jìn)入等待連接請求狀態(tài)
error_handling("listen() error");
while (1)
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
if (clnt_sock == -1)
continue;
else
puts("new client connected...");
pid = fork(); //此時(shí),父子進(jìn)程分別帶有一個(gè)套接字
if (pid == -1)
{
close(clnt_sock);
continue;
}
if (pid == 0) //子進(jìn)程運(yùn)行區(qū)域,此部分向客戶端提供回聲服務(wù)
{
close(serv_sock); //關(guān)閉服務(wù)器套接字,因?yàn)閺母高M(jìn)程傳遞到了子進(jìn)程
while ((str_len = read(clnt_sock, buf, BUFSIZ)) != 0)
write(clnt_sock, buf, str_len);
close(clnt_sock);
puts("client disconnected...");
return 0;
}
else
close(clnt_sock); //通過 accept 函數(shù)創(chuàng)建的套接字文件描述符已經(jīng)復(fù)制給子進(jìn)程,因?yàn)榉?wù)器端要銷毀自己擁有的
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
void read_childproc(int sig)
{
pid_t pid;
int status;
pid = waitpid(-1, &status, WNOHANG);
printf("removed proc id: %d \n", pid);
}
?運(yùn)行結(jié)果:
????????此時(shí)的服務(wù)端支持同時(shí)給多個(gè)客戶端進(jìn)行服務(wù),每有一個(gè)客戶端連接服務(wù)端,就會(huì)多開一個(gè)子進(jìn)程,所以可以同時(shí)提供服務(wù)。?
通過 fork 函數(shù)復(fù)制文件描述符:
????????示例中給出了通過 fork 函數(shù)復(fù)制文件描述符的過程。父進(jìn)程將 2 個(gè)套接字(一個(gè)是服務(wù)端套接字另一個(gè)是客戶端套接字)文件描述符復(fù)制給了子進(jìn)程。
????????調(diào)用 fork 函數(shù)時(shí)復(fù)制父進(jìn)程的所有資源,但是套接字不是歸進(jìn)程所有的,而是歸操作系統(tǒng)所有,只是進(jìn)程擁有代表相應(yīng)套接字的文件描述符。
????????復(fù)制套接字后,同一端口將對應(yīng)多個(gè)套接字。
?????????如上圖所示,1 個(gè)套接字存在 2 個(gè)文件描述符時(shí),只有 2 個(gè)文件描述符都終止(銷毀)后,才能銷毀套接字。如果維持圖中的狀態(tài),即使子進(jìn)程銷毀了與客戶端連接的套接字文件描述符,也無法銷毀套接字(服務(wù)器套接字同樣如此)。因此調(diào)用 fork 函數(shù)后,要將無關(guān)緊要的套接字文件描述符關(guān)掉,如下圖所示:
10.5 分割 TCP 的 I/O 程序
分割 I/O 的優(yōu)點(diǎn):
????????我們已經(jīng)實(shí)現(xiàn)的回聲客戶端的數(shù)據(jù)回聲方式如下:向服務(wù)器傳輸數(shù)據(jù),并等待服務(wù)器端回復(fù)。無條件等待,直到接收完服務(wù)器端的回聲數(shù)據(jù)后,才能傳輸下一批數(shù)據(jù)。? ? ?
????????傳輸數(shù)據(jù)后要等待服務(wù)器端返回的數(shù)據(jù),因?yàn)槌绦虼a中重復(fù)調(diào)用了 read 和 write 函數(shù)。只能這么寫的原因之一是,程序在 1 個(gè)進(jìn)程中運(yùn)行,現(xiàn)在可以創(chuàng)建多個(gè)進(jìn)程,因此可以分割數(shù)據(jù)收發(fā)過程。默認(rèn)分割過程如下圖所示:
????????從圖中可以看出,客戶端的父進(jìn)程負(fù)責(zé)接收數(shù)據(jù),額外創(chuàng)建的子進(jìn)程負(fù)責(zé)發(fā)送數(shù)據(jù),分割后,不同進(jìn)程分別負(fù)責(zé)輸入輸出,這樣,無論客戶端是否從服務(wù)器端接收完數(shù)據(jù)都可以進(jìn)程傳輸。
分割 I/O 程序的另外一個(gè)好處是,可以提高頻繁交換數(shù)據(jù)的程序性能,如下圖所示:
????????下面是回聲客戶端的 I/O 分割的代碼實(shí)現(xiàn):?
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
void read_routine(int sock, char *buf);
void write_routine(int sock, char *buf);
int main(int argc, char *argv[])
{
int sock;
pid_t pid;
char buf[BUF_SIZE];
struct sockaddr_in serv_adr;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("connect() error!");
pid = fork();
if (pid == 0)
write_routine(sock, buf);
else
read_routine(sock, buf);
close(sock);
return 0;
}
void read_routine(int sock, char *buf)
{
while (1)
{
int str_len = read(sock, buf, BUF_SIZE);
if (str_len == 0)
return;
buf[str_len] = 0;
printf("Message from server: %s", buf);
}
}
void write_routine(int sock, char *buf)
{
while (1)
{
fgets(buf, BUF_SIZE, stdin);
if (!strcmp(buf, "q\n") || !strcmp(buf, "Q\n"))
{
shutdown(sock, SHUT_WR); //向服務(wù)器端傳遞 EOF,因?yàn)閒ork函數(shù)復(fù)制了文件描述度,所以通過1次close調(diào)用不夠
return;
}
write(sock, buf, strlen(buf));
}
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
運(yùn)行結(jié)果:
習(xí)題:?
1、請說明進(jìn)程變?yōu)榻┦M(jìn)程的過程以及預(yù)防措施。
????????進(jìn)程變?yōu)榻┦M(jìn)程的過程:
-
創(chuàng)建子進(jìn)程:當(dāng)一個(gè)進(jìn)程創(chuàng)建子進(jìn)程后,子進(jìn)程會(huì)拷貝父進(jìn)程的資源和狀態(tài),包括代碼、數(shù)據(jù)、打開的文件等。
-
子進(jìn)程終止:子進(jìn)程執(zhí)行完任務(wù)后,會(huì)調(diào)用
exit()
或者從main
函數(shù)中返回一個(gè)值來終止。此時(shí),子進(jìn)程的終止?fàn)顟B(tài)和退出碼會(huì)被保存在內(nèi)核的進(jìn)程表項(xiàng)中,等待父進(jìn)程回收。 -
父進(jìn)程未及時(shí)回收子進(jìn)程:父進(jìn)程可能因?yàn)楦鞣N原因,無法及時(shí)處理子進(jìn)程的終止?fàn)顟B(tài),比如父進(jìn)程忙于其他任務(wù)、死鎖、阻塞在某個(gè)操作上等。在這種情況下,子進(jìn)程的進(jìn)程表項(xiàng)仍然保留在系統(tǒng)進(jìn)程表中,成為僵尸進(jìn)程。盡管子進(jìn)程已經(jīng)終止,但內(nèi)核仍然保存它的狀態(tài)信息,以便父進(jìn)程在合適的時(shí)候獲取。文章來源:http://www.zghlxwxcb.cn/news/detail-605428.html
????????預(yù)防措施:通過 wait 和 waitpid 函數(shù)加上信號(hào)函數(shù)寫代碼來預(yù)防。文章來源地址http://www.zghlxwxcb.cn/news/detail-605428.html
到了這里,關(guān)于《TCP IP網(wǎng)絡(luò)編程》第十章的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!