Linux篇第十一篇——进程间通信(管道+system V共享内存)

Posted 呆呆兽学编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux篇第十一篇——进程间通信(管道+system V共享内存)相关的知识,希望对你有一定的参考价值。

⭐️ 本篇博客要给大家介绍一些关于进程间通信的一些知识。Linux下进程通信常见的几种方式,例如管道、共享内存等。

目录


🌏介绍

概念: 进程间通信(IPC,Interprocess communication)是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。这使得一个程序能够在同一时间里处理许多用户的要求。IPC方法包括管道(PIPE)、消息排队、旗语、共用内存以及套接字(socket)(本篇博客只介绍共享内存和管道两种)。
通信目的:

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知某些或某个进程发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

如何实现通信?
要让两个不同的进程实现通信,前提条件是让它们看到同一份资源。所以要想办法让他们看到同一份资源,就需要采取一些手段,可以分为下面几种
通信方式分类:

  1. 管道
  • 匿名管道pipe
  • 命名管道
  1. System V IPC
  • System V 消息队列
  • System V 共享内存
  • System V 信号量
  1. POSIX IPC
  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

🌏管道

🌲认识管道

概念: 管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。它的特点是单向传输数据的,先进先出。
管道相信大家之前都知道一些。我们之前也会用到管道命令‘|’。例如:cat file.txt | head -1。
cat是一个进程,这个进程先处理,然后将处理后得到的标准输出到管道中,再由head进程通过标准输入将管道中的数据读出,再进行处理。

🌲匿名管道

概念: 匿名管道用于进程之间通信,这两个进程需要具有亲缘关系(父子进程等)。

🍯创建匿名管道——pipe

这里介绍一个系统调用接口——pipe。

功能: 创建一个匿名管道
函数原型:

#include <unistd>
int pipe(int pipefd[2])

参数:
fd:文件描述符数组,这是一个输出型参数,fd[0]表示读端,fd[1]表示写端
返回值:
创建管道成功返回0,失败返回-1

匿名管道创建原理:
调用pipe函数后,OS会在fd_array数组中分配两个文件描述符给管道,一个是读,一个是写,并把这两个文件描述符放到用户传进来的数组中,fd[0]代表管道读端,fd[1]代表管道写端。这样一个管道就创建好了。

实例演示:
实例1: 观察fd[0]和fd[1]的值

#include <stdio.h>
#include <unistd.h>

int main()

	int pipefd[2];
	int ret = pipe(pipefd);
	if (ret == -1)
	  // 管道创建失败
	  perror("make piep");
	  exit(-1);
	
	// 成功返回0
	// pipefd[0] 代表读端
	// pipefd[1] 代表写端
	printf("fd[0]:%d, fd[1]:%d\\n", pipefd[0], pipefd[1]);
	return0;

代码运行结果: 显然,pipefd这个数组里面放的是两个文件描述符

实例2: 尝试使用管道读写数据

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>

int main()

	  int pipefd[2];
	  int ret = pipe(pipefd);
	  if (ret == -1)
	    // 管道创建失败
	    perror("make piep");
	    exit(-1);
	  
	
	  char buf[64] = "hello world";
	  // 写数据
	  write(pipefd[1], buf, sizeof(buf)/sizeof(buf[0]));
	  // 读数据
	  buf[0] = 0;// 清空buf
	  ssize_t s = read(pipefd[0], buf, 11);
	  buf[s] = '\\0';
	  printf("%s\\n", buf);
	  return 0;

代码运行结果如下: 可以看出,管道也可以读写数据,和文件使用方法是一致的

上面介绍的都是关于管道如何创建,接下来就要介绍如何使用管道进行通信。

🍯管道的本质

Linux下一切皆文件,看待管道,其实时可以像看待文件一样。且管道和文件使用方法是一致的。管道的生命周期随进程

🍯使用匿名管道进行通信

匿名管道是提供给有亲缘关系两个进程进行通信的。所以我们可以在创建管道之后通过fork函数创建子进程,这样父子进程就看到同一份资源,且父子进程都有这个管道的读写文件描述符。我们可以关闭父进程的读端,关闭子进程的写端,这样子进程往管道里面写数据,父进程往管道里面读数据,这样两个进程就可以实现通信了。
具体过程如下:

  1. 父进程创建管道

  2. foek创建子进程

  3. 关闭父进程的写端,子进程的读端

    实例演示: 子进程每隔1秒往管道里面写数据,父进程每隔1秒往管道里读数据

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>

int main()

  int pipefd[2];
  int ret = pipe(pipefd);
  if (ret == -1)
    // 管道创建失败
    perror("make piep");
    exit(-1);
  
  pid_t id = fork();
  if (id < 0)
    perror("fork failed");
    exit(-1);
  
  else if (id == 0)
    // child
    // 关闭读端
    close(pipefd[0]);
    const char* msg = "I am child...!\\n";
    //int count = 0;
    // 写数据
    while (1)
      ssize_t s = write(pipefd[1], msg, strlen(msg));
      printf("child is sending message...\\n");
      sleep(1);
    
  
  else
    // parent
    close(pipefd[1]);
    char buf[64];
    while (1)
      ssize_t s = read(pipefd[0], buf, sizeof(buf)/sizeof(buf[0])-1);
      if (s > 0)
        buf[s] = '\\0';// 字符串后放一个'\\0'
        printf("father get message:%s", buf);
      
      else if (s == 0)
        // 读到文件结尾  写端关闭文件描述符 读端会读到文件结尾
        printf("father read end of file...\\n ");
      
      sleep(1);
    
  

  return 0;

代码运行结果如下:

🍯匿名管道的读写规则

在这里我们分四种情况来进行研究:

  1. 写端速度小于读端速度,管道大部分时间内为空,即读条件不满足 让子进程每5秒写一次,父进程每1秒读一次,观察现象
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()

  int pipefd[2];
  int ret = pipe(pipefd);
  if (ret == -1)
    // 管道创建失败
    perror("make piep");
    exit(-1);
  
  pid_t id = fork();
  if (id < 0)
    perror("fork failed");
    exit(-1);
  
  else if (id == 0)
    // child
    // 关闭读端
    close(pipefd[0]);
    const char* msg = "I am child...!\\n";
    //int count = 0;
    // 写数据
    while (1)
      ssize_t s = write(pipefd[1], msg, strlen(msg));
      sleep(5);// 管道大部分时间是空的,读条件不满足时,读端处于阻塞状态
      printf("child is sending message...\\n");
    
  
  else
    // parent
    close(pipefd[1]);
    char buf[64];
    //int count = 0;
    while (1)
      ssize_t s = read(pipefd[0], buf, sizeof(buf)/sizeof(buf[0])-1);
      if (s > 0)
        buf[s] = '\\0';// 字符串后放一个'\\0'
        printf("father get message:%s", buf);
      
      else if (s == 0)
        // 读到文件结尾  写端关闭文件描述符 读端会读到文件结尾
        printf("father read end of file...\\n ");
      
      sleep(1);
    
  

  return 0;

代码运行结果如下: 读端处于阻塞

总结: 当读条件不满足时,读端进程会处于阻塞,从task_struct会从运行队列调到等待队列,知道有数据来,才会转移到运行队列中。

  1. 写端速度大于读端速度,管道大部分时间内是满的,即写调整不满足 让子进程一直,父进程每5秒读一次,观察现象
    这里这方核心代码,在上面那个例子的代码进行了一定改造
pid_t id = fork();
if (id < 0)
  perror("fork failed");
  exit(-1);

else if (id == 0)
  // child
  // 关闭读端
  close(pipefd[0]);
  const char* msg = "I am child...!\\n";
  //int count = 0;
  // 写数据
  while (1)
    ssize_t s = write(pipefd[1], msg, strlen(msg));
    printf("child is sending message...\\n");
  

else
  // parent
  close(pipefd[1]);
  char buf[64];
  //int count = 0;
  while (1)
    ssize_t s = read(pipefd[0], buf, sizeof(buf)/sizeof(buf[0])-1);
    if (s > 0)
      buf[s] = '\\0';// 字符串后放一个'\\0'
      printf("father get message:%s", buf);
    
    else if (s == 0)
      // 读到文件结尾  写端关闭文件描述符 读端会读到文件结尾
      printf("father read end of file...\\n ");
      sleep(5);// 管道大部分时间都是满的,写条件不满足时,写端处于阻塞状态
    
  

代码运行结果如下: 写端写了一会后,管道满了,此时写端处于阻塞状态

总结: 当写条件不满足时,写端处于阻塞状态

  1. 关闭写端 让写端先写5秒,然后关闭写端,观察现象
// child
// 关闭读端
close(pipefd[0]);
const char* msg = "I am child...!\\n";
int count = 0;
// 写数据
while (1)
  ssize_t s = write(pipefd[1], msg, strlen(msg));
  printf("child is sending message...\\n");
  
  printf("CHILD: %d\\n", count++);
  if (count == 5)
    close(pipefd[1]);
    exit(-1);
 
  sleep(1);

// parent
close(pipefd[1]);
char buf[64];
while (1)
  ssize_t s = read(pipefd[0], buf, sizeof(buf)/sizeof(buf[0])-1);
  if (s > 0)
    buf[s] = '\\0';// 字符串后放一个'\\0'
    printf("father get message:%s", buf);
    sleep(5);// 管道大部分时间都是满的,写条件不满足时,写端处于阻塞状态
  
  else if (s == 0)
    // 读到文件结尾  写端关闭文件描述符 读端会读到文件结尾
    printf("father read end of file...\\n ");
   

代码运行结果如下: 3s后,关闭写端,读端会读到文件结尾

总结: 如果关闭写端,读端进程会读到文件结尾

  1. 关闭读端 5秒后关闭读端
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()

  int pipefd[2];
  int ret = pipe(pipefd);
  if (ret == -1)
    // 管道创建失败
    perror("make piep");
    exit(-1);
  

  pid_t id = fork();
  if (id < 0)
    perror("fork failed");
    exit(-1);
  
  else if (id == 0)
    // child
    // 关闭读端
    close(pipefd[0]);
    const char* msg = "I am child...!\\n";
    // int count = 0;
    // 写数据
    while (1)
      ssize_t s = write(pipefd[1], msg, strlen(msg));
      printf("child is sending message...\\n");
      
      sleep(1);
    
  
  else
    // parent
    close(pipefd[1]);
    char buf[64];
    int count = 0;
    while (1)
      ssize_t s = read(pipefd[0], buf, sizeof(buf)/sizeof(buf[0])-1);
      if (s > 0)
        buf[s] = '\\0';// 字符串后放一个'\\0'
        printf("father get message:%s", buf);
        //sleep(5);// 管道大部分时间都是满的,写条件不满足时,写端处于阻塞状态
      
      else if (s == 0)
        // 读到文件结尾  写端关闭文件描述符 读端会读到文件结尾
        printf("father read end of file...\\n ");
      
      sleep(1);
      if (count++ == 3)
        close(pipefd[0]);// 读端关闭文件描述符,写端进程后序会被操作系统直接杀掉,没有进程读,写时没有意义的
        break;
      
    
    int status;
    pid_t ret = waitpid(id, &status, 0);
    if (ret > 0)
      // 等待成功
      printf("child exit singal is %d\\n", status&0x7f);
    
    else
      // 等待失败
      perror<

以上是关于Linux篇第十一篇——进程间通信(管道+system V共享内存)的主要内容,如果未能解决你的问题,请参考以下文章

python学习第十一篇网络编程

Linux从青铜到王者第十一篇:Linux进程间信号第一篇

嵌入式Linux从入门到精通之第十一节:进程间通信

Linux从青铜到王者第九篇:Linux进程间通信第一篇

2019-2020-1 20175313 《信息安全系统设计基础》第十周学习总结

Linux从青铜到王者第十篇:Linux进程间通信第二篇