【Linux系统编程】管道pipe详解

管道(Pipe)是Unix或类Unix系统中常用的一种进程间通信机制,用于在不同进程之间传递数据。它可以将一个进程的输出与另一个进程的输入相连接,形成一个数据流的传输通道。

本文将详细介绍管道相关的知识,包括管道的基本原理、使用方法、管道类型、管道的应用场景以及管道的注意事项等。

一. 管道的基本原理

管道的基本原理是,将一个进程的输出作为另一个进程的输入,将数据流从一个进程传递到另一个进程。在Unix或类Unix系统中,管道通常是通过操作系统内核中的一个缓冲区实现的,其输入和输出被映射到两个文件描述符上。一个进程通过向管道写入数据(将数据写入管道的输入端),另一个进程通过从管道读取数据(从管道的输出端读取数据),从而实现数据的传输。在实现上,管道可以是基于内存的匿名管道(Anonymous Pipe)或基于文件系统的命名管道(Named Pipe)。

管道的使用方法
Unix或类Unix系统中的管道通常是通过命令行管道符“|”来创建的,例如:

$ cat file.txt | grep keyword

上述命令将读取文件“file.txt”的内容,并将其传递给grep命令进行过滤。这里的“|”就是管道符,它将cat命令的输出与grep命令的输入相连接,形成了一个数据流的传输通道。

除了使用命令行管道符外,C语言等编程语言也可以通过系统调用来创建管道。在Unix或类Unix系统中,创建管道需要使用pipe()系统调用,它会返回两个文件描述符,分别用于读取管道的输出和写入管道的输入。例如:

#include <unistd.h>

int pipe(int fd[2]);

在上述代码中,fd[0]和fd[1]分别是读取管道输出和写入管道输入的文件描述符。调用pipe()函数后,系统会创建一个管道,并将其输入和输出分别映射到这两个文件描述符上。

二. 管道的类型

管道有两种类型,分别是匿名管道和命名管道。

1. 匿名管道

匿名管道(Anonymous Pipe)是一种基于内存的管道,没有与文件系统中的任何文件相关联。它是通过pipe()系统调用创建的,通常只能用于在具有亲缘关系的进程之间传递数据。匿名管道只能在创建它的进程及其子进程之间使用,无法在其他进程之间共享。

2. 命名管道

命名管道(Named Pipe,也称FIFO)是一种基于文件系统的管道,它是通过文件系统中的特殊文件来实现的。命名管道有一个文件名和文件系统中的其他文件一样,可以被多个进程打开和使用,用于在不同的进程之间传递数据。使用命名管道需要调用mkfifo()函数来创建一个特殊的文件,然后打开这个文件并通过读写文件来传递数据。

命名管道通常用于需要在不同进程之间传递数据的场景,例如多进程并发编程、客户端-服务器架构、管道通信等。

三. 管道的应用场景

管道是Unix或类Unix系统中非常重要的进程间通信机制之一,具有广泛的应用场景。下面是一些常见的应用场景:

1. 管道通信

管道通信是最常见的应用场景之一。例如,一个进程可以将数据写入一个管道,另一个进程可以从同一管道读取数据。这种通信机制通常用于单向数据传输,但也可以通过创建两个管道实现双向通信。

2. 管道过滤

管道过滤是指通过管道传输数据并对其进行过滤处理。例如,一个进程可以将文件的内容输出到管道中,另一个进程可以从同一管道读取数据并对其进行过滤(如grep命令对文件内容进行搜索)。这种方式可以实现复杂的数据处理和转换,例如文本处理、数据格式转换等。

3. 多进程并发编程

在多进程并发编程中,不同进程之间需要共享数据或信息。管道可以作为一种进程间通信机制,用于在多个进程之间传递数据或信息。例如,在Web服务器中,每个请求通常由一个独立的进程或线程来处理,而这些进程之间需要共享一些状态信息(如请求计数器、进程池等)。通过管道,不同进程可以共享这些信息,从而实现更高效的进程间通信。

四. 管道的注意事项

在使用管道时,需要注意以下几点:

1. 管道大小限制

管道的大小通常是有限制的,取决于系统的配置和资源限制。在读写管道时,需要考虑管道的缓冲区大小,以避免数据丢失或阻塞等问题。

2. 管道阻塞

当管道的缓冲区已满或已空时,对管道的写入和读取操作会被阻塞。这种情况下,程序可能会出现死锁或阻塞等问题。为了避免这种情况,可以使用非阻塞IO或异步IO方式来读取和写入管道。

3. 管道的关闭

当使用管道通信时,需要确保及时关闭管道。当进程打开管道时,操作系统会为其分配一些资源(如缓冲区、文件描述符等)。如果不及时关闭管道,这些资源可能会一直被占用,最终导致系统资源耗尽或进程崩溃等问题。

五. 管道的实现

管道的实现通常有两种方式:基于内存的管道和基于文件的管道。

1. 基于内存的管道

基于内存的管道是指使用系统内存中的缓冲区作为管道的数据存储空间。这种方式的优点是速度快、效率高,但缺点是数据不稳定,容易丢失。

2. 基于文件的管道

基于文件的管道是指通过文件系统中的特殊文件来实现管道。这种方式的优点是数据稳定、不易丢失,但缺点是速度较慢,效率较低。

在Unix和类Unix系统中,基于文件的管道通常使用mkfifo()函数来创建管道文件,然后通过打开文件、读写文件等方式来进行数据的传输和通信。基于文件的管道通常具有良好的可移植性和兼容性,在不同系统和平台上都可以使用。

六. 管道的示例代码

1. 下面是一个基于 文件的管道 的示例代码:

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

#define BUFFER_SIZE 256
#define PIPE_NAME "/tmp/my_pipe"

int main()
{
    int fd;
    char buffer[BUFFER_SIZE];
    pid_t pid;

    // 创建管道文件
    if (mkfifo(PIPE_NAME, 0666) < 0) {
        perror("mkfifo");
        exit(1);
    }

    // 创建子进程
    if ((pid = fork()) < 0) {
        perror("fork");
        exit(1);
    }
    else if (pid == 0) {  // 子进程
        // 打开管道文件
        if ((fd = open(PIPE_NAME, O_WRONLY)) < 0) {
            perror("open");
            exit(1);
        }

        // 写入数据
        sprintf(buffer, "Hello, parent!\n");
        write(fd, buffer, sizeof(buffer));

        // 关闭文件
        close(fd);
    }
    else {  // 父进程
        // 打开管道文件
        if ((fd = open(PIPE_NAME, O_RDONLY)) < 0) {
            perror("open");
            exit(1);
        }

        // 读取数据
        read(fd, buffer, sizeof(buffer));
        printf("Received message from child: %s", buffer);

        // 关闭文件
        close(fd);

        // 删除管道文件
        unlink(PIPE_NAME);
    }

    return 0;
}

该程序创建了一个基于文件的管道,然后创建了一个子进程和一个父进程。子进程向管道中写入数据,父进程从管道中读取数据,并将读取到的数据输出到控制台上。程序最后删除管道文件。

在程序中,首先使用mkfifo()函数创建管道文件,然后使用fork()函数创建子进程。子进程中使用open()函数打开管道文件,并使用write()函数向管道中写入数据。父进程中使用open()函数打开管道文件,并使用read()函数从管道中读取数据。最后,程序通过unlink()函数删除管道文件。

需要注意的是,该程序中的管道是基于文件的管道。程序首先使用mkfifo()函数创建了一个特殊的文件来作为管道文件,然后通过open()函数打开该文件,并通过read()和write()函数来进行数据的读写。在实际应用中,管道通常用于在父子进程之间进行通信,可以用于传递数据、同步进程等操作。

2. 下面是一个基于 管道通信 的示例程序:

该程序可以实现父子进程之间的通信,并将父进程传递的消息打印到控制台上。该程序使用无名管道(匿名管道)进行通信,无名管道是一种特殊的管道,不需要使用mkfifo()函数创建。

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define BUFFER_SIZE 256

int main()
{
    int pipe_fd[2];
    pid_t pid;
    char buffer[BUFFER_SIZE];

    if (pipe(pipe_fd) < 0) {
        printf("Failed to create pipe\n");
        return -1;
    }

    pid = fork();

    if (pid < 0) {
        printf("Failed to fork\n");
        return -1;
    }
    else if (pid == 0) {
        // child process
        close(pipe_fd[0]); // close the read end of the pipe
        char *msg = "Hello, parent process!";
        write(pipe_fd[1], msg, strlen(msg)+1);
        exit(0);
    }
    else {
        // parent process
        close(pipe_fd[1]); // close the write end of the pipe
        int nbytes = read(pipe_fd[0], buffer, BUFFER_SIZE);
        printf("Received message from child process: %s\n", buffer);
    }

    return 0;
}

该程序中,首先使用pipe()函数创建无名管道,并使用fork()函数创建子进程。在子进程中,关闭管道的读端,并向管道的写端写入数据。在父进程中,关闭管道的写端,并从管道的读端读取数据。读取到的数据被打印到控制台上。最后,程序结束,操作系统会自动关闭管道。

除了无名管道,Linux还支持命名管道(也叫命令管道或FIFO),命名管道可以在文件系统中创建一个文件,然后可以像普通文件一样进行读写。命名管道可以在不同的进程之间进行通信,也可以在同一进程中不同线程之间进行通信。

命名管道可以使用mkfifo()函数创建,创建成功后,可以使用open()函数打开管道文件,然后使用read()和write()函数进行读写操作。

3. 下面是一个基于 命名管道通信 的示例程序:

该程序可以实现两个进程之间的通信。程序首先使用mkfifo()函数创建了一个管道文件,然后使用fork()函数创建了两个子进程。子进程1从控制台读取用户输入的消息,并将消息写入到管道文件中;子进程2从管道文件中读取数据,并将读取到的数据打印到控制台上。父进程中等待两个子进程执行完成,并删除管道文件。

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

#define MAX_BUF 1024

int main()
{
    int fd;
    char * fifo = "/tmp/myfifo";
    mkfifo(fifo, 0666);

    pid_t pid1, pid2;
    pid1 = fork();
    if (pid1 < 0) {
        printf("Failed to fork\n");
        return -1;
    }
    else if (pid1 == 0) {
        // child process 1
        char input[MAX_BUF];
        printf("Enter a message: ");
        fgets(input, MAX_BUF, stdin);
        fd = open(fifo, O_WRONLY);
        write(fd, input, strlen(input)+1);
        close(fd);
        exit(0);
    }
    else {
        pid2 = fork();
        if (pid2 < 0) {
            printf("Failed to fork\n");
            return -1;
        }
        else if (pid2 == 0) {
            // child process 2
            char buf[MAX_BUF];
            fd = open(fifo, O_RDONLY);
            read(fd, buf, MAX_BUF);
            printf("Received message: %s", buf);
            close(fd);
            exit(0);
        }
        else {
            // parent process
            waitpid(pid1, NULL, 0);
            waitpid(pid2, NULL, 0);
            unlink(fifo);
        }
    }

    return 0;
}

需要注意的是,管道的容量是有限的,如果管道已满,写入操作将被阻塞,直到有足够的空间可以写入数据。同样地,如果管道为空,读取操作也将被阻塞,直到有数据可以读取。因此,在使用管道进行进程间通信时,需要注意读写的时序问题,以避免产生死锁或数据丢失等问题。

在Linux中,除了无名管道和命名管道,还有一种管道叫做socketpair(套接字对),它可以用于进程间的通信。socketpair()函数可以创建一对相互连接的套接字,这对套接字可以用于进程间的通信,与命名管道不同的是,socketpair不会在文件系统中创建任何文件。

4. 下面是一个基于 ocketpair通信 的示例程序:

该程序可以实现两个进程之间的通信。程序首先使用socketpair()函数创建了一个套接字对,然后使用fork()函数创建了两个子进程。子进程1从控制台读取用户输入的消息,并将消息写入到套接字中;子进程2从套接字中读取数据,并将读取到的数据打印到控制台上。父进程中等待两个子进程执行完成。

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

#define MAX_BUF 1024

int main()
{
    int sockfd[2];
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockfd) == -1) {
        perror("Failed to create socketpair");
        return -1;
    }

    pid_t pid1, pid2;
    pid1 = fork();
    if (pid1 < 0) {
        printf("Failed to fork\n");
        return -1;
    }
    else if (pid1 == 0) {
        // child process 1
        char input[MAX_BUF];
        printf("Enter a message: ");
        fgets(input, MAX_BUF, stdin);
        write(sockfd[0], input, strlen(input)+1);
        close(sockfd[0]);
        exit(0);
    }
    else {
        pid2 = fork();
        if (pid2 < 0) {
            printf("Failed to fork\n");
            return -1;
        }
        else if (pid2 == 0) {
            // child process 2
            char buf[MAX_BUF];
            read(sockfd[1], buf, MAX_BUF);
            printf("Received message: %s", buf);
            close(sockfd[1]);
            exit(0);
        }
        else {
            // parent process
            close(sockfd[0]);
            close(sockfd[1]);
            waitpid(pid1, NULL, 0);
            waitpid(pid2, NULL, 0);
        }
    }

    return 0;
}

需要注意的是,与管道不同的是,socketpair是双向的,也就是说,每个套接字可以同时进行读写操作。此外,socketpair也有容量限制,当发送的数据超过接收方的缓冲区大小时,发送方将被阻塞,直到有足够的空间可以写入数据。同样地,如果接收方的缓冲区为空,读取操作也将被阻塞,直到有数据可以读取。在使用socketpair进行进程间通信时,同样需要注意读写的时序问题。

当一个进程打开管道进行读取时,如果没有数据可读,则进程将被阻塞,直到另一个进程写入数据到管道中。类似地,当一个进程打开管道进行写入时,如果管道已满,则进程将被阻塞,直到另一个进程读取数据。因此,管道的读写操作是阻塞的,也就是说,它们会一直等待直到有数据可读或空间可用。

当管道的读取端被关闭时,管道中所有的数据都被读取完毕。此时,任何尝试向管道中写入数据的操作都将引发信号SIGPIPE。同样地,当管道的写入端被关闭时,任何尝试从管道中读取数据的操作也将立即返回0。

由于管道是一种半双工通信机制,因此它只能用于单向数据传输。如果需要进行双向通信,则需要建立两个管道进行数据传输。在实际应用中,通常使用匿名管道或命名管道实现进程间通信。

Linux系统编程,管道pipe部分讲解结束
如有帮助,可以 点赞+收藏 ~~~
如有问题,也欢迎在评论区下方指出哦~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值