管道与命名管道
OVERVIEW
一、管道
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,用于完成数据传递。调用pipe系统函数即可创建管道,其有以下特质:
1.管道特性
-
特性:
- 其本质为一个伪文件(实为内核缓冲区)
- 由两个文件描述符引用,一个表示读端,一个表示写端。
- 规定数据从管道的写端流入管道,从读端流出。
-
原理:
- 管道实际为内核使用环形队列机制,借助内核缓冲区4k实现。
- 向管道文件读写数据其实是在读写内核缓冲区。
-
局限性与优点:
优点:
- 使用简单,相比于信号、套接字实现进程间通信更简单。
局限性:
- 数据不能自己写、自己读。
- 管道中的数据不可反复读取,一旦读走则管道中不再存在。
- 采用半双工通信方式,数据只能在单方向上流动,双向通信需要建立两个管道。
- 只能用于父子、兄弟进程(共同祖先)间通信,该问题后续由命名管道fifo解决。
-
查看管道大小:
-
通过
ulimit -a
命令来查看当前系统中,创建管道文件所对应的内核缓冲区大小: -
也可使用函数,借助参数选项来查看。使用该宏应引入头文件
#include <unistd.h>
long fpathconf(int fd, int name);
long pathconf(char *path, int name);
成功返回管道的大小,失败返回-1并设置errno,
-
2.基本使用
创建并打开管道pip:
int pipe(int pipefd[2]);
- 成功返回0,失败返回-1并设置errno,
- 函数调用成功返回r/w两个文件描述符,无需open但需手动close。规定读端fd[0]->r,写端fd[1]->w。
管道在父子进程中的使用方式:
注:父子进程共享文件描述符
-
父进程调用pipe函数创建管道,得到两个文件描述符pipefd[0]、pipefd[1]分别指向管道的读端和写端。
-
父进程调用fork函数创建子进程,那么子进程也有两个文件描述符指向同一个管道。
-
父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程从管道中读出数据。
由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
char *msg = "This is the content written by the father process.";
int main()
{
int fd[2];
if (pipe(fd) == -1) {
perror("pipe");
exit(1);
}
pid_t pid;
if ((pid = fork()) < 0) {
perror("fork");
exit(1);
}
// 利用管道让父进程写入子进程读出
if (pid > 0) {
printf("i am the father! fd[0] = %d, fd[1] = %d !\n", fd[0], fd[1]);
close(fd[0]);
write(fd[1], msg, strlen(msg));
wait(NULL);
} else {
printf("i am the child! fd[0] = %d, fd[1] = %d !\n", fd[0], fd[1]);
close(fd[1]);
char buff[1024] = {0};
int buff_len = read(fd[0], buff, strlen(msg));
printf("read from pipe[%s], read length[%d]\n", buff, buff_len);
// write(STDOUT_FILENO, buff, buff_len);
}
return 0;
}
3.管道读写行为
使用管道需要注意以下4种特殊情况(假设都是阻塞IO操作,没有设置O_NONBLOCK标志)
读管道:
- 管道中有数据:read返回实际读取到的字节数
- 管道中无数据:
- 写端被全部关闭,read返回0,类似读到文件结尾。
- 写端没有被全部关闭,read阻塞等待(不久的将来可能会有数据到达,此时会让出cpu)
写管道:
- 管道读端全部被关闭:进程异常终止(可对SIGPIPE信号捕捉不终止进程)
- 管道读端没有全部关闭:
- 管道已满:write阻塞
- 管道未满:write将数据写入,并返回实际写入的字节数量
多个写端对应单个读端:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
int main()
{
int fd[2];
if (pipe(fd) == -1) {
perror("pipe");
exit(1);
}
int idx;
pid_t pid;
for (idx = 0; idx < 2; ++idx) {
pid = fork();
if (pid == -1) handle_error("fork error");
if (pid == 0) break;
}
if (idx == 2) { //父进程 打开读端 关闭写端
//sleep(1);
close(fd[1]);
char buff[1024];
int buff_len = read(fd[0], buff, 1024);
write(STDOUT_FILENO, buff, buff_len);
for (int i = 0; i < 2; ++i)
wait(NULL);
} else if (idx == 0) { //子进程1
close(fd[0]);
char msg[1024] = "This is the content written by the chlid process1.\n";
write(fd[1], msg, strlen(msg));
} else if (idx == 1) { //子进程2
close(fd[0]);
char msg[1024] = "This is the content written by the chlid process2.\n";
write(fd[1], msg, strlen(msg));
}
return 0;
}
注意到某些输出中只有子进程1的输出信息,没有子进程2的输出信息,
这是由于管道在被父进程读取后,直接回收掉了,导致子进程2没有完成写入操作。可以在父进程中添加sleep(1)
语句,等所有子进程完成写入后再读取。
4.亲缘进程间通信
父子进程间通信
使用管道实现父子进程间通信,完成 ls | wc -l
命令,假定父进程实现 ls
,子进程实现 wc
命令。
ls
命令正常会将结果写出到stdout,修改为写入管道的写端。wc -l
正常应该从stdin读取数据,修改为读取管道的读端。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
int main() {
int fd[2];
int ret = pipe(fd);
if (ret == -1) {
handle_error("pipe error");
}
pid_t pid = fork();
if (pid == -1) {
handle_error("fork error");
} else if (pid > 0) {
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", NULL); // 父进程
handle_error("execlp ls error");
} else if (pid == 0) {
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL); // 子进程
handle_error("execlp wc error");
}
return 0;
}
兄弟进程间通信
使用管道实现兄弟进程间通信,
- 兄进程:
ls
命令正常会将结果写出到stdout,修改为写入管道的写端。 - 弟进程:
wc -l
正常应该从stdin读取数据,修改为读取管道的读端。 - 父进程:等待回收两个子进程,
- 要求使用循环创建N个子进程模型来创建兄弟进程,使用循环因子
i
来标识。另外需要注意管道的读写行为,
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
int main()
{
// 创建管道
int fd[2];
int ret = pipe(fd);
if (ret == -1) {
handle_error("pipe error");
}
// 创建兄弟进程
int idx;
pid_t pid;
for (int idx = 0; idx < 2; ++idx) {
pid = fork();
if (pid == -1) handle_error("fork error");
if (pid == 0) break;
}
// 兄弟进程间通信
if (idx == 2) { //父进程
close(fd[0]);
close(fd[1]);
for (int i = 0; i < 2; ++i)
wait(NULL);
} else if (idx == 0) { //兄进程
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", NULL);
handle_error("execlp ls error");
} else if (idx == 1) { //弟进程
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
handle_error("execlp wc error");
}
return 0;
}
练习:统计当前系统中进程id大于 10000
的进程个数,
二、命名管道
命名管道fifo(First Input First Output)是一种先进先出的数据缓存器,先进先出队列。
1.命名管道特性
管道只能用于父子/兄弟进程间的通信,对于非血缘关系的进程间通信,可以利用命名管道来解决。
为什么无名管道只适用于亲缘进程间的通信?
无名管道的设计初衷就是为了在亲缘进程之间提供简单高效的通信方式,而不是用于任意进程之间的通信。
-
无名管道:
只能在有亲缘关系的进程间使用,因为通过pipe返回的文件描述符是进程私有的,只有通过fork函数继承的方式才能进行共享。
- 多个进程通过访问在内核申请的管道文件来进行通信,这需要进程知道该管道文件的读写文件描述符,在无法交流的情况下无法通信。
- 而亲缘进程可以在分离进程之前,先在内核空间中申请管道文件,在分离进程之后可通过复制的文件描述符,访问同系统文件表。进而访问管道文件。
-
命名管道:通过文件系统中的路径名实现进程间通信,无需亲缘进程,任何有权限的进程都可以通过路径名访问命名管道。
命名管道是如何实现进程见通信的?
命名管道通过文件系统中的一个特殊文件fifo,来实现进程间通信。
fifo是linux基础文件类型中的一种,但fifo文件在磁盘上没有数据块,仅仅用来标识内核中的一条通道。各进程可以打开这个文件进行read/write,其实际是在读写内核通道,这样就实现了进程间通信。
常见的文件IO函数都可操作于fifo,如:open、close、read、write、unlink等操作,
2.命名基本使用
- 头文件:
<sys/types.h>
、<sys/stat.h>
- 原型:
int mkfifo(const char *pathname);
- pathname:文件路径
- 返回值:成功返回0,失败返回-1
#include <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
int main() {
int flag = mkfifo("fifo_test", 0664);
if (flag < 0) {
if (errno == EEXIST) {//errn = EEXITST管道文件已存在
printf("fifo already exit!\n");
} else {
perror("mkfifo");
exit(1);
}
}
int fifo_fd = open("fifo_test", O_RDWR);
if (fifo_fd < 0) {
perror("open");
exit(1);
}
// 写管道
char buff_in[1024] = { "hello this is a test!\n" };
write(fifo_fd, buff_in, strlen(buff_in));
// 读管道
char buff_out[1024] = { 0 };
int len = read(fifo_fd, buff_out, 22);
write(STDOUT_FILENO, buff_out, len);
return 0;
}
3.命名管道非亲缘进程间通信
利用命名管道实现两个非血缘关系,进程间的通信。
写进程向命名管道中循环写入数据:
#include <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <stdbool.h>
int main()
{
int flag = mkfifo("fifo_test_rw", 0664);
if (flag < 0) {
if (errno == EEXIST) {//errn = EEXITST管道文件已存在
printf("fifo already exit!\n");
} else {
perror("mkfifo");
exit(1);
}
}
int fifo_fd = open("fifo_test", O_WRONLY);
if (fifo_fd < 0) {
perror("open");
exit(1);
}
// 写管道
int i = 0;
char buff_in[1024] = { 0 };
while (true) {
memset(buff_in, 0, sizeof(buff_in));
sprintf(buff_in, "hello this is test[%d]!\n", ++i);
write(fifo_fd, buff_in, strlen(buff_in));
sleep(1);
}
close(fifo_fd);
return 0;
}
读进程从命名管道中循环读出数据:
#include <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <stdbool.h>
int main()
{
int fifo_fd = open("fifo_test", O_RDONLY);
if (fifo_fd < 0) {
perror("open");
exit(1);
}
// 读管道
char buff_out[1024] = { 0 };
while (true) {
memset(buff_out, 0, sizeof(buff_out));
int len = read(fifo_fd, buff_out, 22);
write(STDOUT_FILENO, buff_out, len);
sleep(1);
}
close(fifo_fd);
return 0;
}
三、其他
1.popen使用
popen如果没有pclose则将成为僵尸进程,
#include "head.h"
int main() {
/**
* 创建了一个管道fork
* 选择父子进程谁写谁读
* 使用popen&pclose实现cat查看当前文件内容
*/
//1.打开一个文件 从文件流中读取文件内容
FILE *file;
if ((file = popen("/bin/cat ./3.popen.c", "r")) == NULL) {
perror("popen");
exit(1);
}
//2.父进程通过管道 将管道流的内容去读出来并打印
size_t readsize;
char buff[1024] = {0};
while (readsize = fread(buff, 1, sizeof(buff), file) != 0) {
printf("%s", buff);
}
pclose(file);
return 0;
}
2.popen实现
实现自己的my_popen
程序:
#include "head.h"
#include "my_popen.h"
int main() {
/**
* 创建了一个管道fork
* 选择父子进程谁写谁读
* 使用popen&pclose实现cat查看当前文件内容
*/
//1.打开一个文件 从文件流中读取文件内容
FILE *file;
//if ((file = popen("/bin/cat ./3.popen.c", "r")) == NULL) {
if ((file = my_popen("/bin/cat ./3.popen.c", "r")) == NULL) {
perror("popen");
exit(1);
}
//2.父进程通过管道 将管道流的内容去读出来并打印
size_t readsize;
char buff[1024] = {0};
while (readsize = fread(buff, 1, sizeof(buff), file) != 0) {
printf("%s", buff);
}
//pclose(file);
my_pclose(file);
return 0;
}
//my_popen.h
#ifndef _MY_POPEN_H
#define _MY_POPEN_H
FILE *my_popen(const char *command, const char *type);//popen打开一个文件 返回文件流的指针
int my_pclose(FILE *fp);//pclose
#endif
//my_popen.c
#include "head.h"
//借助文件描述符的使用特性 实现pclose利用文件描述符访问相应进程号(进程号与文件描述符关联起来)
static pid_t *childpid = NULL;
static int maxfd = 0;//能够获得的最大数量的文件描述符个数
FILE *my_popen(const char* command, const char* type) {
FILE *fp;
//对type的错误处理
if ((type[0] != 'r' && type[0] != 'w') || type[1] != '\0') {
errno = EINVAL;
return NULL;
}
//1.pipe创建管道
int pipefd[2];
if (pipe(pipefd) < 0) return NULL;
//2.fork创建进程
pid_t pid;
if ((pid = fork()) < 0) return NULL;
//3.管道逻辑 利用子进程excel做事情
if (pid == 0) {//子进程
//3.1 关闭没有必要的文件描述符
if (type[0] == 'r') {//父进程读取操作
close(pipefd[0]);//需要将子进程用不上的文件描述符关闭
//将子进程应该输出的内容 输出到管道中(利用dup函数将stdout与管道文件关联起来) 让父进程能够拿到这些数据
if (pipefd[1] != STDOUT_FILENO) dup2(pipefd[1], STDOUT_FILENO);//同时可访问子进程的标准输出
close(pipefd[1]);
} else {//父进程写操作
close(pipefd[1]);
if (pipefd[0] != STDIN_FILENO) dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
}
//3.2 执行子进程该做的其他事务
execl("/bin/sh", "sh", "-c", command, NULL);
} else {//父进程
//3.3 要拿到什么数据 并作为my_popen的返回值返回
if (type[0] == 'r') {
close(pipefd[1]);
if ((fp = fdopen(pipefd[0], type)) == NULL) return NULL;//将打卡文件描述符形式 转化为文件流的形式
} else {
close(pipefd[0]);
if ((fp = fdopen(pipefd[1], type)) == NULL) return NULL;//将打卡文件描述符形式 转化为文件流的形式
}
}
//4.将进程id与文件描述符进行关联 fileno将文件流指针转换为文件描述符
if (childpid == NULL) {
maxfd = sysconf(_SC_OPEN_MAX);//sysconf拿到的宏定义(最大能够打开的文件描述符)
if ((childpid = (pid_t *)calloc(maxfd, sizeof(pid_t))) == NULL) {//为childpid申请内存空间
return NULL;
}
}
childpid[fileno(fp)] = pid;
return fp;
}
int my_pclose(FILE *fp) {
int status;
int fd = fileno(fp);//将文件流转换为文件描述符
pid_t pid = childpid[fd];//获取到文件描述符对应的进程pid信息
if (pid == 0) {
errno == EINVAL;
return -1;
}
fflush(fp);//刷新文件流
close(fd);
wait4(pid, &status, 0, NULL);//wait4等待子进程状态改变(根据进程pid拿到关闭状态)
return status;
}