【Linux】进程间通信(一)

news2025/1/8 7:21:49

在这里插入图片描述

目录

  • 一、进程间通信
    • 1.1 进程间通信目的
    • 1.2 理解进程间通信
    • 1.3 进程间通信发展
    • 1.4 进程间通信分类
  • 二、管道
    • 2.1 什么是管道
    • 2.2 管道的原理
    • 2.3 匿名管道
      • 2.3.1 pipe函数
      • 2.3.2 匿名管道的实现
      • 2.3.3 匿名管道小结
        • 2.3.3.1 匿名管道的四种情况
        • 2.3.3.2 匿名管道的五种特性
      • 2.3.4 匿名管道实现进程池
    • 2.4 命名管道
      • 2.4.1 指令级
        • 2.4.1.1 创建命名管道
        • 2.4.1.2 使用命名管道
      • 2.4.2 代码级
        • 2.4.2.1 创建命名管道
        • 2.4.2.2 使用命名管道
  • 结尾

由于进程间通信的篇幅有点大,所以进程间通信这部分将分为两篇文章进行讲述,本篇文章讲述进程间通信的目的、理解、发展及分类和管道相关的知识,下一篇文章将讲述system V共享内存、消息队列、信号量以及内核是如何看待IPC资源的。


一、进程间通信

1.1 进程间通信目的

为了多进程之间的协同,主要分为以下场景:

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

1.2 理解进程间通信

我们都知道进程具有独立性,那么进程1是如何将数据交给进程2的呢?进程1不可能直接将自己的数据交给进程2,这里举个例子来帮助大家理解:

小时候爸爸妈妈可能都吵过架,这时候他们认为自己都没有错,所以两人就不再交流。

当晚上妈妈做好了饭以后,就对你说:“儿/女儿,去叫你爸吃饭”
你就去跟你爸爸说:“爸,妈叫你去吃饭”,
你爸爸又对你说:“告你你妈,我不吃”,
你就回去给你妈妈说:“爸说他不吃饭”,
你妈就让你去给你爸说:“告诉你爸,爱吃不吃”。

在这个例子中,爸爸和妈妈并没有直接交流,而是通过你来进行数据的传递,这样就分别维护了他们两个人的独立性。这里的爸爸和妈妈就分别对应这进程1和进程2,虽然进程1和进程2不能直接将数据交给对方,但是可以通过操作系统也就是“你”来将数据传输给对方。

进程间通信的本质:就是让不同的进程看到同一份资源,这个资源通常由操作系统提供。
对应上面的例子来说,虽然父母不能直接交流,并且你也不在家,但是父母可以把你叫回来,让你做他们两个的中间人,相对于就是想你申请资源。


1.3 进程间通信发展

  • 管道
  • System V进程间通信
  • POSIX进程间通信

1.4 进程间通信分类

管道

  • 匿名管道pipe
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

二、管道

2.1 什么是管道

  • 管道是Unix中最古老的进程间通信的形式。
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
    在这里插入图片描述

2.2 管道的原理

当我们运行一个程序的时候,程序加载到内存变为进程,操作系统会为进程创建PCB,还会创建一个files_struct,PCB中有一个指针会指向files_struct,进程运行起来会默认打开三个标准流,当我们创建一个新文件时,操作系统会为其创建一个struct file结构体,files_struct中的文件描述符表中会有一个位置指向file对象,file对象会指向三个重要的内容,自己的inode对象,方法集和文件缓冲区。我们以该进程为父进程创建一个子进程,操作系统会以父进程为模版为子进程创建自己的PCB,又files_struct是进程的一部分,所以操作系统也会为子进程创建一个files_struct,并将父进程中files_struct的内容以浅拷贝的方式拷贝到子进程的files_struct中,所以父子进程就指向了同一个同一个文件,也就是不同的进程看到了同一份资源。如果说新建的文件是普通文件,最终操作系统会将文件缓冲区中的数据刷新到磁盘中,但是我们不想数据被刷新到磁盘中,我们想要通过文件将一个进程的数据交给另一个数据,所以这个文件就要是特殊的文件,这个文件要是内存级别的文件,我们称这个文件为管道文件。

在这里插入图片描述


管道未来只能是单向通信,管道必须有一端是读端,一端是写段,父进程就需要两个文件描述符分别代表读端和写端,因为只有这样在创建子进程时,子进程的文件描述符表中才回拥有读端和写端,若父进程只有读端,创建出的子进程也只有读端,不能形成单向信道,只有写端也是同样如此,所以我们需要将管道文件以读写的方式分别打开一次。父子进程都有了读写端后,只需要一个进程关闭读端,一个进程关闭写端,就能够形成单向信道。

在这里插入图片描述

文件的属性大部分是在inode中的,少部分存储在file结构体中。
这里我让一个进程分别以读写的方式打开文件,由于file结构体中有一个属性记录着文件的读写位置,所以以两种方式打开文件就会有两个file结构体,我们认定以读方式打开的文件的fd为读端,以写方式打开的文件为的fd写端。

在这里插入图片描述
这里我们以父进程为模版创建一个子进程,子进程的files_struct是浅拷贝父进程的files_struct而来的,所以父子进程下同一个fd指向的是同一个file结构体,通过这样的方式,父子进程就各自拥有了一个读写端。

在这里插入图片描述

file结构体对象中有一个引用计数,用来记录有多少个文件描述符指向我这个file结构体。

进程关闭读写端本质上就是将对应的指向file结构体的指针在文件描述符表中清除,再将对应的file结构体对象中的引用计数减一,当file结构体对象的引用计数为0时,这个文件才会被操作系统关闭,关闭一个文件与进程没有关系,进程只需要将files_struct中文件描述符表中对应的指针清除,再将file对象中的引用计数减一,就可以认为进程已经关闭对应的文件了,实际上文件是否被操作系统关闭,看到是file结构体中的引用计数,引用计数为0了自然就被关闭了,这个引用计数就很好的支撑了进程管理与文件管理的解耦。

这里我想让子进程写,父进程读,所以我们关闭父进程的写端,子进程的读端,由于file结构体中引用计数的存在,文件不会被关闭,file结构体也不会被清除,这样我们就就做到了父子进程各自维护一个file对象,指向同一个资源,这就是让不同进程看到同一份资源。


2.3 匿名管道

2.3.1 pipe函数

#include <unistd.h>
int pipe(int fd[2]);

功能:创建一无名管道

参数:fd:文件描述符数组,这里参数是输出型参数,返回读写对应的文件描述符,其中fd[0]表示读端,fd[1]表示写端

返回值

  • 成功返回0。
  • 失败返回-1,并设置错误码

在这里插入图片描述


2.3.2 匿名管道的实现

#include <iostream>
#include <unistd.h>
#include <cassert>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>

#define MAX 1024

using namespace std;

int main()
{
    // 第一步,建立管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    assert(n == 0);
    (void)n; // 在release版本调试下,assert会被注释掉,本句代码只做防止编译器告警的作用

    cout << "pipefd[0]:" << pipefd[0] << ", pipefd[1]:" << pipefd[1] << endl;

    // 第二步,创建子进程
    pid_t id = fork();

    if (id < 0)
    {
        perror("fork");
        return 1;
    }

    if (id == 0)
    {
        // child
        // w - 这里只是向管道文件中写入,并没有向显示屏中写入
        close(pipefd[0]);

        int cnt = 10;
        while (cnt)
        {
            char message[MAX];
            snprintf(message,sizeof(message),"Hello father , i am child , pid : %d , cnt : %d" , getpid(),cnt);
            write(pipefd[1],message,strlen(message));
            cnt--;
            sleep(1);
        }

        exit(0);
    }

    // 第三步,关闭父子进程不需要的fd,形成单向信道
    // 父进程读,子进程写

    // father
    // r - 这里向管道中读取数据
    close(pipefd[1]);

    char buffer[MAX];
    while(1)
    {
        ssize_t num = read(pipefd[0],buffer,sizeof(buffer)-1);
        // 我们这里默认读到字符串,如果缓冲区中的数据长度超过了1024
        // 我们需要在结尾处留一个位置,用来存放/0
        if(num > 0)
        {
            buffer[num] = '\0';
            cout << getpid() << " , child say :" << buffer << " to me!" << endl;  
        }
    }

    pid_t rid = wait(NULL);
    if (rid == id)
    {
        cout << "wait success" << endl;
    }

    return 0;
}

通过上面代码的实现,我们可以通过一个程序向另一个程序动态的写入数据。之前我们学到过通过创建子进程的方式,父进程可以以继承的方式将数据交给子进程,但是不能将变化的数据交给子进程,并且子进程无论如何都是无法将自己的数据交给父进程的。

在这里插入图片描述


如果说写端写的很慢,导致管道中没有数据了,会发生什么情况呢?

这里我让写端每100秒向管道中写入数据,运行程序观察现象,我们发现进程“卡住”了,实际上这是读端在等待。所以说写端写的很慢,导致管道中没有数据了,读端必须等待,直到管道中有数据了为止
在这里插入图片描述


如果说写端写的快一点,读端读的慢一点,会发生什么情况呢?

这里我让写端一直写,读端两秒钟读一次,运行程序观察现象,我们发现写端是一行一行写的,但是读端确实一下子将管道中所有的数据全部读出来,这就是匿名管道的特性之一:面相字节流,写端并不会因为你怎么写,就约束读端怎么读,读端想怎么读就怎么读。

在这里插入图片描述


如果说读端读的很慢,导致管道写满了,会发生什么情况呢?

这里我让写端一直写,读端200秒读一次,运行程序观察现象,我们发现写端在写了一段时间后就卡住不动了,这就是管道被写满了,在等读端将管道中的数据读走,所以如果说读端读的很慢,导致管道写满了,写端必须等待,直到有空间为止

通过这里和上面的实验现象,我们发现读端和写端都会互相等待,这就是匿名管道的特性之一:默认给读写端提供同步机制

在这里插入图片描述
那么一个管道是多大呢?

这里我让写端一直写,并且每次写一个字符,读端一直不读,查看写端向管道中写入多少次,运行程序观察现象,这里我们发现写端写入了65536次,每次写入一个字节,所以管道的大小是64KB。我们还可以通过命令ulimit -a来查看管道的大小,我们发现管道的大小是4KB,实际上这并不是真正的管道大小。
在这里插入图片描述
在这里插入图片描述


如果写端关闭,读端一直读取,会出现什么情况呢?

这里我让写端每1秒写入一次,写入三次后就直接关闭,读端每1秒读一次一直读,每读一次就输出read的返回值,运行程序观察现象,我们发现写端写入三次以后,read的返回值变为了0,代表读到了文件结尾,也表示写端已经关闭,那么读端也没有存在的意义了,最好也一起关闭了。如果写端关闭,读端一直读取,读端会读到read的返回值为了0,代表读到了文件结尾,表示写端已经关闭。
在这里插入图片描述
在这里插入图片描述

如果我们不显示的将写端关闭,会发生什么情况呢?
与上面的情况一致,这也体现了管道的特性之一:管道的生命周期是跟随进程的。
在这里插入图片描述


如果读端关闭,写端一直写入会发生什么情况呢?

这里我让写端每1秒写入一次一直写,读端每一秒读一次,只读一次后关闭读端,关闭后休眠5秒,,运行程序和脚本观察结果。我们发现写端在读端关闭以后,就直接变为了僵尸状态,也就是说写端被进程杀掉了。所以如果读端关闭,写端一直写入,操作系统会直接杀掉写端对应的进程,操作系统是通过给进程发送SIGPIPE(13)信号将目标进程杀掉的。

在这里插入图片描述

父进程是可以查看子进程的退出信息的,这里我们使用wait函数获取子进程的退出信息,退出信息中的低八位就是子进程的退出信号,我们将其打印出来,发现确实是13号信号。

在这里插入图片描述


2.3.3 匿名管道小结

2.3.3.1 匿名管道的四种情况
  1. 正常情况,如果管道没有数据了,读端必须等待,直到有数据为止(写端向管道写入数据了)
  2. 正常情况,如果管道被写满了,写端必须等待,直到有空间为止(读端读走管道的数据了)
  3. 写端关闭,读端一直读取,读端会读到read返回值为0,表示读到文件结尾
  4. 读端关闭,写端一直写入,os会直接杀掉写端进程,操作系统通过想目标进程发送SIGPIPE(13)来终止程序。
2.3.3.2 匿名管道的五种特性
  1. 匿名管道,可以允许具有血缘关系的进程之间进行进程间通信,常用于父子,仅限于此
  2. 匿名管道,默认给读写端要提供同步机制
  3. 管道是面向字节流的
  4. 管道的生命周期是随进程的
  5. 管道是单向通信的,半双工通信的一种特殊情况

2.3.4 匿名管道实现进程池

需要注意下面第一种建立进程间通信的前提时,会导致如下图除最后一个创建的管道只有一个父进程的fd指向写端,一个子进程的fd指向读端,前面的所有管道都有一个子进程的fd指向读端,一个父进程的fd和多个子进程的fd指向写端所以在回收资源的时候需要注意一下回收顺序,要么先将所有管道的写端全部关闭都再回收子进程,要么从后往前边关闭管道的写端,边回收子进程。

而下面第二种建立进程间通信的前提时,会保证所有管道都有一个子进程的fd指向读端,一个父进程的fd和多个子进程的fd指向写端。使用任意方式关闭写端和回收子进程。
在这里插入图片描述

// ProcessPool.cpp
#include <iostream>
#include <unistd.h>
#include <vector>
#include <string>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"

using namespace std;

const int num = 5;
static int number = 1;

class channel
{
public:
    int _ctrlfd;   // 写端描述符
    int _workerid; // 对应写入的进程
    string _name;  // 管道的名称
public:
    channel(int ctrlfd, int workerid)
        : _ctrlfd(ctrlfd), _workerid(workerid)
    {
        _name = "channel-" + to_string(number++);
    }
};

void Work()
{
    while (true)
    {
        int command = 0;
        ssize_t n = read(0, &command, sizeof(command));

        if (!init.CheckSafe(command))
            continue;

        if (n == sizeof(command))
            init.RunTask(command);
        else if (n == 0)
            break;
        else
        {
            // nothing to do
        }
    }
    cout << "child quit" << endl;
}

// 传参形式建议
// 输入参数:const &
// 输出参数:*
// 输入输出参数:&

// 创建方式1会导致除最后一个创建的管道只有一个父进程的fd指向写端,一个子进程的fd指向读端
// 前面的所有管道都有一个子进程的fd指向读端,一个父进程的fd和多个子进程的fd指向写端
// 所以在回收资源的时候需要注意一下回收顺序
// void CreateChannels(vector<channel> *channels)
// {
//     // 定义并创建管道
//     for (int i = 0; i < num; i++)
//     {
//         int pipefd[2] = {0};
//         int n = pipe(pipefd);
//         assert(n == 0);
//         (void)n;

//         // 创建子进程

//         pid_t id = fork();
//         assert(id >= 0);

//         // 关闭不需要的fd,形成单向管道
//         if (id == 0)
//         {
//             // child

//             close(pipefd[1]);
//             dup2(pipefd[0], 0); // 这里输入重定向,就是为了让Work向0中读取,让Work少一个参数,仅此而已
//             Work();

//             exit(0);
//         }

//         // father
//         close(pipefd[0]);
//         channels->push_back(channel(pipefd[1], id));
//     }
// }

// 建方式2保证了所有管道都有一个子进程的fd指向读端,一个父进程的fd和多个子进程的fd指向写端
void CreateChannels(vector<channel> *channels)
{
    vector<int> oldfd;
    // 定义并创建管道
    for (int i = 0; i < num; i++)
    {
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;

        // 创建子进程

        pid_t id = fork();
        assert(id >= 0);

        // 关闭不需要的fd,形成单向管道
        if (id == 0)
        {
            // child
            // 关闭当前文件的写端
            close(pipefd[1]);
            // 关闭之前管道文件的写端
            if(!oldfd.empty())
            {
                for(auto& fd : oldfd)
                {
                    close(fd);
                }
            }
            dup2(pipefd[0], 0); // 这里输入重定向,就是为了让Work向0中读取,让Work少一个参数,仅此而已
            Work();

            exit(0);
        }

        // father
        close(pipefd[0]);
        channels->push_back(channel(pipefd[1], id));
        oldfd.push_back(pipefd[1]);
    }
}

void Print(const vector<channel> &channels)
{
    for (const auto &channel : channels)
    {
        cout << channel._name << "  " << channel._ctrlfd << "  " << channel._workerid << endl;
    }
}

const bool g_always_loop = 1;

// 一直执行任务,则num为-1,否则num为执行任务的次数
void SendCommand(const vector<channel> &channels, int flag, int num = -1)
{
    int pos = 0;
    while (true)
    {
        // 1.选择任务 --- 随机
        int command = init.SelectTask();
        // 2.选择信道(进程)--- 轮询 --- 将任务较为平均的交给进程
        channel c = channels[pos++];
        pos %= channels.size();
        cout << "sent command " << init.ToDesc(command) << "[" << command << "]"
             << " in " << c._name << " worker is:" << c._workerid << endl;

        // 3.发送任务
        write(c._ctrlfd, &command, sizeof(command));

        // 4.判断是否退出
        if (flag != g_always_loop)
        {
            num--;
            if (num <= 0)
            {
                break;
            }
        }

        sleep(1);
    }

    cout << "SendCommand done..." << endl;
}

void ReleaseChannel(const vector<channel> &channels)
{
	// 每个管道都只有一个fd指向写端时,可以使用该方式回收资源
    for (const auto &channel : channels)
    {
        close(channel._ctrlfd);
        pid_t rid = waitpid(channel._workerid, nullptr, 0);
    }

    // 在多个fd指向读端的情况下,回收资源的方法2
    // 逆序边关闭写端,再回收对应的子进程
    // for (int i = channels.size() - 1; i >= 0; i--)
    // {
    //     close(channels[i]._ctrlfd);
    //     pid_t rid = waitpid(channels[i]._workerid, nullptr, 0);
    // }


    // 在多个fd指向读端的情况下,回收资源的方法1
    // 先将所有写端全部关闭,就可以随意回收子进程了
    // for (const auto &channel : channels)
    // {
    //     close(channel._ctrlfd);
    // }

    // for (const auto &channel : channels)
    // {
    //     pid_t rid = waitpid(channel._workerid, nullptr, 0);
    //     if (rid == channel._workerid)
    //     {
    //         cout << "wait child " << rid << " success" << endl;
    //     }
    // }
}
int main()
{
    vector<channel> channels;
    // 创建信道创建进程
    CreateChannels(&channels);

    // 开始完成任务
    // SendCommand(channels,g_always_loop);
    SendCommand(channels, !g_always_loop, 10);

    // 回收资源,释放管道,关闭写端,等待回收子进程
    ReleaseChannel(channels);

    // Print(channels);
    //  sleep(10);

    return 0;
}
// Task.hpp
#include <iostream>
#include <vector>
#include <functional>

using namespace std;

typedef function<void()> task_t;

void Download()
{
    cout << "我是一个下载任务" << " 处理者:" << getpid() << endl;
}

void PrintLog()
{
    cout << "我是一个打印日志的任务" << " 处理者:" << getpid() << endl;
}

void PushVideoStream()
{
    cout << "这是一个推送视频流的任务" << " 处理者:" << getpid() << endl;
}

class Init
{
public:
    const static int g_download_code = 0;
    const static int g_printlog_code = 1;
    const static int g_pushvideostream_code = 2;

    vector<task_t> tasks;

public:
    Init()
    {
        tasks.push_back(Download);
        tasks.push_back(PrintLog);
        tasks.push_back(PushVideoStream);

        srand(time(nullptr) ^ getpid());
    }

    bool CheckSafe(int code)
    {
        return code >= 0 && code < tasks.size();
    }

    void RunTask(int command)
    {
        tasks[command]();
    }

    int SelectTask()
    {
        return rand() % tasks.size();
    }

    string ToDesc(int command)
    {
        switch (command)
        {
            case g_download_code:
            {
                return "Download";
                break;
            }
            case g_printlog_code:
            {
                return "PrintLog";
                break;
            }
            case g_pushvideostream_code:
            {
                return "PushVideoStream";
                break;
            }
            default:
            {
                return "Unkown";
                break;
            }
        }
    }
};

Init init;

2.4 命名管道

匿名管道只能让具有血缘关系的进程进行进程间通信,如果我们想让两个毫不相关的进程进行进程间通信,那就只能使用命名管道了。

2.4.1 指令级

2.4.1.1 创建命名管道

Linux操作系统中有一个指令叫做mkfifo,mkfifo+管道名就可以创建对应的管道了。
在这里插入图片描述
在这里插入图片描述


2.4.1.2 使用命名管道

当我们使用echo指令向显示屏中写入一段数据后,我们发现这段数据确实显示到了显示屏上,当我们向命名管道中写入一段数据时,我们发现进程卡住了,使用另一台机器发现fifo文件的大小为0,再使用cat指令从命名管道中读取数据,发现将刚刚写入的命名管道中的数据读取出来了。命名管道文件大小为0的原因是管道属于内存级别的文件,并不会将数据写入到磁盘中。
在这里插入图片描述
当我们使用echo指令向管道文件中写入数据时,它就变为了一个进程,当使用cat指令向命名管道文件中读取数据的时候,它也变为了一个进程,这样我们就让两个进程看到了同一份资源,并且这两个进程毫无关系。

我们如何保证这两个进程会看到同一份资源的呢?
是通过路径来保证的,由于路径具有唯一性,所以路径+文件名就可以唯一的让不同进程看到同一份资源。


2.4.2 代码级

2.4.2.1 创建命名管道
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode);

功能:mkfifo 函数可以在Linux操作系统中用于创建命名管道。

参数

  • filename :是一个指向以 null 结尾的字符串的指针,表示要创建的命名管道的文件名。
  • mode :是一个位掩码,指定了新创建的命名管道文件的权限。这些权限位与 chmod 和 stat 系统调用中使用的权限位相同。

返回值

  • 成功时,它返回 0。
  • 如果失败,则返回 -1 并设置全局变量 errno 以指示错误类型。

2.4.2.2 使用命名管道

使用管道需要两个进程,这里我们就写两个源文件,一个server.cpp代表服务端,一个client.cpp代表客户端,具体实现大家可以看一下下面的代码。

这里为了强调进程间通信的本质就是让不同进程看到同一份资源,我这里创建一个头文件comm.h,头文件中记录着命名管道的文件名。

// comm.h
#pragma once

#define FILENAME "fifo"
// Makefile同时编译形成多个可执行程序

.PHONY:all
all:server client

server:server.cpp
	g++ $^ -o $@ -std=c++11
client:client.cpp
	g++ $^ -o $@ -std=c++11

.PHONY:clean
clean:
	rm -f server client fifo
// server.cpp
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include "comm.h"

using namespace std;

// 创建命名管道
bool MakeFifo()
{
    int n = mkfifo(FILENAME, 0666);
    if (n < 0)
    {
        cerr << "errno" << errno << "strerror" << strerror(errno) << endl;
        return false;
    }
    cout << "create fifo success..." << endl;
    return true;
}

int main()
{
Start:
    int rfd = open(FILENAME, O_RDONLY);
    // 有命名管道直接打开,没有则创建,创建完后还需要再一次打开管道
    if (rfd < 0)
    {
        cerr << "errno:" << errno << "  strerror:" << strerror(errno) << endl;
        if (MakeFifo())
            goto Start;
        else
            return 1;
    }
    cout << "open fifo success... read" << endl;

    while (1)
    {
        cout << "Client Say# ";
        char buffer[1024];
        // 将管道中的数据读入到缓冲区中
        ssize_t num = read(rfd, buffer, sizeof(buffer) - 1);
        // 我们默认向管道中写入的是字符串,所以需要在结尾处加上0
        if (num > 0)
        {
            buffer[num] = 0;
        }
        // num == 0 代表写端已经关闭,读端也没必要存在了,关闭读端
        else if (num == 0)
        {
            cout << endl;
            cout << "client close , server close too..." << endl;
            break;
        }
        cout << buffer << endl;
    }

    close(rfd);
    cout << "close fifo success...read" << endl;

    return 0;
}
// client.cpp
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include "comm.h"

using namespace std;

int main()
{
    // 客户端以只写的方式打开管道
    int wfd = open(FILENAME, O_WRONLY);
    if (wfd < 0)
    {
        // 打开失败则退出进程
        cerr << "errno" << errno << "strerror" << strerror(errno) << endl;
        return 1;
    }
    cout << "open fifo success... write" << endl;

    while (1)
    {
        string message;
        cout << "Please Enter# ";
        // 不以空格为结束符的方式,写入一段数据到字符串message
        getline(cin, message);

        // 将字符串中的数据写入到管道中
        ssize_t n = write(wfd, message.c_str(), message.size());
        if (n < 0)
        {
            // 写入失败则退出进程
            cerr << "errno" << errno << "strerror" << strerror(errno) << endl;
            return 2;
        }
    }

    // 关闭写端
    close(wfd);
    cout << "close fifo success...write" << endl;
    return 0;
}

我们使用make以后就会多出来两个可执行程序,当我们使用一号机器运行server(服务端)时,若当前目录下有命名管道就直接打开,否则先创建再打开,当我们再使用二号机器运行客户端时,就形成了单向通信的管道了。当我们在客户端中输入信息时,服务端就可以读取到客户端输入的信息了。

在这里插入图片描述


结尾

如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!🌹🌹
如果这篇文章对你有用的话,希望大家给一个三连支持一下!!🌹🌹

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2272512.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【QT-QTableView实现鼠标悬浮(hover)行高亮显示+并设置表格样式】

1、自定义委托类 HoverDelegate hoverdelegate.h #ifndef HOVERDELEGATE_H #define HOVERDELEGATE_H#include <QObject> #include <QStyledItemDelegate>class hoverdelegate : public QStyledItemDelegate {Q_OBJECT // 添加 Q_OBJECT 宏public:explicit hoverde…

Elasticsearch:基础概念

这里写目录标题 一、什么是Elasticsearch1、基础介绍2、什么是全文检索3、倒排索引4、索引&#xff08;1&#xff09;创建索引a 创建索引基本语法b 只定义索引名&#xff0c;setting、mapping取默认值c 创建一个名为student_index的索引&#xff0c;并设置一些自定义字段 &…

RAG Logger:RAG日志记录工具

您听说过 RAG Logger 吗&#xff1f; 它是一款专为检索增强生成 (RAG) 应用程序设计的开源日志记录工具&#xff01; 据说它可以作为 LangSmith 的轻量级替代方案&#xff0c;满足 RAG 特定的日志记录需求。 查询、搜索结果、LLM 交互和性能指标可以以 JSON 格式记录。 特点 …

Spark-Streaming有状态计算

一、上下文 《Spark-Streaming初识》中的NetworkWordCount示例只能统计每个微批下的单词的数量&#xff0c;那么如何才能统计从开始加载数据到当下的所有数量呢&#xff1f;下面我们就来通过官方例子学习下Spark-Streaming有状态计算。 二、官方例子 所属包&#xff1a;org.…

gesp(C++四级)(4)洛谷:B3851:[GESP202306 四级] 图像压缩

gesp(C四级)&#xff08;4&#xff09;洛谷&#xff1a;B3851&#xff1a;[GESP202306 四级] 图像压缩 题目描述 图像是由很多的像素点组成的。如果用 0 0 0 表示黑&#xff0c; 255 255 255 表示白&#xff0c; 0 0 0 和 255 255 255 之间的值代表不同程度的灰色&#xff0…

链地址法(哈希桶)

链地址法&#xff08;哈希桶&#xff09; 解决冲突的思路 开放定址法中所有的元素都放到哈希表⾥&#xff0c;链地址法中所有的数据不再直接存储在哈希表中&#xff0c;哈希表 中存储⼀个指针&#xff0c;没有数据映射这个位置时&#xff0c;这个指针为空&#xff0c;有多个数…

【通识安全】煤气中毒急救的处置

1.煤气中毒的主要症状与体征一氧化碳中毒&#xff0c;其中毒症状一般分为轻、中、重三种。 (1)轻度&#xff1a;仅有头晕、头痛、眼花、心慌、胸闷、恶心等症状。如迅速打开门窗&#xff0c;或将病人移出中毒环境&#xff0c;使之吸入新鲜空气和休息&#xff0c;给些热饮料&am…

Synthesia技术浅析(二):虚拟人物视频生成

Synthesia 的虚拟人物视频生成模块是其核心技术之一&#xff0c;能够将文本输入转换为带有同步语音和口型的虚拟人物视频。该模块如下所示&#xff1a; 1.文本输入处理 2.语音生成&#xff08;TTS, Text-to-Speech&#xff09; 3.口型同步&#xff08;Lip Syncing&#xff0…

【算法】算法初步

要学好数据结构和算法的设计与分析&#xff0c;请务必先打好C语言基础&#xff0c;因为C语言中的数据存储、内存映射、指针等等概念最接近计算机的底层原理&#xff0c;数据结构是数据在内存空间当中的组织形式&#xff0c;而算法则是提供了解决某个问题的一种思路&#xff0c;…

年会抽奖Html

在这里插入图片描述 <!-- <video id"backgroundMusic" src"file:///D:/background.mp3" loop autoplay></video> --> <divstyle"width: 290px; height: 580px; margin-left: 20px; margin-top: 20px; background: url(D:/nianhu…

LLM 实现Malleable 软件

All computer users may soon have the ability to author small bits of code. What structural changes does this imply for the production and distribution of software? 如果每个终端用户都能修改一部分代码&#xff0c; 这个将会对软件的生产和分发有何重大改变&#…

国产编辑器EverEdit - 两种删除空白行的方法

1 使用技巧&#xff1a;删除空白行 1.1 应用场景 用户在编辑文档时&#xff0c;可能会遇到很多空白行需要删除的情况&#xff0c;比如从网页上拷贝文字&#xff0c;可能就会存在大量的空白行要删除。 1.2 使用方法 1.2.1 方法1&#xff1a; 使用编辑主菜单 选择主菜单编辑 …

出租号平台网站系统源码/单合租用模式 提供用户提现功能

这是一款租号平台源码&#xff0c;采用常见的租号模式对接的易支付。目前网络上还很少见到此类类型的源码。 程序采用thinkphp6.0开发&#xff0c;前端采用layui 程序开发&#xff1a;PHPMySQL 程序演示&#xff1a;zh1.yetukeji.top, 账户 13112215717 &#xff0c;密码qq2…

C++:位与运算符

& 一&#xff0c;位与运算符的运算规则 有0则0。 二&#xff0c;判断奇偶性 %&#xff1a;优先级高&#xff0c;效率低 &&#xff1a;优先级低&#xff0c;效率高 数与1的位与运算结果为1则为奇数&#xff0c;结果为0则为偶数 三&#xff0c;获取一个数二进制的后…

第 31 章 - 源码篇 - Elasticsearch 写入流程深入分析

写入源码分析 接收与处理 请求首先会被 Netty4HttpServerTransport 接收&#xff0c;接着交由 RestController 进行路由分发。 private void tryAllHandlers(final RestRequest request, final RestChannel channel, final ThreadContext threadContext) throws Exception {…

C语言----指针

目录 1.概念 2.格式 3.指针操作符 4.初始化 1. 将普通变量的地址赋值给指针变量 a. 将数组的首地址赋值给指针变量 b. 将指针变量里面保存的地址赋值给另一个指针变量 5.指针运算 5.1算术运算 5.2 关系运算 指针的大小 总结&#xff1a; 段错误 指针修饰 1. con…

Java高频面试之SE-09

hello啊&#xff0c;各位观众姥爷们&#xff01;&#xff01;&#xff01;本牛马baby今天又来了&#xff01;哈哈哈哈哈嗝&#x1f436; final关键字有什么作用&#xff1f; 在 Java 中&#xff0c;final 关键字有多个用途&#xff0c;它可以用于类、方法和变量。根据使用的上…

ChatGPT 主流模型GPT-4/GPT-4o mini的参数规模是多大?

微软论文又把 OpenAI 的机密泄露了&#xff1f;&#xff1f;在论文中明晃晃写着&#xff1a; o1-preview 约 300B&#xff1b;o1-mini 约 100BGPT-4o 约 200B&#xff1b;GPT-4o-mini 约 8BClaude 3.5 Sonnet 2024-10-22 版本约 175B微软自己的 Phi-3-7B&#xff0c;这个不用约…

某纪检工作委员会视频监控网络综合运维项目

随着某纪检工作委员会信息化建设的不断深入&#xff0c;网络基础设施的数量持续增加&#xff0c;对网络设备的运维管理提出了更为复杂和艰巨的要求。为了确保这些关键信息基础设施能够安全稳定地运行&#xff0c;该纪检工作委员会决定引入智能化运维管理系统&#xff0c;以科技…

显示器太薄怎么用屏幕挂灯?使用前先了解屏幕挂灯的最佳角度

人们对用眼健康的重视以及数字化办公和娱乐的普及&#xff0c;屏幕挂灯作为一种能够有效减少屏幕反光、保护眼睛的照明设备&#xff0c;受到了越来越多消费者的青睐。随着科技的进步&#xff0c;显示器设计日益轻薄&#xff0c;为我们的桌面节省了空间并带来了美观的视觉效果。…