【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

相关文章
|
22天前
麒麟系统mate-indicators进程占用内存过高问题解决
【10月更文挑战第7天】麒麟系统mate-indicators进程占用内存过高问题解决
108 2
|
2天前
|
消息中间件 存储 Linux
|
8天前
|
运维 Linux
Linux查找占用的端口,并杀死进程的简单方法
通过上述步骤和命令,您能够迅速识别并根据实际情况管理Linux系统中占用特定端口的进程。为了获得更全面的服务器管理技巧和解决方案,提供了丰富的资源和专业服务,是您提升运维技能的理想选择。
10 1
|
20天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
16 1
|
24天前
麒麟系统mate-indicators进程占用内存过高问题解决
【10月更文挑战第5天】麒麟系统mate-indicators进程占用内存过高问题解决
95 0
|
消息中间件 Linux
Linux IPC实践(6) --System V消息队列(3)
消息队列综合案例 消息队列实现回射客户/服务器  server进程接收时, 指定msgtyp为0, 从队首不断接收消息 server进程发送时, 将mtype指定为接收到的client进程的p...
959 0
|
消息中间件 Linux
Linux IPC实践(4) --System V消息队列(1)
消息队列概述    消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法(仅局限于本机);    每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值.
907 0
|
消息中间件 Linux Windows
Linux IPC实践(5) --System V消息队列(2)
消息发送/接收API msgsnd函数 int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); 参数    ms...
800 0
|
11天前
|
运维 安全 Linux
Linux中传输文件文件夹的10个scp命令
【10月更文挑战第18天】本文详细介绍了10种利用scp命令在Linux系统中进行文件传输的方法,涵盖基础文件传输、使用密钥认证、复制整个目录、从远程主机复制文件、同时传输多个文件和目录、保持文件权限、跨多台远程主机传输、指定端口及显示传输进度等场景,旨在帮助用户在不同情况下高效安全地完成文件传输任务。
100 5
|
11天前
|
Linux
Linux系统之expr命令的基本使用
【10月更文挑战第18天】Linux系统之expr命令的基本使用
42 4