進(jìn)程間通信
在兩個(gè)進(jìn)程之間,每個(gè)進(jìn)程各自有不同的用戶地址空間,任何一個(gè)進(jìn)程的全局變量在另一個(gè)進(jìn)程中都看不到。比如,在父進(jìn)程中的全局變量,如果在子進(jìn)程中去改變這個(gè)全局變量,則子進(jìn)程中被改變的這個(gè)值不會(huì)去影響父進(jìn)程,因?yàn)樽舆M(jìn)程中的所有數(shù)據(jù)都是通過(guò)寫時(shí)拷貝自父進(jìn)程的,兩個(gè)進(jìn)程的地址空間不同。
父進(jìn)程和子進(jìn)程之間并沒(méi)有共享數(shù)據(jù),所以進(jìn)程之間要交換數(shù)據(jù)必須通過(guò)內(nèi)核,在內(nèi)核中開辟一塊緩沖區(qū),進(jìn)程1把數(shù)據(jù)從用戶空間拷到內(nèi)核緩沖區(qū),進(jìn)程2再?gòu)膬?nèi)核緩沖區(qū)把數(shù)據(jù)讀走,內(nèi)核提供的這種機(jī)制稱為進(jìn)程間通信。
進(jìn)程間通信--匿名管道
匿名管道是一種最基本的IPC機(jī)制,有pipe函數(shù)創(chuàng)建,pipe的基本格式為int pipe(int fds[2]);
調(diào)用pipe函數(shù)時(shí)在內(nèi)核中開辟一塊緩沖區(qū)(稱為管道)用于通信,它有一個(gè)讀端一個(gè)寫端,然后通過(guò)fds參數(shù)傳出給用戶程序兩個(gè)文件描述符,fds[0]指向管道的讀端,fds[1]指向管道的寫端。所以管道在用戶程序看起來(lái)就像一個(gè)打開的文件,通過(guò)read(fds[0]);或者write(fds[1]);向這個(gè)文件讀寫數(shù)據(jù)其實(shí)是在讀寫內(nèi)核緩沖區(qū)。pipe函數(shù)調(diào)用成功返回0,調(diào)用失敗返回-1。
如何實(shí)現(xiàn)兩個(gè)進(jìn)程之間的通信呢?我們可以按照下面的步驟通信:
- 父進(jìn)程創(chuàng)建管道:父進(jìn)程調(diào)用pipe開辟管道,得到兩個(gè)文件描述符指向管道的兩端。
- 父進(jìn)程fork出子進(jìn)程:父進(jìn)程調(diào)用fork創(chuàng)建子進(jìn)程,那么子進(jìn)程也有兩個(gè)文件描述符指向同一管道 。
- 父進(jìn)程關(guān)閉fds[0],子進(jìn)程關(guān)閉fds[1],父進(jìn)程關(guān)閉管道讀端,子進(jìn)程關(guān)閉管道寫端。父進(jìn)程可以往管道里寫,子進(jìn)程可以從管道里讀,管道是用環(huán)形隊(duì)列實(shí)現(xiàn)的,數(shù)據(jù)從寫端流入從讀端流出,這樣就實(shí)現(xiàn)了進(jìn)程間通信。
簡(jiǎn)單代碼來(lái)演示:
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
int fds[2];
if (pipe(fds) < 0 ) {
perror("pipe error\n");
exit(1);
}
pid_t pid = fork();
if (pid == 0) {
close(fds[0]);
char *msg = "hey! dad! i am child";
while (1) {
write(fds[1], msg, strlen(msg));
sleep(1);
}
}else {
char buff[100];
bzero(buff, 100);
close(fds[1]);
while (1) {
ssize_t s = read(fds[0], buff, sizeof(buff)-1);
if (s > 0) {
printf("%s\n", buff);
}
}
}
return 0;
}
tree@tree:~$ gcc pipe.c -o pipe
tree@tree:~$ ./pipe
hey! dad! i am child
hey! dad! i am child
hey! dad! i am child
hey! dad! i am child
hey! dad! i am child
hey! dad! i am child
可以發(fā)現(xiàn) “hey! dad! i am child” 這句話是在子進(jìn)程中的,即由子進(jìn)程寫到管道里面的;然后打印是在父進(jìn)程里面打印的,即由父進(jìn)程從管道里面讀到的。這就完成了父子進(jìn)程之間的通信,但同時(shí),我們知道在通信前,子進(jìn)程關(guān)閉了寫端,父進(jìn)程關(guān)閉了讀端;之所以這樣處理是因?yàn)樾枰ケ苊忮e(cuò)誤的發(fā)生,因?yàn)?code>兩個(gè)進(jìn)程通過(guò)一個(gè)管道只能實(shí)現(xiàn)單向通信 。比如上面的例子,子進(jìn)程寫父進(jìn)程讀,如果有時(shí)候也需要父進(jìn)程寫子進(jìn)程讀,就必須另開一個(gè)管道。
這種管道我們又稱為匿名管道,它的一些特點(diǎn)是:
① 只能進(jìn)行單向通信;
② 管道依賴于文件系統(tǒng),進(jìn)程退出,管道隨之退出,即生命周期是隨進(jìn)程的;
③ 常用于父子進(jìn)程間的通信,這種管道只能用于具有親緣關(guān)系的進(jìn)程;
④ 管道是基于流的,是按照數(shù)據(jù)流的方式讀寫的;
⑤ 同步訪問(wèn),即管道訪問(wèn)是自帶同步機(jī)制的。
另外,使用管道需要注意一下四種特殊情況(假設(shè)都是阻塞IO操作,沒(méi)有設(shè)置O_NONBLOCK標(biāo)志):
1、如果所有指向管道寫端的文件秒描述符都關(guān)閉了(管道寫端的計(jì)數(shù)為0),而任然有進(jìn)程從管道的讀端讀數(shù)據(jù),那么管道中剩余的數(shù)據(jù)都被讀取后,再次read會(huì)返回0,就像讀到文件末尾一樣。
2、如果有指向管道寫端的文件描述符沒(méi)關(guān)閉(管道寫端的引用計(jì)數(shù)大于0),?持有管道寫端的 進(jìn)程也沒(méi)有向管道中寫數(shù)據(jù),這時(shí)有進(jìn)程從管道讀端讀數(shù)據(jù),那么管道中剩余的數(shù)據(jù)都被讀取后,再次read會(huì)阻塞,直到管道中有數(shù)據(jù)可讀了才讀取數(shù)據(jù)并返回。
3、如果所有指向管道讀端的文件描述符都關(guān)閉了(管道讀端的引用計(jì)數(shù)等于0),這時(shí)有進(jìn)程向管道的寫端write,那么該進(jìn)程會(huì)收到信號(hào)SIGPIPE,通常會(huì)導(dǎo)致進(jìn)程異常終止。
4、如果有指向管道讀端的文件描述符沒(méi)關(guān)閉(管道讀端的引用計(jì)數(shù)大于0),而持有管道讀端的進(jìn)程也沒(méi)有從管道中讀數(shù)據(jù),這時(shí)有進(jìn)程向管道寫端寫數(shù)據(jù),那么在管道被寫滿時(shí)再 次write會(huì)阻塞,直到管道中有空位置了才寫入數(shù)據(jù)并返回。
進(jìn)程間通信--命名管道
命名管道:
FIFO (First in, First out)為一種特殊的文件類型,它在文件系統(tǒng)中有對(duì)應(yīng)的路徑。當(dāng)一個(gè)進(jìn)程以讀(r)的方式打開該文件,而另一個(gè)進(jìn)程以寫(w)的方式打開該文件,那么內(nèi)核就會(huì)在這兩個(gè)進(jìn)程之間建立管道,所以FIFO實(shí)際上也由內(nèi)核管理,不與硬盤打交道。之所以叫FIFO,是因?yàn)楣艿辣举|(zhì)上是一個(gè)先進(jìn)先出的隊(duì)列數(shù)據(jù)結(jié)構(gòu),最早放入的數(shù)據(jù)被最先讀出來(lái),從而保證信息交流的順序。FIFO只是借用了文件系統(tǒng)(file system,命名管道是一種特殊類型的文件,因?yàn)長(zhǎng)inux中所有事物都是文件,它在文件系統(tǒng)中以文件名的形式存在。)來(lái)為管道命名。寫模式的進(jìn)程向FIFO文件中寫入,而讀模式的進(jìn)程從FIFO文件中讀出。當(dāng)刪除FIFO文件時(shí),管道連接也隨之消失。FIFO的好處在于我們可以通過(guò)文件的路徑來(lái)識(shí)別管道,從而讓沒(méi)有親緣關(guān)系的進(jìn)程之間建立連接。
創(chuàng)建命名管道:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode);
int mknod(const char *filename, mode_t mode | S_IFIFO, (dev_t)0);
這兩個(gè)函數(shù)都能創(chuàng)建一個(gè)FIFO文件,注意是創(chuàng)建一個(gè)真實(shí)存在于文件系統(tǒng)中的文件,filename指定了文件名,而mode則指定了文件的讀寫權(quán)限。mknod是比較老的函數(shù),而使用mkfifo函數(shù)更加簡(jiǎn)單和規(guī)范,所以建議在可能的情況下,盡量使用mkfifo而不是mknod。
訪問(wèn)命名管道:
1、打開FIFO文件,與打開其他文件一樣,F(xiàn)IFO文件也可以使用open調(diào)用來(lái)打開。注意,mkfifo函數(shù)只是創(chuàng)建一個(gè)FIFO文件,要使用命名管道還是將其打開。
但是有兩點(diǎn)要注意,1. 就是程序不能以O(shè)_RDWR模式打開FIFO文件進(jìn)行讀寫操作,而其行為也未明確定義,因?yàn)槿缫粋€(gè)管道以讀/寫方式打開,進(jìn)程就會(huì)讀回自己的輸出,同時(shí)我們通常使用FIFO只是為了單向的數(shù)據(jù)傳遞。2. 就是傳遞給open調(diào)用的是FIFO的路徑名,而不是正常的文件。
打開FIFO文件通常有四種方式:
open(const char *path, O_RDONLY);
open(const char *path, O_RDONLY | O_NONBLOCK);
open(const char *path, O_WRONLY);
open(const char *path, O_WRONLY | O_NONBLOCK);
在open函數(shù)的調(diào)用的第二個(gè)參數(shù)中,有一個(gè)選項(xiàng)O_NONBLOCK,O_NONBLOCK表示非阻塞,加上這個(gè)選項(xiàng)后,表示open調(diào)用是非阻塞的,如果沒(méi)有這個(gè)選項(xiàng),則表示open調(diào)用是阻塞的。
open調(diào)用的阻塞是什么一回事呢?
對(duì)于以只讀方式(O_RDONLY)打開的FIFO文件,如果open調(diào)用是阻塞的(即第二個(gè)參數(shù)為O_RDONLY),除非有一個(gè)進(jìn)程以寫方式打開同一個(gè)FIFO,否則它不會(huì)返回;如果open調(diào)用是非阻塞的的(即第二個(gè)參數(shù)為O_RDONLY | O_NONBLOCK),則即使沒(méi)有其他進(jìn)程以寫方式打開同一個(gè)FIFO文件,open調(diào)用將成功并立即返回。
對(duì)于以只寫方式(O_WRONLY)打開的FIFO文件,如果open調(diào)用是阻塞的(即第二個(gè)參數(shù)為O_WRONLY),open調(diào)用將被阻塞,直到有一個(gè)進(jìn)程以只讀方式打開同一個(gè)FIFO文件為止;如果open調(diào)用是非阻塞的(即第二個(gè)參數(shù)為O_WRONLY | O_NONBLOCK),open總會(huì)立即返回,但如果沒(méi)有其他進(jìn)程以只讀方式打開同一個(gè)FIFO文件,open調(diào)用將返回-1,并且FIFO也不會(huì)被打開。
簡(jiǎn)單代碼演示:
server.c
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#define SIZE 100
#define FIFONAME "myfifo"
int main(int argc, char const *argv[])
{
if (access(FIFONAME, F_OK)) {
mkfifo(FIFONAME, 0666);
}
int fd = open(FIFONAME, O_WRONLY);
char buff[SIZE];
while (1) {
bzero(buff, SIZE);
fgets(buff, SIZE, stdin);
write(fd, buff, strlen(buff));
}
return 0;
}
client.c
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#define SIZE 100
#define FIFONAME "myfifo"
int main(int argc, char const *argv[])
{
if (access(FIFONAME, F_OK)) {
mkfifo(FIFONAME, 0666);
}
int fd = open(FIFONAME, O_RDONLY);
char buff[SIZE];
while (1) {
bzero(buff, SIZE);
read(fd, buff, SIZE);
printf("%s\n", buff);
}
return 0;
}
命名管道的安全問(wèn)題:
前面的例子是兩個(gè)進(jìn)程之間的通信問(wèn)題,也就是說(shuō),一個(gè)進(jìn)程向FIFO文件寫數(shù)據(jù),而另一個(gè)進(jìn)程則在FIFO文件中讀取數(shù)據(jù)。試想這樣一個(gè)問(wèn)題,只使用一個(gè)FIFO文件,如果有多個(gè)進(jìn)程同時(shí)向同一個(gè)FIFO文件寫數(shù)據(jù),而只有一個(gè)讀FIFO進(jìn)程在同一個(gè)FIFO文件中讀取數(shù)據(jù)時(shí),會(huì)發(fā)生怎么樣的情況呢,會(huì)發(fā)生數(shù)據(jù)塊的相互交錯(cuò)是很正常的?而且個(gè)人認(rèn)為多個(gè)不同進(jìn)程向一個(gè)FIFO讀進(jìn)程發(fā)送數(shù)據(jù)是很普通的情況。
為了解決這一問(wèn)題,就是讓寫操作的原子化。怎樣才能使寫操作原子化呢?系統(tǒng)規(guī)定:在一個(gè)以O(shè)_WRONLY(即阻塞方式)打開的FIFO中, 如果寫入的數(shù)據(jù)長(zhǎng)度小于等待PIPE_BUF,那么或者寫入全部字節(jié),或者一個(gè)字節(jié)都不寫入。如果所有的寫請(qǐng)求都是發(fā)往一個(gè)阻塞的FIFO的,并且每個(gè)寫記請(qǐng)求的數(shù)據(jù)長(zhǎng)度小于等于PIPE_BUF字節(jié),系統(tǒng)就可以確保數(shù)據(jù)決不會(huì)交錯(cuò)在一起。