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

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

命名管道


命名管道的原理


匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间的通信,通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。

如果要实现两个毫不相关进程之间的通信,可以使用命名管道来做到。命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。

注意:

  • 普通文件是很难做到通信的,即便做到通信也无法解决一些安全问题。
  • 命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。

使用命令创建命名管道


我们可以使用mkfifo命令创建一个命名管道。

image.png

可以看到,创建出来的文件类型是p,代表该文件是命名管道文件。

image.png

使用这个命名管道文件,就能实现两个进程之间的通信了。我们在一个进程(进程A)中用shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用cat命令从命名管道当中进行读取。现象就是当进程A启动后,进程B会每秒从命名管道中读取一个字符串打印到显示器上。这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。

image.png

之前我们说过,当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉,在这里就可以很好的得到验证:当我们终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时bash就会被操作系统杀掉,我们的云服务器也就退出了。

image.png

创建一个命名管道


在程序创建命名管道使用mkfifo函数,mkfifo函数的函数原型如下:

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

mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。

  • 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。(注意当前路径的含义)
  • 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。

mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。

例如,将mode设置为0666,则命名管道文件创建出来的权限如下:

image.png

但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。

image.png

若想创建出来命名管道文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0

umask(0); //将文件默认掩码设置为0

mkfifo函数的返回值

  • 命名管道创建成功,返回0。
  • 命名管道创建失败,返回-1

创建一个名为fifo的命名管道:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FILE_NAME "fifo"
int main()
{
  umask(0); //将文件默认掩码设置为0
  if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
    perror("fifo");
    return 1;
  }
  //create success...
  return 0;
}

运行结果:

image.png

命令管道的打开规则


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

  • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
  • O_NONBLOCK enable:立刻返回成功。
  • 2.如果当前打开操作是为写而打开FIFO时。
  • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。
  • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。

用命名管道实现serve&client通信


实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了。

服务端代码:

//server.c
#include "comm.h"
int main()
{
  umask(0); //将文件默认掩码设置为0
  if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
    perror("mkfifo");
    return 1;
  }
  int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
  if (fd < 0){
    perror("open");
    return 2;
  }
  char msg[128];
  while (1){
    msg[0] = '\0'; //每次读之前将msg清空
    //从命名管道当中读取信息
    ssize_t s = read(fd, msg, sizeof(msg)-1);
    if (s > 0){
      msg[s] = '\0'; //手动设置'\0',便于输出
      printf("client# %s\n", msg); //输出客户端发来的信息
    }
    else if (s == 0){
      printf("client quit!\n");
      break;
    }
    else{
      printf("read error!\n");
      break;
    }
  }
  close(fd); //通信完毕,关闭命名管道文件
  return 0;
}

而对于客户端来说,因为服务端运行起来后命名管道文件就已经被创建了,所以客户端只需以写的方式打开该命名管道文件,之后客户端就可以将通信信息写入到命名管道文件当中,进而实现和服务端的通信。

客户端代码:

//client.c
#include "comm.h"
int main()
{
  int fd = open(FILE_NAME, O_WRONLY); //以写的方式打开命名管道文件
  if (fd < 0){
    perror("open");
    return 1;
  }
  char msg[128];
  while (1){
    msg[0] = '\0'; //每次读之前将msg清空
    printf("Please Enter# "); //提示客户端输入
    fflush(stdout);
    //从客户端的标准输入流读取信息
    ssize_t s = read(0, msg, sizeof(msg)-1);
    if (s > 0){
      msg[s - 1] = '\0';
      //将信息写入命名管道
      write(fd, msg, strlen(msg));
    }
  }
  close(fd); //通信完毕,关闭命名管道文件
  return 0;
}

.对于如何让客户端和服务端使用同一个命名管道文件,这里我们可以让客户端和服务端包含同一个头文件,该头文件当中提供这个共用的命名管道文件的文件名,这样客户端和服务端就可以通过这个文件名,打开同一个命名管道文件,进而进行通信了。

共用头文件:

//comm.h
#pragma once
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道

代码编写完毕后,先将服务端进程运行起来,之后我们就能在客户端看到这个已经被创建的命名管道文件。接着再将客户端也运行起来,此时我们从客户端写入的信息被客户端写入到命名管道当中,服务端再从命名管道当中将信息读取出来打印在服务端的显示器上,该现象说明服务端是能够通过命名管道获取到客户端发来的信息的,换句话说,此时这两个进程之间是能够通信的。

09538e06916742dc939d65fca3303d58.gif

当客户端和服务端运行起来时,我们还可以通过ps命令查看这两个进程的信息,可以发现这两个进程确实是两个毫不相关的进程,因为它们的PID和PPID都不相同。也就证明了,命名管道是可以实现两个毫不相关进程之间的通信的。

image.png

服务端和客户端之间的退出关系

当客户端退出后,服务端将管道当中的数据读完后就再也读不到数据了,那么此时服务端也就会去执行它的其他代码了(在当前代码中是直接退出了)。

image.png

当服务端退出后,客户端写入管道的数据就不会被读取了,也就没有意义了,那么当客户端下一次再向管道写入数据时,就会收到操作系统发来的13号信号(SIGPIPE),此时客户端就被操作系统强制杀掉了。

fba14dd6e51d485e8866307e39651648.gif

通信是在内存当中进行的

若是我们只让客户端向管道写入数据,而服务端不从管道读取数据,那么这个管道文件的大小会不会发生变化呢?

//server.c
#include "comm.h"
int main()
{
  umask(0); //将文件默认掩码设置为0
  if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
    perror("mkfifo");
    return 1;
  }
  int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
  if (fd < 0){
    perror("open");
    return 2;
  }
  while (1){
    //服务端不读取管道信息
  }
  close(fd); //通信完毕,关闭命名管道文件
  return 0;
}

以看到,尽管服务端不读取管道当中的数据,但是管道当中的数据并没有被刷新到磁盘,使用ll命令看到命名管道文件的大小依旧为0,也就说明了双方进程之间的通信依旧是在内存当中进行的,和匿名管道通信是一样的。

image.png

用命名管道实现进程遥控


比较有意思的是,我们可以通过一个进程来控制另一个进程的行为,比如我们从客户端输入命令到管道当中,再让服务端将管道当中的命令读取出来并执行。

下面我们只实现了让服务端执行不带选项的命令,若是想让服务端执行带选项的命令,可以对管道当中获取的命令进行解析处理。这里的实现非常简单,只需让服务端从管道当中读取命令后创建子进程,然后再进行进程程序替换即可。

这里也无需更改客户端的代码,只需改变服务端处理通信信息的逻辑即可。

#include "comm.h"
int main()
{
  umask(0); //将文件默认掩码设置为0
  if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
    perror("mkfifo");
    return 1;
  }
  int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
  if (fd < 0){
    perror("open");
    return 2;
  }
  char msg[128];
  while (1){
    msg[0] = '\0'; //每次读之前将msg清空
    //从命名管道当中读取信息
    ssize_t s = read(fd, msg, sizeof(msg)-1);
    if (s > 0){
      msg[s] = '\0'; //手动设置'\0',便于输出
      printf("client# %s\n", msg);
      if (fork() == 0){
        //child
        execlp(msg, msg, NULL); //进程程序替换
        exit(1);
      }
      waitpid(-1, NULL, 0); //等待子进程
    }
    else if (s == 0){
      printf("client quit!\n");
      break;
    }
    else{
      printf("read error!\n");
      break;
    }
  }
  close(fd); //通信完毕,关闭命名管道文件
  return 0;
}

此时服务端接收到客户端的信息后,便进行进程程序替换,进而执行客户端发送过来的命令。

image.png

用命名管道实现文件拷贝


这里我们再用命名管道实现一个文件的拷贝。

需要拷贝的文件是file.txt,该文件当中的内容如下:

image.png

我们要做的就是,让客户端将file.txt文件通过管道发送给服务端,在服务端创建一个file-bat.txt文件,并将从管道获取到的数据写入file-bat.txt文件当中,至此便实现了file.txt文件的拷贝。

image.png

其中服务端需要做的就是,创建命名管道并以读的方式打开该命名管道,再创建一个名为file-bat.txt的文件,之后需要做的就是将从管道当中读取到的数据写入到file-bat.txt文件当中即可。

服务端的代码如下:

//server.c
#include "comm.h"
int main()
{
  umask(0); //将文件默认掩码设置为0
  if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
    perror("mkfifo");
    return 1;
  }
  int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件
  if (fd < 0){
    perror("open");
    return 2;
  }
  //创建文件file-bat.txt,并以写的方式打开该文件
  int fdout = open("file-bat.txt", O_CREAT | O_WRONLY, 0666);
  if (fdout < 0){
    perror("open");
    return 3;
  }
  char msg[128];
  while (1){
    msg[0] = '\0'; //每次读之前将msg清空
    //从命名管道当中读取信息
    ssize_t s = read(fd, msg, sizeof(msg)-1);
    if (s > 0){
      write(fdout, msg, s); //将读取到的信息写入到file-bat.txt文件当中
    }
    else if (s == 0){
      printf("client quit!\n");
      break;
    }
    else{
      printf("read error!\n");
      break;
    }
  }
  close(fd); //通信完毕,关闭命名管道文件
  close(fdout); //数据写入完毕,关闭file-bat.txt文件
  return 0;
}

而客户端需要做的就是,以写的方式打开这个已经存在的命名管道文件,再以读的方式打开file.txt文件,之后需要做的就是将file.txt文件当中的数据读取出来并写入管道当中即可。

客户端的代码如下:

//client.c
#include "comm.h"
int main()
{
  int fd = open(FILE_NAME, O_WRONLY); //以写的方式打开命名管道文件
  if (fd < 0){
    perror("open");
    return 1;
  }
  int fdin = open("file.txt", O_RDONLY); //以读的方式打开file.txt文件
  if (fdin < 0){
    perror("open");
    return 2;
  }
  char msg[128];
  while (1){
    //从file.txt文件当中读取数据
    ssize_t s = read(fdin, msg, sizeof(msg));
    if (s > 0){
      write(fd, msg, s); //将读取到的数据写入到命名管道当中
    }
    else if (s == 0){
      printf("read end of file!\n");
       break;
    }
    else{
      printf("read error!\n");
      break;
    }
  }
  close(fd); //通信完毕,关闭命名管道文件
  close(fdin); //数据读取完毕,关闭file.txt文件
  return 0;
}

共用头文件的代码和之前的一样,如下:

//comm.h
#pragma once
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道

编写完代码后,先运行服务端,再运行客户端,一瞬间这两个进程就相继运行结束了。

image.png

此时使用ll命令就可以看到,已经完成了file.txt文件的拷贝。

image.png

使用cat命令打印file-bat.txt文件当中的内容,发现和file.txt文件当中的内容相同,拷贝文件成功。

image.png

使用管道实现文件的拷贝有什么意义?


因为这里是使用管道在本地进行的文件拷贝,所以看似没什么意义,但我们若是将这里的管道想象成“网络”,将客户端想象成“Windows Xshell”,再将服务端想象成“centos服务器”。那我们此时实现的就是文件上传的功能,若是将方向反过来,那么实现的就是文件下载的功能。

image.png

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


  • 匿名管道由pipe函数创建并打开
  • 命名管道由mkfifo函数创建,由open函数打开
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于它们创建与打开的方式不同,一旦这些工作完成之后,它们具有相同的语义。

命令行当中的管道


现有data.txt文件,文件当中的内容如下:

image.png

我们可以利用管道(“|”)同时使用cat命令和grep命令,进而实现文本过滤。

image.png

那么在命令行当中的管道(“|”)到底是匿名管道还是命名管道呢?

由于匿名管道只能用于有亲缘关系的进程之间的通信,而命名管道可以用于两个毫不相关的进程之间的通信,因此我们可以先看看命令行当中用管道(“|”)连接起来的各个进程之间是否具有亲缘关系。

下面通过管道(“|”)连接了三个进程,通过ps命令查看这三个进程可以发现,这三个进程的PPID是相同的,也就是说它们是由同一个父进程创建的子进程

image.png

而它们的父进程实际上就是命令行解释器,这里为bash

image.png

也就是说,由管道(“|”)连接起来的各个进程是有亲缘关系的,它们之间互为兄弟进程。

现在我们已经知道了,若是两个进程之间采用的是命名管道,那么在磁盘上必须有一个对应的命名管道文件名,而实际上我们在使用命令的时候并不存在类似的命名管道文件名,因此命令行上的管道实际上是匿名管道。

相关文章
|
3天前
|
NoSQL Linux 程序员
【linux进程信号(一)】信号的概念以及产生信号的方式
【linux进程信号(一)】信号的概念以及产生信号的方式
|
3天前
|
Linux
【linux进程间通信(一)】匿名管道和命名管道
【linux进程间通信(一)】匿名管道和命名管道
|
3天前
|
Java Shell Linux
【linux进程控制(三)】进程程序替换--如何自己实现一个bash解释器?
【linux进程控制(三)】进程程序替换--如何自己实现一个bash解释器?
|
3天前
|
算法 Linux Shell
【linux进程(二)】如何创建子进程?--fork函数深度剖析
【linux进程(二)】如何创建子进程?--fork函数深度剖析
|
3天前
|
存储 Linux Shell
【linux进程(一)】深入理解进程概念--什么是进程?PCB的底层是什么?
【linux进程(一)】深入理解进程概念--什么是进程?PCB的底层是什么?
|
4天前
|
消息中间件 Unix Linux
Linux的学习之路:17、进程间通信(1)
Linux的学习之路:17、进程间通信(1)
18 1
|
4天前
|
存储 安全 Linux
Linux的学习之路:9、冯诺依曼与进程(1)
Linux的学习之路:9、冯诺依曼与进程(1)
18 0
|
9天前
|
算法 Linux 调度
深入理解Linux内核的进程调度机制
【4月更文挑战第17天】在多任务操作系统中,进程调度是核心功能之一,它决定了处理机资源的分配。本文旨在剖析Linux操作系统内核的进程调度机制,详细讨论其调度策略、调度算法及实现原理,并探讨了其对系统性能的影响。通过分析CFS(完全公平调度器)和实时调度策略,揭示了Linux如何在保证响应速度与公平性之间取得平衡。文章还将评估最新的调度技术趋势,如容器化和云计算环境下的调度优化。
|
10天前
|
监控 Linux
linux监控指定进程
请注意,以上步骤提供了一种基本的方式来监控指定进程。根据你的需求,你可以选择使用不同的工具和参数来获取更详细的进程信息。
14 0
|
11天前
|
消息中间件 监控 Linux
Linux进程和计划任务管理
通过这些命令和工具,你可以有效地管理Linux系统中的进程和计划任务,监控系统的运行状态并保持系统的稳定和可靠性。 买CN2云服务器,免备案服务器,高防服务器,就选蓝易云。百度搜索:蓝易云
103 2