pipe
管道(pipe)
管道是一种最基本的IPC机制, 作用于有血缘关系的进程之间通信, 完成数据传递.
本质:
伪文件, 实质为内核缓冲区不占用磁盘空间
特点:
两部分:
读端, 写端, 对应两个文件描述符
数据写端流入, 读端流出
操作管道的进程被销毁之后, 管道自动占用的存储空间自动被释放
管道默认阻塞, 读写都为阻塞
原理:
内部实现方式: 环形队列
缓冲区大小默认为4k, 大小会根据时间情况做适当调整
管道的局限性:
自己先读数据时, 不能自己写, 阻塞
自己先写数据时, 可以自己读
数据自己读不能自己写? 有些疑问, 阻塞了?
数据一旦被读走之后, 便不在管道中存在, 不可反复读取
半双工的通信方式. 数据只能在一个方向上流动
只能在公共祖先的进程间使用管道
管道的读写行为
使用管道需要注意以下4中特殊情况(假设都是阻塞I/O操作, 没有设置O_NONBLOCK标志):
(1)如果所有指向管道写端的文件描述符都关闭(管道写端的引用计数为0), 而仍然有进程从管道的读端读数据, 那么管道中剩余的数据都被读取后, 再次read会返回0, 就像读到文件末尾一样
(2)如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0), 而持有管道写端的进程也没有向管道中写数据, 这时有进程从管道读端读数据, 那么管道中剩余的数据都被读取后, 再次read会阻塞, 直到管道中有数据可读才读取数据并返回
(3)如果所有指向管道读端的文件描述符都关闭(管道读端引用计数为0), 而持有管道写端的进程也没有向管道中写数据, 这时有进程向管道的写端write, 那么该进程会收到信号SIGPIPE, 通常会导致进程异常终止. 当然可以对SIGPIPE信号进行捕捉, 不终止进程
(4)如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0), 而持有管道读端的进程也没有从管道中读数据, 这时有进程向管道写端写数据, 那么在管道被写满时再次write会阻塞, 直到管道中有空位置了才写入数据
总结
读操作:
有数据: read(fd), 正常读, 返回读出的字节数
无数据: (1) 写端全部关闭, read解除阻塞, 返回0, 相当于读文件到尾部; (2) 没有全部关闭, read阻塞
写操作
读端全部关闭: 管道破裂, 进程被终止, 内核给当前进程发送SIGPIPE
读端没有全部关闭: (1) 缓冲区写满, write阻塞; (2) write继续写
管道的优劣
优点: 简单, 相比信号和套接字实现进程间通信简单很多
缺点:
- 只能单向通信, 双向通信需要建立两个管道
- 只能用于父子, 兄弟(有共同祖先)间通信. 可使用命名管道fifo解决
基础API
pipe
头文件: #include <unistd.h>
int pipe(int pipefd[2]);
函数调用成功后返回r/w两个文件描述符. 无须open, 但需手动close. 规定fd[0]读, fd[1]写. 向文件读写数据其实是在读写内核缓冲区
管道创建成功以后, 创建该管道的进程(父进程)同时掌握着管道的读端和写端. 采用以下步骤实现父子进程间的通信:
(1)父进程调用pipe函数创建管道, 得到两个文件描述符fd[0]和fd[1]指向管道的读端和写端
(2)父进程调用fork创建子进程, 那么子进程也有两个文件描述符指向同一管道
(3)父进程关闭管道读端, 子进程关闭管道写端. 父进程可以像管道中写入数据, 子进程将管道中的数据读出. 由于管道是利用环形队列实现的, 数据从写端流入管道, 从读端流出, 这样就实现了进程间通信
父子进程使用管道通信, 思考:
父子进程间通信是否需要sleep函数?
父进程写的慢, 子进程读的快
如何设置非阻塞?
默认读写两端都阻塞
设置读端为非阻塞pipe(fd)
fcntl-变参函数: (1) 赋值文件描述符; (2) 修改文件属性-对应open时的flag属性
设置方法:
int flags = fcntl(fd[0], F_GETFL);
// 设置新的flags
flag |= O_NONBLOCK;
fcntl(fd[0], F_SETFL, flags);
fpathconf与pathconf
头文件: #include <unistd.h>
long fpathconf(int fd, int name);
long pathconf(char *path, int name);
查看管道缓冲区大小:
命令: ulimit -a
函数: fpathconf
示例程序
自己读写阻塞情况
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
int main(void) {
int fd[2];
char buf[10];
char p[] = "haha";
int ret = pipe(fd);
if (ret == -1) {
perror("pipe error");
exit(1);
}
printf("pipe[0] = %d\n", fd[0]);
printf("pipe[1] = %d\n", fd[1]);
// 先读, 阻塞
//ret = read(fd[0], buf, sizeof(buf));
write(fd[1], p, sizeof(p));
// 后写, 没有被阻塞
ret = read(fd[0], buf, sizeof(buf));
printf("ret = %d, %s, sizeof(buf)=%d\n", ret, buf, sizeof(buf));
close(fd[0]);
close(fd[1]);
return 0;
}
/*
pipe[0] = 3
pipe[1] = 4
ret = 5, haha, sizeof(buf)=10
*/
管道执行ps和grep命令
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
int main(int argc, const char* argv[]) {
int fd[2];
int ret = pipe(fd);
if (ret == -1) {
perror("pipe error");
exit(1);
}
pid_t pid = fork();
if (pid == -1) {
perror("fork error");
exit(1);
}
// 父进程: ps -aux
if (pid > 0) {
// 写管道操作, 关闭读端
close(fd[0]);
// 文件描述符重定向, STDOUT_FILENO,
dup2(fd[1], STDOUT_FILENO);
// 执行ps aux
execlp("ps", "ps", "aux", NULL);
perror("excelp");
exit(1);
}
// 子进程: grep "bash"
else if (pid == 0) {
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("grep", "grep", "bash", "--color=auto", NULL);
}
printf("pipe[0] = %d \n", fd[0]);
printf("pipe[1] = %d \n", fd[1]);
close(fd[0]);
close(fd[1]);
return 0;
}
进程通信
兄弟进程间通信, 父进程用于回收子进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/wait.h>
int main(int argc, const char* argv[]) {
int fd[2];
int ret = pipe(fd);
if (ret == -1) {
perror("pipe error");
exit(1);
}
int i = 0;
for (i = 0; i < 2; i++) {
pid_t pid = fork();
if (pid == 0)
break;
if (pid == -1) {
perror("fork error");
exit(1);
}
}
// 子进程1: ps -aux
if (i == 0) {
// 写管道操作, 关闭读端
close(fd[0]);
// 文件描述符重定向, STDOUT_FILENO,
dup2(fd[1], STDOUT_FILENO);
// 执行ps aux
execlp("ps", "ps", "aux", NULL);
perror("excelp");
exit(1);
}
// 子进程2: grep "bash"
else if (i == 1) {
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("grep", "grep", "bash", "--color=auto", NULL);
}
// 父进程回收子进程
else if (i == 2) {
close(fd[0]);
close(fd[1]);
wait(NULL);
// 回收子进程
pid_t wpid;
while ((wpid = waitpid(-1, NULL, WNOHANG)) != -1) {
if (wpid == 0)
continue;
printf("child die pid = %d\n", wpid);
}
}
printf("pipe[0] = %d \n", fd[0]);
printf("pipe[1] = %d \n", fd[1]);
return 0;
}
查看管道缓冲区
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
int main(int argc, const char* argv[]) {
int fd[2];
int ret = pipe(fd);
if (ret == -1) {
perror("pipe error");
exit(1);
}
// 测试管道缓冲区大小
long size = fpathconf(fd[0], _PC_PIPE_BUF);
printf("size = %ld\n", size);
printf("pipe[0] = %d \n", fd[0]);
printf("pipe[1] = %d \n", fd[1]);
close(fd[0]);
close(fd[1]);
return 0;
}