【Linux】进程间通信(一)

在这里插入图片描述

由于进程间通信的篇幅有点大,所以进程间通信这部分将分为两篇文章进行讲述,本篇文章讲述进程间通信的目的、理解、发展及分类和管道相关的知识,下一篇文章将讲述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(服务端)时,若当前目录下有命名管道就直接打开,否则先创建再打开,当我们再使用二号机器运行客户端时,就形成了单向通信的管道了。当我们在客户端中输入信息时,服务端就可以读取到客户端输入的信息了。

在这里插入图片描述


结尾

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

在这里插入图片描述

评论 28
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值