前言
学习了匿名管道之后,就会有一个疑问,是不是还有其他类型的管道,本文就详细讲解一下管道的另一种形式 -- 命名管道。
匿名管道详解:进程间通信--匿名管道【Linux】
匿名管道实例1:匿名管道实例--进程控制【Linux】
匿名管道实例2:匿名管道实例--进程池【Linux】
一、进程间通信目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
通信背景
1.由于进程是具有独立性的,进程想交互数据,成本会非常高。但是有些情况下需要多进程处理一件事情。
2.进程独立并不是彻底独立,有时候我们需要双方能够进行一定程度的信息交互。
我们要学的进程间通信,不是告诉我们如何通信,是他们两个如何先看到同一份资源。(文件,内存块...等方式)
两个进程同时访问磁盘上的一个文件进行读写
但由于进程在磁盘上读写太慢,所以进程间通信一般读写内存中的文件。
两个进程同时访问内存上的一个文件进行读写
二、管道
什么是管道
- 管道是Unix中最古老的进程间通信的形式。
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
2.1 原理
父进程指向一个管道文件,子进程继承父进程的指向关系,从而子进程也指向管道,进行通信。
2.2 特点
生活中管道的特点
- 都是单向的
- 管道是为了传输资源的 -- 数据
所以计算机中的管道 -- 单向的,传输数据的。
三、命名管道
- 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
- 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
- 命名管道是一种特殊类型的文件
特点
命名管道在文件系统中存储,它们被视为一种特殊的文件类型。它们的数据不会存储在磁盘上,而是存储在内存中。在磁盘上操作系统会为命名管道创建一个挂载点,以保证命名管道的存在。
创建一个命名管道
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
$ mkfifo filename
命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename, mode_t mode);
其中,filename是管道名称,mode是管道权限。
创建命名管道:
int main(int argc, char *argv[])
{
mkfifo("p1", 0600);
return 0;
}
匿名管道与命名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完 成之后,它们具有相同的语义。
实例代码
用命名管道实现server和client通信
1. 头文件
头文件,包含库文件和管道路径
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define IPC_PATH "./.fifo"
头文件源代码
2. 服务端
我们让服务端实现读取客户端内容的功能,只需要用mkfifo创建,然后用open打开,其他操作与匿名管道相同。
大致流程为:
①创建管道
②打开管道
③循环读取数据
④关闭管道
⑤结束程序
// 读取
#include "comm.h"
using namespace std;
int main()
{
// 创建管道
if(mkfifo(IPC_PATH, 0600) != 0)
{
cerr << "mkfifo error" << endl;
return 1;
}
int pipeFd = open(IPC_PATH, O_RDONLY);
if(pipeFd < 0)
{
cerr << "open fifo error" << endl;
return 2;
}
#define NUM 1024
//正常的通信过程
char buffer[NUM];
while(true)
{
ssize_t s = read(pipeFd, buffer, sizeof(buffer) - 1);
if(s > 0)
{
buffer[s] = '\0';
cout << "客户端->服务器# " << buffer << endl;
}
else if(s == 0)
{
cout << "客户退出啦,我也退出吧";
break;
}
else
{
cout << "read: " << strerror(errno) << endl;
break;
}
}
close(pipeFd);
unlink(IPC_PATH);
cout << "客户端退出啦" << endl;
return 0;
}
服务端源代码
3. 客户端
我们让客户端实现给服务端发送数据的功能,大致流程为:
①打开管道
②循环读取用户信息
③将信息输入管道中
④关闭管道结束程序
// 写入
#include "comm.h"
using namespace std;
int main()
{
int pipeFd = open(IPC_PATH, O_WRONLY);
if(pipeFd < 0)
{
cerr << "open: " << strerror(errno) << endl;
return 1;
}
#define NUM 1024
char line[NUM];
while(true)
{
printf("请输入你的消息# ");
fflush(stdout);
memset(line, 0, sizeof(line));
if(fgets(line, sizeof(line), stdin) != nullptr)
{
line[strlen(line) - 1] = '\0';
write(pipeFd, line, strlen(line));
}
else
{
break;
}
}
close(pipeFd);
cout << "客户端退出啦" << endl;
return 0;
}
客户端源代码
其中要注意的是,当我们用line接受用户数据时,会一并接受最后的换行符,所以在将数据输入管道时,需要处理换行符。
line[strlen(line) - 1] = '\0';
将换行符置为'\0'
输出样例
客户端发送消息,服务端可以同步接收
可以看到,服务端和客户端的
pid与ppid都毫无关系,是两个毫不相干的进程。
注意事项
-
当服务端没有写入数据的时候,客户端在等。所以,客户端写入之后,服务端才能
read(会返回)到数据,服务端打印读取数据要以客户端的节奏为主。 -
客户端和服务端读写的时候是有一定的顺序性的。
管道内部,没有数据,读端就必须阻塞等待(read)。
管道内部,如果数据被写满,写端就必须阻塞等待(write)。
阻塞等待时,两个进程会把自己放在管道的等待队列里。 -
也就是说,
pipe内部,自带访问控制机制(同步和互斥机制)。
而在两个进程各自printf(向显示器写入)时没有顺序,此时缺乏访问控制。
四、特征总结
- 命名管道可以实现任意两个进程间的通信。
- 管道只能单向通信(内核实现决定的),是半双工的一种特殊情况。
- 管道自带同步机制 -- 自带访问控制。
- 管道是面向字节流的,没有格式边界,需要用户来自定义区分内容的边界。
- 管道的生命周期,随进程退出而退出。
总结
本文详细介绍了进程间通信的一种方式 -- 管道,详细介绍了命名管道的原理和实现。大家也可以尝试去使用一下命名管道,这样可以更深入地了解其中的细节。喜欢的话,欢迎点赞支持和关注~