管道
一、介绍
在 Linux 中,管道(pipe)是一种进程间通信(IPC)的方式,它允许一个进程将其输出直接连接到另一个进程的输入,而无需使用临时文件或文件系统进行中间存储。
二、管道的特点
- 单向通信:管道只能用于单向通信,即数据只能从一个进程的写端流向另一个进程的读端。
- 父子进程间通信:管道通常用于具有血缘关系的进程之间,特别是父子进程。
- 基于字节流的通信:管道中的数据以字节流的形式进行传输。
- 数据一致性:管道内部实现了同步机制,保证了数据的一致性。
- 生命周期:管道的生命周期与进程相关,当进程结束时,管道也会被销毁。
在 Linux 中,管道(pipe)是用于进程间通信(IPC)的一种机制。管道允许一个进程(称为写入者)将数据写入管道,并由另一个进程(称为读取者)从管道中读取数据。根据管道是否有名字,可以将其分为无名管道(也称为普通管道)和有名管道(也称为命名管道或 FIFO)。
1. 无名管道(Anonymous Pipe)
pipe()
函数用于创建一个无名管道(匿名管道),允许相关的进程间进行半双工的数据传输。这个函数的主要目的是在具有共同祖先的进程间建立一个通信渠道,通常是在父子进程之间。无名管道是通过文件描述符实现的,其中一个文件描述符用于读取,另一个用于写入。
函数原型如下:
#include <unistd.h>
int pipe(int pipefd[2]);
- pipefd[2] :这是一个整型数组,用于接收由
pipe()
函数返回的两个文件描述符。 -
pipefd[0]
是读取端(只读) -
pipefd[1]
是写入端(只写) - 如果函数执行成功,它将返回 0;如果出现错误,则返回-1 并设置
errno
变量。
示例(C 语言) :
#include <unistd.h> // 包含unistd.h头文件,用于定义pipe、fork、write、read等函数
#include <stdio.h> // 标准输入输出库,用于printf等
#include <sys/types.h> // 包含pid_t类型定义
#include <string.h> // 字符串操作函数库,如strlen
#include <stdlib.h> // 通用工具库,如exit()
#include <sys/wait.h> // 包含wait/waitpid等用于等待子进程的函数
int main()
{
int fd[2]; // 创建一个整型数组fd,用于存储管道的两个描述符
// 创建管道,fd[0]为读端,fd[1]为写端。失败则返回-1。
if(pipe(fd)==-1){
printf("create pipe failed\n");
return 1; // 失败则退出程序
}
pid_t pid; // 创建pid_t类型的变量pid,用于存储fork()返回的子进程ID
char buf[128]; // 创建一个大小为128的字符数组,用于存储读取的数据
// 调用fork创建子进程
pid = fork();
if(pid<0){
perror("fork failed"); // fork失败打印错误信息
return 1; // 并退出程序
}else if(pid>0){ // 父进程执行的代码块
sleep(3); // 父进程等待3秒,确保子进程先运行并准备好读取
printf("this is father\n");
close(fd[0]); // 父进程中不需要读取,关闭读端描述符
// 向管道写入数据,内容为"hello from father"及字符串结束符'\0'
write(fd[1],"hello from father",strlen("hello from father")+1);
wait(NULL); // 等待子进程结束
}else if(pid == 0){ // 子进程执行的代码块
printf("this is son\n");
close(fd[1]); // 子进程中不需要写入,关闭写端描述符
// 从管道读取数据到buf中,最多读取127字节
read(fd[0],buf,127);
buf[127]='\0'; // 确保buf是字符串,手动添加结束符
printf("read from father:%s\n",buf); // 打印读取到的信息
exit(0); // 子进程执行完毕,正常退出
}
return 0; // 父进程执行完毕,正常退出
}
2. 有名管道(Named Pipe 或 FIFO)
有名管道克服了无名管道只能在具有亲缘关系的进程间通信的限制,允许任何两个进程通过文件名来访问和使用管道。使用 mkfifo()
或 mknod()
系统调用来创建一个有名管道。
有名管道(Named Pipe),也称为 FIFO(First In First Out),在 Linux 中可以使用 mkfifo()
系统调用来创建。以下是 mkfifo()
函数的原型和一些基本的使用说明。
函数原型
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
-
pathname
:指定要创建的有名管道的路径名。 -
mode
:指定新创建的有名管道的权限位,与chmod
命令的模式参数相似,通常会结合S_IRUSR
、S_IWUSR
、S_IRGRP
、S_IWGRP
、S_IROTH
、S_IWOTH
等宏来设置。
返回值
- 成功时返回 0。
- 出错时返回-1,并设置
errno
。
错误代码
常见的错误代码包括但不限于:
-
EACCES
:没有足够的权限去创建文件。 -
EEXIST
:指定的路径名已经存在并且不是一个 FIFO。 -
ENOENT
:路径名的某个目录成分不存在。 -
ENOMEM
:无法分配内存。
使用说明
-
创建有名管道:首先,你需要使用
mkfifo()
函数在一个指定的路径下创建一个有名管道。例如:#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <errno.h> int main() { if (mkfifo("/tmp/myfifo", 0666) == -1) { // 0666为权限模式 perror("mkfifo"); return 1; } printf("FIFO created successfully.\n"); return 0; }
-
读写操作:一旦有名管道创建成功,就可以像普通文件那样通过文件描述符进行读写操作。通常,一个进程打开管道进行写入,另一个进程打开同一管道进行读取。
- 读取端:使用
open()
函数以读取模式(O_RDONLY
)打开管道,然后使用read()
函数读取数据。 - 写入端:使用
open()
函数以写入模式(O_WRONLY
)打开管道,然后使用write()
函数写入数据。
- 读取端:使用
-
关闭管道:读写完成后,应使用
close()
函数关闭文件描述符。
注意:有名管道遵循先进先出的原则,且必须先有读进程打开管道,否则写进程在尝试写入时会被阻塞(除非使用了非阻塞模式)。同样,如果只有写进程而没有读进程,则写进程在写满管道缓存后也会被阻塞。
三、有名管道(FIFO)在 Linux 中的阻塞行为主要体现在读取和写入操作上,具体表现如下:
1、阻塞读取(读端)
当一个进程尝试从有名管道读取数据时,它的行为取决于管道的状态:
-
有数据可用:如果管道中有数据等待读取,
read()
调用将立即返回,读取数据并解除阻塞状态。 -
管道为空:
- 阻塞模式:如果该进程是以阻塞模式打开管道(即在
open()
调用中没有指定O_NONBLOCK
标志),那么read()
调用将会阻塞,直到有数据可读或者接收到一个信号中断了阻塞状态。 - 非阻塞模式:如果是以非阻塞模式打开(指定了
O_NONBLOCK
),read()
会立即返回-1,并设置errno
为EAGAIN
或EWOULDBLOCK
,表明没有数据可读且不希望进程阻塞。
- 阻塞模式:如果该进程是以阻塞模式打开管道(即在
2、阻塞写入(写端)
写入有名管道的行为也依赖于管道的状态:
-
空间足够:如果管道的缓冲区有足够的空间来容纳待写入的数据,
write()
调用将立即执行并返回实际写入的字节数。 -
管道已满:
- 阻塞模式:如果写进程是以阻塞模式打开管道,
write()
调用将阻塞,直到有其他进程从管道中读取数据腾出空间,或者接收到一个信号中断了阻塞状态。 - 非阻塞模式:在非阻塞模式下,如果管道已满,
write()
调用会立即返回-1,并设置errno
为EAGAIN
或EWOULDBLOCK
,告知调用者现在不能写入更多数据。
- 阻塞模式:如果写进程是以阻塞模式打开管道,
3、实际应用中的考虑
在设计使用有名管道的程序时,开发者需要根据程序的需求选择合适的阻塞模式:
- 同步操作:如果需要保证数据的顺序性和完整性,可能会选择阻塞模式,确保数据被完全处理后再进行下一步操作。
- 异步操作:对于高性能或实时性要求较高的应用,非阻塞模式更合适,程序可以在数据不可用时执行其他任务,而不是等待。
四、管道程序例子示范
首先是 read4.c
文件,这个程序的目的是创建一个命名管道(FIFO),然后从这个管道中读取数据。
#include <sys/types.h> // 包含基本的系统类型定义
#include <sys/stat.h> // 包含文件和文件系统相关的系统级操作
#include <cstdio> // 包含标准输入输出流的定义
#include <errno.h> // 包含错误号定义
#include <fcntl.h> // 包含文件控制选项的定义
#include <unistd.h> // 包含UNIX标准函数,如close()
int main() {
// 创建一个命名管道,mode设置为0600,表示只有所有者有读写权限
if(mkfifo("./file", 0600) == -1 && errno != EEXIST) {
printf("mkfifo failed\n"); // 如果创建失败且不是因为管道已存在
perror("why"); // 打印错误原因
}
int fd = open("./file", O_RDONLY); // 以只读方式打开命名管道
if(fd == -1) {
perror("open failed"); // 如果打开失败,打印错误原因
return 1; // 退出程序
}
printf("open succeed\n"); // 成功打开,打印提示信息
char buf[30] = {0}; // 创建一个缓冲区用于存储读取的数据
ssize_t n_read = 0; // 用于存储每次读取的字节数
while(1) { // 无限循环,持续读取
n_read = read(fd, buf, 20); // 尝试从文件描述符fd读取最多20个字节到缓冲区buf
if(n_read == -1) { // 如果读取失败
perror("read failed"); // 打印错误原因
close(fd); // 确保读取失败时关闭文件描述符
return 1;
} else if(n_read == 0) {
printf("FIFO closed by the writer\n");
break;
}
buf[n_read] = '\0'; // 添加字符串结束符
printf("read:%zd, byte:%s\n", n_read, buf); // 打印读取的字节数和内容
}
close(fd); // 关闭文件描述符
return 0; // 正常退出程序
}
接下来是 write4z.c
文件,这个程序的目的是向先前创建的命名管道中写入数据。
#include <sys/types.h> // 包含基本的系统类型定义
#include <sys/stat.h> // 包含文件和文件系统相关的系统级操作
#include <cstdio> // 包含标准输入输出流的定义
#include <errno.h> // 包含错误号定义
#include <fcntl.h> // 包含文件控制选项的定义
#include <unistd.h> // 包含UNIX标准函数,如write()和close()
#include <cstring> // 包含字符串操作函数,如strlen()
int main() {
char *str = "message from fifo"; // 要写入管道的消息
int cnt = 0; // 计数器,用于控制写入次数
int fd = open("./file", O_WRONLY); // 以只写方式打开命名管道
if(fd == -1) {
perror("open failed"); // 如果打开失败,打印错误原因
return 1; // 退出程序
}
printf("write open succeed\n"); // 成功打开,打印提示信息
while(1) {
ssize_t n_written = write(fd, str, strlen(str));
if(n_written == -1) {
perror("write failed");
close(fd); // 确保写入失败时关闭文件描述符
return 1;
}
if(n_written < strlen(str)) {
printf("Buffer not large enough to write the whole string\n");
break;
}
sleep(1);
cnt++; // 增加计数器
if(cnt == 5) {
break;
}
}
close(fd); // 关闭文件描述符
return 0; // 正常退出程序
}
这两个程序共同演示了 Linux系统中命名管道(FIFO)的使用。read4.c
创建并打开一个 FIFO 用于读取,而 write4z.c
打开同一个 FIFO 用于写入。read4.c
中的循环会持续读取 FIFO 中的数据,直到程序读取 5 次中断退出。write4z.c
中的循环会写入固定的消息到 FIFO 中,并且每写入一次就暂停一秒,5 次后退出
要使这两个程序协同工作,首先需要运行 read4.c
程序,然后运行 write4z.c
程序。这样,write4z.c
程序就可以向 FIFO 中写入数据,而 read4.c
程序则可以从 FIFO 中读取这些数据。需要注意的是,FIFO 是一种进程间通信的方式,通常在不同的进程中运行读取和写入操作。