进程间通信-匿名管道

本文详细介绍了Linux进程间通信中的匿名管道,包括如何创建管道、匿名管道的工作原理及特点。通过实例展示了子进程不断写入而父进程不读取、写端关闭后读端的读取行为,强调了管道的单向通信、字节流特性和血缘关系限制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

进程间通信

计算机系统中有很多进程,这些进程之间可能会存在特定的需要协同工作的场景,而这种一个进程把自己的数据交付给另一个进程,让其进行处理这就叫做进程间通信。但是进程具有独立性,我们想要让进程之间进行交互,成本一定很高,因此操作系统需要对通信方式进行一定的设计。那么如何设计才能让两个进程之间互相通信呢?两个进程之间想要互相通信,首先需要让他们两个能够看到同一份公共资源,这里的资源就是一段内存,两个进程都能访问同一部分内存,这样两个进程都能够进行对这段内存的读写操作,就可以实现通信,因此进程通信的本质其实是由操作系统参与,提供一份所有通信进程能看到的公共资源。

进程间通信方式

现在常用的有如下几种通信方式:管道、System V进程间通信、POSIX进程间通信,其中他们各自又可以分为具体的几种方式

管道

管道可以分为匿名管道pipe和命名管道,这篇博客主要介绍管道的用法

System V IPC

  1. System V 消息队列
  2. System V 共享内存
  3. System V 信号量

POSIX IPC

  1. 消息队列
  2. 共享内存
  3. 信号量
  4. 互斥量
  5. 条件变量
  6. 读写锁

管道

我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”,管道是一个只能单向通信的通信信道,大致结构如下:
在这里插入图片描述

创建管道

在代码中可以使用pipe命令创建一个管道,其中pipefd[2]是一个输出型参数,我们想通过这个参数读取到打开的两个fd。
在这里插入图片描述
执行如下代码,可以观察pipe所创建的两个文件描述符的具体内容,其中打印结果分别为3和4.

  int pipefd[2]={0};
  if(pipe(pipefd) != 0)//等于0创建成功
  {
    perror("pipe error!\n");
    return 1;
  }
  printf("pipefd[0]: %d \n",pipefd[0]);
  printf("pipefd[1]: %d \n",pipefd[1]);
  return 0;

匿名管道

进程中存在父进程中创建了子进程的情况,对于这种情况子进程和父进程之间进行通信,可以使用匿名管道,代码测试如下:这里是让子进程每隔1s写入,父进程实时进行读取操作。

	int pipefd[2]={0};
  if(pipe(pipefd) != 0)//等于0创建成功
  {
    perror("pipe error!\n");
    return 1;
  }
  //0:读取端
  //1:写入端
  //目的是想让父进程进行读取,子进程写入
  if(fork()==0)
  {
    //子进程
    close(pipefd[0]);
    const char* msg="hello world\n";
    while(1)
    {
        write(pipefd[1],msg,strlen(msg));
        sleep(1);
    }
    exit(0);    
  }
close(pipefd[1]);
  while(1)
  {
    char buffer[64]={0};
    ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
    buffer[s]=0;
    if(s == 0)
    {
      printf("child quit\n");
      break;
    }
    else if(s > 0)
    {
      buffer[s]=0;
      printf("child %s\n",buffer);//没有让父进程sleeip
    }
    else 
    {
      printf("read error\n");
      break;
    }
  }
  close(pipefd[0]);
  }
 

在这里插入图片描述
这里可以看到父进程可以读取到子进程写入的内容,并且读端会等待写端写入。那么如果将父进程每隔一段时间读取一次会出现什么情况呢?
这里我们代码让子进程不断进行写入,父进程每隔1秒进行一次读取

	int pipefd[2]={0};
  if(pipe(pipefd) != 0)//等于0创建成功
  {
    perror("pipe error!\n");
    return 1;
  }
  //0:读取端
  //1:写入端
  //目的是想让父进程进行读取,子进程写入
  if(fork()==0)
  {
    //子进程
    close(pipefd[0]);
    const char* msg="hello world\n";
    while(1)
    {
        write(pipefd[1],msg,strlen(msg));
    }
    exit(0);    
  }
close(pipefd[1]);
  while(1)
  {
  sleep(1);
    char buffer[64]={0};
    ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
    buffer[s]=0;
    if(s == 0)
    {
      printf("child quit\n");
      break;
    }
    else if(s > 0)
    {
      buffer[s]=0;
      printf("child %s\n",buffer);//没有让父进程sleeip
    }
    else 
    {
      printf("read error\n");
      break;
    }
  }
  close(pipefd[0]);
  }

在这里插入图片描述
如上运行结果可以看到,父进程确实读到了子进程写入的内容,但是读取的结果并不是完整的,因此可以说明pipe中只要有缓冲区,就会一直写入,同时读数据的时候,只要有数据就可以一直读。

子进程不断写入,父进程不进行读取

int pipefd[2]={0};
  if(pipe(pipefd) != 0)//等于0创建成功
  {
    perror("pipe error!\n");
    return 1;
  }
  //目的是想让父进程进行读取,子进程写入
  if(fork()==0)
  {
    //子进程
    //0:读取端
    //1:写入端
    close(pipefd[0]);
    int count = 0;
    while(1)
    {
      write(pipefd[1],"a",1);
      count++;
      printf("count:%d\n",count);
    }
    exit(0);
    
  }
  close(pipefd[1]);
  while(1)
  {
  }
  close(pepefd[0]);

这段代码让子进程不断以字符来写入信息,但是父进程不进行读取操作,这样可以看出这个管道大概有多大,运行结果如下:

在这里插入图片描述
这个65536对于的大小大概是64KB,在写满64kb时候write就不再写入了,因为管道有大小,当write写满的时候,就要让read来读,不写的本质是要等对方来读。
如果管道写满了的情况下,让父进程进行读取操作,但是每次只读取一个字符,那么子进程是否会再次写入将管道写满呢?

int pipefd[2]={0};
  if(pipe(pipefd) != 0)//等于0创建成功
  {
    perror("pipe error!\n");
    return 1;
  }
  //目的是想让父进程进行读取,子进程写入
  if(fork()==0)
  {
    //子进程
    //0:读取端
    //1:写入端
    close(pipefd[0]);
    int count = 0;
    while(1)
    {
      write(pipefd[1],"a",1);
      count++;
      printf("count:%d\n",count);
    }
    exit(0);
    
  }
  close(pipefd[1]);
  while(1)
  {
  sleep(10);
   char c = 0;
    read(pipefd[0],&c,1);
    printf("father take:%c\n",c);

  }
  close(pepefd[0]);

在这里插入图片描述
这里我们看到,父进程读取了字符后子进程仍然没有再次进行写入,那么如果父进程每次读一个很大的字符串呢?

int pipefd[2]={0};
  if(pipe(pipefd) != 0)//等于0创建成功
  {
    perror("pipe error!\n");
    return 1;
  }
  //目的是想让父进程进行读取,子进程写入
  if(fork()==0)
  {
    //子进程
    //0:读取端
    //1:写入端
    close(pipefd[0]);
    int count = 0;
    while(1)
    {
      write(pipefd[1],"a",1);
      count++;
      printf("count:%d\n",count);
    }
    exit(0);
    
  }
  close(pipefd[1]);
  while(1)
  {
    sleep(10);
    char buffer[1024*2+1]={0};
    ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
    buffer[s]=0;
    if(s == 0)
    {
      printf("child quit\n");
      break;
    }
    else if(s > 0)
    {
      buffer[s]=0;
      printf("child %s\n",buffer);//没有让父进程sleeip
    }
    else 
    {
      printf("read error\n");
      break;
    }


  }
  close(pepefd[0]);

在这里插入图片描述
运行这段代码后可以看到,在一次读取大量数据后,子进程可以继续写入数据直到再次写满,这里说明管道中也存在类似缓冲区的概念,通过man手册可以看到:当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性,当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。
在这里插入图片描述

写端一直写入,读端完成一次读取后退出

  int pipefd[2]={0};
  if(pipe(pipefd) != 0)//等于0创建成功
  {
    perror("pipe error!\n");
    return 1;
  }
  //目的是想让父进程进行读取,子进程写入
  if(fork()==0)
  {
    //子进程
    //0:读取端
    //1:写入端
    close(pipefd[0]);
  	const char* msg="hello world\n";
    while(1)
    {
    	write(pipefd[1],msg,strlen(msg));
      	sleep(1);
    }
    exit(0);
    
  }
  close(pipefd[1]);
  while(1)
  {
  sleep(10);
    char buffer[64]={0};
    ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
    buffer[s]=0;
    printf("father take: %s\n",buffer);
    break;
   }
  close(pipefd[0]);

在这里插入图片描述
这里可以看到,在读端完成一次读取后退出的情况下,管道也会随之关闭,并不会继续进行写入,这是因为如果没人读取,但是还在写入,本质上浪费操作系统资源,操作系统会直接终止写入过程,操作系统给目标进程发送SIGPIPE(kill),可以通过退出信号来验证这一点

  int pipefd[2]={0};
  if(pipe(pipefd) != 0)//等于0创建成功
  {
    perror("pipe error!\n");
    return 1;
  }
  //目的是想让父进程进行读取,子进程写入
  if(fork()==0)
  {
    //子进程
    //0:读取端
    //1:写入端
    close(pipefd[0]);
  	const char* msg="hello world\n";
    while(1)
    {
    	write(pipefd[1],msg,strlen(msg));
      	sleep(1);
    }
    exit(0);
    
  }
  close(pipefd[1]);
  while(1)
  {
  sleep(10);
    char buffer[64]={0};
    ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
    buffer[s]=0;
    printf("father take: %s\n",buffer);
    break;
   }
  close(pipefd[0]);
  int status=0;
  waitpid(-1,&status,0);
  //查看子进程是如何结束
  printf("exit code: %d\n",status>>8 & 0xFF);
  printf("exit signal: %d\n",status & 0x7F);

在这里插入图片描述

写端关闭,读端一直读取

  int pipefd[2]={0};
  if(pipe(pipefd) != 0)//等于0创建成功
  {
    perror("pipe error!\n");
    return 1;
  }
  //printf("pipefd[0]: %d \n",pipefd[0]);
  //printf("pipefd[1]: %d \n",pipefd[1]);
  //目的是想让父进程进行读取,子进程写入
  if(fork()==0)
  {
    //子进程
    //0:读取端
    //1:写入端
    close(pipefd[0]);
    exit(0);  
  }
  close(pipefd[1]);
  while(1)
  {
    char buffer[64]={0};
    ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
    buffer[s]=0;
    printf("father take: %s\n",buffer);
  }
  close(pipefd[0]);

在这里插入图片描述
可以看到写端关闭,读端会一直进行读取操作,但是没有读到内容,为了避免这个问题可以使用read返回值的进行判断,代码如下:

  int pipefd[2]={0};
  if(pipe(pipefd) != 0)//等于0创建成功
  {
    perror("pipe error!\n");
    return 1;
  }
  //目的是想让父进程进行读取,子进程写入
  if(fork()==0)
  {
    //子进程
    //0:读取端
    //1:写入端
    close(pipefd[0]);
    const char* msg="hello world\n";
    write(pipefd[1],msg,strlen(msg));
    exit(0);
    
  }
  close(pipefd[1]);
  while(1)
  {
    sleep(1);
    char buffer[64]={0};
    ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
    buffer[s]=0;
    printf("father take: %s\n",buffer);
   if(s == 0)
   {
     printf("child quit\n");
     break;
   }
   else if(s > 0)
   {
     buffer[s]=0;
     printf("child %s\n",buffer);//没有让父进程sleeip
   }
   else 
   {
     printf("read error\n");
     break;
   }
  }
  close(pipefd[0]);
  return 0;

在这里插入图片描述

总结

管道一般有如下4种情况:

  1. 读端不读或者读的慢,写端要等读端
  2. 读端关闭,写端收到SIGPIPE信号 直接终止
  3. 写端不写或者写的慢,读端等写端
  4. 写端关闭,读端读完pipe内部数据然后在读,会读到0,表示读到文件结尾

匿名管道特点

  1. 管道是一个只能单向通信的通信信道
  2. 管道是面向字节流的
  3. 仅限于父子进程,具有血缘关系的进程进行进程间通信
  4. 管道自带同步机制,原子性写入
  5. 管道的生命周期是随进程的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值