【Linux】进程间通信之管道篇

首先我们来了解一下什么是进程间通信

进程间通信就如同人与人之间的交流一样,注重方式,各有利弊。通俗点讲进程间通信就是在不同进程之间传播或交换信息,那么,我们都知道进程之间是相互独立的,那么进程间通信的本质就是让两个毫不相干的进程看到同一份资源,那么我们又是如何解决这些矛盾的呢?

我们实现进程间通信的主要目的是什么呢?

1.数据传输:一个进程需要将它的数据发送给另一个进程

2.资源共享:多个进程之间共享同样的资源

3.通知事件:一个进程需要向另一个或者一组进程发送消息,通知它(它们)发生了某种事件(如进程终止要通知父进程)

4.进程控制:有些进程希望完全控制另一个进程的执行,此时控制进程希望进程能够截断另一个进程的所有陷入和异常,并能够及时知道它的状态改变

进程间通信的主要分类

进程间通信主要包括管道, 系统IPC(包括消息队列,信号,共享存储), 套接字(SOCKET)等等

今天,我们先来看一种-------->管道

什么是管道

想想我们生活中的各种管道(比如水管,电缆等),他们是不是都是用来传输某种资源的呢?类比而言,管道是Unix种最古老的进程间通信的方式,我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。

管道的分类

匿名管道和命名管道

什么是匿名管道

匿名管道是一种最基本的IPC机制

匿名管道的特点

1.只能在具有亲缘关系的进程之间进行通信(通常一个管道由一个进程创建,然后该进程调用fork创建子进程,此后父子进程就可应用该管道了)

2.管道是基于字节流的

3.管道生命周期随进程(进程退出,管道不复存在)

4.内核会对管道操作进行同步与互斥

5.管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立两个管道(如下图)


如何创建匿名管道?

功能:创建一个无名管道
头文件:#include<unistd.h>
原型: int pipe(int fd[2]);
参数fd: 文件描述符数组,其中fd[0]表示读端,fd[1]表示写端
返回值:成功返回0,失败返回错误代码

应用实例1:

从键盘上读取数据,写入管道,读取管道,写到屏幕

 #include<stdio.h>                                                                                    
   #include<stdlib.h>
   #include<string.h>
   #include<unistd.h>
   int main()
  {
       int fds[2];
       char buf[100];
       int len;
       if(pipe(fds)==-1)
          perror("make pipe"),exit(1);
       //read from stdin
       while(fgets(buf,100,stdin)){
          len=strlen(buf);
          //write into pipe
           if(write(fds[1],buf,len)!=len){
              perror("write to pipe");
                 break;
          }
          memset(buf,0x00,sizeof(buf));
          //read from pipe
          if((len=read(fds[0],buf,100))==-1){
              perror("read from pipe");
              break;
          }
          //write to stdout
          if(write(1,buf,len)!=len){
              perror("write to stdout");
              break;
          }
      }

结果显示


应用实例2

首先我们先来分析一下怎么用fork来共享管道原理(如图)


代码实现:

 #include<stdio.h>
 #include<unistd.h>
 #include<string.h>
 int main()
 {
    int fd[2];
	if (pipe(fd) == -1){
		perror("error pipe");
	    return 1;
	}
	//father write child read
	pid_t pid = fork();//create child
    if (pid<0){
		perror("error fork");
        return 2;
	}
     //chlid
     else if (pid == 0){
         close(fd[1]);//child close to write
         char arr[128];
         int count = 5;
         while (count--){
             ssize_t ss = read(fd[0], arr, sizeof(arr));
             if (ss>0){
                arr[ss] = '\0';
                printf("father--->child: %s\n", arr);
			 }
             else if (ss == 0){
                 printf("father close write");
                 break;
			 }
             else{
                 perror("error read");
                 break;
			 }
		 }
         close(fd[0]);
	 }
     //father
     else{
		 close(fd[0]);//father close to read
         char *str = "hai,luying";
         int count = 5;
         while (count--){
             write(fd[1], str, strlen(str));
             sleep(1);
		 }
         close(fd[1]);
	 }
     return 0;
 }

结果显示:


管道读写规则

1.当没有数据可读时(read 没有一点数据可读或 write 没有一点空间可以写入,如果disable O_NONBLOCK 则会阻塞,如果enable O_NONBLOCK 则会返回-1,errno = EAGAIN | EWOULDBLOCK 错误。

(1)O_NONBLOCK bisable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 #include<fcntl.h>
  5 int main()
  6 {
  7     int fd[2];
  8     if(pipe(fd)==-1){
  9         perror("pipe error");
 10         exit(EXIT_FAILURE);
 11     }
 12     pid_t pid;
 13     pid=fork();
 14     if(pid==-1){
 15         perror("fork error");
 16         exit(EXIT_FAILURE);
 17     }
 18     if(pid==0){
 19         close(fd[0]);//close to read
 20         sleep(5);
 21         char* str="luying";
 22         int len=strlen(str);
 23         write(fd[1],str,len);
 24         exit(EXIT_SUCCESS);
 25     }                                                                                                
 26     close(fd[1]);//close to write
 27     char buf[20]={0};
 28     read(fd[0],buf,20);
 29     printf("wait to receive:  %s\n",buf);
 30     return 0;
 31 }   

运行结果


分析:子进程睡眠了5秒后,read调用阻塞,父进程暂时没有读出数据,5秒后子进程向管道中写入数据,父进程才读到数据

(2)O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN

#include<stdio.h>
 #include<unistd.h>
 #include<stdlib.h>
 #include<fcntl.h>
 #include<string.h>
 int main()
 {
     int fd[2];
     if (pipe(fd) == -1){
         perror("pipe error");

	}
     pid_t pid;
     if (pid == -1){
         perror("fork error");
	}
     if (pid == 0){
         close(fd[0]);//close to read
         sleep(5);
         char* str = "luying";
         int len = strlen(str);
        write(fd[1], str, len);
	}
     close(fd[1]);//close to write
     char buf[20] = { 0 };
     int ret = fcntl(fd[0], F_GETFL);//F_GETFL读取文件状态标志
     //int fcntl(int fd, int cmd, ... /* arg */ );
     fcntl(fd[0], F_SETFL, ret | O_NONBLOCK);//设置为阻塞模式
     int net = read(fd[0], buf, 20);
     if (net == -1){
         perror("read error");
	 }
     printf(" receive:  %s\n", buf);
     return 0;
 }

运行结果:


分析:

2.当管道满的时候

(1)O_NONBLOCK disable:write调用阻塞,直到有进程读走数据

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h> 
int main(void)
{
    int fds[2];
    if(pipe(fds) == -1){
        perror("pipe error");
        exit(EXIT_FAILURE);
    }
    int ret;
    int count = 0;
    while(1){
        ret = write(fds[1],"luying",6);//fds[1]默认是阻塞模式
        if(ret == -1){
            perror("write error");
            break;
        }
        count++;
    }
    return 0;
}

运行结果

fd打开时默认是阻塞模式,当pipe缓冲区满时,write操作确实阻塞了,等待其他进程将数据从管道中取走

(2)O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

接下来我们来验证几种有趣的情况~

(1)写端一直在写,读端不关闭自己的读端,但是他也不读,会出现什么问题呢?

我们用代码实现一下:

#include<stdio.h>
 #include<unistd.h>
 #include<string.h>
int main()
{
	int fd[2];
	int ret = pipe(fd);
	if (ret < 0){
		perror("error pipe");
		return 1;
	}
	pid_t id = fork();
	if (id == 0){//child  ->w
		close(fd[0]);
		const char * msg = "hello father,I am child\n";
		int count = 0;
		while (1){
			write(fd[1], msg, strlen(msg));
			printf("%d\n", count++);
		}
	}
	else{//father  ->r
		close(fd[1]);
		while (1){}
	}
}

结果显示:


分析,我们发现,写端写满之后,暂时不在写数据,那是否是在等待读端读走之后再继续写呢?为了验证我们的想法,我们修改下代码

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
	int fd[2];
	int ret = pipe(fd);
	if (ret < 0){
		perror("error pipe");
		return 1;
	}
	pid_t id = fork();
	if (id == 0){//child  ->w
		close(fd[0]);
		const char * msg = "hello father,I am child\n";
		int count = 0;
		while (1){
			write(fd[1], msg, strlen(msg));
			printf("%d\n", count++);
		}
	}
	else{//father  ->r
		close(fd[1]);
		while (1){
             sleep(5);
             ssize_t s = read(fd[0], buf, sizeof(buf)-1);
             if (s>0){
                 buf[s] = '\0';
                 printf("father say:%s\n", buf);
			}
	}
}
运行结果如下:


分析:我们发现,验证了我们的猜测,子进程写满之后,隔了5秒父进程读走了数据之后,子进程开始继续写入文件

(2)如果今天写端间歇性的写,读端一直在读,那么会发生什么呢:

代码修改为:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
	int fd[2];
	int ret = pipe(fd);
	if (ret < 0){
		perror("error pipe");
		return 1;
	}
	pid_t id = fork();
	if (id == 0){//child  ->w
		close(fd[0]);
		const char * msg = "hello father,I am child\n";
		while (1){
			write(fd[1], msg, strlen(msg));
			sleep(10);
		}
	}
	else{//father  ->r
		close(fd[1]);
		int count = 0;
		while (1){
             ssize_t s = read(fd[0], buf, sizeof(buf)-1);
             if (s>0){
                 buf[s] = '\0';
                 printf("father say:%s\n", buf);
			}
			 printf("father read %d\n", count++);
	}
}
运行结果:


分析,我们发现写端每隔秒写一次,与此同时,读端也在等待写端写入,从而才读入数据

管道如果没数据,读端会一直等待,直到有数据写入,就立即读出

(3)那么如果今天写端写入固定数据之后不仅不写了,而且把写端关闭了,会出现什么情况呢?

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
	int fd[2];
	int ret = pipe(fd);
	if (ret < 0){
		perror("error pipe");
		return 1;
	}
	pid_t id = fork();
	int count = 0;
	if (id == 0){//child  ->w
		close(fd[0]);
		const char * msg = "hello father,I am child\n";
		while (1){
			write(fd[1], msg, strlen(msg));
			printf("%d\n", count++);
			if (count > 10){
				close(fd[1]);
				break;
			}
			sleep(1);
		}
	}
	else{//father  ->r
		close(fd[1]);
		int count = 0;
		while (1){
             ssize_t s = read(fd[0], buf, sizeof(buf)-1);
             if (s>0){
                 buf[s] = '\0';
                 printf("father say:%s\n", buf);
			 }
			 else if (s == 0){
				 printf("pipe close\n");
				 break;
			 }
	}
}

运行结果:



分析:我们可以看到,当写端不再写的时候,读端读到管道的末尾之后退出

(4)写端一致在写,读端不但不读,而且还关闭了读端,那么写端会发生什么情况呢?

修改代码如下:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
	int fd[2];
	int ret = pipe(fd);
	if (ret < 0){
		perror("error pipe");
		return 1;
	}
	pid_t id = fork();
	int count = 0;
	if (id == 0){//child  ->w
		close(fd[0]);
		const char * msg = "hello father,I am child\n";
		while (1){
			write(fd[1], msg, strlen(msg));
			printf("%d\n", count++);
			sleep(1);
		}
	}
	else{//father  ->r
		close(fd[1]);
		int count = 0;
		while (1){
             ssize_t s = read(fd[0], buf, sizeof(buf)-1);
             if (s>0){
                 buf[s] = '\0';
                 printf("father say:%s\n", buf);
				 sleep(3);
				 break;
			 }
		}
		close(fd[0]);
		int status = 0;
		wait(&status);
		printf("sig:%d\n", status & 0x7F);
	}
}

运行结果如下:


分析:如果所有管道 读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出

【注意】

1.如果所有管道写端对应的文件描述符被关闭,则read返回0;

2.如果所有管道 读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出

3.当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性

4.当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性

那么PIPE_BUF是多大呢?


认识了匿名管道,我们来看看命名管道

命名管道

如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作

命名管道与匿名管道的区别

(1)匿名管道由pipe函数创建并打开,而命名管道由mkfifo函数创建,打开用open

(2)匿名管道只能在具有血缘关系的进程间通信

(3)FIFO(命名管道)与pipe(匿名管道)之间只是创建于打开的方式不同,工作完成之后,他们具有相同的意义

创建命名管道

(1)命令行上创建

$ mkfifo filename

(2)程序中创建,相关函数

int mkfifo(const char *filename,mode_t mode);

创建命名管道:

int main(int argc, char *argv[])
{
mkfifo("p2", 0644);
return 0;
}

命名管道的打开规则

1.如果当前打开操作是为读而打开FIFO时

(1)O_NONBLOCK disable:阻塞直到有相关进程为写而打开该FIFO

  (2)   O_NONBLOCK enable:立刻返回成功

2.如果当前打开操作是为写而打开FIFO时

(1)O_NONBLOCK disable:阻塞直到有相关进程为读而打开该FIFO

(2) (2)   O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

应用实例----》用命名管道实现server&client通信

Makefile

.PHONY:all
all : server client
server : server.c
    gcc - o $@ $ ^
client:client.c
    gcc - o $@ $ 
.PHONY:clean
 clean :
    rm - f server client

server.c

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<fcntl.h>
int main()
{
     if (mkfifo("./fifo", 0644)<0){
         perror("error mkfifo\n");
         return 1;
	}
     int fd = open("./fifo", O_RDONLY);
     if (fd < 0){
         perror("error open");
         return 2;
	}
     char buf[100];
     while (1){
         ssize_t s = read(fd, buf, sizeof(buf)-1);
         if (s > 0){
             buf[s] = '\0';
             printf("client---->server: %s\n", buf);
		}
		else if (s == 0){
             printf("client quit\n");
             break;
		}
	}
     close(fd);
     return 0;
}

运行结果


至此,今天,我们就把管道讨论完了,认识比较浅,代码能力也比较差,希望大家多多指教!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值