
之前说过的匿名管道只能实现有"血缘关系"进程之间的通信,如果想要实现进程和进程之间的通信就要用命名管道。进程和进程之间的通信也是需要让不同的进程看到同一份资源,使⽤FIFO⽂件来做这项⼯作。
1.命名管道的创建和删除
1.1 命令行创建和删除
命名管道可以从命令⾏上创建,命令⾏⽅法是使⽤下⾯这个命令:
- mkfifo 文件名:创建命名管道
- unlink 文件名:删除管道文件
1.2 程序⾥创建和删除
首先创建两个文件server.cc和client.cc,这两个文件是不同的两个进程还有一个comm.hpp文件,里面提供一个路径,让server.cc和client.cc都能看到。
cpp
//comm.hpp文件
#pragma once
#include <iostream>
using namespace std;
#define FIFO_FILE "fifo"
cpp
//server.cc文件
#include "comm.hpp"
int main()
{
return 0;
}
cpp
//client.cc文件
#include "comm.hpp"
int main()
{
return 0;
}
bash
//Makefile文件
.PHONY:all
all:client server
client:client.cc
g++ -o $@ $^
server:server.cc
g++ -o $@ $^
.PHONY:clean
clean:
rm -f client server
创建FIFO文件用函数mkfifo,第一个参数在指定路径下创建fifo文件,第二个参数是创建这个管道文件时的起始权限,一般为666,创建成功返回0,创建失败返回-1。
我们只需要一方创建出命名管道就可以了,这里选择让server创建。
cpp
//server.cc文件
#include <sys/stat.h>
#include <sys/types.h>
#include "comm.hpp"
int main()
{
int n = mkfifo(FIFO_FILE, 0666);
if(n < 0) return 1;
return 0;
}

但是我们会发现这个fifo文件的权限并不是666,而是664,因为系统的umask会屏蔽other的写权限,可以把umask设为0。
cpp
int main()
{
umask(0);
int n = mkfifo(FIFO_FILE, 0666);
if(n < 0)
{
cout << "fifo创建失败" << endl;
return 1;
}
cout << "创建成功" << endl;
return 0;
}

删除管道用函数unlink,把指定路径的管道文件删除,删除成功返回0,删除失败返回-1.

cpp
int main()
{
umask(0);
//创建
int n = mkfifo(FIFO_FILE, 0666);
if(n < 0)
{
cout << "fifo创建失败" << endl;
return 1;
}
cout << "创建成功" << endl;
//删除
n = unlink(FIFO_FILE);
if(n < 0)
{
cout << "fifo删除失败" << endl;
return 1;
}
cout << "删除成功" << endl;
return 0;
}

2.进程间通信
2.1 server进程
我们让server进程从管道里读,步骤就是先打开文件,然后读数据,然后关闭文件。
cpp
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "comm.hpp"
int main()
{
umask(0);
//创建fifo
int n = mkfifo(FIFO_FILE, 0666);
if(n < 0)
{
cout << "fifo创建失败" << endl;
return 1;
}
cout << "fifo创建成功" << endl;
//只读打开
int fd = open(FIFO_FILE, O_RDONLY);
if(fd < 0)
{
cout << "fifo打开失败" << endl;
exit(1);
}
cout << "fifo打开成功" << endl;
char buffer[1024];
while(1)
{
int m = read(fd, buffer, sizeof(buffer)-1);
if(m > 0)
{
buffer[m] = 0;
cout << "从client读取到的内容为:" << buffer << endl;
}
else if(m == 0)
{
cout << "读取完成" << endl;
break;
}
else
{
cout << "读取失败" << endl;
break;
}
}
//关闭文件
close(fd);
//删除fifo
n = unlink(FIFO_FILE);
if(n < 0)
{
cout << "fifo删除失败" << endl;
return 1;
}
cout << "fifo删除成功" << endl;
return 0;
}
2.2 client进程
管道已经在server里创建好了,所以 client只要对文件进行操作就行了。
cpp
#include "comm.hpp"
int main()
{
int fd = open(FIFO_FILE, O_WRONLY); //写方式打开
if(fd < 0)
{
cout << "client打开fifo失败";
exit(1);
}
string message;
int num = 1;
while(true)
{
cout << "请输入:" << endl;
getline(cin, message);
message += (", message num:" + to_string(num++) + ", pid:" + to_string(getpid()));
write(fd, message.c_str(), message.size());
}
close(fd);
return 0;
}
所有头文件都放在comm.hpp里的,两个文件准备好之后,直接make,然后一个shell运行server进程,另一个运行client进程。
先运行server。

当我们只运行server时,进程会卡在fifo创建之后,open之前,阻塞住了,他在等client启动,与client进行通信。
再运行client。

client一运行,server进程的fifo就打开成功了。
结论:write方(此处的client进程)没有执行open时,read方(此处的server进程)就要在open内部阻塞住,直到有人把管道文件打开了,open才会返回。如果读方的进程先启动,会阻塞住,因为写方没启动时读方自己启动没有意义。
fifo打开之后就可以通信了,client发消息,server接收消息。

至此我们就基本实现了两个进程之间的通信。
当我们把client的进程ctrl+c直接结束掉时,也就是写端关闭,写端关闭后读端会读到文件的结束,返回0。

到这里进程之间的通信其实就实现好了,我们现在可以对这个代码进行包装一下。
3.代码面向对象化
NamedPipe类
首先就是fifo文件的创建和删除操作,这里只需要知道FIFO文件在哪创建,创建的fifo文件叫什么名字,所以类成员就要有两个,path和name,为了方便mkfifo函数的传参,我们把path和name合并成一个字符串,中间用"/"连接。
cpp
//comm.hpp文件
#pragma once
#include <iostream>
#include <unistd.h>
#include <string>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
using namespace std;
#define FIFO_FILE "fifo"
class NamedPipe
{
public:
NamedPipe(const string& path, const string& name)
:_path(path), _name(name)
{
_fifo_name = _path + "/" + _name;
}
~NamedPipe()
{
}
private:
string _path;
string _name;
string _fifo_name;
};
在构造和析构函数内进行创建和删除fifo文件。
cpp
class NamedPipe
{
public:
NamedPipe(const string& path, const string& name)
:_path(path), _name(name)
{
_fifo_name = _path + "/" + _name;
umask(0);
int n = mkfifo(_fifo_name.c_str(), 0666);
if(n < 0)
{
cout << "fifo创建失败" << endl;
exit(1);
}
cout << "fifo创建成功" << endl;
}
~NamedPipe()
{
int n = unlink(_fifo_name.c_str());
if(n < 0)
{
cout << "fifo删除失败" << endl;
exit(1);
}
cout << "fifo删除成功" << endl;
}
private:
string _path;
string _name;
string _fifo_name;
};
ERR_EXIT宏替换
当程序返回值不符合我们的预期的时候,比如前面的n<0的情况,我们一般会打印错误信息然后让程序退出,这里可以使用一个宏替换,要包含头文件stdio.h,如下。
cpp
#include <stdio.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
然后就可以用这个ERR_EXIT打印出错信息并且让程序退出。

在server里构建管道时只要实例化对象,传路径和文件名就行。
cpp
//server.cc文件
#include "comm.hpp"
int main()
{
NamedPipe fifo(".", "fifo"); //当前路径下创建名字为fifo的fifo文件
return 0;
}
FileOper类
我们再用一个FileOper类,里面提供了对fifo文件的操作接口,类成员变量和前面一样,这里还需要多加一个存文件描述符的变量,然后先把fd初始化为-1。
cpp
class FileOper
{
public:
FileOper(const string& path, const string& name)
:_path(path), _name(name), _fd(-1)
{
_fifo_name = _path + "/" + _name;
}
~FileOper()
{}
private:
string _path;
string _name;
string _fifo_name;
int _fd;
};
对fifo文件的操作接就包括读方式打开文件,写方式打开文件,读数据,写数据,就是前面一样的实现。
cpp
class FileOper
{
public:
FileOper(const string& path, const string& name)
:_path(path), _name(name), _fd(-1)
{
_fifo_name = _path + "/" + _name;
}
void OpenForRead()
{
_fd = open(_fifo_name.c_str(), O_RDONLY);
if(_fd < 0)
{
ERR_EXIT("open");
}
cout << "fifo打开成功" << endl;
}
void Read()
{
char buffer[1024];
while(1)
{
int n = read(_fd, buffer, sizeof(buffer)-1);
if(n > 0)
{
buffer[n] = 0;
cout << "从client读取到的内容为:" << buffer << endl;
}
else if(n == 0)
{
cout << "读取完成" << endl;
break;
}
else
{
cout << "读取失败" << endl;
break;
}
}
}
void OpenForWrite()
{
_fd = open(_fifo_name.c_str(), O_WRONLY); //写方式打开
if(_fd < 0)
{
ERR_EXIT("open");
}
cout << "client打开fifo成功" << endl;
}
void Write()
{
string message;
int num = 1;
while(true)
{
cout << "请输入:" << endl;
getline(cin, message);
message += (", message num:" + to_string(num++) + ", pid:" + to_string(getpid()));
write(_fd, message.c_str(), message.size());
}
}
void Close()
{
if(_fd > 0) close(_fd);
}
~FileOper()
{}
private:
string _path;
string _name;
string _fifo_name;
int _fd;
};
此时server.cc文件和client.cc文件就只要调用类的接口就行了。
cpp
//server.cc文件
#include "comm.hpp"
int main()
{
NamedPipe fifo(".", "fifo"); //当前路径下创建名字为fifo的fifo文件
//文件操作
FileOper read_file(".", "fifo");
read_file.OpenForRead();
read_file.Read();
read_file.Close();
return 0;
}
cpp
//client.cc文件
#include "comm.hpp"
int main()
{
FileOper write_file(".", "fifo");
write_file.OpenForWrite();
write_file.Write();
write_file.Close();
return 0;
}
此时client就只有文件的操作,管道的所有操作都在server进程里。
本次分享就到这里了,我们下篇见~
