一、
實現進程間通信最簡單的方式就是通過管道機制來實現,管道是一種最基本的IPC機制,由pipe函數創建。pipe所創建的管道實際上是在內核中開闢一塊緩衝區,它有一個獨端和寫段,通過read和write來實現往管道里寫和讀,由於管道是通過父進程調用pipe函數所產生的管道,所以和它通信的只能是它的子進程或者和它有血緣關係的進程。後面一會具體講。先通過圖示來看看管道是怎麼來實現通信的
通信機制:
1、父進程先通過調用pipe函數創建一個管道。父進程的文件描述符讀和寫分別指向管道的兩端。
2、父進程通過調用fork函數來創建子進程,然後子進程的文件描述符的讀和寫也分別指向管道的兩端。
3、父進程關閉自己的讀端,子進程關閉自己的寫端。然後父進程往管道里寫東西,子進程從管道里讀東西,以此來實現父子進程之間的通信。
管道的特點:
1、管道只能進行單向通道。f[0]爲讀,f[1]爲寫。
2、只有具有血緣關係的兩個進程才能用管道來通信。
3、管道是面向數據流的服務。
4、管道的生命週期隨進程。
5、同步與互斥
先來看看父進程如何通過代碼來創建管道,並且和子進程連接上的。
#include <stdlib.h>
#include<stdio.h>
#include <unistd.h>
#include <string.h>
#include <error.h>
int main()
{
int _pipe[2];
int ret = pipe(_pipe);
if(ret == -1)
{
perror("errno");
return 1;
}
pid_t id = fork();
if(id < 0)
{
perror("errno");
return 2;
}
else if(id == 0)
{//child
close(_pipe[0]);
int i = 0;
char *msg = NULL;
while(i<100)
{
msg = "hello pipe! i am child!\n";
write(_pipe[1],msg,strlen(msg));
sleep(1);
printf("pipe count %d\n",i++);
}
exit(0);
}
else{//father
close(_pipe[1]);
char _msg[100];
int i = 0;
while(i<100){
int ret = read(_pipe[0],_msg,sizeof(_msg));
printf("%scode is:%d\n",_msg,ret);
}
}
return 0;
}
這個管道里子進程先關閉它的讀端,再發送一串字符串,加一個計數器,表示往管道里寫了幾個消息。父進程關閉寫段,然後在管道里讀子進程寫的數據。
但使用管道時要注意以下四種情況
1、如果子進程在往管道里寫完數據並且關閉了寫段,而父進程的讀端一直沒有關閉。這時,在父進程讀完管道里的數據之後,就相當於讀到了管道的結束部分,執行完父進程程序之後直接退出。
#include <stdlib.h>
#include<stdio.h>
#include <unistd.h>
#include <string.h>
#include <error.h>
int main()
{
int _pipe[2];
int ret = pipe(_pipe);
if(ret == -1)
{
perror("errno");
return 1;
}
pid_t id = fork();
if(id < 0)
{
perror("errno");
return 2;
}
else if(id == 0)
{//child
close(_pipe[0]);
int i = 0;
char *msg = NULL;
while(i<5)
{
msg = "hello pipe! i am child!";
write(_pipe[1],msg,strlen(msg));
sleep(1);
printf("pipe count %d\n",i++);
}
close(_pipe[1]);
exit(0);
}
else{//father
close(_pipe[1]);
char _msg[100];
int i = 0;
while(i<10){
sleep(1);
int ret = read(_pipe[0],_msg,sizeof(_msg));
printf("%scode is:%d\n",_msg,ret);
i++;
}
if(waitpid(id, NULL, 0)<0)
{
return 3;
}
}
return 0;
}
2、如果在子進程往管道里寫數據後,沒有繼續再往管道里寫數據。這時讀端直接從管道里讀取數據,而當讀端讀完數據後,會一直等到寫段繼續給管道里寫東西。這時就會發生阻塞,直到寫段繼續往管道里寫數據讀端才能返回退出。
#include <stdlib.h>
#include<stdio.h>
#include <unistd.h>
#include <string.h>
#include <error.h>
int main()
{
int _pipe[2];
int ret = pipe(_pipe);
if(ret == -1)
{
perror("errno");
return 1;
}
pid_t id = fork();
if(id < 0)
{
perror("errno");
return 2;
}
else if(id == 0)
{//child
close(_pipe[0]);
int i = 0;
char *msg = NULL;
while(i<10)
{
msg = "hello pipe! i am child!";
write(_pipe[1],msg,strlen(msg));
sleep(1);
}
//close(_pipe[1]);
exit(0);
}
else{//father
close(_pipe[1]);
char _msg[100];
int i = 0;
while(i<5){
int ret = read(_pipe[0],_msg,sizeof(_msg));
printf("%scode is:%d\n",_msg,ret);
i++;
}
if(waitpid(id, NULL, 0)<0)
{
return 3;
}
return 0;
}
3、如果讀端的文件描述符全部關閉了,這時再往寫段寫數據。進程就會收到一個信號SIGPIPE,通常會導致進程異常終止退出。
#include <stdlib.h>
#include<stdio.h>
#include <unistd.h>
#include <string.h>
#include <error.h>
int main()
{
int _pipe[2];
int ret = pipe(_pipe);
if(ret == -1)
{
perror("errno");
return 1;
}
pid_t id = fork();
if(id < 0)
{
perror("errno");
return 2;
}
else if(id == 0)
{//child
close(_pipe[0]);
int i = 0;
char *msg = NULL;
while(i<10)
{
if(i<5){
msg = "hello pipe! i am child!";
write(_pipe[1],msg,strlen(msg));
sleep(1);
i++;
}
}
//close(_pipe[1]);
exit(0);
}
else{//father
close(_pipe[1]);
char _msg[100];
int i = 0;
while(i<3){
int ret = read(_pipe[0],_msg,sizeof(_msg));
printf("%s:code is:%d\n",_msg,ret);
i++;
}
close(_pipe[0]);
sleep(2);
if(waitpid(id, NULL, 0)<0)
{
return 3;
}
return 0;
}
4、在讀端沒有繼續讀取數據並且沒有關閉自己的文件描述符,而這時寫段也沒有停止往管道里寫數據。那麼就會發生寫段把管道寫滿的情況,這時會造成阻塞,直到管道里有空位置時,進程才能返回退出!
#include <stdlib.h>
#include<stdio.h>
#include <unistd.h>
#include <string.h>
#include <error.h>
int main()
{
int _pipe[2];
int ret = pipe(_pipe);
if(ret == -1)
{
perror("errno");
return 1;
}
pid_t id = fork();
if(id < 0)
{
perror("errno");
return 2;
}
else if(id == 0)
{//child
close(_pipe[0]);
int i = 0;
char *msg = NULL;
while(1)
{
msg = "hello pipe! i am child!";
write(_pipe[1],msg,strlen(msg));
printf("%d\n",i++);
}
//close(_pipe[1]);
exit(0);
}
else{//father
close(_pipe[1]);
char _msg[100];
int i = 0;
while(i<3){
int ret = read(_pipe[0],_msg,sizeof(_msg));
printf("%s:code is:%d\n",_msg,ret);
i++;
}
if(waitpid(id, NULL, 0)<0)
{
return 3;
}
}
return 0;
}
這裏的2850是管道被寫滿的情況,間接說明這裏的管道容量是這麼大。
二、命名管道
匿名管道的不足之處就是沒有名字,因此只能實現具有血緣關係的進程間通信。因此就要提出一種新的管道技術來實現任意進程間的通信,即命名管道(FIFO)。命名管道是以路徑名來命名的,只要任意兩個進程訪問相同的路徑,就可以達到通信的效果。值得注意的是,命名管道是按照先進先出的原則,即最先被寫入的數據先被讀出來。
Linux下有兩種兩式創建命名管道。一是在Shell下交互地建立一個命名管道,二是在程序中使用系統函數建立命名管道。Shell方式下可使用mknod或mkfifo命令,下列命令使用mknod創建了一個命名管道: mknod namedpipe
這兩個函數都能創建一個FIFO文件,注意是創建一個真實存在於文件系統中的文件,filename指定了文件名,而mode則指定了文件的讀寫權限。mknod是比較老的函數,而使用mkfifo函數更加簡單和規範,所以建議在可能的情況下,儘量使用mkfifo而不是mknod。
mkfifo函數的原型:int mkfifo(const char *filename, mode_t mode);
第一個參數是文件名,第二個參數是文件的權限
先來看看代碼實現
write端
#include<stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define _PATH_ "../fifo/my_fifo"
#define _SIZE_ 100
int main()
{
umask(0);
int ret = mkfifo(_PATH_,0666|S_IFIFO);
if(ret == -1){
printf("mkfifo error!\n");
return 1;
}
int fd = open(_PATH_,O_RDWR);
if(fd < 0){
printf("open error!\n");
}
char buf[_SIZE_];
memset(buf,'\0',sizeof(buf));
while(1){
printf("Please Enter# ");
fflush(stdout);
int ret = read(0,buf,sizeof(buf)-1);
if(ret < 0){
printf("write error!\n");
break;
}
if(ret > 0){
buf[ret] = 0;
write(fd,buf,strlen(buf));
}
if(strncmp(buf,"quit",4) == 0){
break;
}
}
close(fd);
return 0;
}
#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define _PATH_ "../fifo/my_fifo"
#define _SIZE_ 100
int main()
{
int fd = open(_PATH_,O_RDWR);
if(fd <= 0){
printf("open file error!\n");
return 1;
}
char buf[_SIZE_];
while(1){
int ret = read(fd,buf,sizeof(buf));
if(ret <= 0){
printf("read file to end!\n");
break;
}
if(ret > 0)
{
buf[ret] = 0;
printf("client say# %s",buf);
}
if(strncmp(buf,"quit",4) == 0){
break;
}
}
close(fd);
return 0;
}
在這裏面注意的就是在創建命名管道時,注意文件的權限,可以通過修改umask來改變它,這裏將管道文件權限設置爲666。而文件是通過調用FIFE*來讀寫的,所以在寫端的讀寫方式上採用讀寫(O_RWONLY),否則會造成阻塞。
可以看到,在通過FIFO實現兩個進程通信之後,管道的權限變爲了666並且標誌位爲P,表明它是管道。那麼如果再次向通過管道寫數據的話還能不能往裏寫呢?答案是不能,因爲創建的管道已經存在了。這時就應該先銷燬上面創建的管道,然後在重新創建一個管道進行通信。
FIFO還可以直接在shell下創建一個管道,然後通過echo重定向到管道里。
這裏我寫一段簡單的腳本來證明一下
可以看到,左邊的窗口沒隔1秒往FIFO裏寫句hello bit,右邊窗口直接去管道里讀。而且值得注意的是在shell裏創建的管道也存在同步與互斥,可以再寫段腳本看看。先讓寫段每隔一秒往管道里寫句數據,然後讀端每隔兩秒在管道里讀取數據,結果只要是寫段往裏寫的速度和讀端的一直就會保證他的原子性。
這裏看的不是很清楚,自己可以試試看是不是和預想的結果一樣!
上面爲了讓兩個進程實現通信用了兩種方式來實現,分別是匿名管道(PIPE)和命名管道(FIFO)。那麼就來總結一下兩個管道都有什麼區別:
1、FIFO可以實現任意進程之間的通信,而PIPE則只支持具有血緣關係進程的通信。
2、FIFO是以讀寫文件的方式來實現的,所以可以雙向通信,而PIPE只支持單方面的通信。
3、FIFO可以支持多終端的通信,可以有多個讀端。而PIPE只支持一個讀和寫端。
4、FIFO是通過文件的形式創建的管道,而PIPE是在內核中開闢一塊空間通過文件描述符來實現的通信。