进程间通信方式—管道(使用最简单)
文章目录
1.概述
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:
- 其本质是一个伪文件(实为内核缓冲区)
- 由两个文件描述符引用,一个表示读端,一个表示写端
- 规定数据从管道的写端流入管道,从读端流出
管道的原理:
管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现
管道的局限性:
1 数据不能进程自己写,自己读
2.管道中数据不可反复读取。一旦读走,管道中不再存在
3 采用双向半双工通信方式,数据只能在单方向上流动
4 只能在有公共祖先的进程间使用管道
双向半双工例子:
对于管道来说,一旦我进程间通信时,第一次发生数据交换是A进程读B进程写,那第二次就不可以A写B读了,只能是A读B写。
常见通信方式:单工通信、半双工通信、全双工通信。
注意:因为父子进程共享文件描述符所以父进程已经创建并打开的管道子进程也能用
2.使用
创建并打开管道
#include<unistd.h>
int pipe(int fd[2]);
参数:
fd[0]表示读端
fd[1]表示写端
返回值:
成功0失败-1
父进程写子进程读:
一开始父子进程都持有读端和写端
父进程关闭写端子进程关闭读端后就有一条明确的数据流通方向
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(void)
{
pid_t pid;
char buf[1024];
int fd[2];
char *p = "test for pipe\n";
if (pipe(fd) == -1)
sys_err("pipe");
pid = fork();
if (pid < 0) {
sys_err("fork err");
} else if (pid == 0) {
//子进程关闭写端
close(fd[1]);
int len = read(fd[0], buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
close(fd[0]);
} else {
//父进程关闭读端
close(fd[0]);
write(fd[1], p, strlen(p));
sleep(1);
wait(NULL);
close(fd[1]);
}
return 0;
}
3.管道的读写行为
1.读管道
1. 管道中有数据,read返回实际读到的字节数
2.管道中无数据:
(1)管道写端被全部关闭,没有人会继续往管道写数据了,read返回0(好像读到文件结尾)
(2)写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)
2.写管道
-
管道读端全部被关闭,进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)
-
管道读端没有全部关闭:
(1)管道已满,write阻塞
(2)管道未满,write将数据写入,并返回实际写入的字节数
3.练习
1.管道实现 ls|wc-l
使用管道实现父子进程间通信,完成:ls|wc-l。假定父进程实现ls,子进程实现wc
Is命令正常会将结果集写出到stdout,但现在会写入管道的写端;wc-l 正常应该从stdin读取数据,但
此时会从管道的读端读
1.创建打开管道
2.fork子进程
3.关闭父进程读端,关闭子进程写端
4.父进程调用execlp执行ls命令
5.子进程调用execlp执行wc -l
6.父进程调用dup2把标准输出重定向到管道写端(往管道写数据,原来ls的输出是在标准输出的,现在输出到管道)
7.子进程调用dup2把标准输入重定向到管道读端(从管道拿数据,原来wc从标准输入拿数据,现在从管道拿)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid;
int fd[2];
pipe(fd);
pid = fork();
if (pid == 0) { //child
close(fd[1]); //子进程从管道中读数据,关闭写端
dup2(fd[0], STDIN_FILENO); //让wc从管道中读取数据
execlp("wc", "wc", "-l", NULL); //wc命令默认从标准读入取数据
} else {
close(fd[0]); //父进程向管道中写数据,关闭读端
dup2(fd[1], STDOUT_FILENO); //将ls的结果写入管道中
execlp("ls", "ls", NULL); //ls输出结果默认对应屏幕
}
return 0;
}
- 程序不时的会出现先打印$提示符,再出程序运行结果的现象。
- 这是因为:父进程执行ls命令,将输出结果给通过管道传递给子进程去执行wc命令,这时父进程若先于子进程打印wc运行结果之前被shell使用wait函数成功回收,shell就会先于子进程打印wc运行结果之前打印$提示符。
- 在这之中子进程一定得等父进程写完数据以后才会执行自己的代码,所以一定是父进程先执行完毕。
- 所以解决方法:让子进程执行ls,父进程执行wc命令。或者在兄弟进程间完成。
2.管道实现兄弟进程通信
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid;
int fd[2], i;
pipe(fd);
for (i = 0; i < 2; i++) {
if((pid = fork()) == 0) {
break;
}
}
if (i == 0) { //兄
close(fd[0]); //写,关闭读端
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", NULL);
} else if (i == 1) { //弟
close(fd[1]); //读,关闭写端
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
} else {
close(fd[0]);
close(fd[1]);
for(i = 0; i < 2; i++) //两个儿子wait两次
wait(NULL);
}
return 0;
}
注意要关闭父进程持有的读端和写端,不然形不成数据的单向流动
3.测试管道是否允许一个pipe有一个写端多个读端?有一个读端多个写端?
是允许的,但是一般都是写成一个读端一个写端
- 管道允许一个读端多个写端
- 管道是可以有一个读端和多个写端的。这在很多场景下是非常有用的,例如在日志系统中,多个不同的进程可以作为写端向一个管道写入日志信息,而一个专门的日志收集进程作为读端从管道中读取这些日志信息进行处理。多个写端可以同时向管道写入数据,不过需要注意数据的同步问题,因为如果多个写端同时写入可能会导致数据混乱,通常需要配合信号量等同步机制来保证数据的有序写入。
- 管道也允许一个写端多个读端
- 管道同样允许一个写端多个读端。当数据被写入管道后,所有的读端都可以读取到这些数据。数据从管道中被读取后,对于管道中的其他读端来说,数据仍然存在(只要没有被其他读端全部读取完)。
- 例如,在一个数据分发系统中,一个进程作为写端向管道写入数据,多个其他进程作为读端可以从管道中读取相同的数据进行不同的处理,如一个读端用于数据显示,另一个读端用于数据存储等。管道中的数据是可以被多个读端共享读取的,并不是一个读端读取后数据就消失了。管道内部维护了一个缓冲区,数据存储在这个缓冲区中,读端从缓冲区读取数据,只要缓冲区中的数据没有被全部读取,其他读端仍然可以读取剩余的数据。
测试代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
int main(void)
{
pid_t pid;
int fd[2], i, n;
char buf[1024];
int ret = pipe(fd);
if(ret == -1){
perror("pipe error");
exit(1);
}
for(i = 0; i < 2; i++){
if((pid = fork()) == 0)
break;
else if(pid == -1){
perror("pipe error");
exit(1);
}
}
if (i == 0) {
close(fd[0]);
write(fd[1], "1.hello\n", strlen("1.hello\n"));
} else if(i == 1) {
close(fd[0]);
write(fd[1], "2.world\n", strlen("2.world\n"));
} else {
close(fd[1]); //父进程关闭写端,留读端读取数据
sleep(1); //sleep的原因是为了让两个写端都把数据写上,而不是就只有其中一个写上之后父进程就读了
n = read(fd[0], buf, 1024); //从管道中读数据
write(STDOUT_FILENO, buf, n);
for(i = 0; i < 2; i++) //两个儿子wait两次
wait(NULL);
}
return 0;
}
-
当两个子进程快速地向管道写入数据时,管道缓冲区可能在第一个子进程写入
1.hello\n
后,父进程就开始读取数据。由于管道缓冲区的数据可能没有被第二个子进程的2.world\n
完全覆盖或者父进程读取操作已经完成,就可能导致父进程只读取到1.hello\n
而没有读取到2.world\n
。 -
管道的读取操作在缓冲区有数据时就会开始读取,而不会等待所有子进程都写入数据。
sleep
函数在这里起到了让父进程暂停一下的作用,给两个子进程足够的时间将数据都写入管道缓冲区,从而保证父进程能够读取到两个子进程写入的完整数据。
4.管道大小
默认4KB
5.管道优劣
**优点:**简单,相比信号,套接字实现进程间通信,简单很多
缺点:
- 1.只能单向通信,双向通信需建立两个管道
- 2.只能用于父子、兄弟进程(有共同祖先)间通信。该问题后来使用fifo有名管道解决
6.有名管道FIFO
FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间。但通过FIFO,不相
关的进程也能交换数据。
FIFO是Linux基础文件类型中的一种。但,FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。备
进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信。
创建方式:
1.命令:mkfifo 管道名
mkfifo 管道名
mkfifo myfifo
2.库函数:
#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(const char *pathname,mode_t mode);
参数:
pathname:文件名
mode:8进制的权限,比如0644这种的
**返回值:**成功:0;失败 :- 1
一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。如:close、read、
write、unlink等。
例子:
int ret = mkfifo("mytestfifo", 0664);
if (ret == -1)
sys_err("mkfifo error");
实现没有血缘关系的进程通信
读端
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
void sys_err(char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
int fd, len;
char buf[4096];
if (argc < 2) {
printf("./a.out fifoname\n");
return -1;
}
//不提前创建fifo,直接写fifo
//int fd = mkfifo("testfifo", 644);
//open(fd, ...);
fd = open(argv[1], O_RDONLY); // 打开管道文件
if (fd < 0)
sys_err("open");
while (1) {
len = read(fd, buf, sizeof(buf)); // 从管道的读端获取数据
write(STDOUT_FILENO, buf, len);
sleep(3); //多個读端时应增加睡眠秒数,放大效果.
}
close(fd);
return 0;
}
写端
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
void sys_err(char *str)
{
perror(str);
exit(-1);
}
int main(int argc, char *argv[])
{
int fd, i;
char buf[4096];
if (argc < 2) {
printf("Enter like this: ./a.out fifoname\n");
return -1;
}
fd = open(argv[1], O_WRONLY); //打开管道文件
if (fd < 0)
sys_err("open");
i = 0;
while (1) {
sprintf(buf, "hello itcast %d\n", i++);
write(fd, buf, strlen(buf)); // 向管道写数据
sleep(1);
}
close(fd);
return 0;
}