您的当前位置:首页正文

【Linux系统编程】第三十三弹---深入探索进程间通信:原理、方式、及管道技术详解

2024-11-07 来源:个人技术集锦

个人主页: 熬夜学编程的小林

?系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】

1、进程为什么要通信

2、进程如何通信

3、进程间常见的通信方式

4、管道

4.1、什么是管道

4.2、匿名管道 

4.2.1、定义

4.2.2、特点

4.2.3、创建与使用

4.2.4、测试管道接口


1、进程为什么要通信

事实:进程具有独立性,进程 = 内核数据结构 + 数据和代码,独立即内核数据结构独立 和 数据和代码独立。

进程间通信目的

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

2、进程如何通信

进程间通信,成本可能会稍微高一些,因此进程的独立性。

进程间通信的前提:先让不同的进程看到同一份(操作系统)资源("一段内存")。

1、一定是某一个进程先需要通信,然后让OS创建一块共享资源

2、OS必须提供很多的系统调用。

  • OS创建的共享资源不同,系统调用接口也不同 ---- 进程间通信的种类也会有不同的种类。

3、进程间常见的通信方式

管道(直接复用内核代码通信)

  • 匿名管道pipe
  • 命名管道

System V IPC

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

POSIX IPC

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

4、管道

4.1、什么是管道

管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。

4.2、匿名管道 

4.2.1、定义

匿名管道本质上是一个内存级的文件,用于在进程之间(特别是父子进程之间)传输数据。与命名管道(Named Pipe,也称为FIFO)不同,匿名管道没有文件名,且在文件系统中没有对应的节点。它通常仅存在于内存中,随进程的结束而消失。

4.2.2、特点

4.2.3、创建与使用

匿名管道通过调用pipe()系统调用来创建。该函数会返回一个包含两个文件描述符的数组,其中数组的第一个元素是读端文件描述符第二个元素是写端文件描述符。然后,可以通过fork()系统调用创建子进程,并通过这两个文件描述符在父子进程间进行数据传输。

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

测试创建管道

会用到的头文件和常量

#include <iostream>
#include <cerrno>  // <==> errno.h
#include <cstring> // <==> string.h
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string>

const int size = 1024;

主函数

// 测试创建管道
int main()
{
    // close(0);// 关闭文件描述符
    // 1、创建管道
    int pipefd[2];
    int n = pipe(pipefd); // 输出型参数 rfd wfd
    // 返回0表示成功,不等于0则失败且更新错误码
    if(n != 0)
    {
        std::cerr << "errno: "<< errno << "errstring: " << strerror(errno) << std::endl;
        return 1;
    }
    // pipefd[0] 读(0 -> 嘴巴 -> r) pipefd[1] 写(1 -> 笔 -> w)
    // 打印文件描述符
    std::cout << "pipefd[0]: " << pipefd[0] << ",pipefd[1]: " << pipefd[1] << std::endl;

    return 0;
}

执行结果

 正确打印出文件描述符,表示成功创建管道。

关闭0号文件描述符,继续测试是否满足文件描述符规则!

 同样符合规则,先占用0号文件描述符,且成功创建管道。

进行通信测试

获取信息函数

std::string GetOtherMessage()
{
    static int cnt = 0;// 静态变量,全局的,在函数能使用
    std::string messageid = std::to_string(cnt); // stoi string -> int
    cnt++;
    pid_t id = getpid();
    std::string stringid = std::to_string(id);

    std::string message = "messageid: ";
    message += messageid;
    message += "stringid: ";
    message += stringid;

    return message;
}

子进程写入信息

// 子进程进行写入
void SubProcessWrite(int wfd)
{
    int pipesize = 0;
    std::string message = "father,I am your son process!";
    char c = 'A';
    while(true)
    {
        std::string info = message + GetOtherMessage(); // 这条消息,就是我们子进程发给父进程的消息
        write(wfd,info.c_str(),info.size()); // 写入管道的时候,没有写入\0, 有没有必要?没有必要

        sleep(1); // 子进程写慢一点
    }
}

父进程读取信息

// 父进程进行读取
void FatherProcessRead(int rfd)
{
    char buffer[size];
    while(true)
    {
        ssize_t n = read(rfd,buffer,sizeof(buffer) - 1); // strlen()
        // 返回值大于0表示成功读取
        if(n > 0)
        {
            buffer[n - 1] = 0;// \0
            std::cout << buffer << std::endl;
        }
        // 返回值是0,表示写端直接关闭了,我们读到了文件的结尾
        else if(n == 0)
        {
            std::cout << "client quit, father get return val: " << n << " father quit too!" << std::endl;
            break;
        }
        // 返回值小于0表示读取错误
        else if(n < 0)
        {
            std::cerr << "read error" << std::endl;
            break;
        }
    }
}

主函数

int main()
{
    // close(0); // 关闭文件描述符
    // 1、创建管道
    int pipefd[2];
    int n = pipe(pipefd); // 输出型参数 rfd wfd
    // 返回0表示成功,不等于0则失败且更新错误码
    if(n != 0)
    {
        std::cerr << "errno: "<< errno << "errstring: " << strerror(errno) << std::endl;
        return 1;
    }
    // pipefd[0] 读(0 -> 嘴巴 -> r) pipefd[1] 写(1 -> 笔 -> w)
    std::cout << "pipefd[0]: " << pipefd[0] << ",pipefd[1]: " << pipefd[1] << std::endl;
    // 2、创建子进程
    pid_t id = fork();
    if(id == 0)
    {
        std::cout << "子进程关闭不需要的fd了,准备发消息了" << std::endl;
        // 子进程  --write
        // 3、关闭不需要的文件描述符 read
        close(pipefd[0]);

        SubProcessWrite(pipefd[1]);

        close(pipefd[1]);
        exit(0);
    }
    sleep(1);
    // 父进程
    // 3、关闭不需要的文件描述符 write
    std::cout << "父进程关闭不需要的fd了,准备收消息了" << std::endl;
    close(pipefd[1]);
    FatherProcessRead(pipefd[0]);
    std::cout << "5s,father close rfd" << std::endl;
    sleep(5);
    close(pipefd[0]);

    // 回收子进程
    int status = 0;
    pid_t rid = waitpid(id,&status,0);
    // 返回值大于0回收成功
    if(rid > 0)
    {
        std::cout << "wait child process done, exit sig: " << (status & 0x7f) << std::endl;
        std::cout << "wait child process done, exit code(ign): " << ((status >> 8) & 0xFF) << std::endl;
    }
    return 0;
}

用fork来共享管道原理 

运行结果

原理

 

父进程竟然要关闭不需要的fd,为什么开始需要打开呢?可以不关闭?

必须打开,因为需要让子进程继承下去。可以不关闭,但是建议关闭,防止误写。

为什么父子进程会向同一个终端显示器打印数据呢?

由于父子进程共享了相同的输出环境(通常是终端或显示器)。

进程默认会打开0,1,2号文件描述符,怎么做到的呢?

该进程时bash的子进程,bash打开了,所有子进程也默认打开了,我们只需要做好约定即可。

close();为什么我们子进程主动close(0,1,2),不影响父进程继续使用显示器文件呢? 

因为子进程close(0,1,2)不是直接就关闭文件描述符,而是存在一个引用计数,当有进程指向该文件描述符时就++,关闭文件描述符先对引用计数--,如果引用计数等于0则关闭文件描述符。

4.2.4、测试管道接口

代码验证:

管道的四种情况:

1、如果管道内部是空的 && write fd 没有关闭,读取条件不具备,读进程会被阻塞 -- 即 wait,等待读取条件具备,即写入数据条件具备。

2、管道被写满 && read fd 不读且没有关闭, 写进程会被阻塞,即写条件不具备,需要等待读取数据。

3、管道一直在读 && 写端关闭了wfd,读端返回值会读到0,表示读到文件结尾。 

演示一

演示二

 

4、rfd直接关闭,写段wfd一直写入?写段操作会被操作系统直接使用13号信号关掉。相当于进程出现了异常。

管道的五种特征:

1、匿名管道:只用来进行具有血缘关系的进程之间进行通信,通常用于父子之间进行通信。因为子进程能看到父进程的数据。

2、管道内部,自带进程之间同步机制,多执行流执行代码的时候,具有明显的顺序性。

可能会出现管道被多个进程读取的情况,那么数据就可能出现不一致问题,因此管道内部自带同步机制。

3、管道文件的生命周期是随进程

4、管道文件在通信的时候,是面向字节流的。写的次数和读取的次数不是一一匹配。

代码

运行结果

 

5、管道的通信模式,是一种特殊的半双工模式(在任何给定时间内,一个管道只能用于读或写操作,但不能同时进行)。 

全双工模式:在任何给定时间内,一个管道能用于读和写操作,能同时进行。

补充:

Top