[Linux]管道

管道

进程间通信

通信目的

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

通信的本质

  1. 操作系统需要直接或者间接给通信双方的进程提供“内存空间”。
  2. 要通信的进程必须看到一份共同的资源。

可以说,不同的通信种类本质就是操作系统提供资源的模块不同。

管道

管道是基于文件系统的,分为匿名管道和命名管道。它是一种单向的、先进先出的数据通道,数据只能沿着一个方向流动,从管道的写入端流入,从读取端流出。

匿名管道

匿名管道没有具体的文件名,只能用于具有亲缘关系的进程之间通信(通常是父子进程)。

匿名管道的创建

使用pipe()系统调用来创建匿名管道。

  • 函数原型:int pipe(int pipefd[2]);
  • 参数:pipefd[2]是一个输出型参数,用于存储创建的管道的两个文件描述符。pipefd[0]用于从管道读取数据,pipefd[1]用于向管道写入数据。
  • 返回值:成功返回0,失败返回-1。

匿名管道的使用

前面说过,进程间通信就是让两个进程看到同一份资源。匿名管道是通过创建子进程的方式来实现的。当fork()创建子进程后,子进程会以父进程为模板来生成,此时子进程会和父进程有一张相同的文件描述符表。此时就可以实现让两个进程看到同一份文件。管道文件是一种内存级文件,它里面的内容不需要刷新到磁盘。

而又由于管道只能单向数据通信,所以父子进程都要关闭不用的描述符。例如父进程写数据,子进程读数据,此时父进程就要关闭读端,子进程就要关闭写端(推荐关闭,不关闭的话也没关系,只要不使用就行)。

读写的不同情况

我们通过下面这段代码来讨论读写的几种不同情况

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

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

    pid_t id = fork();
    assert(id >= 0);
    if (id == 0)
    {
        //子进程进行写入
        close(fds[0]);//关闭子进程的读端

        const char *str = "我是子进程,此时正在给你发消息";
        int cnt = 0;
        while (true)
        {
            cnt++;
            char buffer[1024];
            snprintf(buffer, sizeof buffer, "chaild to parent : %s[%d][%d]", str, cnt, getpid());
            //将数据写入管道
            write(fds[1], buffer, strlen(buffer));
            //每隔一秒写一次
            sleep(1);
        }

        close(fds[1]);
        std::cout << "子进程关闭写端" << std::endl;
        exit(0);
    }
    //父进程进行读取
    close(fds[1]);//关闭父进程的写端
    while (true)
    {
        char buffer[1024];
        ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;//添加'\0'
        }
        std::cout << "get message: " << buffer << " | my pid: " << getpid() << std::endl;
    }
    n = waitpid(id, nullptr, 0);
    assert(n == id);
    close(fds[0]);
	return 0;
}
  1. 写慢,读快

    从上面的运行结果你会发现,当子进程每隔1秒才写入一条消息时,父进程会跟随着子进程每隔一秒读一条消息,也就是说父进程的读速度和子进程的一样。

    当子进程的休眠时间改为5秒的时候,同样父进程也会以5秒为时间间隔进行读取。也就是说,当子进程写入完成后进行5秒的休眠,父进程读取完子进程写的历史消息后,进入阻塞等待的状态,直到子进程再次向管道中进行写入操作。

    综上,当管道中的数据都被读取完时,父进程会在read()操作这里阻塞。

  2. 写快,读慢

    这次,让父进程每隔5秒读一次,子进程不进行休眠。

       //父进程每隔五秒读一次
    while (true)
    {
    	char buffer[1024];
    	ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);
    	if (s > 0)
    	{
    		buffer[s] = 0;//添加'\0'
    	}
    	std::cout << "get message: " << buffer << " | my pid: " << getpid() << std::endl;
    	sleep(5);
    }
    
    //子进程不休眠
    while (true)
    {
    	cnt++;
    	char buffer[1024];
    	snprintf(buffer, sizeof buffer, "chaild to parent : %s[%d][%d]", str, cnt, getpid());
    	//将数据写入管道
    	write(fds[1], buffer, strlen(buffer));
    }
    

    此时你会发现,父进程一下会读取一大批的数据(这批数据并不是按行显示的,因为它读取的时候是按照字节流的方式进行读取)。若是再将父进程读取的时间间隔继续增加,你会发现管道可能会被写满,写满后,子进程就会被阻塞,直到父进程从管道中读取数据。

    也就是说,管道是一个有着固定大小的缓冲区,当缓冲区满时,写操作就要被阻塞。

  3. 写关闭,读到0

    这次让子进程写一条数据后就退出,父进程每隔一秒读一次。

    //父进程每隔一秒读一次
    while (true)
    {
    	char buffer[1024];
    	ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);
    	if (s > 0)
    	{
    		buffer[s] = 0;//添加'\0'
    		std::cout << "get message: " << buffer << " | my pid: " << getpid() << std::endl;
    	}
    	else if (s == 0)
    	{
    	std::cout << "read: " << s << std::endl;
    	break;
    	}
    	// else break;
    	sleep(1);
    }
    
    //子进程写一条数据后就关闭
    while (true)
    {
    	cnt++;
    	char buffer[1024];
    	snprintf(buffer, sizeof buffer, "chaild to parent : %s[%d][%d]", str, cnt, getpid());
    	//将数据写入管道
    	write(fds[1], buffer, strlen(buffer));
    	break;
    }
    
    close(fds[1]);
    std::cout << "子进程关闭写端" << std::endl;
    exit(0);
    

    如果是这种情况,父进程迟早会将管道的数据读完。最终读到0后,管道中没有数据了,父进程也会退出。

  4. 读关闭,写

    由于管道是单向通信,当读关闭后,写就没有意义了,此时操作系统就会发出信号终止写进程。

    //父进程读一次后就关闭    
    while (true)
    {
        char buffer[1024];
        ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;//添加'\0'
            std::cout << "get message: " << buffer << " | my pid: " << getpid() << std::endl;
        }
        else if (s == 0)
        {
            std::cout << "read: " << s << std::endl;
            break;
        }
        break;
    }
    close(fds[0]);
    std::cout << "父进程关闭读端" << std::endl;
    
    int status = 0;
    n = waitpid(id, &status, 0);
    assert(n == id);
    //获取退出信号
    std::cout << "pid->" << n << ":" << WTERMSIG(status) << std::endl;
    
    //子进程一直写
    while (true)
    {
        cnt++;
        char buffer[1024];
        snprintf(buffer, sizeof buffer, "chaild to parent : %s[%d][%d]", str, cnt, getpid());
        //将数据写入管道
        write(fds[1], buffer, strlen(buffer));
    }
    
    close(fds[1]);
    std::cout << "子进程关闭写端" << std::endl;
    exit(0);
    

命名管道

命名管道有一个特定的文件名,它可以在不相关的进程之间进行通信,只要这些进程知道管道的文件名即可访问它。

命名管道的创建

命名管道又两种创建方式。

  1. 直接通过命令行创建:mkfifo [文件名]

  2. 通过mkfifo()系统调用创建。

    • 函数原型:int mkfifo(const char *filename, mode_t mode);
    • 参数:第一个参数是文件名,第二个参数是创建文件时指定权限。

匿名管道是通过创建子进程的方式让父子进程看到同一份资源,那命名管道又是如何让没有亲缘关系的两个进程看到同一份资源呢?我们知道,在操作系统中以文件名加文件路径的方式就可以标定一个文件的唯一性。所以让不同的进程打开指定名称(文件名+文件路径)的文件就可以让不同的进程看到同一份资源了。

与匿名管道的区别

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,打开用open

命名管道与匿名管道之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。

总结

  1. 管道的生命周期随着进程。
  2. 匿名管道可以用来在具有亲缘关系的进程间通信。
  3. 命名管道可以用来在不相关的进程之间通信。
  4. 管道是面向字节流的。
  5. 管道是以半双工单向的方式进行通信。
  6. 管道具有同步与互斥机制。
posted @   羡鱼OvO  阅读(21)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示