【Linux 15】进程间通信的方式 - 管道

发布于:2024-07-22 ⋅ 阅读:(155) ⋅ 点赞:(0)

🌈 一、管道介绍

什么是管道

  • 管道是一个进程连接到另一个进程的一个数据流

举个例子

  • 现在有个统计云服务器用户登录个数的指令:who | wc -l
    • who 用于查看当前云服务器的登录用户 (一行一个用户),wc -l 用于统计当前的行数。
  • who 和 wc -l 是两个不同的进程,它们却能很好的配合得出想要的结果,靠的就是管道 |

在这里插入图片描述
在这里插入图片描述

🌈 二、匿名管道

⭐ 1. 匿名管道的概念

  • 匿名管道不需要名字,只用保证具有亲缘关系的进程能够看到即可,常用于父子进程之间的通信
  • 创建子进程时,子进程会拷贝父进程的 PCB、地址空间、页表、文件描述符表 (struct files_struct)。
    • 子进程在拷贝时用的是浅拷贝,因此父子进程的文件描述符表中的 fd_array 数组中的指针指向的都是同一份文件资源,这才导致父子进程能看到同一份文件。
  • 由于进程间通信的本质是让多个进程看到同一份资源,此时父子进程看到同一个文件及缓冲区不就属于进程间通信了吗。

在这里插入图片描述

⭐ 2. 匿名管道的创建

1. 创建匿名管道函数

#include <unistd.h>

int pipe(int pipefd[2]);
  • 功能:创建一条无名管道。
  • 参数:pipefd 是一个文件描述符数组且是个输出型参数,pipefd [0] 表示管道的读端,pipefd [1] 表示管道的写端。
  • 返回:创建匿名管道成功返回 0,失败则返回 - 1 并设置错误码。

2. 创建匿名管道实例

#include <cassert>
#include <unistd.h>
#include <iostream>

using std::cin;
using std::cout;
using std::endl;

int main()
{   
    // 创建管道
    int pipefd[2] = { 0 };
    int n = pipe(pipefd);   
    assert(0 == n);

    cout << "读端 fd: " << pipefd[0] << endl;
    cout << "写端 fd: " << pipefd[1] << endl;

    return 0;
}

在这里插入图片描述

⭐ 3. 匿名管道的本质

1. 读写端的本质

  • 读端和写端说白了就是两个不同 file 结构体,它们指向同一个文件管道文件
  • 读端的 file 结构体负责从管道文件中读消息,写端的 file 结构体负责往管道文件写消息。

2. 实现单向通信的过程

  1. 父进程向 OS 申请创建管道,设置管道的读写端。

在这里插入图片描述

  1. 父进程 fork 出子进程,由于子进程会对父进程进行拷贝,子进程也会持有管道的读写端。

在这里插入图片描述

  1. 保证管道单向通信的特性,父子进程不能同时持有管道的读写端。
    • 父进程向子进程发送数据:需要父进程关闭读端 pipefd[0],子进程关闭写端 pipefd[1]。
    • 子进程向父进程发送数据:需要父进程关闭写端 pipefd[1],子进程关闭读端 pipefd[0]。

在这里插入图片描述
在这里插入图片描述

⭐ 4. 匿名管道的使用

  • 实现一个子进程向父进程发送数据的单向通信管道。

1. 代码展示

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

using std::cin;
using std::cout;
using std::endl;

int main()
{
    /* 1.创建管道 */
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    assert(0 == n);

    /* 2.创建子进程 */
    pid_t id = fork();
    if (id < 0) // 子进程创建失败
        return 1;

    /* 3.建立子写,父读的单向通信的管道 */
    // 父进程关闭写端,子进程关闭读端
    if (0 == id)
    {
    	// 子进程关闭读端
        close(pipefd[0]); 

        // 子进程从写端向管道写入消息
        for (int count = 10; count >= 0; count--)
        {
            char message[1024];
            snprintf(message, sizeof(message) - 1,
                     "hello father, I am child, pid: %d, count: %d",
                     getpid(), count);
            write(pipefd[1], message, strlen(message));
            sleep(1);
        }
		
		// 子进程退出
        exit(0); 
    }

    /* 4.父进程从读端从管道读取消息并打印 */
    close(pipefd[1]); // 父进程关闭写端
    char buffer[1024];
    while (true)
    {
        ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1);
        if (n > 0) 		// 读取成功
        {
            buffer[n] = '\0';
            cout << "child say: " << buffer << " to me!" << endl;
        }
        else if (0 == n)// 读取结束
        {
            break;
        }
    }

    pid_t rid = waitpid(id, nullptr, 0); // 父进程等待子进程退出
    if (rid == id)                       // 等待成功
        cout << "wait success" << endl;

    return 0;
}

2. 结果展示

在这里插入图片描述

⭐ 5. 匿名管道的特点

  • 匿名管道有如下 5 种特性。

1. 匿名管道只能用于具有共同祖先的进程

  • 匿名管道只能用于具有亲缘关系的进程之间进行通信,常用于父子进程之间的通信。

2. 匿名管道提供的是流式服务

  • 流式概念:提供一个通信的信道,写端就负责写,读端就负责读。但是,具体写多少、读多少完全由上层决定。底层就只提供一个数据通信的信道。它不关心数据本身的一些细节或格式,这叫做面向字节流。
  • 流式服务:数据没有明确的分割,一次拿多少数据都行。

3. 匿名管道内部自带同步与互斥机制

  • 同步:父子进程在执行时,具有一定的顺序性。
  • 互斥:父子进程不管谁是写端,谁是读端,同时只能有一个访问管道文件。

4. 匿名管道的生命周期随进程结束而结束

  • 管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统。
  • 当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说匿名管道的生命周期随进程的结束而结束。

5. 匿名管道采用半双工的通信方式

  • 匿名管道是一个只允许单向通信的共享资源文件。
    • 即一个管道只允许进程 A 写进程 B 读,或进程 B 写进程 A 读。
  • 如果想实现双向通信,只需要建立两个匿名管道即可。
    • 即管道 1 用于进程 A 写进程 B 读,管道 2 用于进程 B 写进程 A 读。

在这里插入图片描述

⭐ 6. 匿名管道的大小

  • 管道文件的容量也是有极限的,如果管道已满,那么写端将阻塞或写失败,可以通过以下 2 种方式获取管道的大小。

1. 编写代码验证

  • 现在已经知道了写端在管道满时会阻塞等待,可以根据这个特性获取管道文件的大小。
    • 实现方式:定义一个值为 0 的计数器,写端每次往管道种写入 1 个字节,然后让计数器 +1,直到写端阻塞住时的计数器的值就是管道的大小。
#include <cassert>
#include <unistd.h>
#include <iostream>
#include <sys/wait.h>
#include <sys/types.h>

using std::cout;
using std::endl;

int main()
{
    /* 1.创建管道 */
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    assert(0 == n);

    /* 2.创建子进程 */
    pid_t id = fork();

    /* 3.只让子进程不停的向管道写入 */
    if (0 == id)
    {
    	// 子进程关闭读端
        close(pipefd[0]); 

        // 子进程不停的从写端向管道写入消息
        int count = 0;
        while (true)
        {
            char ch = 'a';
            write(pipefd[1], &ch, 1);   // 每次向管道中写入 1 个字节的内容
            cout << "write ......: " << ++count << endl;
        }

        exit(0); // 子进程退出
    }

    /* 4.父进程停止从管道种读取数据 */
    close(pipefd[1]); 					 // 父进程关闭写端
    pid_t rid = waitpid(id, nullptr, 0); // 父进程等待子进程退出
    if (rid == id)                       // 等待成功
        cout << "wait success" << endl;

    return 0;
}

在这里插入图片描述

2. 使用 ulimit -a 指令查看

在这里插入图片描述

🌈 三、命名管道

⭐ 1. 命名管道的概念

  • 匿名管道的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
  • 如果想实现多个不相关的进程之间的通信,可以使用命名管道
  • 命名管道是一种特殊类型的文件 (FIFO 文件),命名管道有自己的路径 + 文件名。
  • 进程通过命名管道唯一名字 (路径 + 文件名) 打开同一个管道文件,让多个进程看到同一份资源,即可实现进程间通信。

⭐ 2. 命名管道的创建

  • 创建命名管道的方式有如下 2 种。

1. 通过 mkfifo 指令创建命名管道

  • 可以在命令行中输入如下指令创建一个命名管道,命名管道的名字可自定义。
mkfifo filename	

在这里插入图片描述

2. 通过 mkfifo 函数创建命名管道

#include <sys/stat.h>
#include <sys/types.h>

int mkfifo(const char* pathname, mode_t mode);
  • 参数:
    • pathname:如果该参数是个路径,则在指定路径下创建命名管道;如果只是个文件名,则在当前路径下创建命名管道。
    • mode:创建命名管道时,要赋予该命名管道文件的默认权限。
  • 返回:如果创建命名管道成功则返回 0,失败则返回 -1。
#include <cassert>
#include <sys/stat.h>
#include <sys/types.h>

const char* pathname = "name_pipe";

int main()
{
    // 在当前路径创建名为 name_pipe 的命名管道并赋予其 0666 的权限
    int n = mkfifo(pathname, 0666);
    assert(0 == n);

    return 0;
}

在这里插入图片描述

⭐ 3. 命名管道的使用

  • 命名管道有如下 2 种使用方式。

1. 通过指令使用命名管道

  • 使用输出重定向的方式往命名管道中写入文件。
    • 如:使用 echo “hello pipe” > name_pipe 向之前创建的命名管道写入数据。
  • 使用输入重定向的方式从命名管道中读取文件。
    • 如:使用 cat < name_pipe 从命名管道中读取数据,然后打印到屏幕上。

在这里插入图片描述

2. 通过代码使用命名管道

  • 由于命名管道就是个管道文件,那么只要一个进程以读方式打开管道文件,另一个进程以写方式打开管道文件,即可实现进程之间的互相通信。
    • 例:实现一个两个进程打开同一份命名管道,客户端进程负责向管道写入,服务端进程负责从管道读取。
  1. server.cpp :创建命名管道,以读方式打开命名管道,作为读端从命名管道中读取数据并打印。
#include <fcntl.h>
#include <cassert>
#include <unistd.h>
#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>

using std::cout;
using std::endl;

#define FILENAME "fifo" // 命名管道的名字

int main()
{
    // 创建命名管道
    int n = mkfifo(FILENAME, 0666);
    assert(0 == n);

    // 以只方式打开命名管道文件
    int rfd = open(FILENAME, O_RDONLY); 
    assert(rfd >= 0);

    // 从管道中读取数据
    char buffer[1024];
    while (true)
    {
        ssize_t s = read(rfd, buffer, sizeof(buffer) - 1);
        if (s > 0)       // 读取成功
        {
            buffer[s] = '\0';
            cout << "client say: " << buffer << endl;
        }
        else if (0 == s) // 写端关闭
        {
            break;
        }
    }

    // 关闭读端
    close(rfd);

    return 0;
}
  1. client.cpp :以写方式打开命名管道,作为写端获取用户输入并向命名管道中写入数据。
#include <string>
#include <fcntl.h>
#include <cassert>
#include <unistd.h>
#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>

using std::cin;
using std::cout;
using std::endl;
using std::string;
using std::getline;

#define FILENAME "fifo" // 命名管道的名字

int main()
{
    // 以写方式打开命名管道文件
    int wfd = open(FILENAME, O_WRONLY);
    assert(wfd >= 0);

    // 获取用户输入并将其写入管道
    string message;
    while (true)
    {   
        cout << "请输入: ";
        getline(cin, message);
        ssize_t s = write(wfd, message.c_str(),  message.size());
        assert(s >= 0);
    }
    
    // 关闭写端
    close(wfd);

    return 0;
}
  • 结果展示:

在这里插入图片描述

⭐ 4. 命名管道的特点

1. 任何进程都可以使用命名管道

  • 命名管道不受进程间亲缘关系的限制,任何进程都可以通过管道的名称来访问管道进行通信

2. 匿名管道提供的是流式服务

  • 流式概念:提供一个通信的信道,写端就负责写,读端就负责读。但是,具体写多少、读多少完全由上层决定。底层就只提供一个数据通信的信道。它不关心数据本身的一些细节或格式,这叫做面向字节流。
  • 流式服务:数据没有明确的分割,一次拿多少数据都行。

3. 匿名管道内部自带同步与互斥机制

  • 同步:父子进程在执行时,具有一定的顺序性。
  • 互斥:父子进程不管谁是写端,谁是读端,同时只能有一个访问管道文件。

4. 命名管道不随进程的终止而消失

  • 命名管道是有全局唯一的名称 (路径 + 文件名),其是作为文件系统中的特殊文件存在的。
  • 即使创建它的进程终止,只要该管道文件未被删除,其他进程依然可以通过命名管道的名字来使用它。

5. 命名管道提供半/全双工通信

  • 命名管道可以是单向通信,也可以是双向通信,具体取决于管道的打开方式和通信协议。

🌈 四、管道的特殊情况

  1. 正常情况下,如果管道文件没数据了,读端必须等待,直到有数据 (写端写入数据) 了为止 。
  2. 正常情况下,如果管道文件被写满了,写端必须等待,直到有空间 (读端读出数据) 了为止 。
  3. 写端关闭,读端一直读取:读端会读到 read 的返回值为 0,表示读到了文件的结尾。
  4. 读端关闭,写端一直写入:OS 不会做任何浪费时空的事情,会直接向目标进程发送 SIGPIPE (13 号) 信号将写端进程终止。

网站公告

今日签到

点亮在社区的每一天
去签到