【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月前
|
缓存 监控 Linux
Linux内存问题排查命令详解
Linux服务器卡顿?可能是内存问题。掌握free、vmstat、sar三大命令,快速排查内存使用情况。free查看实时内存,vmstat诊断系统整体性能瓶颈,sar实现长期监控,三者结合,高效定位并解决内存问题。
324 0
Linux内存问题排查命令详解
|
8月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
303 67
|
7月前
|
缓存 Linux 数据安全/隐私保护
Linux环境下如何通过手动调用drop_caches命令释放内存
总的来说,记录住“drop_caches” 命令并理解其含义,可以让你在日常使用Linux的过程中更加娴熟和自如。
1234 23
|
7月前
|
Web App开发 Linux 程序员
获取和理解Linux进程以及其PID的基础知识。
总的来说,理解Linux进程及其PID需要我们明白,进程就如同汽车,负责执行任务,而PID则是独特的车牌号,为我们提供了管理的便利。知道这个,我们就可以更好地理解和操作Linux系统,甚至通过对进程的有效管理,让系统运行得更加顺畅。
215 16
|
7月前
|
Unix Linux
对于Linux的进程概念以及进程状态的理解和解析
现在,我们已经了解了Linux进程的基础知识和进程状态的理解了。这就像我们理解了城市中行人的行走和行为模式!希望这个形象的例子能帮助我们更好地理解这个重要的概念,并在实际应用中发挥作用。
143 20
|
6月前
|
监控 Shell Linux
Linux进程控制(详细讲解)
进程等待是系统通过调用特定的接口(如waitwaitpid)来实现的。来进行对子进程状态检测与回收的功能。
131 0
|
6月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
200 0
|
6月前
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
133 0
|
6月前
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
180 0
|
9月前
|
监控 Linux Python
Linux系统资源管理:多角度查看内存使用情况。
要知道,透过内存管理的窗口,我们可以洞察到Linux系统运行的真实身姿,如同解剖学家透过微观镜,洞察生命的奥秘。记住,不要惧怕那些高深的命令和参数,他们只是你掌握系统"魔法棒"的钥匙,熟练掌握后,你就可以骄傲地说:Linux,我来了!
311 27

热门文章

最新文章