Linux进程间通信 - 匿名管道(1)

之间我们学习了基础IO中有关文件,动静态库等知识,后面我们将讲述进程间通信的内容,在本文中就将来展示匿名管道。

进程间通信

进程间通信的目的

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

进程间通信发展

  • 管道
  • System V进程间通信
  • POSIX进程间通信

我们知道进程是具有独立性的,因此想要进程间进行通信需要增加通信的成本。为了解决进程间通信的问题,首要的就是要让两个进程看到同一份资源。然后让一方进行读取,另一方进行写入,完成通信。

管道

什么是管道

管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。之前我们在学习Linux的基本指令的时候就使用过 | 这个指令,这被我们称为管道,例如这个指令 who | wc -l 就是将本来应该打印到显示器文件的信息传入到了wc文件中,再由wc文件进行处理输出。

匿名管道

#include <unistd.h>
功能:创建一无名管道
原型
        int pipe(int fd[2]); // int fd[2] 这是一个输出型参数用管道的时候需要有读端和写端的记录
参数
        fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码 

匿名管道原理

进程在内存中创建PCB,然后通过进程所对应的文件描述符表打开对应的文件,匿名管道就是OS提供的一个内存级文件,进程的文件描述附表以读写的方式打开这个文件。然后进行fork()创建子进程,子进程会复制父进程相关的数据结构包括文件描述符表,不会打开父进程曾经打开的文件,这样父子进行就都会指向同一个文件,目标文件就会被多个进程看到。未来如果父进程向文件写入信息,子进程就可以接收到相关的信息,另外这种管道只支持单向通信,因此需要确定数据的流向,关闭不需要的fd。

简单的小例子

创建管道

    // 让不同的进程看到同一份资源
    // 任何一种进程间通信,一定要 先 保证不同的进程看到同一份资源
    int pipefd[2] = {0};
    // 1、创建管道
    int n = pipe(pipefd);    
    if (n < 0)
    {
        std::cout << "pipe error," << errno << ":" << strerror(errno) << std::endl;
        return 1;
    }
    std::cout << "pipefd[0]:" << pipefd[0] << std::endl; // 读端
    std::cout << "pipefd[1]:" << pipefd[1] << std::endl; // 写端

 

可以看到创建管道之后文件描述符3和4确实连接到了匿名管道文件的读端与写端。

子进程写入

// 2、创建子进程
    pid_t id = fork();
    assert(id != -1); // 正常应该使用判断,这里使用断言:意料之外使用if,意料之中使用assert        
    if (id == 0)
    {
        // Scene1(pipefd); // 正常通信场景
        // Scene2(pipefd); // 让子进程不断地去写入,但是父进程每隔10秒读一次
        // Scene3(pipefd); // 让子进程写慢一点,父进程读取不受限制
        // Scene4(pipefd); // 关闭写端
        // Scene5(pipefd); // 关闭读端
    }
void Scene1(int pipefd[])
{
    // child
    // 3、关闭不需要的文件描述符,让父进程进行读取,子进程进行写入
    close(pipefd[0]);

    // 4、开始通信 -- 结合某种场景
    const std::string namestr = "hello, 我是子进程";
    int cnt = 1;
    char buffer[1024];
    while (true)
    {
        snprintf(buffer, sizeof(buffer), "%s, 计数器: %d, 我的PID: %d\n", namestr.c_str(), cnt++, getpid());
        write(pipefd[1], buffer, strlen(buffer));
        sleep(1);
    }

    close(pipefd[1]);
    exit(0);
}
void Scene2(int pipefd[])
{
    // child
    // 3、关闭不需要的文件描述符,让父进程进行读取,子进程进行写入
    close(pipefd[0]);

    // 4、开始通信 -- 结合某种场景
    const std::string namestr = "hello, 我是子进程";
    int cnt = 1;
    char buffer[1024];
    while (true)
    {
       char x = 'X';
       write(pipefd[1], &x, 1);
       std::cout << "Cnt:" << cnt++ << std::endl;
    }
    close(pipefd[1]);
    exit(0);
}

void Scene3(int pipefd[])
{
    // child
    // 3、关闭不需要的文件描述符,让父进程进行读取,子进程进行写入
    close(pipefd[0]);

    // 4、开始通信 -- 结合某种场景
    const std::string namestr = "hello, 我是子进程";
    int cnt = 1;
    char buffer[1024];
    while (true)
    {
        snprintf(buffer, sizeof(buffer), "%s, 计数器: %d, 我的PID: %d", namestr.c_str(), cnt++, getpid());
        write(pipefd[1], buffer, strlen(buffer));
        sleep(10);
    }

    close(pipefd[1]);
    exit(0);
}

void Scene4(int pipefd[])
{
    // child
    // 3、关闭不需要的文件描述符,让父进程进行读取,子进程进行写入
    close(pipefd[0]);

    // 4、开始通信 -- 结合某种场景
    int cnt = 0;
    while (true)
    {
        char x = 'X';
        write(pipefd[1], &x, 1);
        std::cout << "Cnt:" << cnt++ << std::endl;
        sleep(1);
        break;
    }

    close(pipefd[1]);
    exit(0);
}

void Scene5(int pipefd[])
{
    // child
    // 3、关闭不需要的文件描述符,让父进程进行读取,子进程进行写入
    close(pipefd[0]);

    // 4、开始通信 -- 结合某种场景
    int cnt = 0;
    while (true)
    {
        char x = 'X';
        write(pipefd[1], &x, 1);
        std::cout << "Cnt:" << cnt++ << std::endl;
        sleep(2);
    }

    close(pipefd[1]);
    exit(0);
}

父进程读取

// parent
    // 3、关闭不需要的文件描述符,让父进程进行读取,子进程进行写入
    close(pipefd[1]);

    // 4、开始通信 -- 结合某种场景
    char buffer[1024];
    int cnt = 0;
    while (true)
    {
        // sleep(10); // Scene2 父进程每隔10秒进行读取 65535 2^16 
        // sleep(1); // Scene5
        int n = read(pipefd[0], buffer, sizeof(buffer) - 1); // 最多读1023个字符,需要预留出一个空位来赋给'\0'
        if (n > 0)
        {
            buffer[n] = '\0';
            std::cout << "我是父进程: child give me message: " << buffer << std::endl;
        }
        else if (n == 0)
        {
            std::cout << "我是父进程, 读到了文件结尾"<< std::endl;
            break;
        }
        else
        {
            std::cout << "我是父进程, 读异常了" << std::endl;
            break;
        }
        // if (cnt++ > 3) break; // Scene5 读端读一次就退出
    }
    close(pipefd[0]);
    int status = 0;
    waitpid(id, &status, 0);
    std::cout << "sig:" << (status &  0x7F) << std::endl; // 获取信号编号

关闭写端场景 

关闭读端场景 

 

匿名管道的特点

1、单向通信 -- 半双工

2、管道的本质是文件,因为fd的生命周期随进程,管道的生命周期也是随进程的

3、管道通信,通常用来进行具有血缘关系的进程,进行进程间通信。通常用与父子通信 -- pipe打开管道,并不清楚管道的名字,匿名管道

4、在管道通信中写入的次数,和读取的次数,不是严格匹配的 读写次数多少没有强相关 --- 表现 --- 字节流

5、具有一定的协同能力,让reader和writer能够按照一定的步骤进行通信 --- 自带同步机制

匿名管道通信的4种场景:

1、如果我们read读取完毕了所有的管道数据,如果对方不发,我们就只能等待

2、如果我们writer将管道写满了,我们还能写吗?不能

3、如果我们关闭了写端,读取完毕管道数据,再读就会read返回0

4、写端一只写读端关闭,会发生什么? 没有意义。OS不会维护无意义,低效率,或者浪费资源的事情。OS会杀死一直在写入的进程。OS会通过信号来终止进程 13)SIGPIPE

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值