在 十、多进程编程 中介绍了,虽然父子进程有独立的数据空间,但它们具有相同的内核空间,因此得以实现进程间通信
管道是一种基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递;调用 pipe 系统函数即可创建管道,具有以下性质:
原理:管道实际上是内核使用环形队列机制,借助内核缓冲区(4KB)实现
管道具有如下局限性:
常见的通信方式有:单工通信(数据单向流动)、半双工通信(同时单向流动)、全双工通信(可同时双向流动)
包含头文件:
#include
int pipe(int pipefd[2]);
创建并打开一个管道
int pipefd[2]
传出参数,分别代表管道的读写端的文件描述符:pipefd[0]
表示读端,pipefd[1]
表示写端
返回值
成功时返回 0,失败返回 -1、errno
通过 pipe()
创建管道后,管道的读端和写端均打开,由于父子进程共享文件描述符,因此父子进程均可访问到管道的读写端
✅当 父进程写,子进程读 时:父进程应调用 close(fd[0]);
关闭读端;子进程应调用 close(fd[1]);
关闭写端
✅当 父进程读,子进程写 时:父进程应调用 close(fd[1]);
关闭写端;子进程应调用 close(fd[0]);
关闭读端
然后通过 write()
和 read()
函数进行IPC
例如:通过管道实现子进程写,父进程读
#include
#include
#include
#include
#include
#include
// 出错检查函数
void sys_err(int ret, const char *str)
{
if(ret == -1)
{
perror(str);
exit(1);
}
}
int main(int argc, char *argv[])
{
// 管道读写端
int fd[2];
// 创建管道
int ret = pipe(fd);
sys_err(ret, "pipe error");
// 创建子进程
pid_t pid = fork();
sys_err(pid, "fork error");
if(pid == 0)
{
// 子进程进行写操作
close(fd[0]); // 关闭读端
char *str = "Hello Pipe IPC!\n";
write(fd[1], str, strlen(str));
close(fd[1]);
}else
{
wait(NULL); // 回收子进程
// 父进程进行读操作
int n;
close(fd[1]); // 关闭写端
char buf[1024];
// 如果读到了数据,就进入循环
while((n = read(fd[0], buf, sizeof(buf))) != 0)
{
// 写到屏幕上
write(STDOUT_FILENO, buf, n);
}
close(fd[0]);
}
return 0;
}
✅读取管道行为:
✅写入管道行为:
例如:实现终端命令 ls | wc -l
wc [选项] [文件名]
根据选项对文件执行指令;
如果不指定文件,可以使用管道 | ,则以管道内容为目标;
若没有指定文件名,也没有使用管道,则以键盘输入stdin中的内容为目标
[选项]:
-c
显示一个文件的字节数
-m
显示一个文件的字符数
-l
显示一个文件的行数
-L
显示一个文件中的最长行的长度
-w
显示一个文件的字数
因此 ls | wc -l
就是通过管道,统计 ls
打印的内容的文件行数(Terminal以空格为一行)
代码:
#include
#include
#include
#include
#include
#include
void sys_err(int ret, const char *str)
{
if(ret == -1)
{
perror(str);
exit(1);
}
}
int main(int argc, char *argv[])
{
int fd[2];
int ret = pipe(fd);
sys_err(ret, "pipe error");
pid_t pid = fork();
sys_err(pid, "fork error");
if(pid == 0)
{
// 关闭读端
close(fd[0]);
// 将输出屏幕的内容重定向到管道写端
ret = dup2(fd[1], STDOUT_FILENO);
sys_err(ret, "dup2 error");
execlp("ls", "ls", NULL); // 执行 ls
sys_err(-1, "execlp ls error");
}else
{
// 阻塞等待子进程
wait(NULL);
// 关闭写端
close(fd[1]);
// 由从键盘获取重定向到从管道读端读取
ret = dup2(fd[0], STDIN_FILENO);
sys_err(ret, "dup2 error");
execlp("wc", "wc", "-l", NULL); // 执行 wc -l
sys_err(-1, "execlp wc error");
}
return 0;
}
注意:管道允许有一个写端,多个读端,即一个进程写,多个进程读,但数据一旦被一个进程读走了就不会再被读到了
注意:管道允许有一个读端,多个写端,即多个进程写,一个进程读,但写入的顺序不能保证,需要通过一些机制来进行调整,如 sleep()
等
ulimit -a
✅优势:
✅劣势:
创建有名管道的方法有两个:1.终端命令,2.库函数 mkfifo(第 3 卷)
mkfifo [文件名]
创建有名管道
包含头文件:
#include
#include
#include
已包含 #include
int mkfifo(const char *pathname, mode_t mode);
创建有名管道
const char *pathname
文件路径
mode_t mode
创建有名管道时指定的权限,受 umask 约束,即 权限 =mode & ~(umask)
返回值
成功时返回 0,失败返回 -1、errno
例如:创建有名管道
#include
#include
#include
#include
void sys_err(int ret, const char *str)
{
if(ret == -1)
{
perror(str);
exit(1);
}
}
int main(int argc, char *argv[])
{
int ret = mkfifo("myfifo", 0664);
sys_err(ret, "mkfifo error");
return 0;
}
例如:通过FIFO有名管道实现两个文件间的通信,即两个没有血缘关系的进程之间的通信(假设已创建有名管道,且文件名为 myfifo)
fifo_write.c 文件代码:
#include
#include
#include
#include
#include
#include
void sys_err(int ret, const char *str)
{
if(ret == -1)
{
perror(str);
exit(1);
}
}
int main(int argc, char *argv[])
{
// 判断用户是否有输入管道文件
if(argc < 2)
{
printf("Please enter like this: ./a.out fifoname");
}
int fd = open(argv[1], O_WRONLY);
sys_err(fd, "open error");
int i = 0;
char buf[4096];
// 每隔1秒向管道中写入内容
while(1)
{
sprintf(buf, "It's No.%d\n", i++);
write(fd, buf, strlen(buf));
sleep(1);
}
close(fd);
return 0;
}
fifo_read.c 文件代码:
#include
#include
#include
#include
#include
#include
void sys_err(int ret, const char *str)
{
if(ret == -1)
{
perror(str);
exit(1);
}
}
int main(int argc, char *argv[])
{
// 判断用户是否有输入管道文件
if(argc < 2)
{
printf("Please enter like this: ./a.out fifoname");
}
int fd = open(argv[1], O_RDONLY); // 打开管道
sys_err(fd, "open error");
int n;
char buf[4096];
// 向管道中读取内容
while((n = read(fd, buf, sizeof(buf))) != 0)
{
sys_err(n, "read error");
write(STDOUT_FILENO, buf, strlen(buf)); // 把读到的内容打印到屏幕上
}
close(fd);
return 0;
}