目录
- 一、进程间通信
- 1.1 进程间通信目的
- 1.2 理解进程间通信
- 1.3 进程间通信发展
- 1.4 进程间通信分类
- 二、管道
- 2.1 什么是管道
- 2.2 管道的原理
- 2.3 匿名管道
- 2.3.1 pipe函数
- 2.3.2 匿名管道的实现
- 2.3.3 匿名管道小结
- 2.3.3.1 匿名管道的四种情况
- 2.3.3.2 匿名管道的五种特性
- 2.3.4 匿名管道实现进程池
- 2.4 命名管道
- 2.4.1 指令级
- 2.4.1.1 创建命名管道
- 2.4.1.2 使用命名管道
- 2.4.2 代码级
- 2.4.2.1 创建命名管道
- 2.4.2.2 使用命名管道
- 结尾
由于进程间通信的篇幅有点大,所以进程间通信这部分将分为两篇文章进行讲述,本篇文章讲述进程间通信的目的、理解、发展及分类和管道相关的知识,下一篇文章将讲述system V共享内存、消息队列、信号量以及内核是如何看待IPC资源的。
一、进程间通信
1.1 进程间通信目的
为了多进程之间的协同,主要分为以下场景:
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
1.2 理解进程间通信
我们都知道进程具有独立性,那么进程1是如何将数据交给进程2的呢?进程1不可能直接将自己的数据交给进程2,这里举个例子来帮助大家理解:
小时候爸爸妈妈可能都吵过架,这时候他们认为自己都没有错,所以两人就不再交流。
当晚上妈妈做好了饭以后,就对你说:“儿/女儿,去叫你爸吃饭”
你就去跟你爸爸说:“爸,妈叫你去吃饭”,
你爸爸又对你说:“告你你妈,我不吃”,
你就回去给你妈妈说:“爸说他不吃饭”,
你妈就让你去给你爸说:“告诉你爸,爱吃不吃”。
在这个例子中,爸爸和妈妈并没有直接交流,而是通过你来进行数据的传递,这样就分别维护了他们两个人的独立性。这里的爸爸和妈妈就分别对应这进程1和进程2,虽然进程1和进程2不能直接将数据交给对方,但是可以通过操作系统也就是“你”来将数据传输给对方。
进程间通信的本质:就是让不同的进程看到同一份资源,这个资源通常由操作系统提供。
对应上面的例子来说,虽然父母不能直接交流,并且你也不在家,但是父母可以把你叫回来,让你做他们两个的中间人,相对于就是想你申请资源。
1.3 进程间通信发展
- 管道
- System V进程间通信
- POSIX进程间通信
1.4 进程间通信分类
管道
- 匿名管道pipe
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
二、管道
2.1 什么是管道
- 管道是Unix中最古老的进程间通信的形式。
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
2.2 管道的原理
当我们运行一个程序的时候,程序加载到内存变为进程,操作系统会为进程创建PCB,还会创建一个files_struct,PCB中有一个指针会指向files_struct,进程运行起来会默认打开三个标准流,当我们创建一个新文件时,操作系统会为其创建一个struct file结构体,files_struct中的文件描述符表中会有一个位置指向file对象,file对象会指向三个重要的内容,自己的inode对象,方法集和文件缓冲区。我们以该进程为父进程创建一个子进程,操作系统会以父进程为模版为子进程创建自己的PCB,又files_struct是进程的一部分,所以操作系统也会为子进程创建一个files_struct,并将父进程中files_struct的内容以浅拷贝的方式拷贝到子进程的files_struct中,所以父子进程就指向了同一个同一个文件,也就是不同的进程看到了同一份资源。如果说新建的文件是普通文件,最终操作系统会将文件缓冲区中的数据刷新到磁盘中,但是我们不想数据被刷新到磁盘中,我们想要通过文件将一个进程的数据交给另一个数据,所以这个文件就要是特殊的文件,这个文件要是内存级别的文件,我们称这个文件为管道文件。
管道未来只能是单向通信,管道必须有一端是读端,一端是写段,父进程就需要两个文件描述符分别代表读端和写端,因为只有这样在创建子进程时,子进程的文件描述符表中才回拥有读端和写端,若父进程只有读端,创建出的子进程也只有读端,不能形成单向信道,只有写端也是同样如此,所以我们需要将管道文件以读写的方式分别打开一次。父子进程都有了读写端后,只需要一个进程关闭读端,一个进程关闭写端,就能够形成单向信道。
文件的属性大部分是在inode中的,少部分存储在file结构体中。
这里我让一个进程分别以读写的方式打开文件,由于file结构体中有一个属性记录着文件的读写位置,所以以两种方式打开文件就会有两个file结构体,我们认定以读方式打开的文件的fd为读端,以写方式打开的文件为的fd写端。
这里我们以父进程为模版创建一个子进程,子进程的files_struct是浅拷贝父进程的files_struct而来的,所以父子进程下同一个fd指向的是同一个file结构体,通过这样的方式,父子进程就各自拥有了一个读写端。
file结构体对象中有一个引用计数,用来记录有多少个文件描述符指向我这个file结构体。
进程关闭读写端本质上就是将对应的指向file结构体的指针在文件描述符表中清除,再将对应的file结构体对象中的引用计数减一,当file结构体对象的引用计数为0时,这个文件才会被操作系统关闭,关闭一个文件与进程没有关系,进程只需要将files_struct中文件描述符表中对应的指针清除,再将file对象中的引用计数减一,就可以认为进程已经关闭对应的文件了,实际上文件是否被操作系统关闭,看到是file结构体中的引用计数,引用计数为0了自然就被关闭了,这个引用计数就很好的支撑了进程管理与文件管理的解耦。
这里我想让子进程写,父进程读,所以我们关闭父进程的写端,子进程的读端,由于file结构体中引用计数的存在,文件不会被关闭,file结构体也不会被清除,这样我们就就做到了父子进程各自维护一个file对象,指向同一个资源,这就是让不同进程看到同一份资源。
2.3 匿名管道
2.3.1 pipe函数
#include <unistd.h>
int pipe(int fd[2]);
功能:创建一无名管道
参数:fd:文件描述符数组,这里参数是输出型参数,返回读写对应的文件描述符,其中fd[0]表示读端,fd[1]表示写端
返回值:
- 成功返回0。
- 失败返回-1,并设置错误码
2.3.2 匿名管道的实现
#include <iostream>
#include <unistd.h>
#include <cassert>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#define MAX 1024
using namespace std;
int main()
{
// 第一步,建立管道
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n == 0);
(void)n; // 在release版本调试下,assert会被注释掉,本句代码只做防止编译器告警的作用
cout << "pipefd[0]:" << pipefd[0] << ", pipefd[1]:" << pipefd[1] << endl;
// 第二步,创建子进程
pid_t id = fork();
if (id < 0)
{
perror("fork");
return 1;
}
if (id == 0)
{
// child
// w - 这里只是向管道文件中写入,并没有向显示屏中写入
close(pipefd[0]);
int cnt = 10;
while (cnt)
{
char message[MAX];
snprintf(message,sizeof(message),"Hello father , i am child , pid : %d , cnt : %d" , getpid(),cnt);
write(pipefd[1],message,strlen(message));
cnt--;
sleep(1);
}
exit(0);
}
// 第三步,关闭父子进程不需要的fd,形成单向信道
// 父进程读,子进程写
// father
// r - 这里向管道中读取数据
close(pipefd[1]);
char buffer[MAX];
while(1)
{
ssize_t num = read(pipefd[0],buffer,sizeof(buffer)-1);
// 我们这里默认读到字符串,如果缓冲区中的数据长度超过了1024
// 我们需要在结尾处留一个位置,用来存放/0
if(num > 0)
{
buffer[num] = '\0';
cout << getpid() << " , child say :" << buffer << " to me!" << endl;
}
}
pid_t rid = wait(NULL);
if (rid == id)
{
cout << "wait success" << endl;
}
return 0;
}
通过上面代码的实现,我们可以通过一个程序向另一个程序动态的写入数据。之前我们学到过通过创建子进程的方式,父进程可以以继承的方式将数据交给子进程,但是不能将变化的数据交给子进程,并且子进程无论如何都是无法将自己的数据交给父进程的。
如果说写端写的很慢,导致管道中没有数据了,会发生什么情况呢?
这里我让写端每100秒向管道中写入数据,运行程序观察现象,我们发现进程“卡住”了,实际上这是读端在等待。所以说写端写的很慢,导致管道中没有数据了,读端必须等待,直到管道中有数据了为止。
如果说写端写的快一点,读端读的慢一点,会发生什么情况呢?
这里我让写端一直写,读端两秒钟读一次,运行程序观察现象,我们发现写端是一行一行写的,但是读端确实一下子将管道中所有的数据全部读出来,这就是匿名管道的特性之一:面相字节流,写端并不会因为你怎么写,就约束读端怎么读,读端想怎么读就怎么读。
如果说读端读的很慢,导致管道写满了,会发生什么情况呢?
这里我让写端一直写,读端200秒读一次,运行程序观察现象,我们发现写端在写了一段时间后就卡住不动了,这就是管道被写满了,在等读端将管道中的数据读走,所以如果说读端读的很慢,导致管道写满了,写端必须等待,直到有空间为止。
通过这里和上面的实验现象,我们发现读端和写端都会互相等待,这就是匿名管道的特性之一:默认给读写端提供同步机制。
那么一个管道是多大呢?
这里我让写端一直写,并且每次写一个字符,读端一直不读,查看写端向管道中写入多少次,运行程序观察现象,这里我们发现写端写入了65536次,每次写入一个字节,所以管道的大小是64KB。我们还可以通过命令ulimit -a
来查看管道的大小,我们发现管道的大小是4KB,实际上这并不是真正的管道大小。
如果写端关闭,读端一直读取,会出现什么情况呢?
这里我让写端每1秒写入一次,写入三次后就直接关闭,读端每1秒读一次一直读,每读一次就输出read的返回值,运行程序观察现象,我们发现写端写入三次以后,read的返回值变为了0,代表读到了文件结尾,也表示写端已经关闭,那么读端也没有存在的意义了,最好也一起关闭了。如果写端关闭,读端一直读取,读端会读到read的返回值为了0,代表读到了文件结尾,表示写端已经关闭。
如果我们不显示的将写端关闭,会发生什么情况呢?
与上面的情况一致,这也体现了管道的特性之一:管道的生命周期是跟随进程的。
如果读端关闭,写端一直写入会发生什么情况呢?
这里我让写端每1秒写入一次一直写,读端每一秒读一次,只读一次后关闭读端,关闭后休眠5秒,,运行程序和脚本观察结果。我们发现写端在读端关闭以后,就直接变为了僵尸状态,也就是说写端被进程杀掉了。所以如果读端关闭,写端一直写入,操作系统会直接杀掉写端对应的进程,操作系统是通过给进程发送SIGPIPE(13)信号将目标进程杀掉的。
父进程是可以查看子进程的退出信息的,这里我们使用wait函数获取子进程的退出信息,退出信息中的低八位就是子进程的退出信号,我们将其打印出来,发现确实是13号信号。
2.3.3 匿名管道小结
2.3.3.1 匿名管道的四种情况
- 正常情况,如果管道没有数据了,读端必须等待,直到有数据为止(写端向管道写入数据了)
- 正常情况,如果管道被写满了,写端必须等待,直到有空间为止(读端读走管道的数据了)
- 写端关闭,读端一直读取,读端会读到read返回值为0,表示读到文件结尾
- 读端关闭,写端一直写入,os会直接杀掉写端进程,操作系统通过想目标进程发送SIGPIPE(13)来终止程序。
2.3.3.2 匿名管道的五种特性
- 匿名管道,可以允许具有血缘关系的进程之间进行进程间通信,常用于父子,仅限于此
- 匿名管道,默认给读写端要提供同步机制
- 管道是面向字节流的
- 管道的生命周期是随进程的
- 管道是单向通信的,半双工通信的一种特殊情况
2.3.4 匿名管道实现进程池
需要注意下面第一种建立进程间通信的前提时,会导致如下图除最后一个创建的管道只有一个父进程的fd指向写端,一个子进程的fd指向读端,前面的所有管道都有一个子进程的fd指向读端,一个父进程的fd和多个子进程的fd指向写端所以在回收资源的时候需要注意一下回收顺序,要么先将所有管道的写端全部关闭都再回收子进程,要么从后往前边关闭管道的写端,边回收子进程。
而下面第二种建立进程间通信的前提时,会保证所有管道都有一个子进程的fd指向读端,一个父进程的fd和多个子进程的fd指向写端。使用任意方式关闭写端和回收子进程。
// ProcessPool.cpp
#include <iostream>
#include <unistd.h>
#include <vector>
#include <string>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"
using namespace std;
const int num = 5;
static int number = 1;
class channel
{
public:
int _ctrlfd; // 写端描述符
int _workerid; // 对应写入的进程
string _name; // 管道的名称
public:
channel(int ctrlfd, int workerid)
: _ctrlfd(ctrlfd), _workerid(workerid)
{
_name = "channel-" + to_string(number++);
}
};
void Work()
{
while (true)
{
int command = 0;
ssize_t n = read(0, &command, sizeof(command));
if (!init.CheckSafe(command))
continue;
if (n == sizeof(command))
init.RunTask(command);
else if (n == 0)
break;
else
{
// nothing to do
}
}
cout << "child quit" << endl;
}
// 传参形式建议
// 输入参数:const &
// 输出参数:*
// 输入输出参数:&
// 创建方式1会导致除最后一个创建的管道只有一个父进程的fd指向写端,一个子进程的fd指向读端
// 前面的所有管道都有一个子进程的fd指向读端,一个父进程的fd和多个子进程的fd指向写端
// 所以在回收资源的时候需要注意一下回收顺序
// void CreateChannels(vector<channel> *channels)
// {
// // 定义并创建管道
// for (int i = 0; i < num; i++)
// {
// int pipefd[2] = {0};
// int n = pipe(pipefd);
// assert(n == 0);
// (void)n;
// // 创建子进程
// pid_t id = fork();
// assert(id >= 0);
// // 关闭不需要的fd,形成单向管道
// if (id == 0)
// {
// // child
// close(pipefd[1]);
// dup2(pipefd[0], 0); // 这里输入重定向,就是为了让Work向0中读取,让Work少一个参数,仅此而已
// Work();
// exit(0);
// }
// // father
// close(pipefd[0]);
// channels->push_back(channel(pipefd[1], id));
// }
// }
// 建方式2保证了所有管道都有一个子进程的fd指向读端,一个父进程的fd和多个子进程的fd指向写端
void CreateChannels(vector<channel> *channels)
{
vector<int> oldfd;
// 定义并创建管道
for (int i = 0; i < num; i++)
{
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n == 0);
(void)n;
// 创建子进程
pid_t id = fork();
assert(id >= 0);
// 关闭不需要的fd,形成单向管道
if (id == 0)
{
// child
// 关闭当前文件的写端
close(pipefd[1]);
// 关闭之前管道文件的写端
if(!oldfd.empty())
{
for(auto& fd : oldfd)
{
close(fd);
}
}
dup2(pipefd[0], 0); // 这里输入重定向,就是为了让Work向0中读取,让Work少一个参数,仅此而已
Work();
exit(0);
}
// father
close(pipefd[0]);
channels->push_back(channel(pipefd[1], id));
oldfd.push_back(pipefd[1]);
}
}
void Print(const vector<channel> &channels)
{
for (const auto &channel : channels)
{
cout << channel._name << " " << channel._ctrlfd << " " << channel._workerid << endl;
}
}
const bool g_always_loop = 1;
// 一直执行任务,则num为-1,否则num为执行任务的次数
void SendCommand(const vector<channel> &channels, int flag, int num = -1)
{
int pos = 0;
while (true)
{
// 1.选择任务 --- 随机
int command = init.SelectTask();
// 2.选择信道(进程)--- 轮询 --- 将任务较为平均的交给进程
channel c = channels[pos++];
pos %= channels.size();
cout << "sent command " << init.ToDesc(command) << "[" << command << "]"
<< " in " << c._name << " worker is:" << c._workerid << endl;
// 3.发送任务
write(c._ctrlfd, &command, sizeof(command));
// 4.判断是否退出
if (flag != g_always_loop)
{
num--;
if (num <= 0)
{
break;
}
}
sleep(1);
}
cout << "SendCommand done..." << endl;
}
void ReleaseChannel(const vector<channel> &channels)
{
// 每个管道都只有一个fd指向写端时,可以使用该方式回收资源
for (const auto &channel : channels)
{
close(channel._ctrlfd);
pid_t rid = waitpid(channel._workerid, nullptr, 0);
}
// 在多个fd指向读端的情况下,回收资源的方法2
// 逆序边关闭写端,再回收对应的子进程
// for (int i = channels.size() - 1; i >= 0; i--)
// {
// close(channels[i]._ctrlfd);
// pid_t rid = waitpid(channels[i]._workerid, nullptr, 0);
// }
// 在多个fd指向读端的情况下,回收资源的方法1
// 先将所有写端全部关闭,就可以随意回收子进程了
// for (const auto &channel : channels)
// {
// close(channel._ctrlfd);
// }
// for (const auto &channel : channels)
// {
// pid_t rid = waitpid(channel._workerid, nullptr, 0);
// if (rid == channel._workerid)
// {
// cout << "wait child " << rid << " success" << endl;
// }
// }
}
int main()
{
vector<channel> channels;
// 创建信道创建进程
CreateChannels(&channels);
// 开始完成任务
// SendCommand(channels,g_always_loop);
SendCommand(channels, !g_always_loop, 10);
// 回收资源,释放管道,关闭写端,等待回收子进程
ReleaseChannel(channels);
// Print(channels);
// sleep(10);
return 0;
}
// Task.hpp
#include <iostream>
#include <vector>
#include <functional>
using namespace std;
typedef function<void()> task_t;
void Download()
{
cout << "我是一个下载任务" << " 处理者:" << getpid() << endl;
}
void PrintLog()
{
cout << "我是一个打印日志的任务" << " 处理者:" << getpid() << endl;
}
void PushVideoStream()
{
cout << "这是一个推送视频流的任务" << " 处理者:" << getpid() << endl;
}
class Init
{
public:
const static int g_download_code = 0;
const static int g_printlog_code = 1;
const static int g_pushvideostream_code = 2;
vector<task_t> tasks;
public:
Init()
{
tasks.push_back(Download);
tasks.push_back(PrintLog);
tasks.push_back(PushVideoStream);
srand(time(nullptr) ^ getpid());
}
bool CheckSafe(int code)
{
return code >= 0 && code < tasks.size();
}
void RunTask(int command)
{
tasks[command]();
}
int SelectTask()
{
return rand() % tasks.size();
}
string ToDesc(int command)
{
switch (command)
{
case g_download_code:
{
return "Download";
break;
}
case g_printlog_code:
{
return "PrintLog";
break;
}
case g_pushvideostream_code:
{
return "PushVideoStream";
break;
}
default:
{
return "Unkown";
break;
}
}
}
};
Init init;
2.4 命名管道
匿名管道只能让具有血缘关系的进程进行进程间通信,如果我们想让两个毫不相关的进程进行进程间通信,那就只能使用命名管道了。
2.4.1 指令级
2.4.1.1 创建命名管道
Linux操作系统中有一个指令叫做mkfifo,mkfifo+管道名就可以创建对应的管道了。
2.4.1.2 使用命名管道
当我们使用echo指令向显示屏中写入一段数据后,我们发现这段数据确实显示到了显示屏上,当我们向命名管道中写入一段数据时,我们发现进程卡住了,使用另一台机器发现fifo文件的大小为0,再使用cat指令从命名管道中读取数据,发现将刚刚写入的命名管道中的数据读取出来了。命名管道文件大小为0的原因是管道属于内存级别的文件,并不会将数据写入到磁盘中。
当我们使用echo指令向管道文件中写入数据时,它就变为了一个进程,当使用cat指令向命名管道文件中读取数据的时候,它也变为了一个进程,这样我们就让两个进程看到了同一份资源,并且这两个进程毫无关系。
我们如何保证这两个进程会看到同一份资源的呢?
是通过路径来保证的,由于路径具有唯一性,所以路径+文件名就可以唯一的让不同进程看到同一份资源。
2.4.2 代码级
2.4.2.1 创建命名管道
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode);
功能:mkfifo 函数可以在Linux操作系统中用于创建命名管道。
参数:
- filename :是一个指向以 null 结尾的字符串的指针,表示要创建的命名管道的文件名。
- mode :是一个位掩码,指定了新创建的命名管道文件的权限。这些权限位与 chmod 和 stat 系统调用中使用的权限位相同。
返回值:
- 成功时,它返回 0。
- 如果失败,则返回 -1 并设置全局变量 errno 以指示错误类型。
2.4.2.2 使用命名管道
使用管道需要两个进程,这里我们就写两个源文件,一个server.cpp代表服务端,一个client.cpp代表客户端,具体实现大家可以看一下下面的代码。
这里为了强调进程间通信的本质就是让不同进程看到同一份资源,我这里创建一个头文件comm.h,头文件中记录着命名管道的文件名。
// comm.h
#pragma once
#define FILENAME "fifo"
// Makefile同时编译形成多个可执行程序
.PHONY:all
all:server client
server:server.cpp
g++ $^ -o $@ -std=c++11
client:client.cpp
g++ $^ -o $@ -std=c++11
.PHONY:clean
clean:
rm -f server client fifo
// server.cpp
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include "comm.h"
using namespace std;
// 创建命名管道
bool MakeFifo()
{
int n = mkfifo(FILENAME, 0666);
if (n < 0)
{
cerr << "errno" << errno << "strerror" << strerror(errno) << endl;
return false;
}
cout << "create fifo success..." << endl;
return true;
}
int main()
{
Start:
int rfd = open(FILENAME, O_RDONLY);
// 有命名管道直接打开,没有则创建,创建完后还需要再一次打开管道
if (rfd < 0)
{
cerr << "errno:" << errno << " strerror:" << strerror(errno) << endl;
if (MakeFifo())
goto Start;
else
return 1;
}
cout << "open fifo success... read" << endl;
while (1)
{
cout << "Client Say# ";
char buffer[1024];
// 将管道中的数据读入到缓冲区中
ssize_t num = read(rfd, buffer, sizeof(buffer) - 1);
// 我们默认向管道中写入的是字符串,所以需要在结尾处加上0
if (num > 0)
{
buffer[num] = 0;
}
// num == 0 代表写端已经关闭,读端也没必要存在了,关闭读端
else if (num == 0)
{
cout << endl;
cout << "client close , server close too..." << endl;
break;
}
cout << buffer << endl;
}
close(rfd);
cout << "close fifo success...read" << endl;
return 0;
}
// client.cpp
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include "comm.h"
using namespace std;
int main()
{
// 客户端以只写的方式打开管道
int wfd = open(FILENAME, O_WRONLY);
if (wfd < 0)
{
// 打开失败则退出进程
cerr << "errno" << errno << "strerror" << strerror(errno) << endl;
return 1;
}
cout << "open fifo success... write" << endl;
while (1)
{
string message;
cout << "Please Enter# ";
// 不以空格为结束符的方式,写入一段数据到字符串message
getline(cin, message);
// 将字符串中的数据写入到管道中
ssize_t n = write(wfd, message.c_str(), message.size());
if (n < 0)
{
// 写入失败则退出进程
cerr << "errno" << errno << "strerror" << strerror(errno) << endl;
return 2;
}
}
// 关闭写端
close(wfd);
cout << "close fifo success...write" << endl;
return 0;
}
我们使用make以后就会多出来两个可执行程序,当我们使用一号机器运行server(服务端)时,若当前目录下有命名管道就直接打开,否则先创建再打开,当我们再使用二号机器运行客户端时,就形成了单向通信的管道了。当我们在客户端中输入信息时,服务端就可以读取到客户端输入的信息了。
结尾
如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!🌹🌹
如果这篇文章对你有用的话,希望大家给一个三连支持一下!!🌹🌹