Linux下的進(jìn)程間通信:套接字和信號(hào)
學(xué)習(xí)在 Linux 中進(jìn)程是如何與其他進(jìn)程進(jìn)行同步的。
本篇是 Linux 下進(jìn)程間通信(IPC)系列的第三篇同時(shí)也是最后一篇文章。第一篇文章聚焦在通過(guò)共享存儲(chǔ)(文件和共享內(nèi)存段)來(lái)進(jìn)行 IPC,第二篇文章則通過(guò)管道(無(wú)名的或者命名的)及消息隊(duì)列來(lái)達(dá)到相同的目的。這篇文章將目光從高處(套接字)然后到低處(信號(hào))來(lái)關(guān)注 IPC。代碼示例將用力地充實(shí)下面的解釋細(xì)節(jié)。
套接字
正如管道有兩種類型(命名和無(wú)名)一樣,套接字也有兩種類型。IPC 套接字(即 Unix 套接字)給予進(jìn)程在相同設(shè)備(主機(jī))上基于通道的通信能力;而網(wǎng)絡(luò)套接字給予進(jìn)程運(yùn)行在不同主機(jī)的能力,因此也帶來(lái)了網(wǎng)絡(luò)通信的能力。網(wǎng)絡(luò)套接字需要底層協(xié)議的支持,例如 TCP(傳輸控制協(xié)議)或 UDP(用戶數(shù)據(jù)報(bào)協(xié)議)。
與之相反,IPC 套接字依賴于本地系統(tǒng)內(nèi)核的支持來(lái)進(jìn)行通信;特別的,IPC 通信使用一個(gè)本地的文件作為套接字地址。盡管這兩種套接字的實(shí)現(xiàn)有所不同,但在本質(zhì)上,IPC 套接字和網(wǎng)絡(luò)套接字的 API 是一致的。接下來(lái)的例子將包含網(wǎng)絡(luò)套接字的內(nèi)容,但示例服務(wù)器和客戶端程序可以在相同的機(jī)器上運(yùn)行,因?yàn)榉?wù)器使用了 localhost
(127.0.0.1)這個(gè)網(wǎng)絡(luò)地址,該地址表示的是本地機(jī)器上的本地機(jī)器地址。
套接字以流的形式(下面將會(huì)討論到)被配置為雙向的,并且其控制遵循 C/S(客戶端/服務(wù)器端)模式:客戶端通過(guò)嘗試連接一個(gè)服務(wù)器來(lái)初始化對(duì)話,而服務(wù)器端將嘗試接受該連接。假如萬(wàn)事順利,來(lái)自客戶端的請(qǐng)求和來(lái)自服務(wù)器端的響應(yīng)將通過(guò)管道進(jìn)行傳輸,直到其中任意一方關(guān)閉該通道,從而斷開(kāi)這個(gè)連接。
一個(gè)迭代服務(wù)器(只適用于開(kāi)發(fā))將一直和連接它的客戶端打交道:從最開(kāi)始服務(wù)第一個(gè)客戶端,然后到這個(gè)連接關(guān)閉,然后服務(wù)第二個(gè)客戶端,循環(huán)往復(fù)。這種方式的一個(gè)缺點(diǎn)是處理一個(gè)特定的客戶端可能會(huì)掛起,使得其他的客戶端一直在后面等待。生產(chǎn)級(jí)別的服務(wù)器將是并發(fā)的,通常使用了多進(jìn)程或者多線程的混合。例如,我臺(tái)式機(jī)上的 Nginx 網(wǎng)絡(luò)服務(wù)器有一個(gè) 4 個(gè)工人的進(jìn)程池,它們可以并發(fā)地處理客戶端的請(qǐng)求。在下面的代碼示例中,我們將使用迭代服務(wù)器,使得我們將要處理的問(wèn)題保持在一個(gè)很小的規(guī)模,只關(guān)注基本的 API,而不去關(guān)心并發(fā)的問(wèn)題。
最后,隨著各種 POSIX 改進(jìn)的出現(xiàn),套接字 API 隨著時(shí)間的推移而發(fā)生了顯著的變化。當(dāng)前針對(duì)服務(wù)器端和客戶端的示例代碼特意寫(xiě)的比較簡(jiǎn)單,但是它著重強(qiáng)調(diào)了基于流的套接字中連接的雙方。下面是關(guān)于流控制的一個(gè)總結(jié),其中服務(wù)器端在一個(gè)終端中開(kāi)啟,而客戶端在另一個(gè)不同的終端中開(kāi)啟:
- 服務(wù)器端等待客戶端的連接,對(duì)于給定的一個(gè)成功連接,它就讀取來(lái)自客戶端的數(shù)據(jù)。
- 為了強(qiáng)調(diào)是雙方的會(huì)話,服務(wù)器端會(huì)對(duì)接收自客戶端的數(shù)據(jù)做回應(yīng)。這些數(shù)據(jù)都是 ASCII 字符代碼,它們組成了一些書(shū)的標(biāo)題。
- 客戶端將書(shū)的標(biāo)題寫(xiě)給服務(wù)器端的進(jìn)程,并從服務(wù)器端的回應(yīng)中讀取到相同的標(biāo)題。然后客戶端和服務(wù)器端都在屏幕上打印出標(biāo)題。下面是服務(wù)器端的輸出,客戶端的輸出也和它完全一樣:
Listening on port 9876 for clients...
War and Peace
Pride and Prejudice
The Sound and the Fury
示例 1. 使用套接字的客戶端程序
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include "sock.h"
void report(const char* msg, int terminate) {
perror(msg);
if (terminate) exit(-1); /* failure */
}
int main() {
int fd = socket(AF_INET, /* network versus AF_LOCAL */
SOCK_STREAM, /* reliable, bidirectional: TCP */
0); /* system picks underlying protocol */
if (fd < 0) report("socket", 1); /* terminate */
/* bind the server's local address in memory */
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr)); /* clear the bytes */
saddr.sin_family = AF_INET; /* versus AF_LOCAL */
saddr.sin_addr.s_addr = htonl(INADDR_ANY); /* host-to-network endian */
saddr.sin_port = htons(PortNumber); /* for listening */
if (bind(fd, (struct sockaddr *) &saddr, sizeof(saddr)) < 0)
report("bind", 1); /* terminate */
/* listen to the socket */
if (listen(fd, MaxConnects) < 0) /* listen for clients, up to MaxConnects */
report("listen", 1); /* terminate */
fprintf(stderr, "Listening on port %i for clients...\n", PortNumber);
/* a server traditionally listens indefinitely */
while (1) {
struct sockaddr_in caddr; /* client address */
int len = sizeof(caddr); /* address length could change */
int client_fd = accept(fd, (struct sockaddr*) &caddr, &len); /* accept blocks */
if (client_fd < 0) {
report("accept", 0); /* don't terminated, though there's a problem */
continue;
}
/* read from client */
int i;
for (i = 0; i < ConversationLen; i++) {
char buffer[BuffSize + 1];
memset(buffer, '\0', sizeof(buffer));
int count = read(client_fd, buffer, sizeof(buffer));
if (count > 0) {
puts(buffer);
write(client_fd, buffer, sizeof(buffer)); /* echo as confirmation */
}
}
close(client_fd); /* break connection */
} /* while(1) */
return 0;
}
上面的服務(wù)器端程序執(zhí)行典型的 4 個(gè)步驟來(lái)準(zhǔn)備回應(yīng)客戶端的請(qǐng)求,然后接受其他的獨(dú)立請(qǐng)求。這里每一個(gè)步驟都以服務(wù)器端程序調(diào)用的系統(tǒng)函數(shù)來(lái)命名。
socket(…)
:為套接字連接獲取一個(gè)文件描述符bind(…)
:將套接字和服務(wù)器主機(jī)上的一個(gè)地址進(jìn)行綁定listen(…)
:監(jiān)聽(tīng)客戶端請(qǐng)求accept(…)
:接受一個(gè)特定的客戶端請(qǐng)求
上面的 socket
調(diào)用的完整形式為:
int sockfd = socket(AF_INET, /* versus AF_LOCAL */
SOCK_STREAM, /* reliable, bidirectional */
0); /* system picks protocol (TCP) */
第一個(gè)參數(shù)特別指定了使用的是一個(gè)網(wǎng)絡(luò)套接字,而不是 IPC 套接字。對(duì)于第二個(gè)參數(shù)有多種選項(xiàng),但 SOCK_STREAM
和 SOCK_DGRAM
(數(shù)據(jù)報(bào))是最為常用的?;诹鞯奶捉幼种С挚尚磐ǖ?,在這種通道中如果發(fā)生了信息的丟失或者更改,都將會(huì)被報(bào)告。這種通道是雙向的,并且從一端到另外一端的有效載荷在大小上可以是任意的。相反的,基于數(shù)據(jù)報(bào)的套接字大多是不可信的,沒(méi)有方向性,并且需要固定大小的載荷。socket
的第三個(gè)參數(shù)特別指定了協(xié)議。對(duì)于這里展示的基于流的套接字,只有一種協(xié)議選擇:TCP,在這里表示的 0
。因?yàn)閷?duì) socket
的一次成功調(diào)用將返回相似的文件描述符,套接字可以被讀寫(xiě),對(duì)應(yīng)的語(yǔ)法和讀寫(xiě)一個(gè)本地文件是類似的。
對(duì) bind
的調(diào)用是最為復(fù)雜的,因?yàn)樗从吵隽嗽谔捉幼?API 方面上的各種改進(jìn)。我們感興趣的點(diǎn)是這個(gè)調(diào)用將一個(gè)套接字和服務(wù)器端所在機(jī)器中的一個(gè)內(nèi)存地址進(jìn)行綁定。但對(duì) listen
的調(diào)用就非常直接了:
if (listen(fd, MaxConnects) < 0)
第一個(gè)參數(shù)是套接字的文件描述符,第二個(gè)參數(shù)則指定了在服務(wù)器端處理一個(gè)拒絕連接錯(cuò)誤之前,有多少個(gè)客戶端連接被允許連接。(在頭文件 sock.h
中 MaxConnects
的值被設(shè)置為 8
。)
accept
調(diào)用默認(rèn)將是一個(gè)阻塞等待:服務(wù)器端將不做任何事情直到一個(gè)客戶端嘗試連接它,然后進(jìn)行處理。accept
函數(shù)返回的值如果是 -1
則暗示有錯(cuò)誤發(fā)生。假如這個(gè)調(diào)用是成功的,則它將返回另一個(gè)文件描述符,這個(gè)文件描述符被用來(lái)指代另一個(gè)可讀可寫(xiě)的套接字,它與 accept
調(diào)用中的第一個(gè)參數(shù)對(duì)應(yīng)的接收套接字有所不同。服務(wù)器端使用這個(gè)可讀可寫(xiě)的套接字來(lái)從客戶端讀取請(qǐng)求然后寫(xiě)回它的回應(yīng)。接收套接字只被用于接受客戶端的連接。
在設(shè)計(jì)上,服務(wù)器端可以一直運(yùn)行下去。當(dāng)然服務(wù)器端可以通過(guò)在命令行中使用 Ctrl+C
來(lái)終止它。
示例 2. 使用套接字的客戶端
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <netdb.h>
#include "sock.h"
const char* books[] = {"War and Peace",
"Pride and Prejudice",
"The Sound and the Fury"};
void report(const char* msg, int terminate) {
perror(msg);
if (terminate) exit(-1); /* failure */
}
int main() {
/* fd for the socket */
int sockfd = socket(AF_INET, /* versus AF_LOCAL */
SOCK_STREAM, /* reliable, bidirectional */
0); /* system picks protocol (TCP) */
if (sockfd < 0) report("socket", 1); /* terminate */
/* get the address of the host */
struct hostent* hptr = gethostbyname(Host); /* localhost: 127.0.0.1 */
if (!hptr) report("gethostbyname", 1); /* is hptr NULL? */
if (hptr->h_addrtype != AF_INET) /* versus AF_LOCAL */
report("bad address family", 1);
/* connect to the server: configure server's address 1st */
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr =
((struct in_addr*) hptr->h_addr_list[0])->s_addr;
saddr.sin_port = htons(PortNumber); /* port number in big-endian */
if (connect(sockfd, (struct sockaddr*) &saddr, sizeof(saddr)) < 0)
report("connect", 1);
/* Write some stuff and read the echoes. */
puts("Connect to server, about to write some stuff...");
int i;
for (i = 0; i < ConversationLen; i++) {
if (write(sockfd, books[i], strlen(books[i])) > 0) {
/* get confirmation echoed from server and print */
char buffer[BuffSize + 1];
memset(buffer, '\0', sizeof(buffer));
if (read(sockfd, buffer, sizeof(buffer)) > 0)
puts(buffer);
}
}
puts("Client done, about to exit...");
close(sockfd); /* close the connection */
return 0;
}
客戶端程序的設(shè)置代碼和服務(wù)器端類似。兩者主要的區(qū)別既不是在于監(jiān)聽(tīng)也不在于接收,而是連接:
if (connect(sockfd, (struct sockaddr*) &saddr, sizeof(saddr)) < 0)
對(duì) connect
的調(diào)用可能因?yàn)槎喾N原因而導(dǎo)致失敗,例如客戶端擁有錯(cuò)誤的服務(wù)器端地址或者已經(jīng)有太多的客戶端連接上了服務(wù)器端。假如 connect
操作成功,客戶端將在一個(gè) for
循環(huán)中,寫(xiě)入它的請(qǐng)求然后讀取返回的響應(yīng)。在會(huì)話后,服務(wù)器端和客戶端都將調(diào)用 close
去關(guān)閉這個(gè)可讀可寫(xiě)套接字,盡管任何一邊的關(guān)閉操作就足以關(guān)閉它們之間的連接。此后客戶端可以退出了,但正如前面提到的那樣,服務(wù)器端可以一直保持開(kāi)放以處理其他事務(wù)。
從上面的套接字示例中,我們看到了請(qǐng)求信息被回顯給客戶端,這使得客戶端和服務(wù)器端之間擁有進(jìn)行豐富對(duì)話的可能性。也許這就是套接字的主要魅力。在現(xiàn)代系統(tǒng)中,客戶端應(yīng)用(例如一個(gè)數(shù)據(jù)庫(kù)客戶端)和服務(wù)器端通過(guò)套接字進(jìn)行通信非常常見(jiàn)。正如先前提及的那樣,本地 IPC 套接字和網(wǎng)絡(luò)套接字只在某些實(shí)現(xiàn)細(xì)節(jié)上面有所不同,一般來(lái)說(shuō),IPC 套接字有著更低的消耗和更好的性能。它們的通信 API 基本是一樣的。
信號(hào)
信號(hào)會(huì)中斷一個(gè)正在執(zhí)行的程序,在這種意義下,就是用信號(hào)與這個(gè)程序進(jìn)行通信。大多數(shù)的信號(hào)要么可以被忽略(阻塞)或者被處理(通過(guò)特別設(shè)計(jì)的代碼)。SIGSTOP
(暫停)和 SIGKILL
(立即停止)是最應(yīng)該提及的兩種信號(hào)。這種符號(hào)常量有整數(shù)類型的值,例如 SIGKILL
對(duì)應(yīng)的值為 9
。
信號(hào)可以在與用戶交互的情況下發(fā)生。例如,一個(gè)用戶從命令行中敲了 Ctrl+C
來(lái)終止一個(gè)從命令行中啟動(dòng)的程序;Ctrl+C
將產(chǎn)生一個(gè) SIGTERM
信號(hào)。SIGTERM
意即終止,它可以被阻塞或者被處理,而不像 SIGKILL
信號(hào)那樣。一個(gè)進(jìn)程也可以通過(guò)信號(hào)和另一個(gè)進(jìn)程通信,這樣使得信號(hào)也可以作為一種 IPC 機(jī)制。
考慮一下一個(gè)多進(jìn)程應(yīng)用,例如 Nginx 網(wǎng)絡(luò)服務(wù)器是如何被另一個(gè)進(jìn)程優(yōu)雅地關(guān)閉的。kill
函數(shù):
int kill(pid_t pid, int signum); /* declaration */
可以被一個(gè)進(jìn)程用來(lái)終止另一個(gè)進(jìn)程或者一組進(jìn)程。假如 kill
函數(shù)的第一個(gè)參數(shù)是大于 0
的,那么這個(gè)參數(shù)將會(huì)被認(rèn)為是目標(biāo)進(jìn)程的 pid
(進(jìn)程 ID),假如這個(gè)參數(shù)是 0
,則這個(gè)參數(shù)將會(huì)被視作信號(hào)發(fā)送者所屬的那組進(jìn)程。
kill
的第二個(gè)參數(shù)要么是一個(gè)標(biāo)準(zhǔn)的信號(hào)數(shù)字(例如 SIGTERM
或 SIGKILL
),要么是 0
,這將會(huì)對(duì)信號(hào)做一次詢問(wèn),確認(rèn)第一個(gè)參數(shù)中的 pid
是否是有效的。這樣優(yōu)雅地關(guān)閉一個(gè)多進(jìn)程應(yīng)用就可以通過(guò)向組成該應(yīng)用的一組進(jìn)程發(fā)送一個(gè)終止信號(hào)來(lái)完成,具體來(lái)說(shuō)就是調(diào)用一個(gè) kill
函數(shù),使得這個(gè)調(diào)用的第二個(gè)參數(shù)是 SIGTERM
。(Nginx 主進(jìn)程可以通過(guò)調(diào)用 kill
函數(shù)來(lái)終止其他工人進(jìn)程,然后再停止自己。)就像許多庫(kù)函數(shù)一樣,kill
函數(shù)通過(guò)一個(gè)簡(jiǎn)單的可變語(yǔ)法擁有更多的能力和靈活性。
示例 3. 一個(gè)多進(jìn)程系統(tǒng)的優(yōu)雅停止
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
void graceful(int signum) {
printf("\tChild confirming received signal: %i\n", signum);
puts("\tChild about to terminate gracefully...");
sleep(1);
puts("\tChild terminating now...");
_exit(0); /* fast-track notification of parent */
}
void set_handler() {
struct sigaction current;
sigemptyset(¤t.sa_mask); /* clear the signal set */
current.sa_flags = 0; /* enables setting sa_handler, not sa_action */
current.sa_handler = graceful; /* specify a handler */
sigaction(SIGTERM, ¤t, NULL); /* register the handler */
}
void child_code() {
set_handler();
while (1) { /` loop until interrupted `/
sleep(1);
puts("\tChild just woke up, but going back to sleep.");
}
}
void parent_code(pid_t cpid) {
puts("Parent sleeping for a time...");
sleep(5);
/* Try to terminate child. */
if (-1 == kill(cpid, SIGTERM)) {
perror("kill");
exit(-1);
}
wait(NULL); /` wait for child to terminate `/
puts("My child terminated, about to exit myself...");
}
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return -1; /* error */
}
if (0 == pid)
child_code();
else
parent_code(pid);
return 0; /* normal */
}
上面的停止程序模擬了一個(gè)多進(jìn)程系統(tǒng)的優(yōu)雅退出,在這個(gè)例子中,這個(gè)系統(tǒng)由一個(gè)父進(jìn)程和一個(gè)子進(jìn)程組成。這次模擬的工作流程如下:
- 父進(jìn)程嘗試去
fork
一個(gè)子進(jìn)程。假如這個(gè)fork
操作成功了,每個(gè)進(jìn)程就執(zhí)行它自己的代碼:子進(jìn)程就執(zhí)行函數(shù)child_code
,而父進(jìn)程就執(zhí)行函數(shù)parent_code
。 - 子進(jìn)程將會(huì)進(jìn)入一個(gè)潛在的無(wú)限循環(huán),在這個(gè)循環(huán)中子進(jìn)程將睡眠一秒,然后打印一個(gè)信息,接著再次進(jìn)入睡眠狀態(tài),以此循環(huán)往復(fù)。來(lái)自父進(jìn)程的一個(gè)
SIGTERM
信號(hào)將引起子進(jìn)程去執(zhí)行一個(gè)信號(hào)處理回調(diào)函數(shù)graceful
。這樣這個(gè)信號(hào)就使得子進(jìn)程可以跳出循環(huán),然后進(jìn)行子進(jìn)程和父進(jìn)程之間的優(yōu)雅終止。在終止之前,進(jìn)程將打印一個(gè)信息。 - 在
fork
一個(gè)子進(jìn)程后,父進(jìn)程將睡眠 5 秒,使得子進(jìn)程可以執(zhí)行一會(huì)兒;當(dāng)然在這個(gè)模擬中,子進(jìn)程大多數(shù)時(shí)間都在睡眠。然后父進(jìn)程調(diào)用SIGTERM
作為第二個(gè)參數(shù)的kill
函數(shù),等待子進(jìn)程的終止,然后自己再終止。
下面是一次運(yùn)行的輸出:
% ./shutdown
Parent sleeping for a time...
Child just woke up, but going back to sleep.
Child just woke up, but going back to sleep.
Child just woke up, but going back to sleep.
Child just woke up, but going back to sleep.
Child confirming received signal: 15 ## SIGTERM is 15
Child about to terminate gracefully...
Child terminating now...
My child terminated, about to exit myself...
對(duì)于信號(hào)的處理,上面的示例使用了 sigaction
庫(kù)函數(shù)(POSIX 推薦的用法)而不是傳統(tǒng)的 signal
函數(shù),signal
函數(shù)有移植性問(wèn)題。下面是我們主要關(guān)心的代碼片段:
-
假如對(duì)
fork
的調(diào)用成功了,父進(jìn)程將執(zhí)行parent_code
函數(shù),而子進(jìn)程將執(zhí)行child_code
函數(shù)。在給子進(jìn)程發(fā)送信號(hào)之前,父進(jìn)程將會(huì)等待 5 秒:puts("Parent sleeping for a time...");
sleep(5);
if (-1 == kill(cpid, SIGTERM)) {
...sleepkillcpidSIGTERM...
假如
kill
調(diào)用成功了,父進(jìn)程將在子進(jìn)程終止時(shí)做等待,使得子進(jìn)程不會(huì)變成一個(gè)僵尸進(jìn)程。在等待完成后,父進(jìn)程再退出。 -
child_code
函數(shù)首先調(diào)用set_handler
然后進(jìn)入它的可能永久睡眠的循環(huán)。下面是我們將要查看的set_handler
函數(shù):void set_handler() {
struct sigaction current; /* current setup */
sigemptyset(¤t.sa_mask); /* clear the signal set */
current.sa_flags = 0; /* for setting sa_handler, not sa_action */
current.sa_handler = graceful; /* specify a handler */
sigaction(SIGTERM, ¤t, NULL); /* register the handler */
}
上面代碼的前三行在做相關(guān)的準(zhǔn)備。第四個(gè)語(yǔ)句將為
graceful
設(shè)定為句柄,它將在調(diào)用_exit
來(lái)停止之前打印一些信息。第 5 行和最后一行的語(yǔ)句將通過(guò)調(diào)用sigaction
來(lái)向系統(tǒng)注冊(cè)上面的句柄。sigaction
的第一個(gè)參數(shù)是SIGTERM
,用作終止;第二個(gè)參數(shù)是當(dāng)前的sigaction
設(shè)定,而最后的參數(shù)(在這個(gè)例子中是NULL
)可被用來(lái)保存前面的sigaction
設(shè)定,以備后面的可能使用。
使用信號(hào)來(lái)作為 IPC 的確是一個(gè)很輕量的方法,但確實(shí)值得嘗試。通過(guò)信號(hào)來(lái)做 IPC 顯然可以被歸入 IPC 工具箱中。
這個(gè)系列的總結(jié)
在這個(gè)系列中,我們通過(guò)三篇有關(guān) IPC 的文章,用示例代碼介紹了如下機(jī)制:
- 共享文件
- 共享內(nèi)存(通過(guò)信號(hào)量)
- 管道(命名和無(wú)名)
- 消息隊(duì)列
- 套接字
- 信號(hào)
甚至在今天,在以線程為中心的語(yǔ)言,例如 Java、C# 和 Go 等變得越來(lái)越流行的情況下,IPC 仍然很受歡迎,因?yàn)橄啾扔谑褂枚嗑€程,通過(guò)多進(jìn)程來(lái)實(shí)現(xiàn)并發(fā)有著一個(gè)明顯的優(yōu)勢(shì):默認(rèn)情況下,每個(gè)進(jìn)程都有它自己的地址空間,除非使用了基于共享內(nèi)存的 IPC 機(jī)制(為了達(dá)到安全的并發(fā),競(jìng)爭(zhēng)條件在多線程和多進(jìn)程的時(shí)候必須被加上鎖),在多進(jìn)程中可以排除掉基于內(nèi)存的競(jìng)爭(zhēng)條件。對(duì)于任何一個(gè)寫(xiě)過(guò)即使是基本的通過(guò)共享變量來(lái)通信的多線程程序的人來(lái)說(shuō),他都會(huì)知道想要寫(xiě)一個(gè)清晰、高效、線程安全的代碼是多么具有挑戰(zhàn)性。使用單線程的多進(jìn)程的確是很有吸引力的,這是一個(gè)切實(shí)可行的方式,使用它可以利用好今天多處理器的機(jī)器,而不需要面臨基于內(nèi)存的競(jìng)爭(zhēng)條件的風(fēng)險(xiǎn)。
當(dāng)然,沒(méi)有一個(gè)簡(jiǎn)單的答案能夠回答上述 IPC 機(jī)制中的哪一個(gè)更好。在編程中每一種 IPC 機(jī)制都會(huì)涉及到一個(gè)取舍問(wèn)題:是追求簡(jiǎn)潔,還是追求功能強(qiáng)大。以信號(hào)來(lái)舉例,它是一個(gè)相對(duì)簡(jiǎn)單的 IPC 機(jī)制,但并不支持多個(gè)進(jìn)程之間的豐富對(duì)話。假如確實(shí)需要這樣的對(duì)話,另外的選擇可能會(huì)更合適一些。帶有鎖的共享文件則相對(duì)直接,但是當(dāng)要處理大量共享的數(shù)據(jù)流時(shí),共享文件并不能很高效地工作。管道,甚至是套接字,有著更復(fù)雜的 API,可能是更好的選擇。讓具體的問(wèn)題去指導(dǎo)我們的選擇吧。
盡管所有的示例代碼(可以在我的網(wǎng)站上獲取到)都是使用 C 寫(xiě)的,其他的編程語(yǔ)言也經(jīng)常提供這些 IPC 機(jī)制的輕量包裝。這些代碼示例都足夠短小簡(jiǎn)單,希望這樣能夠鼓勵(lì)你去進(jìn)行實(shí)驗(yàn)。