pipe系统调用及示例

我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 pipe 函数,它用于创建匿名管道,这是一种在相关进程(如父子进程)之间进行单向数据通信的重要机制。

pipe 函数介绍


1. 函数介绍

pipe 是一个 Linux 系统调用,用于创建一个匿名管道(Anonymous Pipe)。管道是一种半双工(单向)的进程间通信(IPC)机制,数据只能在一个方向上流动。

管道通常用于具有亲缘关系的进程之间通信,最常见的场景是父进程和子进程之间的数据传递。创建管道后,会得到两个文件描述符:一个用于读取(read end),一个用于写入(write end)。写入端写入的数据会被内核缓冲,然后可以从读取端读取出来。

管道是 Unix/Linux “一切皆文件” 哲学的体现,管道的两端都可以像普通文件一样使用 read() 和 write() 系统调用进行操作。

重要特性:

  • 匿名性: 管道没有名字,不能通过文件系统路径访问。
  • 单向性: 数据只能从写端流向读端。
  • 亲缘性: 通常用于有亲缘关系的进程间通信。
  • 阻塞性: 默认情况下,读写操作可能会阻塞。
  • 缓冲性: 内核提供缓冲区来存储管道中的数据。

2. 函数原型

#include <unistd.h> // 必需

int pipe(int pipefd[2]);

3. 功能

  • 创建管道: 在内核中创建一个管道缓冲区。
  • 返回文件描述符: 通过 pipefd 数组返回两个文件描述符:
    • pipefd[0]: 管道的读取端(read end)。
    • pipefd[1]: 管道的写入端(write end)。

4. 参数

  • int pipefd[2]: 一个包含两个整数的数组,用于接收返回的文件描述符。
    • pipefd[0]: 管道的读取端文件描述符。进程可以使用 read(pipefd[0], buffer, size) 从此端读取数据。
    • pipefd[1]: 管道的写入端文件描述符。进程可以使用 write(pipefd[1], buffer, size) 向此端写入数据。
    • 注意: 创建管道后,通常会使用 fork() 创建子进程,然后父子进程分别关闭不需要的端。例如,父进程负责写入,则应关闭 pipefd[0];子进程负责读取,则应关闭 pipefd[1]

5. 返回值

  • 成功时: 返回 0。
  • 失败时:
    • 返回 -1,并设置全局变量 errno 来指示具体的错误原因:
      • EFAULTpipefd 数组指针无效。
      • EMFILE: 进程已打开的文件描述符数量达到上限 (RLIMIT_NOFILE)。
      • ENFILE: 系统范围内已打开的文件数量达到上限。

6. 相似函数,或关联函数

  • pipe2(int pipefd[2], int flags)pipe 的扩展版本,允许设置额外的标志,如 O_CLOEXEC(执行时关闭)或 O_NONBLOCK(非阻塞模式)。
  • mkfifo(const char *pathname, mode_t mode): 创建命名管道(FIFO),它是一个存在于文件系统中的特殊文件,可以让无亲缘关系的进程通信。
  • socketpair(int domain, int type, int protocol, int sv[2]): 创建一对相互连接的套接字,可以实现全双工通信。
  • popen(const char *command, const char *type)pclose(FILE *stream): 高级函数,创建一个管道并启动一个 shell 来执行命令,方便地实现程序间的数据交换。

7. 示例代码

示例 1:基本的父子进程管道通信

这个例子演示了最经典的管道使用场景:父进程向子进程发送数据。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>

#define BUFFER_SIZE 256

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

    printf("=== 基本父子进程管道通信 ===\n");

    // 1. 创建管道
    if (pipe(pipefd) == -1) {
        perror("pipe 创建失败");
        exit(EXIT_FAILURE);
    }
    printf("管道创建成功: 读端=%d, 写端=%d\n", pipefd[0], pipefd[1]);

    // 2. 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork 失败");
        // 清理管道文件描述符
        close(pipefd[0]);
        close(pipefd[1]);
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // 子进程:读取数据
        printf("子进程 (PID: %d) 开始读取数据...\n", getpid());
        
        // 关闭写端(子进程不需要写入)
        close(pipefd[1]);
        
        // 从管道读取数据
        ssize_t bytes_read = read(pipefd[0], buffer, BUFFER_SIZE - 1);
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0'; // 确保字符串以 null 结尾
            printf("子进程读取到数据: %s", buffer);
        } else if (bytes_read == 0) {
            printf("子进程读取结束 (写端已关闭)\n");
        } else {
            perror("子进程读取失败");
        }
        
        // 关闭读端
        close(pipefd[0]);
        printf("子进程结束\n");
        exit(EXIT_SUCCESS);
        
    } else {
        // 父进程:写入数据
        printf("父进程 (PID: %d) 开始写入数据...\n", getpid());
        
        // 关闭读端(父进程不需要读取)
        close(pipefd[0]);
        
        // 向管道写入数据
        const char *message = "Hello from parent process through pipe!\n";
        ssize_t bytes_written = write(pipefd[1], message, strlen(message));
        if (bytes_written == -1) {
            perror("父进程写入失败");
        } else {
            printf("父进程写入 %ld 字节数据\n", bytes_written);
        }
        
        // 关闭写端,这会通知读端数据已写完
        close(pipefd[1]);
        
        // 等待子进程结束
        int status;
        waitpid(pid, &status, 0);
        if (WIFEXITED(status)) {
            printf("子进程正常退出,退出码: %d\n", WEXITSTATUS(status));
        } else {
            printf("子进程异常退出\n");
        }
        
        printf("父进程结束\n");
    }

    return 0;
}

代码解释:

  1. 首先调用 pipe(pipefd) 创建管道,得到读端 pipefd[0] 和写端 pipefd[1]
  2. 调用 fork() 创建子进程。此时,父子进程都拥有管道两端的文件描述符副本。
  3. 在子进程中:
    • 关闭不需要的写端 pipefd[1]
    • 使用 read(pipefd[0], ...) 从管道读取数据。
    • 读取完成后关闭读端 pipefd[0]
  4. 在父进程中:
    • 关闭不需要的读端 pipefd[0]
    • 使用 write(pipefd[1], ...) 向管道写入数据。
    • 写入完成后关闭写端 pipefd[1],这会通知读端没有更多数据。
    • 使用 waitpid() 等待子进程结束。

示例 2:双向管道通信

这个例子演示如何使用两个管道实现父子进程之间的双向通信。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>

#define BUFFER_SIZE 256

int main() {
    int parent_to_child_pipe[2];  // 父进程向子进程发送数据
    int child_to_parent_pipe[2];  // 子进程向父进程发送数据
    pid_t pid;
    char buffer[BUFFER_SIZE];

    printf("=== 双向管道通信 ===\n");

    // 1. 创建两个管道
    if (pipe(parent_to_child_pipe) == -1 || pipe(child_to_parent_pipe) == -1) {
        perror("管道创建失败");
        exit(EXIT_FAILURE);
    }
    printf("管道创建成功\n");
    printf("父到子管道: 读端=%d, 写端=%d\n", parent_to_child_pipe[0], parent_to_child_pipe[1]);
    printf("子到父管道: 读端=%d, 写端=%d\n", child_to_parent_pipe[0], child_to_parent_pipe[1]);

    // 2. 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork 失败");
        close(parent_to_child_pipe[0]);
        close(parent_to_child_pipe[1]);
        close(child_to_parent_pipe[0]);
        close(child_to_parent_pipe[1]);
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // 子进程
        printf("子进程 (PID: %d) 启动\n", getpid());
        
        // 关闭不需要的文件描述符
        close(parent_to_child_pipe[1]);  // 子进程不写入父到子管道
        close(child_to_parent_pipe[0]);  // 子进程不读取子到父管道
        
        // 1. 从父进程接收消息
        printf("子进程等待接收父进程消息...\n");
        ssize_t bytes_read = read(parent_to_child_pipe[0], buffer, BUFFER_SIZE - 1);
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0';
            printf("子进程收到消息: %s", buffer);
            
            // 2. 向父进程发送回复
            const char *reply = "Hello parent! I'm child process.\n";
            ssize_t bytes_written = write(child_to_parent_pipe[1], reply, strlen(reply));
            if (bytes_written == -1) {
                perror("子进程发送回复失败");
            } else {
                printf("子进程发送回复 (%ld 字节)\n", bytes_written);
            }
        }
        
        // 关闭管道
        close(parent_to_child_pipe[0]);
        close(child_to_parent_pipe[1]);
        printf("子进程结束\n");
        exit(EXIT_SUCCESS);
        
    } else {
        // 父进程
        printf("父进程 (PID: %d) 启动\n", getpid());
        
        // 关闭不需要的文件描述符
        close(parent_to_child_pipe[0]);  // 父进程不读取父到子管道
        close(child_to_parent_pipe[1]);  // 父进程不写入子到父管道
        
        // 1. 向子进程发送消息
        const char *message = "Hello child! I'm parent process.\n";
        printf("父进程向子进程发送消息...\n");
        ssize_t bytes_written = write(parent_to_child_pipe[1], message, strlen(message));
        if (bytes_written == -1) {
            perror("父进程发送消息失败");
        } else {
            printf("父进程发送消息 (%ld 字节)\n", bytes_written);
        }
        
        // 2. 等待并接收子进程的回复
        printf("父进程等待子进程回复...\n");
        ssize_t bytes_read = read(child_to_parent_pipe[0], buffer, BUFFER_SIZE - 1);
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0';
            printf("父进程收到回复: %s", buffer);
        }
        
        // 关闭管道
        close(parent_to_child_pipe[1]);
        close(child_to_parent_pipe[0]);
        
        // 等待子进程结束
        int status;
        waitpid(pid, &status, 0);
        printf("父进程结束\n");
    }

    return 0;
}

代码解释:

1. 创建两个管道:一个用于父进程向子进程发送数据,另一个用于子进程向父进程发送数据。
2. 在父子进程中分别关闭不需要的管道端。
3. 通过协调读写操作,实现双向通信。

示例 3:管道与错误处理

这个例子重点演示管道的错误处理和一些特殊情况。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
#include <fcntl.h>

#define BUFFER_SIZE 256

// 演示管道错误处理
void demonstrate_pipe_errors() {
    printf("=== 管道错误处理演示 ===\n");
    
    // 1. 传递无效指针
    printf("1. 传递无效指针给 pipe()...\n");
    if (pipe(NULL) == -1) {
        printf("   错误: %s\n", strerror(errno));
        if (errno == EFAULT) {
            printf("   说明: pipe() 参数不能为 NULL\n");
        }
    }
    
    printf("\n");
}

// 演示管道读写特性
void demonstrate_pipe_characteristics() {
    printf("=== 管道特性演示 ===\n");
    
    int pipefd[2];
    if (pipe(pipefd) == -1) {
        perror("pipe 创建失败");
        return;
    }
    
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork 失败");
        close(pipefd[0]);
        close(pipefd[1]);
        return;
    }
    
    if (pid == 0) {
        // 子进程
        close(pipefd[1]); // 关闭写端
        
        char buffer[BUFFER_SIZE];
        printf("子进程: 尝试从空管道读取 (会阻塞)...\n");
        
        // 读取数据
        ssize_t bytes_read = read(pipefd[0], buffer, BUFFER_SIZE - 1);
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0';
            printf("子进程: 读取到数据: %s", buffer);
        } else if (bytes_read == 0) {
            printf("子进程: 读取到文件结束 (所有写端都已关闭)\n");
        } else {
            perror("子进程: 读取失败");
        }
        
        close(pipefd[0]);
        exit(EXIT_SUCCESS);
    } else {
        // 父进程
        close(pipefd[0]); // 关闭读端
        
        // 写入一些数据
        const char *message = "Message from parent\n";
        printf("父进程: 向管道写入数据...\n");
        write(pipefd[1], message, strlen(message));
        
        // 关闭写端,通知子进程结束
        printf("父进程: 关闭写端,通知子进程结束...\n");
        close(pipefd[1]);
        
        wait(NULL);
    }
    
    printf("\n");
}

// 演示管道容量和阻塞行为
void demonstrate_pipe_capacity() {
    printf("=== 管道容量和阻塞行为演示 ===\n");
    printf("注意: 这个演示可能需要较长时间\n");
    
    int pipefd[2];
    if (pipe(pipefd) == -1) {
        perror("pipe 创建失败");
        return;
    }
    
    // 获取管道容量(Linux 特定)
    int pipe_capacity = fcntl(pipefd[1], F_GETPIPE_SZ);
    if (pipe_capacity != -1) {
        printf("管道容量: %d 字节\n", pipe_capacity);
    }
    
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork 失败");
        close(pipefd[0]);
        close(pipefd[1]);
        return;
    }
    
    if (pid == 0) {
        // 子进程:读取端
        close(pipefd[1]); // 关闭写端
        
        sleep(2); // 让父进程先填满管道
        
        char buffer[1024];
        int total_read = 0;
        ssize_t bytes_read;
        
        printf("子进程: 开始读取数据...\n");
        while ((bytes_read = read(pipefd[0], buffer, sizeof(buffer))) > 0) {
            total_read += bytes_read;
            printf("子进程: 读取 %ld 字节,总计 %d 字节\n", bytes_read, total_read);
        }
        
        printf("子进程: 读取完成,总计 %d 字节\n", total_read);
        close(pipefd[0]);
        exit(EXIT_SUCCESS);
    } else {
        // 父进程:写入端
        close(pipefd[0]); // 关闭读端
        
        char data[1024];
        memset(data, 'A', sizeof(data) - 1);
        data[sizeof(data) - 1] = '\0';
        
        int total_written = 0;
        ssize_t bytes_written;
        
        printf("父进程: 开始写入大量数据...\n");
        // 写入数据直到管道满(会阻塞)
        for (int i = 0; i < 100; i++) {
            bytes_written = write(pipefd[1], data, strlen(data));
            if (bytes_written == -1) {
                perror("父进程: 写入失败");
                break;
            }
            total_written += bytes_written;
            printf("父进程: 写入 %ld 字节,总计 %d 字节\n", bytes_written, total_written);
        }
        
        printf("父进程: 写入完成,总计 %d 字节\n", total_written);
        close(pipefd[1]);
        wait(NULL);
    }
    
    printf("\n");
}

// 演示 pipe2 和非阻塞管道
void demonstrate_pipe2_and_nonblocking() {
    printf("=== pipe2 和非阻塞管道演示 ===\n");
    
#ifdef __linux__
    int pipefd[2];
    
    // 使用 pipe2 创建非阻塞管道
    if (pipe2(pipefd, O_NONBLOCK) == -1) {
        perror("pipe2 创建失败");
        printf("可能是因为系统不支持 pipe2\n");
        return;
    }
    
    printf("使用 pipe2 创建了非阻塞管道: 读端=%d, 写端=%d\n", pipefd[0], pipefd[1]);
    
    // 尝试从空的非阻塞管道读取
    char buffer[10];
    ssize_t bytes_read = read(pipefd[0], buffer, sizeof(buffer));
    if (bytes_read == -1) {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            printf("从空的非阻塞管道读取: %s\n", strerror(errno));
            printf("说明: 非阻塞模式下,没有数据时立即返回错误\n");
        } else {
            perror("读取失败");
        }
    }
    
    close(pipefd[0]);
    close(pipefd[1]);
#else
    printf("pipe2 在此系统上不可用\n");
#endif
    
    printf("\n");
}

int main() {
    printf("管道 (pipe) 函数演示程序\n");
    printf("当前进程 PID: %d\n\n", getpid());
    
    demonstrate_pipe_errors();
    demonstrate_pipe_characteristics();
    // demonstrate_pipe_capacity(); // 这个演示可能需要较长时间,可选择性运行
    demonstrate_pipe2_and_nonblocking();
    
    printf("=== 总结 ===\n");
    printf("管道 (pipe) 关键知识点:\n");
    printf("1. 单向通信: 数据只能从写端流向读端\n");
    printf("2. 亲缘进程: 通常用于父子进程通信\n");
    printf("3. 阻塞特性: 默认情况下读写可能阻塞\n");
    printf("4. 文件结束: 当所有写端关闭时,读端返回 0\n");
    printf("5. 缓冲机制: 内核提供缓冲区存储数据\n");
    printf("6. 错误处理: 注意 EFAULT, EMFILE, ENFILE 等错误\n");
    printf("7. 资源清理: 使用完后必须关闭文件描述符\n");
    printf("8. 双向通信: 需要创建两个管道\n\n");
    
    printf("最佳实践:\n");
    printf("- 及时关闭不需要的管道端\n");
    printf("- 正确处理读写返回值\n");
    printf("- 考虑使用 pipe2 设置 O_CLOEXEC 标志\n");
    printf("- 对于复杂通信,考虑使用命名管道 (FIFO) 或套接字\n");
    
    return 0;
}

代码解释:

1. demonstrate_pipe_errors 演示了传递无效参数给 pipe() 的错误处理。
2. demonstrate_pipe_characteristics 演示了管道的基本读写行为和文件结束条件。
3. demonstrate_pipe_capacity 演示了管道的容量限制和阻塞行为(注释掉了,因为可能运行时间较长)。
4. demonstrate_pipe2_and_nonblocking 演示了 pipe2 函数和非阻塞模式的使用。
5. main 函数协调各个演示部分,并在最后总结关键知识点。

编译和运行:

# 编译示例
gcc -o pipe_example1 pipe_example1.c
gcc -o pipe_example2 pipe_example2.c
gcc -o pipe_example3 pipe_example3.c

# 运行示例
./pipe_example1
./pipe_example2
./pipe_example3

总结:

pipe 函数是 Linux 进程间通信的基础工具之一。它简单高效,特别适合父子进程间的数据传递。理解其单向性、阻塞性和缓冲机制对于正确使用管道至关重要。在实际编程中,要注意及时关闭不需要的文件描述符,正确处理各种返回值和错误情况,并根据需要考虑使用 pipe2 或其他更高级的 IPC 机制。

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注