【Linux】进程间通信之管道

简介: 【Linux】进程间通信之管道

一、管道

1、管道的基本使用

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

例如我们通过who | wc -l命令可以看到who进程将数据传递给了wc -l进程,两个进程通过管道完成了简单的通信。

有一点需要注意的是我们使用管道时,管道两边的的进程都会运行起来,而不是先运行管道左边的进程然后运行管道右边的进程。而且在命令行中用管道链接的进程属于兄弟进程关系

2、管道的原理

Linux中一切皆文件,管道也是文件,而且管道是一个彻彻底底的内存文件,也就是说,管道不能够向磁盘输出数据,因为这个特性管道里面就可以存储两个进程通信的数据了。

当我们的一个父进程分别以读和写的方式打开一个同一管道文件,这时我们的进程中就有了两个文件描述符指向这个管道文件。

然后我们再让父进程创建子进程,这时我们父子进程的内核数据结构是几乎相同的,此时我们子进程的也有两个文件描述符指向管道文件而且这个管道文件与父进程指向的管道文件是同一个一个管道文件。

(注意:创建子进程的时候,fork子进程,只会复制进程相关的数据结构对象不会复制父进程曾经打开的文件对象!

这就是为什么fork之后,父子进程printfcout,都会向同一个显示器终端打印数据的原因!)

这个时候我们的父子进程就可以一个向管道文件写入数据一个从管道中读取数据,这样两个进程就可以完成通信了。

最后一个步骤就是关闭不需要的文件描述符了,如果父进程进行写入,就关闭父进程的读端,关闭子进程的写端。反之则同理。

关闭不需要的文件描述符的原因

因为文件的缓冲区只有一个,一个缓冲区只有一个读和写位置,管道也是。例如父进程向管道中写入数据,然后子进程也写入数据,由于缓冲区的读写位置只有一个,那么我们在读取数据时,父进程与子进程写入管道的数据根本没有办法区分,就有可能造成通信错误。管道这种进程间的通信方式只能进行单向通信。

如果我们想要父子进程都能够进行读写,我们可以创建两个管道,这样它们的读写的数据就不会相互影响了。

补充:我们介绍的这种管道被称为匿名管道,因为所有的文件都要有路径以及文件名的,而这里我们并不知道所以叫匿名管道。

3、实例代码

纸上得来终觉浅,绝知此事要躬行。下面我们尝试在代码中来使用管道来进行进程间的通信。

我们先来了解一个Linux的系统调用pipe,这个系统调用可以帮我们打开一个匿名管道文件。

  • 参数:输出型参数,外部传入一个数组(此数组至少要有两个int的空间),数组的0号下标的位置放的是读端的文件描述符fd,数组的1号下标的位置放的是写端的文件描述符fd
  • 返回值:返回值为0,代表打开管道文件成功,返回值为-1,代表打开管道文件失败了。

我们让多个进程通信大致分为以下几个步骤:

  1. 父进程打开一个匿名管道文件。
  2. 父进程创建子进程,关闭不需要的文件描述符。
  3. 进行进程间的通信。
  4. 关闭所有管道文件,进程退出。

代码示例:

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define NUM 1024
int main()
{
    // 1.打开管道
    int pipefd[2] = {0};
    int err = pipe(pipefd);
    if (err == -1)
    {
        std::cout << "错误码" << errno << ",错误信息:" << strerror(errno) << std::endl;
        exit(-1);
    }
    // 2.1创建子进程
    int id = fork();
    if (id < 0)
    {
        std::cout << "错误码" << errno << ",错误信息:" << strerror(errno) << std::endl;
        exit(-1);
    }
    else if (id == 0)
    {
        // child process
        char buffer[NUM] = {0};
        // 2.2.关闭子进程不需要的文件描述符
        close(pipefd[1]);
        // 3.子进程进行进程间的通信
        ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1);
        if (n < 0)
        {
            std::cout << "错误码" << errno << ",错误信息:" << strerror(errno) << std::endl;
        }
        buffer[n] = '\0';
        std::cout << "父进程给我的消息是:" << buffer << std::endl;
        // 4.子进程关闭管道文件,子进程退出。
        close(pipefd[0]);
        exit(0);
    }
    else
    {
        // parent process
        char buffer[NUM] = {0};
        const char *s = "Hello, I am parent process!";
        snprintf(buffer, NUM, "%s : %d", s, getpid());
        // 2.2.父进程关闭不需要的文件描述符
        close(pipefd[0]);
        // 3.父进程进行进程间的通信
        ssize_t n = write(pipefd[1], buffer, strlen(buffer));
        if (n < 0)
        {
            std::cout << "错误码" << errno << ",错误信息:" << strerror(errno) << std::endl;
        }
    }
    // 4.父进程关闭管道文件,父进程退出。
    close(pipefd[1]);
    int status = 0;
    waitpid(id, &status, 0);
    if (WIFEXITED(status))
    {
        std::cout << "子进程的退出码" << WEXITSTATUS(status) << std::endl;
    }
    else
    {
        std::cout << "子进程异常退出!" << std::endl;
    }
    return 0;
}

代码运行结果:

可以看到通过管道我们能够让两个进程完成进程之间的通信。

4、管道的特点

根据前面我们讲的管道的原理以及实验现象。我们可以总结出一些管道的特点。

  1. 管道只能进行单向通信,管道的一种特殊半双工通信(管道两侧只能一个进行写入,一个读取,一旦分工确定就不能够再进行更改了)
  2. 管道的本质是文件,文件描述符fd的生命周期是随进程的,进程退出时,文件描述符fd也会消失,所以管道的生命周期是随进程的。
  3. 用匿名管道进行通信,这种方式通常只能够让有血缘关系的进程进行通信,因为没有血缘关系的进程并不知道应该打开哪一个管道文件,有血缘关系的进程可以通过继承来打开同一个管道文件。

接下来我们来看一些特殊场景,来帮助我们更好的理解管道的特点:

  1. 当父进程写的比较慢,子进程读的比较快时。

    运行结果:

    运行结果是正常的,说明这种通信是合理的。
  2. 当父进程写的比较快,子进程读的比较慢时。

运行结果:

可以看出,父进程写了多次的内容,子进程一次就全部读取出来了,这就显现出了管道的第四个特点:读和写的次数并没有强相关


接下来我们继续通过一些特殊情况,来研究管道:

  1. 假设我们管道的写端不发数据,那读端会怎么办?

运行结果:

可以看到:如果读端读取完毕了所有的管道数据,写端对方不发,读端就只能等待

  1. 如果我们写端讲管道写满了还能进行写入吗?

    运行结果:

当你运行时,你可以以看到写入65535个后,不再进行写入,过4-5秒以后,可以看到,子进程读取了数据。

运行结果说明了,管道写满了就不能够再进行写入了,而且也说明了管道的大小是64KB

通过1、2这两种特殊情况,我们能够总结出管道的第五个特点:管道有一定的协同能力,让读端和写端能够按照一定的步骤进行通信(自带同步机制)

  1. 如果我们在读端读取数据时,突然关闭了写端,会发生什么?

运行结果:

  1. 写端一直写,读端关闭,会发生什么呢?答案是:这种行为没有意义!OS不会维护无意义,低效率,或者浪费资源的事情。OS会杀死一直在写入的进程! OS会通过信号来终止进程,13)SIGPIPE

运行结果:

管道的第六个特点:

  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

    LinuxPIPE_BUF通常代表的是4096字节。

二、有名管道

管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。

如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道,命名管道是一种特殊类型的文件。

1、创建一个命名管道

命名管道可以从命令行上创建,命令行方法是使用下面这个命令:

mkfifo 文件名

Linux中以p开头的文件类型是管道文件,管道文件不会讲内容刷新到磁盘,所以不管我们怎么操作管道文件,管道文件的大小始终都为0

可以看到的是,本来应该在左边终端里面打印的消息,却被打印到了右边!

另一种方式是在代码中创建有名管道:

  • 参数:第一个是参数是路径加文件名,第二个参数是创建的文件的权限。
  • 返回值 :如果成功创建就返回 0,如果创建失败就返回 -1

实例代码:

#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
int main()
{
    umask(0);
    mkfifo("./fifo", 0666);
    return 0;
}

运行结果:

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

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,打开用open,FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
  • 由于管道不需要刷盘,所以管道文件只有inode没有Data block

3、命名管道的原理

因为命名管道是有名称的,我们可以让两个进程分别以读和写的方式打开文件,然后就可以让两个进程进行通信了,命名管道的使用要比匿名管道的使用简单的多!

4、用命名管道实现server&client通信

实例代码:

通过下面的代码我们能够让两个进程显示相同的消息

command.hpp

#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#define NUM 1024
// 创建一个有名管道
int CreatFifo()
{
    umask(0);
    int err = mkfifo("./fifo", 0664);
    if (err < 0)
    {
        std::cerr << "mkfifo fail: " << errno << strerror(errno) << std::endl;
        return -1;
    }
    return 0;
}

serve.cpp

#include "common.hpp"
int main()
{
    // 1.创建一个有名管道
    int err = CreatFifo();
    if (err < 0)
    {
        return -1;
    }
    std::cout << "create fifo file success" << std::endl;
    // 2.打开管道的读端
    int fd = open("./fifo", O_RDONLY);
    if (fd < 0)
    {
        std::cerr << "open fail: " << errno << strerror(errno) << std::endl;
        return -1;
    }
    std::cout << "open fifo success, begin ipc" << std::endl;
    // 3.等待客户端的消息
    char buffer[NUM] = {0};
    while (true)
    {
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = '\0';
            std::cout << "server// " << buffer << std::endl;
        }
        else if (n == 0)
        {
            std::cout << "客户端退出了,我也退出了" << std::endl;
            break;
        }
        else
        {
            std::cerr << "read fail: " << errno << strerror(errno) << std::endl;
        }
    }
    // 关闭文件描述符
    close(fd);
    // 删除管道文件
    unlink("./fifo");
    return 0;
}

client.cpp

#include "common.hpp"
#include <string>
int main()
{
    // 1.打开文件
    int fd = open("./fifo", O_WRONLY);
    if (fd < 0)
    {
        std::cerr << "open fail: " << errno << strerror(errno) << std::endl;
        return -1;
    }
    // 2.开始通信
    std::string s;
    while (true)
    {
        std::cout << "client// ";
        std::getline(std::cin, s);
        if (s == "quit")
        {
            break;
        }
        ssize_t n = write(fd, s.c_str(), s.size());
        if (n < 0)
        {
            std::cerr << "write fail: " << errno << strerror(errno) << std::endl;
            continue;
        }
    }
    // 关闭文件描述符,进程退出
    close(fd);
    return 0;
}

运行结果:


相关文章
|
13天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
43 4
linux进程管理万字详解!!!
|
4天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
39 8
|
13天前
|
存储 Unix Linux
进程间通信方式-----管道通信
【10月更文挑战第29天】管道通信是一种重要的进程间通信机制,它为进程间的数据传输和同步提供了一种简单有效的方法。通过合理地使用管道通信,可以实现不同进程之间的协作,提高系统的整体性能和效率。
|
12天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
45 4
|
13天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
15天前
|
消息中间件 存储 Linux
|
21天前
|
运维 Linux
Linux查找占用的端口,并杀死进程的简单方法
通过上述步骤和命令,您能够迅速识别并根据实际情况管理Linux系统中占用特定端口的进程。为了获得更全面的服务器管理技巧和解决方案,提供了丰富的资源和专业服务,是您提升运维技能的理想选择。
22 1
|
1月前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
20 1
|
1月前
|
消息中间件 Linux API
Linux c/c++之IPC进程间通信
这篇文章详细介绍了Linux下C/C++进程间通信(IPC)的三种主要技术:共享内存、消息队列和信号量,包括它们的编程模型、API函数原型、优势与缺点,并通过示例代码展示了它们的创建、使用和管理方法。
29 0
Linux c/c++之IPC进程间通信
|
4月前
|
运维 关系型数据库 MySQL
掌握taskset:优化你的Linux进程,提升系统性能
在多核处理器成为现代计算标准的今天,运维人员和性能调优人员面临着如何有效利用这些处理能力的挑战。优化进程运行的位置不仅可以提高性能,还能更好地管理和分配系统资源。 其中,taskset命令是一个强大的工具,它允许管理员将进程绑定到特定的CPU核心,减少上下文切换的开销,从而提升整体效率。
掌握taskset:优化你的Linux进程,提升系统性能