Linux进程间通信【命名管道】

news2024/11/27 3:54:30

✨个人主页: 北 海
🎉所属专栏: Linux学习之旅
🎃操作环境: CentOS 7.6 阿里云远程服务器

成就一亿技术人


文章目录

  • 🌇前言
  • 🏙️正文
    • 1、什么是命名管道
      • 1.1、创建及简单使用
      • 1.2、命名管道的工作原理
      • 1.3、命名管道与匿名管道的区别
    • 2、命名管道的特点及特殊场景
      • 2.1、特点
      • 2.2、四种特殊场景
    • 3、命名管道实操
      • 3.1、实现文件拷贝
      • 3.2、实现进程控制
      • 3.3、实现进程遥控(配合简易版 bash)
      • 3.4、实现字符实时读取
  • 🌆总结


🌇前言

命名管道通信属于 IPC 的其中一种方式,作为管道家族,命名管道的特点就是 自带同步与互斥机制、数据单向流通,与匿名管道不同的是:命名管道有自己的名字,因此可以被没有血缘关系的进程看到,意味着命名管道可以实现毫不相干的两个独立进程间通信

图解


🏙️正文

1、什么是命名管道

简单,给匿名管道起个名字就变成了命名管道

那么如何给 匿名管道 起名字呢?

  • 结合文件系统,给匿名管道这个纯纯的内存文件分配 inode,将文件名与之构建联系,关键点在于不给它分配 Data block,因为它是一个纯纯的内存文件,是不需要将数据刷盘到磁盘中的

可以将命名管道理解为 “挂名” 后的匿名管道,把匿名管道加入文件系统中,但仅仅是挂个名而已,目的就是为了让其他进程也能看到这个文件(文件系统中的文件可以被所有进程看到)

因为没有 Data block,所以命名管道这个特殊文件大小为 0

1.1、创建及简单使用

命令管道的创建依赖于函数 mkfifo,函数原型如下

函数原型

#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 掩码计算

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

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

mkfifo fifo

结果

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

图解
出自:Linux 权限理解和学习

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

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

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

图解

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

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

注意:

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

为了让服务端和客户端能享有同一个文件名,可以创建一个公共头文件 common.h,其中存储 命名管道文件名及默认权限等公有信息

公共资源 common.h

#pragma once

#include <iostream>
#include <string>

std::string fifo_name = "./fifo";   //管道名
uint32_t mode = 0666;   //权限

服务端 server.cc

#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "common.h"

using namespace std;

int main()
{
    // 服务端
    // 1、创建命名管道文件
    int ret = mkfifo(fifo_name.c_str(), mode);
    if (ret < 0)
    {
        cerr << "mkfifo fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    // 2、以读的方式打开文件
    int rfd = open(fifo_name.c_str(), O_RDONLY);
    if (rfd < 0)
    {
        cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    // 3、读取数据
    while (true)
    {
        char buff[64];
        int n = read(rfd, buff, sizeof(buff) - 1);
        buff[n] = '\0';

        if (n > 0)
        {
            cout << "Server get message# " << buff << endl;
        }
        else if (n == 0)
        {
            cout << "写端关闭,读端读取到0,终止读端" << endl;
            break;
        }
        else
        {
            cout << "读取异常" << endl;
            break;
        }
    }

    close(rfd);
    unlink(fifo_name.c_str());  //删除命名管道文件

    return 0;
}

客户端 client.cc

#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "common.h"

using namespace std;

int main()
{
    // 客户端
    // 1、打开文件
    int wfd = open(fifo_name.c_str(), O_WRONLY);
    if (wfd < 0)
    {
        cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    // 2、写入数据,进行通信
    char buff[64] = {0};
    while (true)
    {
        cout << "Client send message# ";
        fgets(buff, sizeof(buff) - 1, stdin);
        buff[strlen(buff) - 1] = '\0'; // 去除 '\n'

        if (strcasecmp("exit", buff) == 0)
            break;

        write(wfd, buff, strlen(buff));
    }

    close(wfd);

    return 0;
}

注:strcasecmp 是一个字符串比较函数,无论字符串大小写,都能进行比较

运行效果:

结果

所以 挂了名之后的命名管道是如何实现独立进程间 IPC 的呢?

1.2、命名管道的工作原理

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

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

图示

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

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

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

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

不同点:

  • 匿名管道只能用于具有血缘关系的进程间通信;而命名管道不讲究,谁都可以用
  • 匿名管道直接通过 pipe 函数创建使用;而命名管道需要先通过 mkfifo 函数创建,然后再通过 open 打开使用
  • 出现多条匿名管道时,可能会出现写端 fd 重复继承的情况;而命名管道不会出现这种情况

在其他方面,匿名管道与命名管道几乎一致

  • 两个都属于管道家族,都是最古老的进程间通信方式,都自带同步与互斥机制,提供的都是流式数据传输

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

命名管道的特点及特殊场景与匿名管道完全一致,这里简单回顾下,详细内容可跳转至 《Linux进程间通信【匿名管道】》

2.1、特点

可以简单总结为:

  • 管道是半双工通信
  • 管道生命随进程而终止
  • 命名管道任意多个进程间通信
  • 管道提供的是流式数据传输服务
  • 管道自带 同步与互斥 机制

2.2、四种特殊场景

四种场景分别为

  1. 管道为空时,读端阻塞,等待写端写入数据
  2. 管道为满时,写端阻塞,等待读端读取数据
  3. 进程通信时,关闭读端,OS 发出 13 号信号 SIGPIPE 终止写端进程
  4. 进程通信时,关闭写端,读端读取到 0 字节数据,可以借此判断终止读端

3、命名管道实操

以下是一些使用命名管道实现的简单小程序,主要目的是为了熟悉命名管道的使用

3.1、实现文件拷贝

下载应用的本质是在下载文件,将服务器看作写端,自己的电脑看作读端,那么 下载 这个动作本质上就是 IPC,不过是在网络层面实现的

我们可以利用 命名管道实现不同进程间 IPC,即进程从文件中读取并写入一批数据,另一个进程一次读取一批数据并保存至新文件中,这样就实现了文件的拷贝

目标:利用命名管道,向空文件 target.txt 中写入数据,即拷贝源文件 file.txt

公共资源 common.h

#pragma once

#include <iostream>
#include <string>

std::string fifo_name = "./fifo";   //管道名
uint32_t mode = 0666;   //权限

服务端(写端) server.cc 提供文件拷贝服务

#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "common.h"

using namespace std;

int main()
{
    // 服务端
    // 1、打开文件
    int wfd = open(fifo_name.c_str(), O_WRONLY);
    if (wfd < 0)
    {
        cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    // 2、打开源文件
    FILE *fp = fopen("file.txt", "r");
    if (fp == NULL)
    {
        cerr << "fopen fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    // 3、读取源文件数据
    char buff[1024];
    int n = fread(buff, sizeof(char), sizeof(buff), fp);

    //IPC区域
    // 4、写入源文件至命名管道
    write(wfd, buff, strlen(buff));
    cout << "服务端已向管道写入: " << n << "字节的数据" << endl;
    //IPC区域

    fclose(fp);
    fp = nullptr;
    close(wfd);
    return 0;
}

客户端(读端) client.cc 从服务端中拷贝文件(下载)

#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "common.h"

using namespace std;

int main()
{
    // 客户端
    // 1、创建命名管道文件
    int ret = mkfifo(fifo_name.c_str(), mode);
    if (ret < 0)
    {
        cerr << "mkfifo fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    // 2、以读的方式打开管道文件
    int rfd = open(fifo_name.c_str(), O_RDONLY);
    if (rfd < 0)
    {
        cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    // 3、打开目标文件
    FILE *fp = fopen("target.txt", "w");
    if (fp == NULL)
    {
        cerr << "fopen fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    //IPC区域
    // 4、读取数据
    char buff[1024];
    int n = read(rfd, buff, sizeof(buff) - 1);
    buff[n] = '\0';

    if (n > 0)
        cout << "客户端已从管道读取: " << n << "字节的数据" << endl;
    else if (n == 0)
        cout << "写端关闭,读端读取到0,终止读端" << endl;
    else
        cout << "读取异常" << endl;
    //IPC区域

    //5、写入目标文件,完成拷贝
    fwrite(buff, sizeof(char), strlen(buff), fp);
    cout << "客户端已成功从服务端下载(拷贝)了文件数据" << endl;

    fclose(fp);
    close(rfd);
    unlink(fifo_name.c_str()); // 删除命名管道文件
    return 0;
}

拷贝结果:成功拷贝

结果

此时 服务端是写端,客户端是读端,实现的是 下载服务;当 服务端是读端,客户端是写端时,实现的就是 上传服务,搞两条管道就能模拟实现简单的 数据双向传输服务

注意: 创建管道文件后,无论先启动读端,还是先启动写端,都要阻塞式的等待另一方进行交互

3.2、实现进程控制

Linux 匿名管道 IPC 中,我们实现了一个简易版的进程控制程序,原理是通过多条匿名管道实现父进程对多个子进程执行任务分配

匿名管道用于有血缘关系间 IPC命名管道也可以

所以我们可以把上一篇文章中的 匿名管道换为命名管道,一样可以实现通信

任务池 Task.hpp

#include <iostream>
#include <string>
#include <functional>
#include <unordered_map>
#include <unistd.h>

using namespace std;

void PrintLOG()
{
    cout << "PID: " << getpid() << " 正在执行打印日志的任务…" << endl;
}

void InsertSQL()
{
    cout << "PID: " << getpid() << " 正在执行数据库插入的任务…" << endl;
}

void NetRequst()
{
    cout << "PID: " << getpid() << " 正在执行网络请求的任务…" << endl;
}

class Task
{
public:
    Task()
    {
        // 装载任务
        _tt = {{"打印日志", PrintLOG}, {"数据库插入", InsertSQL}, {"网络请求", NetRequst}};
    }

    // 展示任务
    void showTask()
    {
        cout << "目前可用任务有:[";
        for (auto e : _tt)
            cout << e.first << " ";
        cout << "]" << endl;
        cout << "输入 退出 以终止程序" << endl;
    }

    // 执行任务
    void Execute(const string &task)
    {
        if (_tt.count(task) == 0)
        {
            cerr << "没有这个任务:" << task << endl;
        }
        else
        {
            _tt[task](); // 函数对象调用
        }
    }

private:
    unordered_map<string, function<void(void)>> _tt;
};

控制程序 namePipeCtrl.cc 包括进程、管道创建,任务执行与进程等待

#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Task.hpp"

using namespace std;

enum
{
    NAME_SIZE = 64
};

// 子进程基本信息类
class ProcINfo
{

public:
    ProcINfo(pid_t pid = pid_t(), int wfd = int())
        : _pid(pid), _wfd(wfd), _num(_cnt++)
    {
        char buff[NAME_SIZE] = {0};
        snprintf(buff, NAME_SIZE, "Process %d | pid:wfd [%d:%d]", _num, _pid, _wfd);
        _name = string(buff);
    }

    pid_t _pid;
    int _wfd;
    int _num;
    string _name;
    static int _cnt;
};

int ProcINfo::_cnt = 0;

// 进程控制类
class ProcCtrl
{
public:
    ProcCtrl(int num = 3, mode_t mode = 0666)
        : _num(num), _mode(mode)
    {
        // 根据 _num 创建命名管道及子进程
        CreatPipeAndProc();
    }

    ~ProcCtrl()
    {
        waitProc();
    }

    // 创建管道及进程
    void CreatPipeAndProc()
    {
        // 因为是继承的,所以也要注意写端重复继承问题
        vector<int> fds;
        for (int i = 0; i < _num; i++)
        {
            // 步骤:创建管道,存入 _vst
            char pipeNameBUff[NAME_SIZE]; // 管道名缓冲区
            snprintf(pipeNameBUff, NAME_SIZE, "./fifo-%d", i);

            int ret = mkfifo(pipeNameBUff, _mode);
            assert(ret != -1);
            (void)ret;

            _vst.push_back(string(pipeNameBUff));

            // 创建子进程,让子进程以只读的方式打开管道文件
            pid_t id = fork();
            if (id == 0)
            {
                // 子进程内
                // 先关闭不必要的写端
                for (auto e : fds)
                    close(e);

                // 打开管道文件,并进入任务等待默认(读端阻塞)
                int rfd = open(_vst[i].c_str(), O_RDONLY);
                assert(rfd != -1);
                (void)rfd;

                waitCommand(rfd);

                close(rfd); // 关闭读端
                exit(0);
            }

            // 父进程以写打开管道,保存 fd 信息
            int wfd = open(_vst[i].c_str(), O_WRONLY);
            assert(wfd != -1);
            (void)wfd;

            // 注册子进程信息
            _vpt.push_back(ProcINfo(id, wfd));
            fds.push_back(wfd);
        }
    }

    // 子进程等待任务派发
    void waitCommand(int rfd)
    {
        while (true)
        {
            char buff[NAME_SIZE] = {0};
            int n = read(rfd, buff, sizeof(buff) - 1);

            buff[n] = '\0';

            if (n > 0)
            {
                Task().Execute(string(buff));
            }
            else if (n == 0)
            {
                cerr << "读端读取到 0,写端已关闭,读端也即将关闭" << endl;
                break;
            }
            else
            {
                cerr << "子进程读取异常!" << endl;
                break;
            }
        }
    }

    // 展示可选进程
    void showProc()
    {
        cout << "目前可用进程有:[";
        int i = 0;
        for (i = 0; i < _num - 1; i++)
            cout << i << "|";
        cout << i << "]" << endl;
    }

    // 下达任务给子进程
    void ctrlProc()
    {
        while (true)
        {
            cout << "==========================" << endl;
            int n = 0;
            do
            {
                showProc();
                cout << "请选择子进程:> ";
                cin >> n;
            } while (n < 0 || n >= _num);

            Task().showTask();
            string taskName;
            cout << "请选择任务:> ";
            cin >> taskName;

            if (taskName == "退出")
                break;

            // 将信息通过命名管道写给子进程
            cout << "选择进程 ->" << _vpt[n]._name << " 执行 " << taskName << " 任务" << endl;
            write(_vpt[n]._wfd, taskName.c_str(), taskName.size());
            sleep(1);
        }
    }

    // 关闭写端、删除文件、等待子进程退出
    void waitProc()
    {
        for (int i = 0; i < _num; i++)
        {
            close(_vpt[i]._wfd);               // 关闭写端
            unlink(_vst[i].c_str());           // 关闭管道文件
            waitpid(_vpt[i]._pid, nullptr, 0); // 等待子进程
        }

        cout << "所有子进程已回收" << endl;
    }

private:
    vector<ProcINfo> _vpt; // 子进程信息表
    vector<string> _vst;   // 命名管道信息表
    int _num;              // 子进程数/命名管道数
    mode_t _mode;          // 命名管道文件的权限
};

int main()
{
    ProcCtrl p1;
    p1.ctrlProc();
    return 0;
}

执行结果如下:

结果

关于 父子进程间使用命名管道通信 值得注意的问题:

  1. 在命名管道创建后,需要先创建子进程,让子进程打开【读端或写端】,然后才让父进程打开【写端或读端】,这是因为假如先让父进程打开【写端或读端】,那么此时父进程就会进入【阻塞】状态,导致无法创建子进程,自然也就无法再打开【读端或写端】;所以正确做法是先让子进程打开,即使子进程【阻塞】了,父进程也还能运行。不要让【阻塞】阻碍子进程的创建
  2. 子进程继承都存在的问题:写端重复继承,因此需要关闭不必要的写端 fd

关于问题一的理解可以看看下面这两张图:

错误用法: 父进程先打开【写端或读端】,再创建子进程,最后才让子进程打开【读端或写端】

图解1

正确用法: 先创建子进程,让子进程打开【读端或写端】,再让父进程打开【写端或读端】

图解2

3.3、实现进程遥控(配合简易版 bash)

利用命名管道就可以远程遥控,原理很简单:简易版 bash 会等待命令输入,将输入源换成命名管道读端,再创建一个独立进程,作为命名管道的写端,此时就可以实现远程遥控进程,执行不同的指令

这里直接用之前写好的 简易版 bash,关于 简易版 bash 的具体实现可以看看这篇文章 《Linux模拟实现【简易版bash】

步骤:

  • 创建命名管道
  • bash 改装,打开命名管道文件,作为 读端
  • 创建独立进程,打开命名管道文件,作为 写端
  • 进行 IPC,发送命令给 bash 执行

公共资源 common.h

#pragma once

#include <iostream>
#include <string>

std::string fifo_name = "./fifo";   //管道名
uint32_t mode = 0666;   //权限

简易版bash mybash.cc

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <string.h>
#include <assert.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "common.h"

using namespace std;

#define COM_SIZE 1024
#define ARGV_SIZE 64
#define DEF_CHAR " "

void split(char *argv[ARGV_SIZE], char *ps)
{
    assert(argv && ps);

    // 调用 C语言 中的 strtok 函数分割字符串
    int pos = 0;
    argv[pos++] = strtok(ps, DEF_CHAR); // 有空格就分割
    while (argv[pos++] = strtok(NULL, DEF_CHAR))
        ; // 不断分割

    argv[pos] = NULL; // 确保安全
}

void showEnv()
{
    extern char **environ; // 使用当前进行的环境变量表
    int pos = 0;
    for (; environ[pos]; printf("%s\n", environ[pos++]))
        ;
}

// 枚举类型,用于判断不同的文件打开方式
enum redir
{
    REDIR_INPUT = 0,
    REDIR_OUTPUT,
    REDIR_APPEND,
    REDIR_NONE
} redir_type = REDIR_NONE; // 创建对象 redir_type,默认为 NONE

// 检查是否出现重定向符
char *checkDir(char *command)
{
    // 从右往左遍历,遇到 > >> < 就置为 '\0'
    size_t end = strlen(command); // 与返回值相匹配
    char *ps = command + end;     // 为了避免出现无符号-1,这里采取错位的方法
    while (end != 0)
    {
        if (command[end - 1] == '>')
        {
            if (command[end - 2] == '>')
            {
                command[end - 2] = '\0';
                redir_type = REDIR_APPEND;
                return ps;
            }

            command[end - 1] = '\0';
            redir_type = REDIR_OUTPUT;
            return ps;
        }
        else if (command[end - 1] == '<')
        {
            command[end - 1] = '\0';
            redir_type = REDIR_INPUT;
            return ps;
        }

        // 如果不是空格,就可以更新 ps指向
        if (*(command + end - 1) != ' ')
            ps = command + end - 1;

        end--;
    }

    return NULL; // 如果没有重定向符,就返回空
}

int main()
{
    char myEnv[COM_SIZE][ARGV_SIZE]; // 大小与前面有关
    int env_pos = 0;                 // 专门维护缓冲区
    int exit_code = 0;               // 保存退出码的全局变量

    // 2023.6.7 更新
    // 创建管道文件
    int ret = mkfifo(fifo_name.c_str(), mode);
    assert(ret != -1);
    (void)ret;

    // 打开管道文件
    int rfd = open(fifo_name.c_str(), O_RDONLY);
    assert(rfd != -1);
    (void)rfd;

    // 这是一个始终运行的程序:bash
    while (1)
    {
        char command[COM_SIZE]; // 存放指令的数组(缓冲区)

        // 打印提示符
        printf("[User@myBash default]$ ");
        fflush(stdout);

        // 读取指令
        //从管道中读取
        int n = read(rfd, command, COM_SIZE - 1);

        if(n == 0)
        {
            cout << "写端已关闭,读端也即将关闭" << endl;
            break;
        }

        command[n] = '\0';
        cout << command << endl;

        // 重定向
        // 在获取指令后进行判断
        // 如果成立,则获取目标文件名 filename
        char *filename = checkDir(command);

        // 指令分割
        // 将连续的指令分割为 argv 表
        char *argv[ARGV_SIZE];
        split(argv, command);

        // 特殊处理
        // 颜色高亮处理,识别是否为 ls 指令
        if (strcmp(argv[0], "ls") == 0)
        {
            int pos = 0;
            while (argv[pos++])
                ;                                   // 找到尾
            argv[pos - 1] = (char *)"--color=auto"; // 添加此字段
            argv[pos] = NULL;                       // 结尾
        }

        // 目录间移动处理
        if (strcmp(argv[0], "cd") == 0)
        {
            // 直接调用接口,然后 continue 不再执行后续代码
            if (strcmp(argv[1], "~") == 0)
                chdir("/home"); // 回到家目录
            else if (strcmp(argv[1], "-") == 0)
                chdir(getenv("OLDPWD"));
            else if (argv[1])
                chdir(argv[1]); // argv[1] 中就是路径
            continue;           // 终止此次循环
        }

        // 环境变量相关
        if (strcmp(argv[0], "export") == 0)
        {
            if (argv[1])
            {
                strcpy(myEnv[env_pos], argv[1]);
                putenv(myEnv[env_pos++]);
            }
            continue; // 一样需要提前结束循环
        }

        // 环境变量表
        if (strcmp(argv[0], "env") == 0)
        {
            showEnv(); // 调用函数,打印父进程的环境变量表
            continue;  // 提前结束本次循环
        }

        // echo 相关
        // 只有 echo $ 才做特殊处理(环境变量+退出码)
        if (strcmp(argv[0], "echo") == 0 && argv[1][0] == '$')
        {
            if (argv[1] && argv[1][0] == '$')
            {
                if (argv[1][1] == '?')
                    printf("%d\n", exit_code);
                else
                    printf("%s\n", getenv(argv[1] + 1));
            }

            continue;
        }

        // 子进程进行程序替换
        pid_t id = fork();
        if (id == 0)
        {
            // 判断是否需要进行重定向
            if (redir_type == REDIR_INPUT)
            {
                int fd = open(filename, O_RDONLY);
                dup2(fd, 0); // 更改输入,读取文件 filename
            }
            else if (redir_type == REDIR_OUTPUT)
            {
                int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
                dup2(fd, 1); // 写入
            }
            else if (redir_type == REDIR_APPEND)
            {
                int fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
                dup2(fd, 1); // 追加
            }

            // 直接执行程序替换,这里使用 execvp
            execvp(argv[0], argv);

            exit(168); // 替换失败后返回
        }

        // 父进程等待子进程终止
        int status = 0;
        waitpid(id, &status, 0); // 在等待队列中阻塞
        exit_code = WEXITSTATUS(status);
        if (WIFEXITED(status))
        {
            // 假如程序替换失败
            if (exit_code == 168)
                printf("%s: Error - %s\n", argv[0], "The directive is not yet defined");
        }
        else
            printf("process run fail! [code_dump]:%d [exit_signal]:%d\n", (status >> 7) & 1, status & 0x7F); // 子进程异常终止的情况
    }

    //关闭管道文件
    close(rfd);
    unlink(fifo_name.c_str());
    return 0;
}

进程控制端 namePipeCtrl.cc

#include <iostream>
#include <cassert>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "common.h"

using namespace std;

int main()
{
    // 打开管道文件 --- 只写
    int wfd = open(fifo_name.c_str(), O_WRONLY);
    assert(wfd != -1);
    (void)wfd;

    char buff[64];
    while (true)
    {
        cout << "远程发送指令:> ";
        fgets(buff, sizeof(buff) - 1, stdin);
        buff[strlen(buff) - 1] = '\0';  // 去除 '\n'

        if (strcasecmp("exit", buff) == 0)
            break;

        // 向管道写入数据
        write(wfd, buff, strlen(buff));
    }

    close(wfd);
    return 0;
}

实际效果如下:

图示

注意: 在进行指令处理时,需要注意 '\n',不能把 '\n' 带入进程替换中

3.4、实现字符实时读取

回车 '\n' 这个东西很难处理,那么有没有一种方式,能实现不输入回车也能写入数据至管道中呢?答案是有的

比如以下代码,可以实现特殊化读取,即 不需要特点条件触发缓冲区冲刷,实时写入字符

公共资源 common.h

#pragma once

#include <iostream>
#include <string>

std::string fifo_name = "./fifo";   //管道名
uint32_t mode = 0666;   //权限

服务端 server.cc 实时读取字符

#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "common.h"

using namespace std;

int main()
{
    // 服务端
    // 1、创建命名管道文件
    int ret = mkfifo(fifo_name.c_str(), mode);
    if (ret < 0)
    {
        cerr << "mkfifo fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    // 2、以读的方式打开文件
    int rfd = open(fifo_name.c_str(), O_RDONLY);
    if (rfd < 0)
    {
        cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    // 3、读取数据
    while (true)
    {
        char buff[64];
        int n = read(rfd, buff, sizeof(buff) - 1);
        buff[n] = '\0';

        if (n > 0)
        {
            buff[n] = 0;
            printf("%c", buff[0]);
            fflush(stdout);
        }
        else if (n == 0)
        {
            cout << "写端关闭,读端读取到0,终止读端" << endl;
            break;
        }
        else
        {
            cout << "读取异常" << endl;
            break;
        }
    }

    close(rfd);
    unlink(fifo_name.c_str()); // 删除命名管道文件

    return 0;
}

客户端 client.cc 实时发送字符

#include <iostream>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "common.h"

using namespace std;

int main()
{
    // 客户端
    // 1、打开文件
    int wfd = open(fifo_name.c_str(), O_WRONLY);
    if (wfd < 0)
    {
        cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    // 2、写入数据,进行通信
    char buff[64] = {0};
    while (true)
    {
        system("stty raw");
        int c = getchar();
        system("stty -raw");

        ssize_t n = write(wfd, (char *)&c, sizeof(char));
        assert(n >= 0);
        (void)n;
    }

    close(wfd);

    return 0;
}

实时读取字符的效果如下:

图示

本文中涉及的所有代码均在此仓库中:《命名管道博客仓库》


🌆总结

以上就是本次关于 Linux 进程间通信之命名管道的全部内容了,作为匿名管道的兄弟,命名管道具备匿名管道的大部分特性,使用方法也基本一致,不过二者在创建和打开方式上各有不同:匿名管道简单,但只能用于具有血缘关系进程间通信,命名管道虽麻烦些,但适用于所有进程间通信场景;在本文的最后,使用命名管道实现了几个简单的小程序,这些小程序的本质都是一样的:创建命名管道 -> 打开命名管道 -> 通信 -> 关闭命名管道,掌握其中一个即可融会贯通


星辰大海

相关文章推荐

Linux基础IO【软硬链接与动静态库】

Linux基础IO【深入理解文件系统】

Linux【模拟实现C语言文件流】

Linux基础IO【重定向及缓冲区理解】

Linux基础IO【文件理解与操作】

===============

Linux【模拟实现简易版bash】

Linux进程控制【进程程序替换】

Linux进程控制【创建、终止、等待】

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

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

相关文章

Shell脚本攻略:文本三剑客之awk

目录 一、理论 1.awk原理 2.awk打印 3.awk条件判断 4.awk数组与循环 5.awk函数 6.常用命令 二、实验 1.统计磁盘可用容量 2.统计/etc下文件总大小 3.CPU使用率 4.统计内存 5.监控硬盘 一、理论 1.awk原理 &#xff08;1&#xff09;概念 awk由 Aho&#xff0c;W…

PriorityBlockingQueue的介绍及方法内部实现

SynchronousQueue的介绍 SynchronousQueue是一个优先级队列&#xff0c;不满足先进先出FIFO的概念。 会将插入的数据进行排序&#xff0c;输出排序之后的结果&#xff08;小根堆&#xff0c;由小变大升序&#xff09; 内部实现原理介绍 SynchronousQueue是基于二叉堆结构实现…

Linux——多线程

Linux多线程 多线程进程内进行资源划分什么是线程进一步理解线程线程的优缺点Linux进程VS线程线程的异常 创建线程两个的接口线程的控制线程的创建线程的终止线程的等待线程取消C的线程库线程的分离如何理解每个线程都有自己独立的栈结构 封装线程接口 多线程 进程内进行资源划…

Java代码块和属性的赋值顺序

代码块 类的成员之四&#xff1a;代码块(初始化块)&#xff08;重要性较属性、方法、构造器差一些&#xff09; 1.代码块的作用&#xff1a;用来初始化类、对象的信息 2.分类&#xff1a;代码块要是使用修饰符&#xff0c;只能使用static 分类&#xff1a;静态代码块 vs 非静态…

nacos升级到2.0.3(单机模式)

前提&#xff1a;https://github.com/alibaba/spring-cloud-alibaba/wiki/版本说明 Spring Cloud AlibabaSpring CloudSpring BootNacos2.2.7.RELEASESpring Cloud Hoxton.SR122.3.12.RELEASE2.0.3 一、pom.xml文件 <parent><groupId>org.springframework.boot&…

网工内推 | 高级网工专场,上市公司,3年经验以上,HCIE证书优先

01 名创优品&#xff08;广州&#xff09;有限责任公司 &#x1f537;招聘岗位&#xff1a;高级网络工程师 &#x1f537;职责描述&#xff1a; 1、负责集团总部有线&#xff06;无线、公有云、仓库的网络规划建设与运维&#xff1b; 2、负责公有云的网络台日常上线部署、规划…

3.3 分析特征内部数据分布与分散状况

3.3 分析特征内部数据分布与分散状况 3.3.1 绘制直方图 bar()3.3.2 绘制饼图 pie()3.3.3 绘制箱线图 boxplot()3.3.4 任务实现1、绘制国民生产总值构成分布直方图2、绘制国民生产总值构成分布饼图3、绘制国民生产总值分散情况箱线图 小结 3.3.1 绘制直方图 bar() 直方图&#x…

Vue源码解析

【尚硅谷】Vue源码解析之虚拟DOM和diff算法 【Vue源码】图解 diff算法 与 虚拟DOM-snabbdom-最小量更新原理解析-手写源码-updateChildren] 文章目录 2. snabbdom 简介 及 准备工作2.1 简介2.2 搭建初始环境1. 安装snabbdom2. 安装webpack5并配置3. 复制官方demo Example 3. …

IJCAI 2023 | 如何从离散时间事件序列中学习因果结构?

本文分享一篇我们在IJCAI 2023的最新工作&#xff0c;文章分析了在离散时间事件序列上存在的瞬时效应问题&#xff0c;提出了一种利用瞬时效应的结构霍克斯模型&#xff0c;且在理论上证明了事件序列上的瞬时因果关系同样是可识别的。 相关论文&#xff1a; Jie Qiao et al. “…

基于SpringBoot的家庭记账管理系统的设计与实现

摘 要 随着社会的发展&#xff0c;社会的方方面面都在利用信息化时代的优势。互联网的优势和普及使得各种系统的开发成为必需。 本文以实际运用为开发背景&#xff0c;运用软件工程原理和开发方法&#xff0c;它主要是采用java语言技术和mysql数据库来完成对系统的设计。整个…

数据结构之线性表

1.线性表的定义 线性表是由n(n≥0)个类型相同的数据元素组成的有限 序列&#xff0c;记作:L &#x1d44e;0, &#x1d44e;1, ⋯ , &#x1d44e;&#x1d456; , ⋯ , &#x1d44e;&#x1d45b;−1 2 线性表的顺序存储结构实现 线性表的顺序存储结构称为顺序表&#xff08;…

2023年前端面试汇总-HTML

1. src和href的区别 src和href都是用来引用外部的资源&#xff0c;它们的区别如下&#xff1a; src表示对资源的引用&#xff0c;它指向的内容会嵌入到当前标签所在的位置。src会将其指向的资源下载并应用到文档内&#xff0c;如请求js脚本。当浏览器解析到该元素时&#xff…

HyperLogLog数据结构

基数计数(cardinality counting) 通常用来统计一个集合中不重复的元素个数&#xff0c;例如统计某个网站的UV&#xff0c;或者用户搜索网站的关键词数量。数据分析、网络监控及数据库优化等领域都会涉及到基数计数的需求。 要实现基数计数&#xff0c;最简单的做法是记录集合中…

34岁出来面试,还被拒绝有多惨?

我强烈建议大家定期去参加一下外面的面试&#xff0c;尤其是BAT大厂的面试&#xff0c;不要一直闷在公司里&#xff0c;不然你很容易被这个世界遗弃。 前言 昨天&#xff0c;我们小组长奉命去面了一个34岁的测试员。 去了大概半个多小时吧&#xff0c;回来后&#xff0c;他的…

图数据库(二):Java操作图数据库

在上篇文章中&#xff0c;我们介绍了什么是Neo4j&#xff0c;什么是Cypher以及Neo4j的使用&#xff0c;今天我们学习一下如何使用Java操作Neo4j图数据库。 Cypher查询 在使用Java操作Neo4j之前&#xff0c;我们先了解一点&#xff0c;Cypher语句简单查询。 本文使用的是Neo4j…

Flutter的状态管理之Provider

Provider简介 Flutter Provider是Flutter中一个非常流行的状态管理库&#xff0c;它可以帮助开发者更加方便地管理应用程序中的状态。Provider提供了一种简单的方式来共享和管理应用程序中的数据&#xff0c;并且可以根据数据的变化来自动更新UI界面。 Provider的核心思想是将…

网络协议——什么是RIP协议?工作原理是什么?

作者&#xff1a;Insist-- 个人主页&#xff1a;insist--个人主页 作者会持续更新网络知识和python基础知识&#xff0c;期待你的关注 目录 一、什么是RIP协议&#xff1f; 二、为什么要使用RIP&#xff1f; 三、RIP用在哪里&#xff1f; 四、RIP协议的工作原理 五、总结 …

Redis安装布隆过滤器

目录 1 什么是布隆过滤器1.1 布隆过滤器的原理1.2 布隆过滤器缺点 2 插件形式安装2.1 下载布隆过滤器插件 3 docker方式单机安装4 Redis集群部署安装4.1 创建目录4.2 redis配置文件4.3 配置docker-compose.yml文件4.4 启动布隆过滤器集群4.5 配置集群4.6 布隆过滤器常用命令4.7…

如何将simulink中的元件(光伏板)导入到plecs中使用

simulink中有一些元件在plecs中是没有的&#xff0c;如果想要直接使用simulink的库&#xff0c;可以这样操作&#xff1a; 1 新建mdl文件&#xff08;simulink的文件类型&#xff09;&#xff0c;并在该文件中搭建好想要的模型、元件&#xff08;只放想要导出的元件就可以了&…

商城检索 DSL

模糊匹配过滤&#xff08;按照属性、分类、品牌、价格区间、库存&#xff09;排序分页高亮聚合分析 一. 搜索关键字 检索字段&#xff1a;商品sku标题 “skuTitle” : “华为 HUAWEI Mate 30 Pro 亮黑色 8GB256GB麒麟990旗舰芯片OLED环幕屏双4000万徕卡电影四摄4G全网通手机”…