自学内容网 自学内容网

【Linux】IPC进程间通信:并发编程实战指南(一)

🌈 个人主页:Zfox_
🔥 系列专栏:Linux

一:🔥 进程间通信介绍

🦋 1.进程通信的目的

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

🦋 2.进程通信的方式

进程通信的前提是能让不同的进程看到操作系统中的同一份资源,但是为了保证进程之间的独立性(各进程不能访问其他进程的地址空间),由操作系统创建一份共享资源。

进程需要通信,就需要让操作系统创建共享资源,所以操作系统必须提供给进程不同的调用接口,用于创建不同的共享资源,实现不同种类的进程通信。

  • 进程通信有两种通信标准:System V 标准和 POSIX 标准(都是本地通信)

System V标准中有三种通信方式:消息队列共享内存信号量

\qquad 🦁 但是 System V 标准需要重新构建操作系统代码来实现进程通信,比较繁琐。在 System V 标准出现之前,还有一种通信方式是 「管道通信」「管道通信」 是直接复用现有操作系统的代码。

(现在本地通信已经被网络通信取代,所以本文只重点介绍管道通信和共享内存通信)

二:🔥 「匿名管道」通信

  • \qquad 进程可以通过 读/写 的方式打开同一个文件,操作系统会创建两个不同的文件对象 file,但是文件对象 file 中的内核级缓冲区、操作方法集合等并不会额外创建,而是一个文件的文件对象的内核级缓冲区、操作方法集合等通过指针直接指向另一个文件的内核级缓冲区、操作方法集合等。这样以读方式打开的文件和以写方式打开的文件共用一个内核级缓冲区。
    在这里插入图片描述

  • \qquad 进程通信的前提是不同进程看到同一份共享资源,所以根据上述原理,父子进程可以看到同一份共享资源:被打开文件的内核级缓冲区。父进程向被打开文件的内核级缓冲区写入,子进程从被打开文件的内核级缓冲区读取,这样就实现了进程通信!这里也将被打开文件的内核级缓冲区称为 「 管道文件」。

🐮 此外,管道通信只支持单向通信,即只允许父进程传输数据给子进程,或者子进程传输数据给父进程。当父进程要传输数据给子进程时,就可以只使用以写方式打开的文件的管道文件,关闭以读方式打开的文件,同样的,子进程只是用以读方式打开的文件的管道文件,关闭掉以写方式打开的文件。父进程向以写方式打开的文件的管道文件写入,子进程再从以读方式打开的文件的管道文件读取,从而实现管道通信。如果是要子进程向父进程传输数据,同理即可。

🦋 匿名管道通信代码

#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

在这里插入图片描述

\qquad 🦁 匿名管道通信(此处为子进程向父进程单向通信)就是使用pipe系统调用接口打开文件并创建子进程,子进程向管道文件中写入,父进程从管道文件中读取。

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

// Father process -> read
// Child process -> write

int main()
{
    // 1.创建管道
    int fds[2] = {0};

    int n = pipe(fds);  // fds:输出型参数
    if(n != 0)
    {
        std::cerr << "pipe error" << std::endl;
        return 1;
    }


    // 2. 创建子进程
    pid_t id = ::fork();
    if(id < 0)
    {
        std::cerr << "fork error" << std::endl;
        return 2;
    }
    else if(id == 0)
    {
        // 子进程
        // 3. 关闭不需要的fd

        ::close(fds[0]);       // 关闭0端 读端

        int cnt = 0;
        while(true)
        {
            std::string message = "hello bit, hello ";
            message += std::to_string(::getpid());
            message += ", ";
            message += std::to_string(cnt);

            ::write(fds[1], message.c_str(), message.size());
            cnt++;
            ::sleep(1);
            break;
        }

        ::close(fds[1]);
        ::exit(0);
    }
    else 
    {
        // 父进程
        // 3. 关闭不需要的fd
        ::close(fds[1]);        // 关闭1端写端

        char buffer[1024];
        while(true)
        {
            ssize_t n = ::read(fds[0], buffer, 1024);
            if(n > 0)
            {
                buffer[n] = 0;
                std::cout << "child->father, message: " << buffer << std::endl;
            }
            else if(n == 0)
            {
                // 如果写端关闭
                // 读端读完管道内部的数据, 在读取的时候,
                // 就会读取到返回值0, 表示对端关闭, 也表示读到文件结尾
                std::cout << "n: " << n << std::endl;
                std::cout << "child quit ??? me too" << std::endl;
                break;
            }
        }

        ::close(fds[0]);
        pid_t rid = waitpid(id, nullptr, 0);
        std::cout << "father wait child success: " << rid << std::endl;
    }

    return 0;
}

🦋 用fork来共享管道原理

在这里插入图片描述

🦋 站在文件描述符角度-深度理解管道

在这里插入图片描述

🦋 站在内核角度-管道本质

在这里插入图片描述
🎯 所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想“

🦋 管道读写规则

  • 当没有数据可读时
    • read 调用阻塞,即进程暂停执行,一直阻塞等待
    • read 调用返回-1,errno值为EAGAIN。
  • 当管道满的时候
    • write 调用阻塞,直到有进程读走数据
    • 调用返回-1,errno值为 EAGAIN
  • 如果所有管道写端对应的文件描述符被关闭,则read返回0
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号

🦋 管道特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务。
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程。
  • 一般而言,内核会对管道操作进行同步与互斥。
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。

三:🔥 进程池

\qquad 父进程创建多个子进程,并为每个子进程创建一个管道文件,父进程为写端,子进程为读端。父进程给子进程通过管道传输任务,这就是进程池。

\qquad 如果父进程没有给子进程传输任务,即管道文件中没有数据,根据进程通信情况1,读端即子进程会阻塞等待父进程传输任务。

\qquad 此外父进程还要给子进程平衡任务,不能让某个进程特别繁忙,其他进程没有任务可做。这就是负载均衡。

🎁 Gitee仓库源码实现

四:🔥 命名管道通信

  • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
  • 命名管道是一种特殊类型的文件

🎀 创建一个命名管道

  • Linux系统中,使用 mkfifo 命令创建有名管道文件,再使用两个进程打开即可
$ mkfifo filename
  • Linux 系统编程中使用 mkfifo 函数创建一个管道文件,再让两个不相关的进程打开:
int mkfifo(const char *pathname, mode_t mode);

mkfifo 函数原型:pathname 是管道文件的路径名,mode 是设置管道的权限。函数成功时返回 0,失败时返回 -1,并设置errno以指示错误原因。

  • unlink 函数用于删除文件系统中的一个文件
int unlink(const char *pathname);

unlink 函数原型:pathname 是管道文件的路径名。函数成功时返回 0,失败时返回 -1,并设置 errno 以指示错误原因。

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

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,打开用open。
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。

🎀 命名管道的打开规则

  • 如果当前打开操作是为读而打开FIFO时
    • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
    • O_NONBLOCK enable:立刻返回成功
  • 如果当前打开操作是为写而打开FIFO时
    • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
    • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

🎀 命名管道通信代码

🦁 一共有五个文件:Client.hpp、Common.hpp、Client.cc、Server.cc、Server.hpp

  • Common.hpp 文件中是全局公共函数和方法
  • Client.cc 文件是客户端,用作写端进程
  • Client.hpp 文件是客户端的封装
  • Server.cc 文件是服务端,用作读端进程
  • Server.hpp 文件是服务端的封装和初始化方法

Common.hpp

#pragma once

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

const std::string gpipeFile="./fifo";
const mode_t gmode = 0600;
const int gdefultfd = -1;
const int gsize = 1024;
const int gForRead = O_RDONLY;
const int gForWrite = O_WRONLY;

int OpenPipe(int flag)
{
    int fd = ::open(gpipeFile.c_str(), flag);
    if(fd < 0)
    {
        std::cerr << "open error" << std::endl;
        return fd;
    }
    return fd;
}

void ClosePipeHelper(int fd)
{
    if(fd >= 0)
        ::close(fd);
}

Server.hpp

#pragma once

#include <iostream>
#include "Comm.hpp"

class Server
{
public:
    Server()
        :_fd(gdefultfd)
    {}

    bool OpenPipeForRead()
    {
        _fd = OpenPipe(gForRead);
        if(_fd < 0) return false;
        return true;
    }

    // std::string *: 输出型参数
    // const std::string &: 输入型参数
    // std::string &: 输入输出型参数
    int RecvPipe(std::string *out)
    {
        char buffer[gsize];
        ssize_t n = ::read(_fd, buffer, sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = 0;
            *out = buffer;
        }
        return n;
    }

    void ClosePipe()
    {
        ClosePipeHelper(_fd);
    }

    ~Server()
    {}
private:
    int _fd;
};


class Init
{
public:
    Init()
    {
        ::umask(0);
        int n = ::mkfifo(gpipeFile.c_str(), gmode);  // 命名管道
        if(n == -1)
        {
            std::cerr << "mkfifo error" << std::endl;
            return ;
        }
        std::cout << "mkfifo success" << std::endl;
    }

    ~Init()
    {
        int n = ::unlink(gpipeFile.c_str());
        if(n == -1)
        {
            std::cerr << "unlink error" << std::endl;
            return ;
        }
        std::cout << "unlink success" << std::endl;
    }
};

Init init;

Server.cc

#include "Server.hpp"
#include <iostream>

int main()
{
    Server server;
    server.OpenPipeForRead();

    std::string message;
    while(true)
    {
        if(server.RecvPipe(&message) > 0)
        {
             std::cout << "client Say# " << message << std::endl;
        }
        else 
        {
            break;
        }
    }

    std::cout << "client quit, me too!" << std::endl;
    server.ClosePipe();
    return 0;
}```

**Client.hpp**

```cpp
#pragma once

#include <iostream>
#include "Comm.hpp"

class Client
{
public:
    Client()
        :_fd(gdefultfd)
    {}

    bool OpenPipeForWrite()
    {
        _fd = OpenPipe(gForWrite);
        if(_fd < 0) return false;
        return true;
    }

    // std::string *: 输出型参数
    // const std::string &: 输入型参数
    // std::string &: 输入输出型参数
    int SendPipe(const std::string &in)
    {
        return ::write(_fd, in.c_str(), in.size());
    }

    void ClosePipe()
    {
        ClosePipeHelper(_fd);
    }

    ~Client()
    {}
private:
    int _fd;
};

Client.cc

#include "Client.hpp"
#include <iostream>

int main()
{
    Client client;
    client.OpenPipeForWrite();

    std::string message;
    while(true)
    {
        std::cout << "Please Enter# ";
        std::getline(std::cin, message);
        client.SendPipe(message);
    }

    client.ClosePipe();
    return 0;
}

五:🔥 共勉

以上就是我对 【Linux】动静态库:构建强大软件生态的基石 的理解,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉
在这里插入图片描述


原文地址:https://blog.csdn.net/weixin_50776420/article/details/143359030

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!