Linux多进程进阶---进程间通信《管道》(一)

本文为一部分:管道相关内容。


前言

  Linux环境编程中进程间通信的重要性和方法:进程间通信是通过传输数据或共享信息来实现的,包括数据交换、共享资源、进程同步和消息传递。

  Linux系统提供了四大类进程间通信方法:管道、SystemVIPC、POSIXIPC和套接字。其中管道适用于具有亲缘关系的进程,SystemVIPC有消息队列、信号量和共享内存,POSIXIPC通过文件实现消息队列、信号量和共享内存。

在这里插入图片描述

  本质就是不用的进程可以访问相同的内存区域。

总共四大类方式如下:
在这里插入图片描述

一、无名管道

因为管道数据是通过队列来维护的,我们先来分析一个管道中数据的特点:
  管道对应的内核缓冲区大小是固定的,默认为4k(也就是队列最大能存储4k数据)
  管道分为两部分:读端和写端(队列的两端),数据从写端进入管道,从读端流出管道。
  管道中的数据只能读一次,做一次读操作之后数据也就没有了(读数据相当于出队列)。
  管道是单工的:数据只能单向流动, 数据从写端流向读端。

对管道的操作(读、写)默认是阻塞的
  读管道:管道中没有数据,读操作被阻塞,当管道中有数据之后阻塞才能解除
  写管道:管道被写满了,写数据的操作被阻塞,当管道变为不满的状态,写阻塞解除

管道在内核中, 不能直接对其进行操作,我们通过什么方式去读写管道呢?
  其实管道操作就是文件IO操作,内核中管道的两端分别对应两个文件描述符,通过写端的文件描述符把数据写入到管道中,通过读端的文件描述符将数据从管道中读出来。读写管道的函数就是Linux中的文件IO函数

1.无名管道原理

无名管道(pipe)是一种特殊的管道,用于在具有亲缘关系的进程之间进行单向通信无名管道特点:
  半双工的通信方式,数据只能单向流动。
  以字节流方式通信,数据格式由用户自行定义。
  无名管道多用于父子进程间通信,也可用于其他亲缘关系进程间通信。

在这里插入图片描述
  父进程调用pipe函数会创建两个文件(读管道文件,写管道文件),这两个文件对应的文件节点为pipe inode,pipeinode为无名管道具体实现。
在这里插入图片描述  父进程调用fork函数创建子进程,子进程拷贝父进程的文件表,由于父子进程文件表内容相同,指向的file相同,所以最终父子进程操作的pipe管道相同。父子进程都能看到pipe管道内存空间,所以父子进程能正常通信。

在这里插入图片描述

  父进程调用fork函数成功后,父子进程不能同时保留读写文件描述符,需要关闭读或者写文件描述符。防止父子进程同时读写引发数据错误。

在这里插入图片描述
  调用一次pipe函数只能产生一个pipe管道(一个缓冲区),所以无名管道是半双工模式。要是实现全双工模式必须有两个pipe管道,且对应两套读写文件。
在这里插入图片描述
  无亲缘关系进程文件表不能访问相同文件,进程间无法访问相同的pipe管道,所以不能通过无名管道进程通信。

2.无名管道代码

int pipe(int pipefd[2]);
int pipe2(int pipefd[2], int flags),
功能:pipe或pipe2函数用于创建一个无名管道(pipe),用于进程间通信。
参数:
pipefd:整型数组,用于存储管道的文件描述符。
pipefd[0]:读文件描述符。
pipefd[1]:写文件描述符。
flags:用于设置管道的标志位的参数,可以是0或者以下常量的按位或:
O NONBLOCK:设置非阻塞模式,即读取和写入管道时不会阻塞进程。
O_CLOEXEC:设置在execve系统调用执行时关闭管道。
返回值:
成功:返回0。
失败:返回-1,并设置errno。

创建无名管道代码

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>

#define BUFFER_SIZE 100
#define TEST_STRING "Hello, Pipe!\n"
#define SIZE_OF_TEST_STRING sizeof(TEST_STRING) - 1

int main() {
    int fd[2];
    pid_t pid;

    // 步骤1: 创建pipe
    if (pipe(fd) == -1) {
        perror("Pipe creation failed");
        return 1;
    }

    // 步骤2: 创建子进程
    pid = fork();
    if (pid < 0) {
        perror("Fork failed");
        return 1;
    } else if (pid == 0) { // 子进程
        // 步骤3: 子进程关闭写端
        close(fd[1]);

        while(1) {
            char rbuf[BUFFER_SIZE] = {0};
            ssize_t bytes_read = read(fd[0], rbuf, BUFFER_SIZE - 1);
            if (bytes_read > 0) {
                rbuf[bytes_read] = '\0'; // 添加字符串结束符
                printf("Child received: %s", rbuf);
            } else if (bytes_read == 0) {
                printf("Pipe closed.\n");
                break; // 读取到EOF,退出循环
            } else {
                perror("Read error");
                break;
            }
        }

        // 在适当的时候关闭读端,这里可以在循环后或使用信号处理来优雅关闭
        close(fd[0]);
    } else { // 父进程
        // 步骤3: 父进程关闭读端
        close(fd[0]);

        while(1) {
            // 写数据到pipe
            ssize_t bytes_written = write(fd[1], TEST_STRING, SIZE_OF_TEST_STRING);
            if (bytes_written != SIZE_OF_TEST_STRING) {
                perror("Write error");
                break;
            }
            printf("Parent wrote data.\n");

            sleep(1); // 暂停一秒

            // 实际应用中可能不需要每次都关闭写端,但这里为了示例清晰每次写完都关闭
            close(fd[1]);
        }
    }

    return 0;
}

创建管道(pipe(fd)): 使用pipe()系统调用创建一个管道。
创建子进程fork()): 调用fork()生成一个子进程。
子进程分支:
关闭管道的写端(close(fd[1]))。
在无限循环中从管道读取数据(read(fd[0], …))并处理。
(注:原循环中关闭读端的操作应在循环外,以避免重复关闭。)
父进程分支:
关闭管道的读端(close(fd[0]))。
在无限循环中向管道写入数据(write(fd[1], …)),每次写入后短暂休眠。
写操作后关闭写端,虽然实际应用中通常不在每次写后关闭,这里为了示例清晰进行了关闭。

二、命名管道(FIFO)

1.命名管道原理

FIFO文件(也称为命名管道)是一种特殊类型的文件,在Linux中用于进程间通信。FIFO文件允许不相关的进程通过读取和写入相同的文件来进行通信。FIFO文件特点:
  FIFO文件位于文件系统中,可以像其他文件一样进行访问和管理。
  FIFO文件可以通过名称进行识别和引用,而不仅仅依赖于文件描述符。
  FIFO文件可以在不同的进程之间进行双向通信,允许同时进行读取和写入操作。

  有名管道拥有管道的所有特性,之所以称之为有名是因为管道在磁盘上有实体文件, 文件类型为p ,有名管道文件大小永远为0,因为有名管道也是将数据存储到内存的缓冲区中,打开这个磁盘上的管道文件就可以得到操作有名管道的文件描述符,通过文件描述符读写管道存储在内核中的数据。

在这里插入图片描述
进程通过调用mkfifo函数创建一个FIFO文件,FIFO文件对应一个fifoinode,fifoinode和pipe inode底层实现相同。
  命名管道使用文件名创建管道,所以称为命名管道。
  命名管道和无名管道底层实现原理相同。

在这里插入图片描述  命名管道通过mkfifo创建完后,进程调用open打开FIFO文件,由于每个文件都对应唯一的inode节点所以多个进程都是打开相同的inode节点,成功打开FIFO文件后,进程能正常读写管道完成进程间通信。
  打开FIFO文件需要知道FIFO文件路径,进程间只要知道FIFO文件路径,就能建立连接,完成通信。
  进程之间都能看到管道内存空间,所以进程间能正常通信。

在这里插入图片描述
  调用一次mkfifo函数只能产生一个管道(一个缓冲区),所以命名管道是半双工模式。多个进程同时读写一个命名管道可能会出现数据异常,所以进程调用open函数时得指定打开标志为O RDONLY或O WRONLY.
  命名管道也需要创建多个命名管道实现全双工通信

2.命名管道代码

int mkfifo(const char *pathname, mode t mode);
功能:mkfifo函数是Linux下的一个系统调用函数,用于一个命名管道(FIFO文件)。
参数:
pathname:命名管道的路径名。
mode:创建的命名管道的权限,可参考open函数。
返回值:
成功:返回0。
失败:返回-1,并设置errno。

创建命名管道代码(写端)

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>

#define FIFO_PATH "/tmp/test_fifo"

int main(int argc, char *argv[]) {
    int fd = 0;

    // 创建FIFO(命名管道)
    if (mkfifo(FIFO_PATH, 0644) == -1) {
        perror("Failed to create FIFO");
        return EXIT_FAILURE;
    }

    // 以只读方式打开FIFO
    fd = open(FIFO_PATH, O_RDONLY);
    if (fd == -1) {
        perror("Failed to open FIFO");
        unlink(FIFO_PATH); // 如果打开失败,尝试删除已创建的FIFO
        return EXIT_FAILURE;
    }

    printf("Reading from FIFO...\n");
    while(1) {
        char rbuf[100];
        ssize_t bytes_read = read(fd, rbuf, 100);
        // 读是阻塞的, 如果管道中没有数据, read自动阻塞
        // 有数据解除阻塞, 继续读数据
        if (bytes_read > 0) {
            rbuf[bytes_read] = '\0'; // 确保字符串被正确终止
            printf("rbuf: %s\n", rbuf);
        } else if (bytes_read == 0) {
            printf("End of FIFO reached.\n");
            break; // 读取到EOF,退出循环
        } else if (errno != EAGAIN) { // EAGAIN 表示非阻塞模式下的无数据可读,通常应该继续读取
            perror("Read error");
            break;
        }
    }

    // 关闭文件描述符
    close(fd);

    // 可选:如果程序结束时不再需要FIFO,可以考虑删除它
    unlink(FIFO_PATH);

    return 0;
}

总结

  关于管道不管是有名的还是匿名的,在进行读写的时候,它们表现出的行为是一致的,下面是对其读写行为的总结:

 读管道,需要根据写端的状态进行分析:
  写端没有关闭 (操作管道写端的文件描述符没有被关闭)
   如果管道中没有数据 ==> 读阻塞, 如果管道中被写入了数据, 阻塞解除
   如果管道中有数据 ==> 不阻塞,管道中的数据被读完了, 再继续读管道还会阻塞
  写端已经关闭了 (没有可用的文件描述符可以写管道了)
   管道中没有数据 ==> 读端解除阻塞, read函数返回0
   管道中有数据 ==> read先将数据读出, 数据读完之后返回0, 不会阻塞了

 写管道,需要根据读端的状态进行分析:
  读端没有关闭
   如果管道有存储的空间, 一直写数据
   如果管道写满了, 写操作就阻塞, 当读端将管道数据读走了, 解除阻塞继续写
  读端关闭了,管道破裂(异常), 进程直接退出

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值