Linux--进程间通信(2)

Linux命名管道通信详解

接上篇,我们讲到了匿名管道的具有血缘关系的了解。

现在,我们继续:

回顾:

回顾之前说到的:进程间通信的本质:让不同进程看到同一份资源。

二、管道通信(以匿名管道为例)
1. 资源本质:
- 管道是内存文件(无需刷盘),内核维护“文件缓冲区”作为通信载体。

2. 内核层面的文件打开逻辑:
两个不同进程打开同一个管道文件时,内核中仅维护1份文件资源( struct file ),但每个进程的 files_struct (文件描述符表)会通过不同的描述符(如读端、写端)指向这份资源。

3. 通信特点:
- 只能单向通信(读端、写端分离)。
- 适用于有血缘关系的进程(如父子进程,可通过继承文件描述符共享管道);

无血缘关系的进程需用命名管道(通过“路径+文件名”的唯一性标识共享资源)。
 
三、核心逻辑
管道通信的本质是:不同进程通过共享内核中的同一份“内存文件资源”,借助其缓冲区完成数据传递。

命名管道:

无血缘关系的进程需用命名管道(通过“路径+文件名”的唯一性标识共享资源)。

创建命名管道

命令行中创建管道:

mkfifo filename(文件名字)
ps:这个文件名字没有在该路径下出现过的

程序中创建管道:

int mkfifo(const char *filename,mode_t mode);

int main(int argc, char *argv[])
{
mkfifo("xx", 0644);
return 0;
}

匿名管道与命名管道的区别:

匿名管道由pipe函数创建并打开。

命名管道由mkfifo函数创建,打开用open

它们唯一的区别在它们创建与打开的方式不同:

命名管道的打开操作:

如果当前打开操作是为读而打开FIFO时

O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功

如果当前打开操作是为写而打开FIFO时

O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO

O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
 

用命名管道实现文件拷贝:

实现的思路跟匿名管道差不多。

通过创建一个可见于文件系统的命名管道,作为两个独立进程(读进程、写进程)的通信媒介,写进程读取源文件数据并写入FIFO,读进程从FIFO读取数据并写入目标文件,最终完成拷贝

#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>

int main()
{
    // 1. 打开目标文件,用于写入
    int outfd = open("abc.aa", O_WRONLY | O_CREAT | O_TRUNC, 0664);
    if (outfd == -1)
    {
        perror("open target file (abc.aa)");
        return 1;
    }

    // 2. 打开 FIFO "mk" 用于读取
    int infd = open("mk", O_RDONLY);
    if (infd == -1)
    {
        perror("open FIFO (mk) for reading");
        close(outfd);
        return 1;
    }

    char buffer[1024];
    int n;

    std::cout << "Reader: Waiting to read from FIFO..." << std::endl;

    while ((n = read(infd, buffer, sizeof(buffer))) > 0)
    {
        if (write(outfd, buffer, n) != n) {
            perror("write to target file");
            break;
        }
    }

    if (n < 0) {
        perror("read from FIFO");
    }

    std::cout << "Reader: Finished reading from FIFO." << std::endl;

    close(infd);
    close(outfd);

    return 0;
}
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>

int main() {
    // 1. 打开源文件,比如 "abc"
    int infd = open("abc", O_RDONLY);
    if (infd == -1) {
        perror("open source file (abc)");
        return 1;
    }

    // 2. 打开 FIFO "mk" 用于写入
    int outfd = open("mk", O_WRONLY);
    if (outfd == -1) {
        perror("open FIFO (mk) for writing");
        close(infd);
        return 1;
    }

    char buffer[1024];
    int n;

    std::cout << "Writer: Sending data to FIFO..." << std::endl;

    while ((n = read(infd, buffer, sizeof(buffer))) > 0) {
        if (write(outfd, buffer, n) != n) {
            perror("write to FIFO");
            break;
        }
    }

    if (n < 0) {
        perror("read from source");
    }

    close(infd);
    close(outfd);

    std::cout << "Writer: Finished sending data." << std::endl;
    return 0;
}

用命名管道实现server与client进行通信

log.hpp部分

再次之前,我们先来实现一个简便版本的日志打印功能的代码,这个后面我们也是可以直接拿来使用的!

我们对于时间的获取:用time/localtime

从上面可以看出来,time里边有个结构体tm,里面就存有其中的年月日时分秒的信息。我们就可以调用它来获取到打印日志时的时间了!

回顾snprintf/vnsprintf等打印函数接口:
#include <stdio.h>
int snprintf(char *str, size_t size, const char *format, ...);

snprintf  是 C 标准库中的安全格式化字符串函数,核心作用是将格式化的数据写入指定字符数组并严格限制写入长度,避免缓冲区溢出(Buffer Overflow),这是它与不安全的 sprintf  的关键区别。

#include <stdio.h>
#include <stdarg.h>

int vsnprintf(char *str, size_t size, const char *format, va_list ap);

vsnprintf  是 C 标准库中的可变参数格式化字符串函数,核心作用是接收一个 va_list  类型的可变参数列表,将其格式化后写入指定字符数组,并通过长度限制防止缓冲区溢出,是 snprintf  的“可变参数列表版本”,主要用于自定义可变参数函数

ps(使用这个函数接口时,需要与va_list封装可变参数,再通过vsnprintf将用户传入的格式化内容写入)

#pragma once
#include <iostream>
#include <string>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<time.h>
#include<stdio.h>
#include<cstdarg>

#define LogFile "log.txt"

//使用枚举的方法进行管理
//根据枚举的特性,按照顺序进行递增
enum
{
    Screen = 1,
    OneFile,
    ClassFile
};

enum
{
    Info = 0,
    Debug,
    Warning,
    Error,
    Fatal
};

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 PrintLog(int level,const std::string&logtxt)
    {
        switch (printmethod)
        {
        case Screen:
            PrintScreen(logtxt);
            break;
        case OneFile:
            PrintOneFile(LogFile,logtxt);
            break;
        case ClassFile:
            PrintfClassFile(level,logtxt);
            break;
        default:
            break;
        }
    }

    //打印到屏幕
    void PrintScreen(const std::string&logtxt)
    {
        std::cout<<logtxt<<std::endl;
    }
    
    //打印到一个文件中
    void PrintOneFile(const std::string& filename,const std::string&logtxt)
    {
        std::string _filename=path+filename;
        int fd=open(_filename.c_str(),O_WRONLY|O_CREAT|O_TRUNC,0664);
        if(fd<0)
        {
            perror("open");
            return;
        }
        char buffer[1024];
        write(fd,logtxt.c_str(),logtxt.size());
        close(fd);
    }

    //按照严重程度去分别写入到文件中
    void PrintfClassFile(int level,const std::string&logtxt)
    {   
        std::string filename=LogFile;
        filename+=".";
        filename+=std::to_string(level);
        PrintOneFile(filename,logtxt);
    }
    
    //运算符重载,仿函数,能够让我们的日志信息像函数一样使用
    void operator()(int level,const char*format,...)
    {
        //时间获取与格式化
        time_t tm=time(nullptr);
        struct tm*ctime=localtime(&tm);

        char leftbuffer[1024];
        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_sec);
        
        //处理可变参数
        va_list s;
        va_start(s,format);
        char rightBuffer[1024];
        vsnprintf(rightBuffer,sizeof(rightBuffer),format,s);
        va_end(s);

        //格式:默认部分+自定义部分
        char logtxt[11024*2];
        snprintf(logtxt,sizeof(logtxt),"%s %s\n",leftbuffer,rightBuffer);
        PrintScreen(logtxt);
    }

    ~Log()
    {
    }

private:
    //打印方法
    int printmethod;
    //路径
    std::string path;
};

server.hpp部分

进行读数据。

#include<iostream>
#include "log.hpp"
#include"comm.hpp"
#include<string.h>

#define SIZE 1024
Log lg;
int main()
{
    Init it;
    lg.Enable(Screen);
    lg(Info,"Init done");
    //打开管道
    int fd=open(MYFIFO,O_RDONLY);
    printf("fd:%d",fd);
    if(fd==-1)
    {
        lg(Fatal,"error string:%s,error code:%d",strerror(errno),errno);
        exit(FIFO_OPEN_ERR);
    }
    lg(Info,"open done...");
    //开始读取数据
    while(true)
    {
        char buffer[SIZE];
        int n=read(fd,buffer,sizeof(buffer));
        if(n<0)
        {
            lg(Fatal,"strerr:%s,error code:%d",strerror(errno),errno);
            break;
        }
        if(n==0)
        {
            lg(Debug,"client quit,me too,strerr:%s,error code:%d",strerror(errno),errno);
            break;
        }
        else
        {
            buffer[n]=0;
            std::cout<<"client say#: "<<buffer<<std::endl;
        }
    }
    close(fd);
    return 0;
}

client.cpp部分

进行写数据

#include<iostream>
#include<string>
#include"log.hpp"
#include"comm.hpp"
#include<cstring>
Log lg;
int main()
{
    int fd=open(MYFIFO,O_WRONLY);
    if(fd<0)
    {
       lg(Fatal,"open fail,strerr:%s,error code:%d",strerror(errno),errno);
       exit(FIFO_OPEN_ERR); 
    }
    lg(Info,"client open done");
    //开始通信
    std::string line;
    while(true)
    {
        std::cout<<"Please Enter# ";
        getline(std::cin,line);
        int s=write(fd,line.c_str(),line.size());
        if(s<0)
        {
            lg(Fatal,"strerr:%s,error code:%d",strerror(errno),errno);
            return 1;
        }

    }
    close(fd);
    return 0;
}

comm.hpp部分

就是创建命名管道部分

#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

#define MYFIFO "./myfifo"
#define MODE 0664

enum
{
    FIFO_CREATE_ERR = 1,
    FIFO_UNLINK_ERR,
    FIFO_OPEN_ERR
};

class Init
{
public:
    //建立命名管道
    Init()
    {
        int fd = mkfifo(MYFIFO, MODE);
        if (fd == -1)
        {
            perror("mkfifo");
            exit(FIFO_CREATE_ERR);
        }
    }

    //删除命名管道
    ~Init()
    {
        int fd = unlink(MYFIFO);
        if (fd == -1)
        {
            perror("unnlink");
            exit(FIFO_UNLINK_ERR);
        }
    }
};

最终效果:

想了想,本篇就到此结束吧,我们下篇再见!希望大家一起进步!!

最后,到了本次鸡汤环节:

成为更好的人,新的约定

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值