个人主页:@Sherry的成长之路
学习社区:Sherry的成长之路(个人社区)
专栏链接:Linux
长路漫漫浩浩,万事皆有期待
上一篇博客:【Linux】Linux进程间通信(一)
匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间的通信,通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。
如果要实现两个毫不相关进程之间的通信,可以使用命名管道来做到。命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。
注意:
普通文件是很难做到通信的,即便做到通信也无法解决一些安全问题。
命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。
使用命令创建命名管道
我们可以使用mkfifo命令创建一个命名管道。
mkfifo fifo
可以看到,创建出来的文件的类型是p,代表该文件是命名管道文件。
使用这个命名管道文件,就能实现两个进程之间的通信了。我们在一个进程(进程A)中用shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用cat命令从命名管道当中进行读取。
现象就是当进程A启动后,进程B会每秒从命名管道中读取一个字符串打印到显示器上。这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。
之前我们说过,当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉,在这里就可以很好的得到验证:当我们终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时bash就会被操作系统杀掉,我们的云服务器也就退出了。
在程序中创建命名管道使用mkfifo函数,mkfifo函数的函数原型如下:
int mkfifo(const char *pathname, mode_t mode);
mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。
若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。
若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。(注意当前路径的含义)
mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。
例如,将mode设置为0666,则命名管道文件创建出来的权限如下:
prw-rw-rw-
但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。
prw-rw-r--
若想创建出来命名管道文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。
umask(0); //将文件默认掩码设置为0
mkfifo函数的返回值。
命名管道创建成功,返回0。
命名管道创建失败,返回-1。
创建命名管道示例:
使用以下代码即可在当前路径下,创建出一个名为myfifo的命名管道。
#include
#include
#include
#define FILE_NAME "myfifo"
int main()
{
umask(0); //将文件默认掩码设置为0
if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
perror("mkfifo");
return 1;
}
//create success...
return 0;
}
1、如果当前打开操作是为读而打开FIFO时。
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
O_NONBLOCK enable:立刻返回成功。
2、如果当前打开操作是为写而打开FIFO时。
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。
实现服务端(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
#include
#include
#include
#include
#include
#define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道
代码编写完毕后,先将服务端进程运行起来,之后我们就能在客户端看到这个已经被创建的命名管道文件。
接着再将客户端也运行起来,此时我们从客户端写入的信息被客户端写入到命名管道当中,服务端再从命名管道当中将信息读取出来打印在服务端的显示器上,该现象说明服务端是能够通过命名管道获取到客户端发来的信息的,换句话说,此时这两个进程之间是能够通信的。
当客户端和服务端运行起来时,我们还可以通过ps命令查看这两个进程的信息,可以发现这两个进程确实是两个毫不相关的进程,因为它们的PID和PPID都不相同。也就证明了,命名管道是可以实现两个毫不相关进程之间的通信的。
服务端和客户端之间的退出关系
当客户端退出后,服务端将管道当中的数据读完后就再也读不到数据了,那么此时服务端也就会去执行它的其他代码了(在当前代码中是直接退出了)。
当服务端退出后,客户端写入管道的数据就不会被读取了,也就没有意义了,那么当客户端下一次再向管道写入数据时,就会收到操作系统发来的13号信号(SIGPIPE),此时客户端就被操作系统强制杀掉了。
通信是在内存当中进行的
若是我们只让客户端向管道写入数据,而服务端不从管道读取数据,那么这个管道文件的大小会不会发生变化呢?
//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,也就说明了双方进程之间的通信依旧是在内存当中进行的,和匿名管道通信是一样的。
需要注意的是两个进程之间的通信,并不是简单的发送字符串而已,服务端是会对客户端发送过来的信息进行某些处理的。
这里我们以客户端向服务端派发计算任务为例,客户端通过管道向服务端发送双操作数的计算请求,服务端接收到客户端的信息后需要计算出相应的结果。
这里我们无需更改客户端的代码,只需改变服务端处理通信信息的逻辑即可。
//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);
//服务端进行计算任务
char* lable = "+-*/%";
char* p = msg;
int flag = 0;
while (*p){
switch (*p){
case '+':
flag = 0;
break;
case '-':
flag = 1;
break;
case '*':
flag = 2;
break;
case '/':
flag = 3;
break;
case '%':
flag = 4;
break;
}
p++;
}
char* data1 = strtok(msg, "+-*/%");
char* data2 = strtok(NULL, "+-*/%");
int num1 = atoi(data1);
int num2 = atoi(data2);
int ret = 0;
switch (flag){
case 0:
ret = num1 + num2;
break;
case 1:
ret = num1 - num2;
break;
case 2:
ret = num1 * num2;
break;
case 3:
ret = num1 / num2;
break;
case 4:
ret = num1 % num2;
break;
}
printf("%d %c %d = %d\n", num1, lable[flag], num2, ret); //打印计算结果
}
else if (s == 0){
printf("client quit!\n");
break;
}
else{
printf("read error!\n");
break;
}
}
close(fd); //通信完毕,关闭命名管道文件
return 0;
}
此时服务端接收到客户端的信息后,需要进行的处理动作就不是将其打印到显示器了,而是需要将信息经过进一步的处理,从而得到相应的结果。
比较有意思的是,我们可以通过一个进程来控制另一个进程的行为,比如我们从客户端输入命令到管道当中,再让服务端将管道当中的命令读取出来并执行。
下面我们只实现了让服务端执行不带选项的命令,若是想让服务端执行带选项的命令,可以对管道当中获取的命令进行解析处理。这里的实现非常简单,只需让服务端从管道当中读取命令后创建子进程,然后再进行进程程序替换即可。
这里也无需更改客户端的代码,只需改变服务端处理通信信息的逻辑即可。
#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;
}
此时服务端接收到客户端的信息后,便进行进程程序替换,进而执行客户端发送过来的命令。
这里我们再用命名管道实现一下文件的拷贝。
我们要做的就是,让客户端将file.txt文件通过管道发送给服务端,在服务端创建一个file-bat.txt文件,并将从管道获取到的数据写入file-bat.txt文件当中,至此便实现了file.txt文件的拷贝。
其中服务端需要做的就是,创建命名管道并以读的方式打开该命名管道,再创建一个名为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
#include
#include
#include
#include
#include
#define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道
编写完代码后,先运行服务端,再运行客户端,一瞬间这两个进程就相继运行结束了。
此时使用ll命令就可以看到,已经完成了file.txt文件的拷贝。
使用cat命令打印file-bat.txt文件当中的内容,发现和file.txt文件当中的内容相同,拷贝文件成功。
使用管道实现文件的拷贝有什么意义?
因为这里是使用管道在本地进行的文件拷贝,所以看似没什么意义,但我们若是将这里的管道想象成“网络”,将客户端想象成“Windows Xshell”,再将服务端想象成“centos服务器”。那我们此时实现的就是文件上传的功能,若是将方向反过来,那么实现的就是文件下载的功能。
匿名管道由pipe函数创建并打开。
命名管道由mkfifo函数创建,由open函数打开。
FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于它们创建与打开的方式不同,一旦这些工作完成之后,它们具有相同的语义。
命令行当中的管道
现有data.txt文件,文件当中的内容如下:
我们可以利用管道(“|”)同时使用cat命令和grep命令,进而实现文本过滤。
cat data.txt | grep sherry
那么在命令行当中的管道(“|”)到底是匿名管道还是命名管道呢?
由于匿名管道只能用于有亲缘关系的进程之间的通信,而命名管道可以用于两个毫不相关的进程之间的通信,因此我们可以先看看命令行当中用管道(“|”)连接起来的各个进程之间是否具有亲缘关系。
下面通过管道(“|”)连接了三个进程,通过ps命令查看这三个进程可以发现,这三个进程的PPID是相同的,也就是说它们是由同一个父进程创建的子进程。
也就是说,由管道(“|”)连接起来的各个进程是有亲缘关系的,它们之间互为兄弟进程。
现在我们已经知道了,若是两个进程之间采用的是命名管道,那么在磁盘上必须有一个对应的命名管道文件名,而实际上我们在使用命令的时候并不存在类似的命名管道文件名,因此命令行上的管道实际上是匿名管道。
今天我们学习了Linux进程间通信的相关知识,了解了进程间通信介绍,管道等 。接下来,我们将继续学习Linux的其他知识。希望我的文章和讲解能对大家的学习提供一些帮助。
当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~