【进程通信】进程通信--匿名管道

简介: 【进程通信】进程通信--匿名管道

进程间通信介绍

进程通信(IPC,Inter-Process Communication)是指不同进程之间传递数据或信号的机制。这是一种能在运行中的程序之间协调或者交互信息的技术。尤其是在操作系统中,不同的进程可能通过某种方式来共享数据。

每个进程都是相互独立的,一般要通过第三方操作系统(OS)提供通信的方式才能实现两个进程通信。

进程通信的本质其实就是让不同的进程看到同一份资源(文件、内存、内核级缓冲区等)。不同的通信方式具体的实现原理也会有不同。

进程之间为什么要通信?

进程通信的目的就是为了使得运行在计算机上的进程能够交换信息和协调动作,以实现复杂的任务和资源共享

举个例子:在一个多人游戏中,每一个玩家的动作都会被其它玩家观察到,并能对其它玩家产生影响,比如攻击。我们把攻击看成是一次数据发送,被攻击的玩家收到”数据后“就做出”扣血‘的反应。这其实也是进程之间的通信。

有哪些通信方式?

管道(Pipes)

  • 匿名管道:通常用于有血缘关系的进程之间(如父子进程),只支持单向数据流
  • 命名管道(FIFOs):允许无血缘之间的进程通信,支持文件系统中的命名访问

System V

System V IPC是类UNIX系统中进程通信的一套标准,主要用于本地通信,主要包括下面三种通信机制:

  • 消息队列(Message Queues)进程可以将消息发送到队列中,其他进程可以读取这些消息。消息队列通过关键字识别,不同进程也可以通过它进行数据交换。
  • 共享内存(Shared Memory):多个进程直接访问同一个内存区域,因为避免了数据在进程中的拷贝,是最快的IPC方式之一。
  • 信号量(Semaphores):主要用于进程之间的同步,确保多个进程可以有序安全的共享资源。信号量类似一个计数器,控制对共享资源的访问。当一个进程访问完共享资源时,会释放一个信号量,这时候才允许其它进程访问资源。

POSIX

POSIX是继System V 之后引入的一套IPC机制,尽管它们在许多方面有一些相似之处,但是在API设计、功能和使用方式上存在一些关键的区别。

  • 信号量:支持命名信号量和无名信号量
  • 消息队列:允许数据在进程间异步传输,支持更多的特性,如消息优先级。
  • 内存共享:支持通过文件映射方式实现内存共享。
  • 其它还有互斥量、条件变量、读写锁等通信方式。由于不是本章重点,略过。

本章节重点介绍管道中的匿名管道。

管道

管道是早期Unix操作系统引入IPC机制。它允许一个进程的输出成为一个另进程的输入,这种机制促进了linux命令行工具的灵活性。在linux中,我们常常使用管道符|来串联多个命令,使得数据以一种我们想要的方式流动。具体一点,命令其实也是一个程序,也就有着自己数据输出的需求,以及数据输入的需求。使用|可以使数据从左边进程的输出流入到右边进程的读入端,以此来实现不同进程之间的信息共享。其中间的原理就是我们下面探讨的重点。


管道的实现方式

简单来说,管道是通过创建一个简单的内存缓冲区来实现的。这个缓冲区由两个文件描述符访问访问:一个用于写入,一个用于读取。如何理解这个缓冲区呢?

同一进程中,不同的方式打开同一个文件,其描述符是不一样的。同样的道理,不同的进程能不能打开同一个文件呢?答案是肯定可以的。

如果两个进程打开了同一个文件,且这个文件有对应的内核级缓冲区,相当于两个进程控制了同一个缓冲区 。这样一来,该缓冲区的数据就可以被认为是进程间的共享数据了。 这个缓冲区其实就是管道实现进程通信的方式 。我们可以将这种用来实现进程通信的文件称为 管道文件

当两个进程都对同一个内存缓冲区都拥有读写的权利,那就相当于能够控制数据从管道种流动的方向,即一个进程输出数据到缓冲区,另一个进程从缓冲区度数据。这也是为什么我们可以让管道符|左边的输出数据流入到右边的读入端

半双工

此外,管道只允许单向通信(半双工),通信时,只能由一端写入另一端读。不能两端同时读或者写。这也就意味着,两个进程在通信时要有一个关闭读端,另一个关闭写端。这样才能保证通信稳定且有序的进行。

什么叫全双工呢?

简单来说,全双工就是通信时允许通信双方同时读或者同时写。

知道了管道的大致原理之后,我们来谈谈管道中的匿名管道。

匿名管道

用于有血缘关系的进程之间通信(如父子进程)。

模型如下:

为什么匿名管道常用于父子进程的通信呢?

这是因为在父进程创建出子进程之后会天然的继承父进程的所有打开文件。这样一旦创建出了子进程,父子进程就看到了同一份管道文件的缓冲区,也同时能通过一端关闭读,另一端关闭写的方式来控制数据在管道缓冲区的流动方向。

pipe函数创建管道

在linux中,pipe()是一个用于创建匿名管道的系统调用。在man手册中我们可以查看到:

int pipe(int pipefd[2])
• 1

其中pipefd是一个输出型参数,当我们传给pipe之后,会通过参数pipefd返回两个文件描述符,pipefd[0]表示读数据端,pipefd[1]表示写数据端。创建成功函数返回0,失败则返回-1.

为什么不用文件路径也能打开一个管道文件并返回其文件描述符呢?

这是因为我们说的管道文件是内存级的,并不是在磁盘上找一个文件打开,而是由操作系统在内存给找的一片空间。只不过我们可以将其视作为普通文件。

使用管道

下面给出代码样例来帮助我们理解如何使用管道:

#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#define _size 1024
using namespace std;

string GetOtherMessage()
{
    static int cnt = 1;
    string messageid = to_string(cnt);
    cnt++;
    pid_t pid = getpid();
    string messagepid = to_string(pid);

    string message = "massageid: ";
    message += messageid;
    message += "massagepid: ";
    message += messagepid;
    return message;
}

void SubProcessWrite(int wfd)
{ // 子进程写入端
    int pipesize = 0;
    string message = "father, i am your son";
    char c = 'A';
    while (true)
    {
        //cerr << "+++++++++++++++++++++++++" << endl;
        string info = message + GetOtherMessage();
        write(wfd, info.c_str(), info.size());
        sleep(1);
       // write(wfd,&c,1);
        
    }
}
void FatherProcessRead(int rfd)
{ // 父进程读入端
    char inbuff[_size];
    while (true)
    {
        cout << "-------------------------" << endl;
        ssize_t n = read(rfd, inbuff, sizeof(inbuff) - 1);
        if (n > 0)
        {
            inbuff[n] = '\0';
            cout << "getmessage: " << inbuff << endl;
        }
        else if (n == 0)
        { // 如果read的返回值为0,表示写入端关闭,我们读到了文件的末尾
            cout << "写入端关闭,父进程放弃读数据" << endl;
        }
        else
        {
            cerr << "read error" << endl;
        }
    }
}

int main()
{
    // 1.创建管道
    int pipefd[2]; // 输出型参数,用来获得rfd、wfd.
    int n = pipe(pipefd);
    if (n != 0)
    {
        cerr << "errno: " << errno << ":"
             << "errstring:" << strerror(errno) << endl;
        return 1;
    }
    cout << "pipefd[0]: " << pipefd[0] << " pipefd[1]: " << pipefd[1] << endl;
    sleep(1);

    // 创建子进程,子进程发送数据,父进程读取数据
    pid_t id = fork();
    if (id < 0)
        cout << "fork" << endl;
    if (id == 0)
    {
        // child
        cout << "子进程关闭不需要的fd了,准备发消息了" << endl;
        close(pipefd[0]); // 子进程关闭读端
        // 发送消息
        SubProcessWrite(pipefd[1]);
        close(pipefd[0]); // 发送结束,关闭写端
        exit(0);
    }
    cout << "父进程关闭不需要的fd了,准备接收消息了" << endl;
    sleep(1);
    close(pipefd[1]);
    FatherProcessRead(pipefd[0]);
    close(pipefd[0]);
    return 0;
}

对以上代码做出简要的解释:

  1. 首先我们利用pipe函数创建一个管道用于父子进程的通信,并且得到两个文件描述符,pipefd[0]是读端的文件描述符,pipefd[1]是写的文件描述符。
  2. 我们的目的是子进程向管道里写入数据,父进程向管道里读取数据并打印到屏幕上。所以在创建子进程之后,父子进程需要关闭不需要的文件描述符
  3. 为了防止写入数据过快,子进程每次写一次数据之后就会等待上一秒。

观察代码现象:

管道的5种情况

通过上面的学习我们已经学会使用系统调用来创建一个管道了,接下来谈谈管道在数据流动的过程中有情况需要我们注意。

  1. 如果当前管道里没有数据且写端没有被关闭。表示读取条件不具备,读端进程就会被阻塞,直到写进程写入数据。
  2. 如果当前管道被写满且读端没有被关闭。表示写条件不具备,写端进程就会被阻塞,直到读进程读入数据。 (管道的空间是有限的,不同系统下的空间大小可能会有所区别。)
  3. 管道一直在读,但是写端已经被关闭。读端的read会返回0,表示读到了文件的末尾。
  4. 管道一直在写,但是读端已经被关闭。操作系统会像写端进程发送一个SIGPIPE(13)信号,强行关闭写端进程。(可以通过waitpid获取退出信号)
  5. 管道也有大小,如果一次传输的数据太大且大于管道容量,linux将不保证写入的原子性。否则保证写入的原子性。

管道的5种特征

结合管道的4种情况,我们能总结出关于管道的5种特征:

  1. 匿名管道只能用于具有血缘关系的进程之间的通信,通常使用于父子进程之间
  2. 管道内部自带同步机制(读写操作的原子性)。这也是为什么我们的读端会在写端写入数据之后才会读取数据。这保证了数据在管道中流通的有序性。
  3. 管道文件的生命周期随着进程的终止而结束
  4. 管道文件在通信时,是以字节流的方式读写数据。
  5. 管道的通信模式是一种半双工模式。
相关文章
|
13天前
|
存储 Unix Linux
进程间通信方式-----管道通信
【10月更文挑战第29天】管道通信是一种重要的进程间通信机制,它为进程间的数据传输和同步提供了一种简单有效的方法。通过合理地使用管道通信,可以实现不同进程之间的协作,提高系统的整体性能和效率。
|
13天前
|
消息中间件 存储 供应链
进程间通信方式-----消息队列通信
【10月更文挑战第29天】消息队列通信是一种强大而灵活的进程间通信机制,它通过异步通信、解耦和缓冲等特性,为分布式系统和多进程应用提供了高效的通信方式。在实际应用中,需要根据具体的需求和场景,合理地选择和使用消息队列,以充分发挥其优势,同时注意其可能带来的复杂性和性能开销等问题。
|
1月前
|
存储 Python
Python中的多进程通信实践指南
Python中的多进程通信实践指南
19 0
|
2月前
|
Java Android开发 数据安全/隐私保护
Android中多进程通信有几种方式?需要注意哪些问题?
本文介绍了Android中的多进程通信(IPC),探讨了IPC的重要性及其实现方式,如Intent、Binder、AIDL等,并通过一个使用Binder机制的示例详细说明了其实现过程。
307 4
|
2月前
|
消息中间件 Unix Linux
C语言 多进程编程(二)管道
本文详细介绍了Linux下的进程间通信(IPC),重点讨论了管道通信机制。首先,文章概述了进程间通信的基本概念及重要性,并列举了几种常见的IPC方式。接着深入探讨了管道通信,包括无名管道(匿名管道)和有名管道(命名管道)。无名管道主要用于父子进程间的单向通信,有名管道则可用于任意进程间的通信。文中提供了丰富的示例代码,展示了如何使用`pipe()`和`mkfifo()`函数创建管道,并通过实例演示了如何利用管道进行进程间的消息传递。此外,还分析了管道的特点、优缺点以及如何通过`errno`判断管道是否存在,帮助读者更好地理解和应用管道通信技术。
|
2月前
|
SQL 网络协议 数据库连接
已解决:连接SqlServer出现 provider: Shared Memory Provider, error: 0 - 管道的另一端上无任何进程【C#连接SqlServer踩坑记录】
本文介绍了解决连接SqlServer时出现“provider: Shared Memory Provider, error: 0 - 管道的另一端上无任何进程”错误的步骤,包括更改服务器验证模式、修改sa用户设置、启用TCP/IP协议,以及检查数据库连接语句中的实例名是否正确。此外,还解释了实例名mssqlserver和sqlserver之间的区别,包括它们在默认设置、功能和用途上的差异。
|
3月前
|
消息中间件 Linux 开发者
Linux进程间通信秘籍:管道、消息队列、信号量,一文让你彻底解锁!
【8月更文挑战第25天】本文概述了Linux系统中常用的五种进程间通信(IPC)模式:管道、消息队列、信号量、共享内存与套接字。通过示例代码展示了每种模式的应用场景。了解这些IPC机制及其特点有助于开发者根据具体需求选择合适的通信方式,促进多进程间的高效协作。
158 3
|
3月前
|
Linux
Linux源码阅读笔记13-进程通信组件中
Linux源码阅读笔记13-进程通信组件中
|
3月前
|
消息中间件 安全 Java
Linux源码阅读笔记13-进程通信组件上
Linux源码阅读笔记13-进程通信组件上
|
3月前
|
开发者 API Windows
从怀旧到革新:看WinForms如何在保持向后兼容性的前提下,借助.NET新平台的力量实现自我进化与应用现代化,让经典桌面应用焕发第二春——我们的WinForms应用转型之路深度剖析
【8月更文挑战第31天】在Windows桌面应用开发中,Windows Forms(WinForms)依然是许多开发者的首选。尽管.NET Framework已演进至.NET 5 及更高版本,WinForms 仍作为核心组件保留,支持现有代码库的同时引入新特性。开发者可将项目迁移至.NET Core,享受性能提升和跨平台能力。迁移时需注意API变更,确保应用平稳过渡。通过自定义样式或第三方控件库,还可增强视觉效果。结合.NET新功能,WinForms 应用不仅能延续既有投资,还能焕发新生。 示例代码展示了如何在.NET Core中创建包含按钮和标签的基本窗口,实现简单的用户交互。
66 0