【进程间通信(二)】【命名管道】

【进程间通信(一)】【管道通信(上)】
【进程间通信(一)】【管道通信(下)】
这两篇文章所说的进程通信,借助的管道都是匿名管道,也就是没有名字的管道,包括我们命令行中 | 符号,这些都是匿名管道。而这篇文章则围绕命名管道进行展开叙述。

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;
}

如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!

感谢各位观看!

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值