什么是管道
管道是Linux进程间的一种通信方式,两个进程可以通过一个共享内存区域来传递信息,并且管道中的数据只能是单向流动的,也就是说只能有固定的写进程和读进程。
管道可以分为两种类型:匿名管道和命名管道。
匿名管道
匿名管道只能在父子进程间进行通信,其具体读写规则有:
- 管道内无数据时,读端会发生阻塞直到有数据可读
- 管道数据满时,写端会发生阻塞,直到读端开始读取数据
- 如果写端对应的文件描述符被关闭,
read
函数返回0,但可以将数据读完 - 如果读端对应的文件描述符被关闭,在执行
write
函数时会产生SIGPIPE
信号,其默认行为会导致当前进程终止。
匿名管道中的数据是存储在内存中的。
可通过函数pipe
创建一个匿名管道,匿名管道相关的函数定义于头文件unistd.h
中:
int pipe (int fd[2]);
该函数需要传入一个长度为2的数组,函数执行完成后,如果返回0则代表管道创建成功,其中fd[0]
为只读属性的文件描述符,fd[1]
为只写属性的文件描述符,分别对应管道的读端和写端操作:
从上图中可以看出,如果我们将数据写入fd[1]
,那么从fd[0]
就可以获得对应的数据:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd[2];
if (pipe(fd) == -1) {
fprintf(stdout, "Can not create pipe.\n");
_exit(1);
}
char buf[512];
while (1) {
fprintf(stdout, "write data to fd[1]->");
fflush(stdout);
fscanf(stdin, "%s", buf); //从键盘读入数据
size_t len = strlen(buf);
write(fd[1], buf, len); //写入到管道
memset(buf, 0, sizeof(buf)); //清空buf缓冲区
read(fd[0], buf, len); //从管道读取数据
fprintf(stdout, "read data from fd[0]->%s\n", buf);
}
return 0;
}
gcc编译上述程序并运行:
[root@localhost Debug]# ./test-app
write data to fd[1]->message
read data from fd[0]->message
write data to fd[1]->abc
read data from fd[0]->abc
write data to fd[1]->^C
单纯在一个进程中使用管道的写端和读端没什么意义,因为管道本身就是为父子进程的通信设计的,fork
出的进程继承了父进程打开的文件描述符,所以我们可以利用这一点来使用管道进行父子进程通信:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd[2];
if (pipe(fd) == -1) {
fprintf(stdout, "Can not create pipe.\n");
_exit(1);
}
pid_t pid = fork();
if (pid < 0) {
fprintf(stdout, "Fork failure\n");
} else if (pid == 0) { //子进程
close(fd[0]); //子进程关闭读端
char str[] = "message"; //将"message"传递给父进程
write(fd[1], str, strlen(str));
} else { //父进程
close(fd[1]); //父进程关闭写端
char str[100];
read(fd[0], str, 100);
fprintf(stdout, "Parent: read data from pipeline (%s)", str);
}
return 0;
}
gcc编译上述程序并执行:
[root@localhost Debug]# ./test-app
Parent: read data from pipeline (message)
可以发现父进程通过管道获得了子进程向管道写入的"message"字符串,此时两个进程的管道示意图如下:
如果在fork进程之后不关闭管道,那么示意图如下:
命名管道
命名管道本质上是一个管道文件,它基于文件系统来实现进程间的通信,其读写端进程可以不是父子进程的关系,只需要进程有权限访问该管道文件即可。需要注意的是,命名管道中的数据实际上是存储在内存中,管道文件在文件系统中相当于是一个标记。
创建命名管道有两种方式:
- 在命令行界面通过命令
mkfifo filename
创建命名管道文件,可以指定其文件名 - 在程序内部调用
mkfifo
函数(定义于头文件sys/stat.h
中),参数filename
为管道文件路径,mode
为管道文件读写权限:
int mkfifo(const char *filename, mode_t mode);
命名管道的读写机制和匿名管道相似。
只不过在使用前我们需要调用open
函数来打开管道文件,通过其返回的文件描述符来读写管道文件。
下面两个程序展示了命名管道的用法:
写端:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
mkfifo("pipe", 0644); //创建一个管道文件pipe,权限为644
int fd = open("pipe", O_WRONLY); //以只写的方式打开管道文件,返回该管道的文件描述符
if (fd == -1) {
fprintf(stderr, "can not open pipe file\n");
_exit(1);
}
char buf[1024];
while (1) {
fprintf(stdout, "Enter message:");
fflush(stdout);
if (fscanf(stdin, "%s", buf) == EOF) { //从键盘读入一串字符串到buf
break;
}
write(fd, buf, strlen(buf)); //将buf写入到管道
}
close(fd);
return 0;
}
读端:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
int fd = open("pipe", O_RDONLY); //以只读方式打开管道文件pipe
if (fd == -1) {
fprintf(stdout, "can not open pipe file\n");
_exit(1);
}
char buf[1024];
while (1) {
ssize_t len = read(fd, buf, sizeof(buf)); //从管道读入数据到buf
if (len <= 0) {
break;
}
fprintf(stdout, "receive:%s\n", buf); //打印buf中的数据
}
close(fd);
return 0;
}
将上述两个源代码文件放在同一个目录下,并使用gcc编译,程序名为read
和write
。同时打开两个终端,前者运行read
程序,后者运行write
程序。在运行write
程序的终端用键盘输入一串消息并按下回车,此时在另外一个终端立刻就能收到这个消息:
[root@localhost Debug]# ./write
Enter message:abcdef
Enter message:abncd
Enter message:sdasda
Enter message:asdadsa
另外一个终端:
[root@localhost Debug]# ./read
receive:abcdef
receive:abncdf
receive:sdasda
receive:asdadsa