【进程间通信(一)】【管道通信(上)】
【进程间通信(一)】【管道通信(下)】
这两篇文章所说的进程通信,借助的管道都是匿名管道,也就是没有名字的管道,包括我们命令行中 | 符号,这些都是匿名管道。而这篇文章则围绕命名管道进行展开叙述。
1. 命名管道
因为匿名管道没有名字,而进程通信的前提需要让双方看到同一份 “资源”,因此只有父子进程才能够以匿名管道的方式进行通信(只要父进程创建管道,子进程就会继承父进程的文件描述符表,这样子进程也同样能够看到这个管道资源),而这种只能在具有血缘关系的进程体系中进行通信。
1.1 现象
所以还有一种命名管道,用于没有任何关系的进程之间的通信!
在命令行中,除了 | 创建匿名管道之外,还可以 mkfifo 指令创建命名管道。
mkfifo myfifo # 创建命名管道
[outlier@localhost fifo]$ mkfifo myfifo
[outlier@localhost fifo]$ ll
total 0
prw-rw-r-- 1 outlier outlier 0 Aug 23 19:54 myfifo
而当我们对该命名管道进行写入时,如果读端没有对管道做读取,那么写端是会处于阻塞状态的。
1.2 理解
-
如果两个不同的进程打开同一个文件时,在内核中,操作系统会打开几个文件?
首先我们能够很清楚的是,不同的进程打开文件,方式有读有写,可能不一样,并且每个进程对文件读写的位置也可能不一致,因此对于 struct file,每个进程肯定是不一样的。但是对于被打开的这个文件,其文件属性、该文件的读写方法,文件缓冲区,这些都是相同的。
缓冲区都是一样的,当不同进程对文件读写数据时,不会导致数据的混乱吗? ---- 当你让不同的进程同时打开一个文件做读写操作,这个问题就不是操作系统所能控制的了。即便给你分配多个缓冲区,但最后你将数据刷到外设时,一样无法保证数据的有序性。因此操作系统干脆不管这个问题,由用户层自己控制。
进程通信的前提是:不同的进程能够看到同一份 “资源”,而既然不同的进程都能打开同一个文件,不就等于看到同一份 “资源" 了吗??不是说看到同一份资源就能够进行通信吗??
但是通信的场景是,一个进程把数据丢进文件缓冲区中,让另一个进程从缓冲区把数据读出去,能够做到这一步就足够了,不需要什么刷盘。所以这肯定不能是普通文件,即便不同进程打开同一个普通文件,看到共同的资源了,依旧不能进行通信,因为普通文件是会进行数据刷盘的!
因此管道文件为什么能够进行通信,就是因为它不用刷盘!它是内存级别的文件!只需要有一个缓冲区即可。所以当不断的向管道文件追加写入数据后,依旧可以发现管道文件的大小是 0,因为它根本就没刷盘,在磁盘中也就自然没有数据。
-
如何得知不同的进程打开的是同一个文件?
这一块与父子进程不同,因为子进程继承了父进程的数据,因此可以子进程可以看到父进程打开的那一个文件。但是如果没有血缘关系的进程,通过管道文件所在的路径 + 文件名,就可以让该文件在该路径下绝对具有唯一性,路径 + 文件名定位文件,那么就能保证不同进程打开的是同一个文件。而只要打开的是同一个文件(内存级),那么就能够实现进程间的通信!
所以 有路径、有名字的管道,就称为命名管道! 关于命名管道的其它原理,与匿名管道是一致的!
1.3 编码通信
命名管道通信中使用到的系统接口
// 创建命名管道的系统调用
// pathname:路径 + 文件名
// mode:初始化管道文件的权限
int mkfifo(const char *pathname, mode_t mode);
RETURN VALUE
On success mkfifo() returns 0. In the case of an error,
-1 is returned (in which case, errno is set appropriately).
示例:
#define FIFO_FILE "./myfifo"
#define MODE 0664
int n = mkfifo(FIFO_FILE, MODE);
// 删除一个文件的系统调用
// path: 路径 + 文件名
int unlink(const char *path);
RETURN VALUE
Upon successful completion, 0 shall be returned. Otherwise, -1 shall be returned and
errno set to indicate the error. If -1 is returned, the named file shall not be changed.
示例:
#define FIFO_FILE "./myfifo"
int m = unlink(FIFO_FILE);
通信代码案例:
// comm.hpp
#pragma once
#include<iostream>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<string>
#define FIFO_FILE "./myfifo"
#define MODE 0664
enum
{
FIFO_CREATE_ERR = 1,
FIFO_DELETE_ERR,
FIFO_OPEN_ERR
};
class Init
{
public:
Init()
{
int n = mkfifo(FIFO_FILE, MODE);
if(n == -1) // 创建管道失败返回 -1
{
perror("mkfifo");
exit(FIFO_CREATE_ERR);
}
}
~Init()
{
int m = unlink(FIFO_FILE); // unlink 删除文件的系统调用
if(m == -1)
{
perror("unlink");
exit(FIFO_DELETE_ERR);
}
}
};
// service.cc
#include "comm.hpp"
using namespace std;
int main()
{
// 1. 创建管道
Init init;
// 2. 打开管道
// 等待写入方打开之后,自己才会打开文件,即如果写端没打开,读的一方会阻塞在open处
// 因为如果写端没打开,都没数据写入,还读什么数据
int fd = open(FIFO_FILE, O_RDONLY);
if(fd < 0)
{
perror("open");
exit(FIFO_OPEN_ERR);
}
cout << "server open file done!\n";
// 3. 通信
while(true)
{
char buffer[1024] = {0};
int sz = read(fd, buffer, sizeof(buffer));
if(sz > 0)
{
buffer[sz] = 0;
cout << "[client]$ " << buffer << endl;
}
else if(sz == 0) // 写端关闭,读端也关闭
{
cout << "client quie!\n";
break;
}
else break;
}
close(fd);
return 0;
}
// client.cc
#include "comm.hpp"
using namespace std;
int main()
{
// 1. 打开管道
int fd = open(FIFO_FILE, O_WRONLY); // 客户端写
if(fd < 0)
{
perror("open");
exit(FIFO_OPEN_ERR);
}
cout << "client open file done!\n";
string line;
while(true)
{
cout << "Please Enter# ";
getline(cin, line);
write(fd, line.c_str(), line.size());
}
close(fd);
return 0;
}
2. 了解日志
在实际开发中,不管是本地软件,还是服务端,难免会存在一些问题,那么这些问题就需要被记录下来,方便开发人员后续的查找和解决。
一般的日志包含时间、等级、内容,甚至有些还会包含文件名和行号等信息。
常见的日志等级有:
- Info:常规消息
- Warning:报警信息
- Error:比较严重了,可能需要立即处理
- Fatal:致命的错误
- Debug:调试
2.1 了解可变参数
在将日志应用到我们的代码案例之前,我们需要先了解一下可变参数的使用,方便后续使用日志。
va_list:
C 库中的一个宏,底层是 char* 结构,在函数实例化参数时,可变参数具有不确定性,而 va_list 用于提取可变参数中每一个参数。
// 让 va_list 指向的可变参数部分,本质就是让 va_list 指向 &last + 1
void va_start(va_list ap, last);
// 根据类型提取可变参数
type va_arg(va_list ap, type);
示例:
int sum(int n, ...)
{
va_list s;
// 让 s 指向可变参数的起始地址,即找到 n 的地址后,指针在向后移动 n 的大小个字节偏移量即可找到。
// 可变参数必须至少要有一个具体的参数,就是因为需要靠这个具体的参数找到可变参数部分。
va_start(s, n);
int ret = 0;
while(n--) ret += va_arg(s, int);
va_end(s); //含义:s == NULL
return ret;
}
// 调用
sum(3, 1, 2, 3); // ret=6
sum(5, 1, 2, 3, 4, 5); // ret=15
2.2 在通信中加入日志信息
了解需要用到的接口
time_t time(time_t *tloc); // 获取时间戳
struct tm *localtime(const time_t *timep); // 将时间戳格式化
struct tm {
int tm_sec; /* seconds */
int tm_min; /* minutes */
int tm_hour; /* hours */
int tm_mday; /* day of the month */
int tm_mon; /* month */
int tm_year; /* year */
int tm_wday; /* day of the week */
int tm_yday; /* day in the year */
int tm_isdst; /* daylight saving time */
};
// 需要注意的是,返回的年份是从1900往后开始计数的,因此计算真实年份时需要加上 1900
// 月份需要 + 1
·tm_mon The number of months since January, in the range 0 to 11.
·tm_year The number of years since 1900.
int snprintf(char *str, size_t size, const char *format, ...);
// 与 snprintf 相似,只不过将可变参数列表换成了 va_list 宏,用于定位可变参数部分
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
// log.hpp
#pragma once
#include<iostream>
#include<stdarg.h>
#include<string>
#include<time.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen; // 默认屏幕打印日志
path = "./log/";
}
void Enable(int method)
{
printMethod = method;
}
std::string levelToString(int level)
{
switch (level)
{
case Info: return "Info";
case Debug: return "Debug";
case Warning: return "Warning";
case Error: return "Error";
case Fatal: return "Fatal";
default: return "None";
}
}
void operator()(int level, const char* format, ...)
{
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftBuffer[SIZE];
snprintf(leftBuffer, sizeof(leftBuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format); // 初始化s,让其指向可变参数部分
char rightBuffer[SIZE * 2];
vsnprintf(rightBuffer, sizeof(rightBuffer), format, s);
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftBuffer, rightBuffer);
printLog(level, logtxt);
}
void printLog(int level, const std::string& logtxt)
{
switch(printMethod)
{
case Screen:
std::cout << logtxt << "\n";
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
}
}
void printOneFile(const std::string& logName, const std::string& logtxt)
{
std::string file = path + logName;
int fd = open(file.c_str(), O_CREAT|O_WRONLY|O_APPEND, 0666);
if(fd < 0) return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string& logtxt)
{
std::string file = LogFile;
file += '.';
file += levelToString(level);
printOneFile(file, logtxt);
}
~Log() {}
private:
int printMethod;
std::string path;
};
日志输出在代码中的应用案例
Log log;
log.Enable(Classfile); // 多文件打印
int fd = open(FIFO_FILE, O_RDONLY);
if(fd < 0)
{
log(Fatal, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
exit(FIFO_OPEN_ERR);
}
while(true)
{
char buffer[1024] = {0};
int sz = read(fd, buffer, sizeof(buffer));
if(sz > 0) {...}
else if(sz == 0) // 写端关闭,读端也关闭
log(Info, "server quit! error string: %s, error code: %d", strerror(errno), errno); break;
else log(Error, "read pipe done, error string: %s, error code: %d", strerror(errno), errno); break;
}
如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!
感谢各位观看!