Linux进程间通信--命名管道

目录

1、什么是命名管道

[1.1 命名管道的创建和使用](#1.1 命名管道的创建和使用)

1.2、命名管道的工作原理

1.3、命名管道与匿名管道的区别

[2. 命名管道的特点及特殊场景](#2. 命名管道的特点及特殊场景)

[2.1 特点](#2.1 特点)

[2.2 四种特殊场景](#2.2 四种特殊场景)

3.日志类的模拟

3.1可变参数的利用

[3.2 time()函数和struct tm类的介绍](#3.2 time()函数和struct tm类的介绍)

[3.3 日期类的实现](#3.3 日期类的实现)


1、什么是命名管道

命名管道 是一种在文件系统中存在的特殊文件类型 ,它允许不同进程通过文件名(即"命名")来访问和进行通信。与匿名管道相比 ,命名管道的最大特点是**允许没有共同祖先(即没有血缘关系)的进程之间进行通信。**这使得命名管道在分布式系统和多进程应用中具有广泛的应用价值。

之所以给管道起名字就是为了不同的进程之间利用管道名,找到管道文件进行进程通信,而不是局限于有亲缘关系的进程。

比起匿名管道,命名管道也是内存级文件,在磁盘上都没有 Data block块,命名管道多了一个inode结构体.

1.1 命名管道的创建和使用

在 Linux 中,我们使用**mkfifo**函数来创建命名管道。该函数的原型如下:

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>

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

关于 mkfifo 函数

组成部分 含义
返回值 int 创建成功返回 0,失败返回 -1
参数1 const char *pathname 创建命名管道文件时的路径+名字
参数2**mode_t mode** 创建命令管道文件时的权限

对于参数1,既可以传递绝对路径**/home/xxx/namePipeCode/fifo** ,也可以传递相对路径**./fifo,当然绝对路径更灵活,但也更长**

对于参数2,mode_t 其实就是对 unsigned int 的封装,等价于 uint32_t,而 mode 就是创建命名管道时的初始权限,实际权限需要经过 umask 掩码计算

不难发现,mkfifo 和 mkdir 非常像,其实 mkfifo 可以直接在命令行中运行

创建一个名为fifo 的命名管道文件

bash 复制代码
mkfifo fifo

成功解锁了一种新的特殊类型文件:p 管道文件

这个管道文件也非常特殊:大小为 0 ,从侧面说明 管道文件就是一个纯纯的内存级文件,有自己的上限 ,出现在文件系统中,只是单纯挂个名而已

可以直接在命令行中使用命名管道:

  • echo 可以进行数据写入,可以重定向至 fifo
  • cat 可以进行数据读取,同样也可以重定向于 fifo
  • 打开两个终端窗口(两个进程),即可进行通信

当然也可以通过程实现两个独立进程 IPC

思路:创建 服务端 server 和 客户端 client 两个独立的进程, 服务端server 创建命名管道 ,并以 的方式打开管道文件,客户端 client 以 的方式打开管道文件,打开后俩进程可以进程通信,通信结束后,由客户端关闭 写端(服务端 读端 读取到 0 后也关闭并删除命令管道文件)

注意:

  • 当管道文件不存在时,文件会打开失败,因此为了确保正常通信,需要先运行服务端 server 创建管道文件
  • 服务端启动后,因为是读端,所以会阻塞等待 客户端(写端)写入数据
  • 通信结束后,需要服务端主动删除管道文件
bash 复制代码
unlink 命令管道文件名	//删除管道文件

服务端 Server.cc

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>

using namespace std;
int main()
{
    int n = mkfifo("pipe", 0664); // 创建管道
    if (n == -1)
    {
        perror("mkfifo error");
        exit(1);
    }

    int fd = open("pipe", O_RDONLY | O_CREAT); // 打开管道文件,以读
    if (fd < 0)
    {
        perror("open pipe error");
        exit(1);
    }

    cout << "读端打开成功" << endl;//读管道只有等写端打开才会,打开
    char buffer[1024] = {0};

    int m = read(fd, buffer, sizeof(buffer));
    if (m < 0)
    {
        perror("read err");
        exit(1);
    }
    else if (m == 0)
    {
        return 0;
    }
    else
    {
        cout << buffer << endl;
    }

    unlink("pipe");

    return 0;
}

客户端 client.cc

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
int main()
{
      int fd=open("pipe",O_WRONLY|O_CREAT);
      
      const char* s="hello,world";
      write(fd,s,sizeof(s));
      
    return 0;
}

makefile

cpp 复制代码
.PHONY:all
all:Server Client

Server:server.cc
	g++ -o Server server.cc
Client:client.cc
	g++ -o Client client.cc

.PHONY:clean
clean:
	rm -rf Server Client
1.2、命名管道的工作原理

把视角拉回文件系统:当重复多次打开同一个文件时,并不会费力的打开多次,而且在第一次打开的基础上,管道文件对 struct file 结构体中的引用计数 ++,所以对于同一个文件,不同进程打开了,看到的就是同一个

  • 具体例子:显示器文件(stdout)只有一个吧,是不是所有进程都可以同时进行写入?
  • 同理,命名管道文件也是如此,先创建出文件,在文件系统中挂个名,然后让独立的进程以不同的方式打开同一个命名管道文件,比如进程 A 以只读的方式打开,进程 B 以只写的方式打开,那么此时进程 B 就可以向进程 A 写文件,即 IPC

因为命名管道适用于独立的进程间 IPC,所以无论是读端和写端,进程 A、进程 B 为其分配的 fd 是一致的,都是 3

  • 如果是匿名管道,因为是依靠继承才看到同一文件的,所以读端和写端 fd 不一样

所以 命名管道匿名管道 还是有区别的

1.3、命名管道与匿名管道的区别
特性 匿名管道(Unnamed Pipe) 命名管道(Named Pipe)
进程关系 只能在父子进程间使用 可以在任何独立进程之间使用
创建方式 使用 pipe() 创建 使用 mkfifo() 创建,并指定路径
生命周期 仅在进程期间存在 在文件系统中持久存在,直到被删除
文件系统 不存在文件名 存在文件名,并且存储在文件系统中
使用方式 通过文件描述符在父子进程间通信 通过文件路径与文件描述符进行通信
描述符 由操作系统自动分配给父子进程 需要进程手动打开文件并获取文件描述符
阻塞行为 管道满时写阻塞,管道空时读阻塞 同样具有管道满时写阻塞,管道空时读阻塞

总结来说,命名管道是比匿名管道更加灵活的进程间通信方式,能够在没有直接关系的进程间传递数据,而匿名管道则适用于具有父子关系的进程。

2. 命名管道的特点及特殊场景

命名管道与匿名管道在许多方面具有相似性,下面回顾命名管道的一些主要特点及其特殊场景。

2.1 特点

命名管道的特点可以总结为以下几点:

  • 半双工通信:管道是单向的数据流通,意味着数据只能一个方向传输,要实现双向通信,通常需要两个管道。

  • 管道生命随进程而终止:命名管道在文件系统中有存在的时间,但它的生命周期由进程的创建和终止来决定,进程关闭时,管道与进程的通信结束。

  • 任意多个进程间通信:命名管道不像匿名管道那样只适用于父子进程,它支持任何两个进程间的通信,只要它们能访问同一个管道文件。

  • 流式数据传输服务:命名管道提供的是数据流式传输,它会将数据作为一个连续的流在进程间传递,而不是一次性传输整个文件内容。

  • 自带同步与互斥机制:管道的设计自动包含了同步与互斥机制,在数据传输时,它保证了写操作和读操作不会同时发生,避免了数据的竞争条件。

2.2 四种特殊场景

命名管道在使用过程中有一些特殊的场景:

  1. 管道为空时,读端阻塞,等待写端写入数据

    • 如果一个进程尝试读取管道,但管道当前没有数据,读操作会阻塞,直到有数据被写入管道。
  2. 管道为满时,写端阻塞,等待读端读取数据

    • 如果管道的缓冲区已满,写端会被阻塞,直到读端读取了部分数据,释放出空间,允许写入新的数据。
  3. 进程通信时,关闭读端,操作系统发出 13 号信号 SIGPIPE 终止写端进程

    • 当进程向管道中写数据,而另一个进程关闭了读端,写端会收到 SIGPIPE 信号,通知写端进程管道已不再可用,从而导致写端进程终止。
  4. 进程通信时,关闭写端,读端读取到 0 字节数据,可以借此判断终止读端

    • 当写端关闭时,读端会读取到 0 字节数据,表示写端已经终止。读端可以利用这个信号来结束其自身的读取过程。

3.日志类的模拟

学习了命名管道,我们可以写出一个记录通信过程中日常的日志类。我们将管道的创建封装在一个类这种,这样一来就不用手动创建和删除了。日志类首先要有时间的,下面介绍相关的知识

3.1可变参数的利用

日志类像printf()函数一样,有可变参数部分,可以接受不同个数的参数。要想模拟出同样的效果,我们也要了解可变参数的解析过程。

在 C 语言中,处理可变参数的主要宏定义都在 stdarg.h 头文件中。这里介绍几种常见的宏,它们用于处理传入的可变参数。

  1. va_list 类型

va_list 实际上是一个指向栈帧中可变参数部分的指针类型,它用于遍历函数中的可变参数。

作用:

va_list 用于存储访问可变参数所需的信息。在 C 语言中,参数的数量和类型通常是在编译时无法知道的,因此我们使用**va_list** 来动态地访问这些参数。它需要配合使用

2. va_star t

va_start 宏用于初始化 va_list 类型的变量,这个变量将用来访问传入的可变参数。

语法

cpp 复制代码
va_start(va_list ap, last_fixed_arg);
  • apva_list 类型的变量,用来访问可变参数。

  • last_fixed_arg指向可变参数前面的一个固定参数。

作用 :初始化 va_list,并将其指向第一个可变参数。因为函数的参数是从右向左依次入栈的,所以利用 last_fixed_arg,将其先取地址,取地址后加1.就可以得到可变参数的第一个变量了。

  1. va_arg

va_arg 用来获取下一个可变参数,并指定它的类型。

语法

cpp 复制代码
type va_arg(va_list ap, type);
  • ap:指向可变参数的 va_list 类型变量。

  • type:你期望的参数类型。

作用 :返回 ap 中的下一个可变参数,并将 ap 指向下一个参数。根据类型,对指针强转为对应类型就可以得到参数,再配上while循环就可以将可变参数解析完毕。

4.va_end

**va_end**宏用于结束对可变参数的访问。

语法

cpp 复制代码
va_end(va_list ap);
  • apva_list 类型的变量。

作用:清理资源,结束访问。

示例:计算多个数字的和

cpp 复制代码
#include <stdio.h>
#include <stdarg.h>

// 求和函数
int sum(int count, ...) {
    va_list args;          // 声明一个 va_list 类型的变量
    va_start(args, count); // 初始化 va_list

    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int); // 获取下一个参数
    }

    va_end(args); // 清理 va_list
    return total;
}

int main() {
    printf("Sum: %d\n", sum(3, 1, 2, 3));  // 输出 6
    printf("Sum: %d\n", sum(5, 1, 2, 3, 4, 5));  // 输出 15
    return 0;
}
3.2 time()函数和struct tm类的介绍

日志需要记录时间的情况,我们介绍time()函数和struct tm结构体:

1. time() 函数

time() 是 C 标准库中的一个函数,主要用于获取当前系统时间。它返回的是自 1970年1月1日00:00:00 UTC 到当前时刻所经过的秒数,通常称为 Unix时间戳。这个时间戳是一个整数,单位是秒。

cpp 复制代码
#include <time.h>

time_t time(time_t *t);
  • 参数

    • t:一个指向 time_t 类型变量的指针。如果 t 不为 NULL,则将当前的时间戳存储到 *t 中;如果为 NULL,则不保存当前时间。
  • 返回值

    • 返回当前时间的时间戳,即从 1970 年 1 月 1 日到当前时间所经过的秒数。如果出现错误,返回 (time_t)(-1)
  1. struct tm 结构体

struct tm 是一个结构体,用于表示某一时刻的日期和时间。它包含了年、月、日、小时、分钟、秒等信息。

定义 (在**<time.h>**头文件中):

cpp 复制代码
struct tm {
    int tm_sec;   // 秒 (0-59)
    int tm_min;   // 分钟 (0-59)
    int tm_hour;  // 小时 (0-23)
    int tm_mday;  // 一个月中的日期 (1-31)
    int tm_mon;   // 月份 (0-11),0表示1月,11表示12月
    int tm_year;  // 从1900年起的年份,1900年对应0
    int tm_wday;  // 一周中的天 (0-6),0表示星期日
    int tm_yday;  // 一年中的天数 (0-365),0表示1月1日
    int tm_isdst; // 夏令时标志(>0表示夏令时,0表示非夏令时,<0表示无法确定)
};

3.localtime() 函数

localtime() 是 C 语言中的一个标准库函数,用于将 time_t 类型的时间戳 (即自 1970 年 1 月 1 日以来的秒数)转换为本地时间。返回的时间是一个 struct tm 类型的结构体,它包含了具体的时间信息,如年、月、日、时、分、秒等。

cpp 复制代码
#include <time.h>

struct tm *localtime(const time_t *timep);
  • 参数

    • timep:指向**time_t**类型的指针,表示自 1970 年 1 月 1 日以来的秒数(即 Unix 时间戳)。
  • 返回值

    • 返回一个指向**struct tm** 的指针。struct tm 中包含了本地时间的各个部分(如年、月、日、小时、分钟、秒等)。

    • 返回的结构体是静态的,因此每次调用 localtime() 都会覆盖上次的结果,所以不应该在多个地方同时使用它返回的指针。

cpp 复制代码
#include <stdio.h>
#include <time.h>

int main() {
    time_t current_time;
    struct tm *tm_info;

    // 获取当前时间戳
    current_time = time(NULL);

    // 将时间戳转换为本地时间
    tm_info = localtime(&current_time);

    // 输出格式化的本地时间
    printf("Current local time: %04d-%02d-%02d %02d:%02d:%02d\n",
           tm_info->tm_year + 1900,  // 年份(1900年后)
           tm_info->tm_mon + 1,      // 月份(1-12)
           tm_info->tm_mday,         // 日(1-31)
           tm_info->tm_hour,         // 时(0-23)
           tm_info->tm_min,          // 分(0-59)
           tm_info->tm_sec);         // 秒(0-59)

    return 0;
}
cpp 复制代码
Current local time: 2025-07-11 13:45:30
3.3 日期类的实现

Client客户端:

cpp 复制代码
#include "log.hpp"
#include "comm.hpp"

int main()
{
   int fd=open(FIFO_FILE,O_WRONLY);//打开管道文件
   if(fd<0)
   {
    perror("open");
    exit(FIFO_OPEN_ERR);
   }
  
   cout<<"client open file done"<<endl;

   string s;
   while(true)
   {
    cout<<"client please enter@ ";
    getline(cin,s);
    int id=write(fd,s.c_str(),s.size());
   }
   close(fd);

    return 0;
}

Server服务器

cpp 复制代码
#include "log.hpp"
#include "comm.hpp"

int main()
{
    Init init;
    Log log;
 
    int fd = open(FIFO_FILE, O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }

    log("Debug", "server open file done, error string: %s, error code: %d", strerror(errno), errno);
    log("Info", "server open file done, error string: %s, error code: %d", strerror(errno), errno);
    log("Warning", "server open file done, error string: %s, error code: %d", strerror(errno), errno);
    log("Fatal", "server open file done, error string: %s, error code: %d", strerror(errno), errno);
    

    while (true)
    {
        char buffer[1024];
        int n = read(fd, buffer, sizeof(buffer));
        if (n > 0)
        {
            buffer[n] = '\0';
            cout << "client say@" << buffer << endl;
        }
        else if(n==0)
        {
            log("Debug", "client quit, me too!, error string: %s, error code: %d", strerror(errno), errno);
            break;
        }
        else
        {
        log("Fatal", "client quit, me too!, error string: %s, error code: %d", strerror(errno), errno);
        }
        
    }
    return 0;
}

log.hpp

cpp 复制代码
#pragma once

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

using namespace std;

#define SIZE 1024
#define LogFile "log.txt" //日志文件的名字

class Log
{
public:
    Log()
    {
        printmethod="Screen";//默认打印在屏幕上面
        path="./mylog/";//默认路径
    }
    void Eable(string method)
    {
        printmethod=method;
    }

    void operator()(const string method,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]",method.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);

        char rightbuffer[SIZE];
        vsnprintf(rightbuffer,sizeof(rightbuffer),format,s);//解析va_arg();的作用
        va_end(s);

        char logtxt[SIZE*2];
        snprintf(logtxt,sizeof(logtxt),"%s %s\n",leftbuffer,rightbuffer);

        Printlog(method,logtxt);
    }

    void Printlog(const string method,string logtxt)
    {
        if(method=="Screen")
        {
            cout<<logtxt<<endl;
        }
        else
        {
            string s=path+method+".txt";//./mylog/init.
            
            int fd=open(s.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
            if(fd<0) return ;
            write(fd,logtxt.c_str(),logtxt.size());
            close(fd);
        }
    }
    

private:
    string printmethod;
    string path;
};

comm.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

using namespace std;

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

enum 
{
   FIFO_OPEN_ERR=1,
   FIFO_DELETE_ERR,
   FIFO_CREAT_ERR
};//像类一样设计

class Init
{
public:
  Init()
  {
     int n=mkfifo(FIFO_FILE,MODE);
     if(n==-1)
     {
        perror("open fifo");
        exit(FIFO_CREAT_ERR);
     }
  }

  ~Init()
  {
    int m=unlink(FIFO_FILE);
    if(m==-1)
    {
        perror("delete fifo");
        exit(FIFO_DELETE_ERR);
    }
  }
};

makfile

cpp 复制代码
.PHONY:all
all:server client

server:server.cc	
	g++ -o server server.cc -std=c++11
client:client.cc
	g++ -o client client.cc  -std=c++11

.PHONY:clean
clean:
	rm -rf client server myfifo

这样一来就是实现了在通信之间实现日志类。