前言
本篇文章将给大家讲解进程间通信中的管道使用方法和概念。
一、管道的概念
管道的概念来源于Unix操作系统,在Unix-like系统(如Linux)中被广泛使用。它也存在于其他操作系统中,如Windows。
管道可以将一个进程的输出直接连接到另一个进程的输入,从而实现数据的流动和传输。通过管道,一个进程产生的输出可以无需写入临时文件,而是直接传递给另一个进程进行处理,这样可以提高系统的效率和响应时间。
二、管道的原理和创建方法
1.管道的原理
管道是一种在内核中实现的进程间通信机制,通过共享内存缓冲区来传递数据。默认情况下,管道的缓冲区大小为4KB,它提供了一种读写进程之间的同步机制,读取进程会阻塞直到有数据可读,写入进程会阻塞直到有足够的空间可写。通过调整缓冲区大小,可以提高管道的读写效率。
2.管道的创建方法
创建管道的方法可以使用系统调用pipe()函数。这个函数会在内核中创建一个管道,并返回两个文件描述符,一个用于读取数据,另一个用于写入数据。
下面是使用pipe()函数创建管道的基本步骤:
1.包含头文件:首先需要包含头文件<unistd.h>,该头文件中声明了pipe()函数的原型。
2.调用pipe()函数:使用pipe()函数创建管道,并将返回的文件描述符存储在一个整型数组中。通常,数组的第一个元素用于读取数据,第二个元素用于写入数据。
#include <unistd.h> int pipe(int fd[2]);
参数fd是一个整型数组,长度为2。调用成功时,fd[0]表示管道的读取端,fd[1]表示管道的写入端。如果调用失败,返回值为-1。
3.使用管道进行进程间通信:通过文件描述符进行数据的读取和写入操作。一般来说,调用fork()函数创建子进程后,可以将父子进程分别关闭不需要的文件描述符,然后使用剩下的文件描述符进行进程间的数据交换。
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <string.h> int main(int argc, char** argv) { pid_t pid; int fd[2]; char buf[1024]; pipe(fd);//创建管道 if((pid = fork()) == 0) { /*子进程*/ close(fd[0]);//关闭读端 write(fd[1], "Hello", strlen("Hello") + 1); close(fd[1]); } else { /*父进程*/ close(fd[1]);//关闭写端 read(fd[0], buf, 1024); printf("parent read buf : %s\n", buf); close(fd[0]); } return 0; }
3.管道的局限性
1.单向传输:管道是单向的,数据只能在一个方向上流动。在创建管道时,需要确定一个进程负责写入数据,另一个进程负责读取数据。如果需要双向通信,需要创建两个管道。
2.半双工通信:管道是半双工的,即同一时间只能有一个进程进行读取或写入操作。当一个进程在读取数据时,另一个进程必须等待,反之亦然。这种限制可以通过使用多线程或者使用多个管道来解决。
3.父子进程限制:管道通常用于父子进程之间的通信。如果需要实现其他进程间通信,如兄弟进程或无关进程之间的通信,使用管道就不够灵活。在这种情况下,可以考虑使用其他进程间通信机制,如命名管道、共享内存或消息队列等。
4.有限的缓冲区:管道在内核中具有有限的缓冲区。当写入数据超过缓冲区大小时,写入进程将被阻塞,直到有足够的空间来容纳数据。同样,当读取进程尝试读取数据时,如果缓冲区为空,读取进程也会被阻塞。这可能导致进程阻塞或数据丢失的问题。
5.匿名管道限制:管道是匿名的,只能在具有共同祖先的进程之间使用。如果需要在没有共同祖先的进程之间进行通信,就不能使用匿名管道,而需要使用其他适合的进程间通信机制。
三、创建两个管道进行双向通信
由于管道特性的原因只能够支持半双工通信,那么想要支持全双工通信该怎么做呢?那么这里就可以创建两个管道出来进行全双工通信:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <string.h> int main(int argc, char** argv) { pid_t pid; int fd1[2]; int fd2[2]; char buf1[1024]; char buf2[1024]; pipe(fd1);//创建管道1 pipe(fd2);//创建管道2 if((pid = fork()) == 0) { /*子进程*/ close(fd1[0]);//关闭读端 write(fd1[1], "Hello Parent", strlen("Hello Parent") + 1); close(fd1[1]); close(fd2[1]);//关闭写端 read(fd2[0], buf2, 1024); printf("child read buf : %s\n", buf2); close(fd2[0]); } else { /*父进程*/ close(fd1[1]);//关闭写端 read(fd1[0], buf1, 1024); printf("parent read buf : %s\n", buf1); close(fd1[0]); close(fd2[0]);//关闭读端 write(fd2[1], "Hello Child", strlen("Hello Child") + 1); close(fd2[1]); } return 0; }
四、兄弟进程间的通信
管道除了可以进行父子之间的通信还可以进行兄弟之间的通信。
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <string.h> #include <sys/wait.h> int main(int argc, char** argv) { pid_t pid; int fd[2]; char buf[1024]; int i = 0; pipe(fd);//创建管道 for(i = 0; i < 2; i++) { if((pid = fork()) == 0) { break; } } if(pid > 0) { /*父进程*/ printf("Parent pid : %d\n", getpid()); close(fd[0]); close(fd[1]); wait(NULL); wait(NULL); } if(i == 0) { /*兄进程*/ printf("old brother pid : %d\n", getpid()); close(fd[0]);//关闭读端 write(fd[1], "Hello brother", strlen("Hello brother") + 1); close(fd[1]); } else if(i == 1) { /*弟进程*/ printf("brother pid : %d\n", getpid()); close(fd[1]);//关闭写端 read(fd[0], buf, 1024); printf("brother read buf : %s\n", buf); close(fd[0]); } return 0; }
注意点:
父进程需要将读端和写端都关闭,将使用权限给兄弟进程,这样才能保证数据的正常传输。
五、管道的读写行为
当读端不存在时
管道的表现取决于操作系统的实现。一般情况下,当读端不存在时,写入进程会收到一个错误信号(SIGPIPE)。这是因为写入进程试图往一个没有读取端的管道写入数据,操作系统会认为这是一个非法操作,因此向写入进程发送SIGPIPE信号,通知其管道的读取端已经不存在。如果写入进程没有处理这个信号,它可能会被终止。因此,在使用管道进行通信时,写入进程应该处理SIGPIPE信号,以避免意外终止。
当写端不存在时
读取进程会从管道中读取数据,直到读取完所有的数据。读取进程不会收到错误信号,因为写入端不存在不会影响读取端的操作。读取进程会读取到写入进程已经写入的数据,并在读取完数据后返回一个特殊的值,通常是0,表示已经读取到了管道的末尾。
总结
本篇文章就讲解到这里,下篇文章讲解fifo有名管道。