一、pipe 机制详解
1.1 管道的基本概念
管道 (pipe) 是 Unix 系统中最古老的 IPC 机制之一,它通过一对文件描述符实现进程间的单向通信:
一个文件描述符用于读取数据 (fd[0]
)
另一个文件描述符用于写入数据 (fd[1]
)
数据在管道中以先进先出 (FIFO) 的方式传输
管道本质上是内核维护的一个缓冲区,其大小因系统而异 (通常为 64KB),当缓冲区满时,写入操作会阻塞;当缓冲区空时,读取操作会阻塞。
1.2 pipe () 系统调用
1 2
| #include <unistd.h> int pipe(int pipefd[2]);
|
1.3 父子进程通信实现
使用 pipe 进行父子进程通信的典型流程:
创建管道
调用 fork()
创建子进程
关闭不需要的文件描述符
进行数据读写操作
关闭所有文件描述符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/wait.h> int main() {
int pipefd[2]; pid_t pid; char buf[1024];
// 创建管道 if (pipe(pipefd) == -1) { perror("pipe创建失败"); exit(EXIT_FAILURE); }
// 创建子进程 pid = fork(); if (pid == 0) { // 子进程 // 子进程关闭写入端 close(pipefd[1]); // 从管道读取数据 ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1); if (n == -1) { perror("读取失败"); exit(EXIT_FAILURE); } buf[n] = '0'; printf("子进程收到: %sn", buf); // 关闭读取端 close(pipefd[0]); exit(EXIT_SUCCESS); } else { // 父进程 // 父进程关闭读取端 close(pipefd[0]); // 向管道写入数据 const char *msg = "Hello from parent"; if (write(pipefd[1], msg, strlen(msg)) == -1) { perror("写入失败"); exit(EXIT_FAILURE); }
// 关闭写入端 close(pipefd[1]); // 等待子进程结束 wait(NULL); exit(EXIT_SUCCESS); } }
|
1.4 双向通信实现
管道本身是单向的,要实现双向通信需要创建两个管道:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/wait.h> int main() { int parent_to_child[2]; // 父进程到子进程的管道 int child_to_parent[2]; // 子进程到父进程的管道 pid_t pid; char buf[1024]; // 创建两个管道 if (pipe(parent_to_child) == -1 || pipe(child_to_parent) == -1) { perror("pipe创建失败"); exit(EXIT_FAILURE); } pid = fork(); if (pid == -1) { perror("fork失败"); exit(EXIT_FAILURE); } if (pid == 0) { // 子进程 // 关闭不需要的文件描述符 close(parent_to_child[1]); // 关闭父->子管道的写入端 close(child_to_parent[0]); // 关闭子->父管道的读取端 // 从父进程读取数据 ssize_t n = read(parent_to_child[0], buf, sizeof(buf) - 1); if (n == -1) { perror("子进程读取失败"); exit(EXIT_FAILURE); } buf[n] = '0'; printf("子进程收到: %sn", buf); // 向父进程发送数据 const char *msg = "Hello from child"; if (write(child_to_parent[1], msg, strlen(msg)) == -1) { perror("子进程写入失败"); exit(EXIT_FAILURE); } // 关闭所有文件描述符 close(parent_to_child[0]); close(child_to_parent[1]); exit(EXIT_SUCCESS); } else { // 父进程 // 关闭不需要的文件描述符 close(parent_to_child[0]); // 关闭父->子管道的读取端 close(child_to_parent[1]); // 关闭子->父管道的写入端 // 向子进程发送数据 const char *msg = "Hello from parent"; if (write(parent_to_child[1], msg, strlen(msg)) == -1) { perror("父进程写入失败"); exit(EXIT_FAILURE); } // 等待子进程响应 sleep(1); // 从子进程读取数据 ssize_t n = read(child_to_parent[0], buf, sizeof(buf) - 1); if (n == -1) { perror("父进程读取失败"); exit(EXIT_FAILURE); } buf[n] = '0'; printf("父进程收到: %sn", buf); // 关闭所有文件描述符 close(parent_to_child[1]); close(child_to_parent[0]); // 等待子进程结束 wait(NULL); exit(EXIT_SUCCESS); } }
|
1.5 pipe 使用注意事项
管道是半双工的,默认情况下不支持双向通信
管道只能在具有亲缘关系的进程间使用 (父子进程、兄弟进程)
必须正确关闭不需要的文件描述符,否则可能导致读取端阻塞
管道有容量限制,写入数据超过缓冲区大小时会阻塞
当所有写入端关闭后,读取端读取会返回 0 (EOF)
当所有读取端关闭后,写入操作会导致进程收到 SIGPIPE 信号
二、socketpair 机制详解
2.1 socketpair 的基本概念
socketpair 创建一对相互连接的套接字,与 pipe 相比:
2.2 socketpair () 系统调用
1 2 3
| #include <sys/types.h> #include <sys/socket.h> int socketpair(int domain, int type, int protocol, int sv[2]);
|
domain
:协议族,通常使用 AF_UNIX
(本地通信)
type
:套接字类型,SOCK_STREAM
(字节流) 或 SOCK_DGRAM
(数据报)
protocol
:协议,通常为 0 (默认协议)
sv
:输出参数,存储创建的两个套接字描述符
成功时返回 0,失败时返回 -1 并设置 errno
2.3 进程间通信实现
使用 socketpair 进行双向通信的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <sys/wait.h> int main() { int sv[2]; pid_t pid; char buf[1024]; // 创建套接字对 if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) { perror("socketpair创建失败"); exit(EXIT_FAILURE); } // 创建子进程 pid = fork(); if (pid == -1) { perror("fork失败"); exit(EXIT_FAILURE); } if (pid == 0) { // 子进程 // 关闭一个套接字(每个进程使用一个) close(sv[1]); // 从套接字读取数据 ssize_t n = read(sv[0], buf, sizeof(buf) - 1); if (n == -1) { perror("子进程读取失败"); exit(EXIT_FAILURE); } buf[n] = '0'; printf("子进程收到: %sn", buf); // 向父进程发送数据 const char *msg = "Hello from child"; if (write(sv[0], msg, strlen(msg)) == -1) { perror("子进程写入失败"); exit(EXIT_FAILURE); } // 关闭套接字 close(sv[0]); exit(EXIT_SUCCESS); } else { // 父进程 // 关闭一个套接字 close(sv[0]); // 向子进程发送数据 const char *msg = "Hello from parent"; if (write(sv[1], msg, strlen(msg)) == -1) { perror("父进程写入失败"); exit(EXIT_FAILURE); } // 从子进程读取数据 ssize_t n = read(sv[1], buf, sizeof(buf) - 1); if (n == -1) { perror("父进程读取失败"); exit(EXIT_FAILURE); } buf[n] = '0'; printf("父进程收到: %sn", buf); // 关闭套接字 close(sv[1]); // 等待子进程结束 wait(NULL); exit(EXIT_SUCCESS); } }
|
2.4 通过 socketpair 传递文件描述符
socketpair 的一个强大功能是能够传递文件描述符,这在许多场景下非常有用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
| #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <sys/wait.h> #include <sys/stat.h> #include <fcntl.h> #include <errno.h> // 传递文件描述符的辅助函数 ssize_t send_fd(int sockfd, int fd) { struct msghdr msg = {0}; struct iovec iov; char buf[1] = {'x'}; // 发送一个虚拟字节 // 设置消息头 union { struct cmsghdr cm; char control[CMSG_SPACE(sizeof(int))]; } control_un; msg.msg_control = control_un.control; msg.msg_controllen = sizeof(control_un.control); // 设置控制消息 struct cmsghdr *cmptr = CMSG_FIRSTHDR(&msg); cmptr->cmsg_len = CMSG_LEN(sizeof(int)); cmptr->cmsg_level = SOL_SOCKET; cmptr->cmsg_type = SCM_RIGHTS; // 用于传递文件描述符 *((int *)CMSG_DATA(cmptr)) = fd; // 设置数据部分 msg.msg_iov = &iov; msg.msg_iovlen = 1; iov.iov_base = buf; iov.iov_len = 1; return sendmsg(sockfd, &msg, 0); } // 接收文件描述符的辅助函数 ssize_t recv_fd(int sockfd, int *fd) { struct msghdr msg = {0}; struct iovec iov; char buf[1]; // 设置数据部分 iov.iov_base = buf; iov.iov_len = 1; msg.msg_iov = &iov; msg.msg_iovlen = 1; // 设置控制消息缓冲区 union { struct cmsghdr cm; char control[CMSG_SPACE(sizeof(int))]; } control_un; msg.msg_control = control_un.control; msg.msg_controllen = sizeof(control_un.control); // 接收消息 ssize_t n = recvmsg(sockfd, &msg, 0); if (n <= 0) { return n; } // 提取文件描述符 struct cmsghdr *cmptr = CMSG_FIRSTHDR(&msg); if (cmptr == NULL) { return -1; // 没有控制消息 } if (cmptr->cmsg_level != SOL_SOCKET || cmptr->cmsg_type != SCM_RIGHTS) { return -1; // 不是预期的控制消息 } *fd = *((int *)CMSG_DATA(cmptr)); return n; } int main() { int sv[2]; pid_t pid; int fd, new_fd; char buf[1024]; // 创建套接字对 if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) { perror("socketpair创建失败"); exit(EXIT_FAILURE); } // 创建测试文件 fd = open("test.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644); if (fd == -1) { perror("文件打开失败"); exit(EXIT_FAILURE); } write(fd, "测试文件内容", strlen("测试文件内容")); close(fd); // 重新以只读方式打开 fd = open("test.txt", O_RDONLY); if (fd == -1) { perror("文件打开失败"); exit(EXIT_FAILURE); } // 创建子进程 pid = fork(); if (pid == 0) { // 子进程 close(sv[1]); // 关闭不使用的套接字 // 接收文件描述符 // 读取通过传递的文件描述符打开的文件 ssize_t n = read(new_fd, buf, sizeof(buf) - 1); buf[n] = '0'; printf("子进程读取到的文件内容: %sn", buf); // 关闭文件和套接字 close(new_fd); close(sv[0]); exit(EXIT_SUCCESS); } else { // 父进程 close(sv[0]); // 关闭不使用的套接字 // 发送文件描述符 if (send_fd(sv[1], fd) == -1) { perror("发送文件描述符失败"); exit(EXIT_FAILURE); } // 关闭文件和套接字 close(fd); close(sv[1]); // 等待子进程结束 wait(NULL); // 清理测试文件 unlink("test.txt"); exit(EXIT_SUCCESS); } }
|
2.5 socketpair 使用注意事项
socketpair 创建的套接字是全双工的,支持双向通信
套接字对可以在任意进程间使用,不限于亲缘进程
可以通过 SCM_RIGHTS
控制消息传递文件描述符
与 pipe 不同,不需要关闭一个方向来使用另一个方向
使用 SOCK_STREAM
类型时提供字节流服务,保证数据有序且不丢失
使用 SOCK_DGRAM
类型时提供数据报服务,保留消息边界
三、pipe 与 socketpair 对比分析
特性 |
pipe |
socketpair |
通信方向 |
半双工 |
全双工 |
适用进程 |
主要用于亲缘进程 |
可用于任意进程 |
通信方式 |
仅支持数据传输 |
支持数据传输和文件描述符传递 |
创建方式 |
简单,单一系统调用 |
稍复杂,需要指定协议族和类型 |
灵活性 |
较低 |
较高,支持多种选项 |