管道与进程间通信
目录
匿名管道
命名管道
socketpair()创建全双工管道
匿名管道
匿名管道是一种半双工的通信机制,数据只能在一个方向上流动,并且只能在具有亲缘关系(如父子进程)的进程间使用。
int pipe(int pipefd[2]);
pipefd
是一个包含两个文件描述符的数组,pipefd[0]
用于读取管道数据,pipefd[1]
用于写入管道数据。
工作原理:
- 父进程调用
pipe
函数创建管道,得到两个文件描述符pipefd[0]
和pipefd[1]
。 - 父进程调用
fork
创建子进程,子进程会继承父进程的文件描述符,因此父子进程都拥有这两个文件描述符。 - 父子进程各自关闭不需要的文件描述符(例如父进程关闭
pipefd[0]
,子进程关闭pipefd[1]
,这样就建立了单向通信)。 - 父进程通过
pipefd[1]
写入数据,子进程通过pipefd[0]
读取数据,实现进程间通信。
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>#define BUFFER_SIZE 1024int main() {int pipefd[2];pid_t pid;char buffer[BUFFER_SIZE];// 创建管道if (pipe(pipefd) == -1) {perror("pipe");return 1;}// 创建子进程pid = fork();if (pid == -1) {perror("fork");close(pipefd[0]);close(pipefd[1]);return 1;} else if (pid == 0) {// 子进程close(pipefd[1]); // 关闭写入端ssize_t bytes_read = read(pipefd[0], buffer, sizeof(buffer) - 1);if (bytes_read == -1) {perror("read");} else {buffer[bytes_read] = '\0';printf("Child received: %s\n", buffer);}close(pipefd[0]); // 关闭读取端} else {// 父进程close(pipefd[0]); // 关闭读取端const char *message = "Hello from parent";if (write(pipefd[1], message, strlen(message)) == -1) {perror("write");}close(pipefd[1]); // 关闭写入端}return 0;
}
输出结果:
命名管道
命名管道是一种特殊类型的文件,它在文件系统中有一个对应的文件名,因此可以在不相关的进程间进行通信。
int mkfifo(const char *pathname, mode_t mode)
pathname
是命名管道的路径名。mode
用于指定命名管道的权限,类似于open
函数中的权限参数。- 当
mkfifo
函数成功创建命名管道时,返回值为0
。这表明命名管道已成功在文件系统中创建,后续可以通过相应的文件操作函数(如open
、read
、write
等)对其进行操作,实现进程间通信。如果mkfifo
函数执行失败,返回值为-1
,并且会设置全局变量errno
来指示具体的错误原因。(errno的两个常见值:1.EACCES
:权限不足。调用进程没有足够的权限在指定路径下创建命名管道。例如,尝试在没有写权限的目录中创建管道时会出现此错误。2.EEXIST
:指定的路径名已经存在,并且它不是一个命名管道。在调用mkfifo
时,如果指定路径下已有同名的普通文件、目录或其他类型的文件,就会返回此错误。不过,通常在代码中可以通过检查errno
是否为EEXIST
来决定是否忽略该错误,因为有时可能只是想确保命名管道存在,而不关心它是否已提前创建。)
工作原理:
- 一个进程调用
mkfifo
创建命名管道,在文件系统中创建一个特殊的 FIFO 文件。 - 不同的进程可以通过
open
函数打开这个 FIFO 文件,一个进程以写入模式打开(O_WRONLY
),另一个进程以读取模式打开(O_RDONLY
)。 - 写入进程通过
write
函数向 FIFO 文件写入数据,读取进程通过read
函数从 FIFO 文件读取数据,从而实现进程间通信。
示例代码如下:
写端代码(writer.c)
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>#define FIFO_PATH "/tmp/myfifo"
#define BUFFER_SIZE 1024int main() {int fd;char buffer[BUFFER_SIZE];const char *message = "Hello from writer";// 创建命名管道if (mkfifo(FIFO_PATH, 0666) == -1 && errno != EEXIST) {perror("mkfifo");return 1;}// 打开命名管道进行写入fd = open(FIFO_PATH, O_WRONLY);if (fd == -1) {perror("open");return 1;}// 写入数据if (write(fd, message, strlen(message)) == -1) {perror("write");}close(fd);return 0;
}
读端代码(reader.c)
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>#define FIFO_PATH "/tmp/myfifo"
#define BUFFER_SIZE 1024int main() {int fd;char buffer[BUFFER_SIZE];// 打开命名管道进行读取fd = open(FIFO_PATH, O_RDONLY);if (fd == -1) {perror("open");return 1;}// 读取数据ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);if (bytes_read == -1) {perror("read");} else {buffer[bytes_read] = '\0';printf("Reader received: %s\n", buffer);}close(fd);return 0;
}
需要注意的是:
mkfifo
创建的命名管道在文件系统中表现为一个特殊类型的文件。如同普通文件一样,除非通过特定的文件删除操作,否则它会一直存在于文件系统中。这与匿名管道不同,匿名管道是内核在内存中创建和管理的,当所有使用它的进程关闭相关文件描述符后,匿名管道会自动消失。- 例如,你在
/tmp
目录下创建了一个命名管道/tmp/myfifo
,即使创建它的进程已经结束运行,只要没有执行删除操作,/tmp/myfifo
这个命名管道文件就会一直保留在/tmp
目录中。
socketpair()创建全双工管道
如果大家尚未拥有Linux网络编程的基础,可以先不着急看这部分内容。
int socketpair(int domain, int type, int protocol, int sv[2]);
domain
:指定套接字域。通常使用AF_UNIX
(也称为AF_LOCAL
),表示本地通信,这意味着套接字对在同一主机上的进程间进行通信,不需要网络协议栈的参与。type
:指定套接字类型。常见的类型有SOCK_STREAM
(面向连接的字节流套接字,提供可靠的、有序的、无差错的数据传输)和SOCK_DGRAM
(无连接的数据报套接字,数据传输不保证顺序和可靠性)。对于socketpair
,一般使用SOCK_STREAM
来确保数据传输的可靠性。protocol
:通常设置为 0,表示使用默认协议。对于给定的domain
和type
,系统会选择合适的默认协议。sv
:是一个整数数组,长度为 2。函数成功时,sv[0]
和sv[1]
分别是创建的套接字对的两个文件描述符- 如果函数成功,返回 0,并且
sv
数组将包含两个有效的套接字文件描述符。 - 如果函数失败,返回 -1,并设置
errno
以指示错误原因。常见的错误包括EAFNOSUPPORT
(不支持指定的地址族)、EINVAL
(无效的参数)等。
本质上来讲socketpair()创建的是两个可以相互通信的套接字,大家都知道套接字是可读可写的。所以说逻辑上可以把socketpair()看做创建了一条全双工管道。
示例代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <errno.h>#define BUFFER_SIZE 1024void handle_error(const char *msg) {perror(msg);exit(EXIT_FAILURE);
}int main() {int socket_pair[2];pid_t pid;char send_buffer[BUFFER_SIZE];char recv_buffer[BUFFER_SIZE];// 创建 socketpairif (socketpair(AF_UNIX, SOCK_STREAM, 0, socket_pair) == -1) {handle_error("socketpair creation failed");}// 创建子进程pid = fork();if (pid == -1) {handle_error("fork failed");} else if (pid == 0) {// 子进程close(socket_pair[0]); // 父进程关闭socket_pair[0]套接字,只使用socket_pair[1]进行读写// 子进程向父进程发送数据const char *child_message = "Hello from child";if (send(socket_pair[1], child_message, strlen(child_message), 0) == -1) {handle_error("child send failed");}printf("Child sent: %s\n", child_message);// 子进程从父进程接收数据ssize_t child_bytes_received = recv(socket_pair[1], recv_buffer, sizeof(recv_buffer) - 1, 0);if (child_bytes_received == -1) {handle_error("child recv failed");}recv_buffer[child_bytes_received] = '\0';printf("Child received from parent: %s\n", recv_buffer);close(socket_pair[1]);} else {// 父进程close(socket_pair[1]); // 父进程关闭socket_pair[1]套接字,只使用socket_pair[0]进行读写// 父进程从子进程接收数据ssize_t parent_bytes_received = recv(socket_pair[0], recv_buffer, sizeof(recv_buffer) - 1, 0);if (parent_bytes_received == -1) {handle_error("parent recv failed");}recv_buffer[parent_bytes_received] = '\0';printf("Parent received from child: %s\n", recv_buffer);// 父进程向子进程发送数据const char *parent_message = "Hello from parent";if (send(socket_pair[0], parent_message, strlen(parent_message), 0) == -1) {handle_error("parent send failed");}printf("Parent sent: %s\n", parent_message);close(socket_pair[0]);}return 0;
}
输出结果如下:
接下来对比一下socketpair()与传统pipe的不同:
socketpair
:创建的是全双工通信通道。意味着两个进程(或线程)通过这对套接字可以同时进行数据的发送和接收操作,数据能够在两个方向上同时传输。例如,在父子进程通过socketpair
通信时,父进程可以在向子进程发送数据的同时,从子进程接收数据。pipe
:建立的是半双工通信管道。数据只能在一个方向上流动,在某一时刻,数据要么从管道的一端流向另一端,不能同时双向传输。比如,父进程向子进程发送数据时,子进程不能同时向父进程发送数据。socketpair
:常用于需要双向实时交互的场景,或者在多线程编程中利用套接字的特性(如异步 I/O、信号驱动 I/O 等)实现更灵活的通信。由于它创建的是套接字对,在功能上更接近网络套接字,因此在一些需要类似网络通信特性但又局限于本地进程间通信的场景中较为适用。pipe
:适用于简单的单向数据传递场景,比如一个进程产生数据,另一个进程消费数据。常见于父子进程间的简单数据传输,例如父进程将一些计算结果传递给子进程进行后续处理。socketpair
:理论上可以用于任何进程间通信,不过在实际应用中,常用于有亲缘关系(如父子进程)的进程间通信,但并不局限于此。在多线程编程中,不同线程也可以使用socketpair
进行通信。pipe
:主要用于具有亲缘关系的进程间通信,通常是父子进程。这是因为管道依赖于文件描述符的继承机制,父进程创建管道后通过fork
创建子进程,子进程继承父进程的文件描述符从而实现通信。socketpair
:创建的套接字对在文件系统中没有对应的实体文件,它们是基于内存的通信机制,不依赖于文件系统。这使得socketpair
的通信效率较高,因为不需要进行文件系统相关的操作。pipe
:匿名管道同样在文件系统中没有对应的实体文件,其生命周期完全依赖于使用它的进程。然而,命名管道(通过mkfifo
创建,与pipe
原理相关)在文件系统中有对应的文件,这使得命名管道可以用于不相关进程间的通信,但也引入了文件系统相关的开销和管理。socketpair
:由于基于套接字机制,支持一些高级特性,如设置套接字选项(setsockopt
)来调整通信行为,包括设置缓冲区大小、启用或禁用某些功能等。例如,可以设置SO_RCVBUF
来调整接收缓冲区的大小,以优化数据接收性能。pipe
:功能相对较为基础,主要专注于简单的数据传输,一般不支持像套接字那样丰富的选项设置。其数据传输行为相对固定,主要围绕基本的读写操作。