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

简介: 【Linux篇】第十二篇——进程间通信(管道+system V共享内存)

进程间通信介绍


概念


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

目的


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

本质


进程间通信的本质就是,让不同的进程看到同一份资源。

由于各个运行进程之间具有独立性,这个独立性主要体现在数据方面,而代码逻辑层面可以私有也可以共有,因此各个进程之间要实现通信是很困难的。

各个进程之间若想实现通信,一定要借助第三方资源,这些进程就可以通过向这个第三方资源写入或读取数据,进而实现进程之间的通信,这个第三方资源实际上就是操作系统提供的一段内存区域。

image.png

因此,进程间通信的本质就是,让不同的进程看到同一份资源,由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信方式。

分类


管道

  • 匿名管道
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

管道


什么是管道


概念:管道是UNIX中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个"管道"。它的特点就是单向传输数据的,先进先出。

例如,统计我们当前使用云服务器上的登录用户个数。

image.png

其中,who命令和wc命令是两个程序,当运行起来就变成两个进程,who进程通过便准输出将数据打到管道中,wc进程再通过标准输入从管道当中读取数据。至此便完成了数据的传输,进而完成数据的进一步加工处理。(who命令用于查看当前云服务器的登录用户(一行一个用户),wc -l用于统计当前的行数)

image.png

匿名管道


匿名管道的原理


匿名管道用于进程间通信,且仅限于本地父子进程之间的通信。

进程间通信的本质就是,让不同的进程看到同一份资源,使用匿名管道实现父子进程间通信的原理就是,让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。

image.png

注意:

  • 这里父子进程看到的同一份文件资源是由操作系统来维护的,所以当父子进程对该进程进行写入操作时,该文件缓冲区当中的数据是不会进行写时拷贝的。
  • 管道虽然用的是文件的方案,但操作系统一定不会把进程进行通信的数据刷新到磁盘当中,因为这样做有IO参与会降低效率。也就是说,这种文件是一批不会把数据写到磁盘当中的文件,换句话说,磁盘文件和内存文件不一定是一一对应的,有些文件只会在内存当中存在,而不会在磁盘中存在。

pipe函数


pipe函数用于创建匿名管道,pip函数的函数原型如下:

int pipe(int pipefd[2]);

pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符

数组元素 含义
pipefd[0] 管道读端的文件描述符
pipefd[1] 管道写端的文件描述符

pipe函数调用成功时返回0,调用失败时返回-1。

匿名管道使用步骤


在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:

  1. 父进程调用pipe函数创建管道
  2. image.png
  3.   2.父进程创建子进程
  4. image.png
  5.   3.父进程关闭写端,子进程关闭读端
  6. image.png
  7. 注意:
  • 管道只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
  • 从管道写端写入的数据会被内核缓冲,直到从管道的读取被读取。

从文件描述符的角度再来看看这三个步骤:

  1. 父进程调用pipe函数创建管道
  2. image.png
  3.   2.父进程创建子进程
  4. image.png
  5. 3.父进程关闭写端,子进程关闭读端
  6. image.png
  7. 在以下代码中,子进程向匿名管道当中写入10行数据,父进程向匿名管道读出数据。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{    //创建匿名管道
  int pipefd[2]={0};
    if(pipe(pipefd)<0)
    {
     perror("pipe error!\n");
     exit(-1);
     }
     //创建子进程
     pid_t id=fork();
     if(id<0)
     {
      perror("fork error!\N");
      exit(-1);
      }
      else if(id==0)
      {
       //子进程关闭读端
       close(pipefd[0]);
        const char* msg="I am child...!\n";
        int count=10;
       while(count--)
       {
        write(pipefd[0],msg,strlen(msg]);
         sleep(1);
        }
    }
     else 
    {
     //关闭写端
      close(pipefd[1]);
      char buf[64];
      while(1)
      {
        ssize_t s=read(pipefd[0],buf,sizeof(buf)/sizeof(buf[0]);
         if(s>0)
       {
        buf[s]=0;
        printf("father get message:%s",buf);
       }
       else if(s==0)
        {
          printf("father read end of file...\n");
         }
         sleep(1);
        }
}
  return 0;
}

运行结果如下:

image.png

管道读写规则


以四种情况来进行研究:

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));
      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 ");
      }
    }
  }
  return 0;
}

运行结果:读端处于阻塞

6075d8ab98804312ace72c6ab216a838.gif

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

2.写端速度大于读端速度,管道大部分时间内是满的,即写调整不满足 让子进程一直写,父进程每3秒读一次,观察现象

代码改造:

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);
      sleep(5);// 管道大部分时间都是满的,写条件不满足时,写端处于阻塞状态
    }
    else if (s == 0){
      // 读到文件结尾  写端关闭文件描述符 读端会读到文件结尾
      printf("father read end of file...\n ");
    }
  }
}

运行结果: 写端写了一会,管道满了,此时写端处于阻塞

3821d9b216994bc3955687c6be8c58c8.gif

3.关闭写端 让写端先写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 ");
   }
}

运行结果:5秒后,关闭写端,读端会读到文件结尾

60157370f9fa4b32bcc6fa3a2934b60b.gif

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

4.关闭读端  3秒后关闭读端

#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("wait failed");
      exit(-1);
    }
  }
  return 0;
}

运行结果:关闭读端后,子进程收到操作系统发送的13个信号杀死

d3afe12590f14a8da47e2c320461764a.gif

读端关闭,写端进程会被操作系统发送信号杀死。


总结:管道是自带同步与互斥机制的,读端进程和写端进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒。


 1.当没有数据可读时:


O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。

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

  2.当管道满的时候


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

O_NONBLOCK enable:write调用返回-1,errno值为EAGAIN。

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


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


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


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

管道的特点


管道内部自带同步与互斥机制

我们将一次只允许一个进程使用的资源,称为临界资源。管道在同一时刻只允许一个进程对其进行写入或读取操作,因此管道也就是一种临界资源。

临界资源需要被保护的,若是我们不对管道这种临界资源进行任何保护机制,那么就可能会出现同一时刻有多个进程对同一管道进行操作的情况,进而导致同时读写,交叉读写以及读取到的数据不一致等问题。

为了避免这些问题,内核会对管道操作进行同步与互斥:

  • 同步:两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行。比如:A任务运行依赖于B任务产生的数据
  • 互斥:一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。

实际上,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。对于管道的场景来说,互斥就是两个进程不可以同时对管道进行操作,它们会相互排斥,必须等一个进程操作1完毕,另一个才能操作,而同步也是指这两个不能同时对管道进行操作,但这两个进程必须要按照某种次序来对管道进行操作。

也就是说,互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,而同步的任务之间则有明确的顺序关系。

2.管道的生命周期随进程

管道本质是通过文件进行通信的,也就是说管道依赖于文件系统,那么当所有该文件的进程都退出后,该文件也会被释放掉,所以说管道的生命周期随进程。

3.管道提供的是流式服务

对于进程A写入管道当中的数据,进程B每次从管道读取的数据多少是任意的,这种被称为流式服务,与之相对应的是数据报服务:

  • 流式服务:数据没有明确的分割,不分一定的报文段。
  • 数据报服务:数据有明确的分割,拿数据按报文段拿。

4.管道是半双工通信的

在数据通信中,数据在线路上的传送方式可以分为以下三种:

  1. 单工通信:单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
  2. 半双工通信:半双工数据传输数据可以在一个信号载体的两个方向上传输,但是不能同时传输。
  3. 全双工通信:全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。

管道是半双工的,数据只能向一个方向流动,需要双方通信时,需要建立起两个管道。

管道的大小


管道的容量是有限的,如果管道满了。那么写端将阻塞或失败,那么管道的最大容量是多少?

方法一:代码测试

若读端进程一直不读取管道中的数据,写端进程一直向管道写入数据,当管道被写满后,写端进程就会被挂起。据此,可以写入代码来测试管道的最大容量。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
  int fd[2] = { 0 };
  if (pipe(fd) < 0){ //使用pipe创建匿名管道
    perror("pipe");
    return 1;
  }
  pid_t id = fork(); //使用fork创建子进程
  if (id == 0){
    //child 
    close(fd[0]); //子进程关闭读端
    char c = 'a';
    int count = 0;
    //子进程一直进行写入,一次写入一个字节
    while (1){
      write(fd[1], &c, 1);
      count++;
      printf("%d\n", count); //打印当前写入的字节数
    }
    close(fd[1]);
    exit(0);
  }
  //father
  close(fd[1]); //父进程关闭写端
  //父进程不进行读取
  waitpid(id, NULL, 0);
  close(fd[0]);
  return 0;
}

可以看到,在读端进程不读取的情况下,写端进程最多写65536字节的数据就被操作系统挂起,意思就是当前Linux版本中管道的最大容量为65536字节。

image.png

方法二:使用man手册

根据man手册,在2.6.11之前的Linux版本中,管道的最大容量与系统页面大小相同,从Linux2.6.11往后,管道的最大容量是65536字节。

image.png

相关文章
|
2天前
|
NoSQL Linux 程序员
【linux进程信号(一)】信号的概念以及产生信号的方式
【linux进程信号(一)】信号的概念以及产生信号的方式
|
2天前
|
Linux
【linux进程间通信(一)】匿名管道和命名管道
【linux进程间通信(一)】匿名管道和命名管道
|
2天前
|
Java Shell Linux
【linux进程控制(三)】进程程序替换--如何自己实现一个bash解释器?
【linux进程控制(三)】进程程序替换--如何自己实现一个bash解释器?
|
2天前
|
算法 Linux Shell
【linux进程(二)】如何创建子进程?--fork函数深度剖析
【linux进程(二)】如何创建子进程?--fork函数深度剖析
|
2天前
|
存储 Linux Shell
【linux进程(一)】深入理解进程概念--什么是进程?PCB的底层是什么?
【linux进程(一)】深入理解进程概念--什么是进程?PCB的底层是什么?
|
3天前
|
消息中间件 Unix Linux
Linux的学习之路:17、进程间通信(1)
Linux的学习之路:17、进程间通信(1)
17 1
|
3天前
|
存储 安全 Linux
Linux的学习之路:9、冯诺依曼与进程(1)
Linux的学习之路:9、冯诺依曼与进程(1)
18 0
|
1月前
|
存储 JSON 监控
Higress Controller**不是将配置信息推送到Istio的内存存储里面的**。
【2月更文挑战第30天】Higress Controller**不是将配置信息推送到Istio的内存存储里面的**。
14 1
|
1月前
|
存储 C语言
C语言--------数据在内存中的存储
C语言--------数据在内存中的存储
26 0
|
6天前
|
存储 NoSQL Oracle
Oracle 12c的内存列存储:数据的“闪电侠”
【4月更文挑战第19天】Oracle 12c的内存列存储以超高速度革新数据处理,结合列存储与内存技术,实现快速查询与压缩。它支持向量化查询和并行处理,提升效率,但需合理配置以平衡系统资源。作为数据管理员,应善用此功能,适应业务需求和技术发展。