文章目录
- 管道
- 作用:
- 分类:
- 前置知识:
- 复制文件描述符
- dup
- dup2 【推荐使用】
- 无名管道
- 概述:
- pipe函数
- 实现: ps -A | grep bash
- 有名管道:
- 实现有名管道的聊天
- 无名管道与有名管道的区别?
管道
作用:
用于进程间通信
- 信号只能携带少量数据(可以理解为不带数据)
- 管道可以携带大量数据
分类:
- 无名管道(管道) 作用于有血缘关系的进程中
- 有名管道 作用 于 没有血缘关系的进程中
前置知识:
复制文件描述符
文件描述符有 : 0 标准输入 1 标准输出 2 标准错误
dup
作用: 复制已有文件描述符
#include <unistd.h>
int dup(int oldfd);
参数:
所需复制的文件描述符
返回值
复制得到的文件描述符
功能:从文件描述符表中,寻找一个最小可能的文件描述符(通过返回值返回)作为
oldfd复制
示例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
// dup() 拷贝文件描述符
int main(int argc, char const *argv[])
{
int fd = open("./test.txt",O_APPEND | O_RDWR | O_CREAT,0666);
int new_fd = dup(fd); //将久的文件描述符作为参数 来获得新文件描述符
write(new_fd,"hello",10); //往new_fd这个文件描述符修饰的文件中写hello写入长度为6
close(new_fd);
close(fd);
return 0;
}
示例2:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
// fd = 3
int fd = open("./test.txt", O_RDWR | O_APPEND);
// 最小可用文件描述符1
close(1);
// nfd = 1
int nfd = dup(fd);
// write(1,"内容",长度);
printf("nfd = %d\n", nfd);
return 0;
}
dup2 【推荐使用】
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
// dup2() 拷贝文件描述符 【最常用】
int main(int argc, char const *argv[])
{
int fd = open("./test.txt",O_APPEND | O_RDWR);
int new_fd = 1;
dup2(fd,new_fd);
printf("new_fd = %d\n",new_fd);
return 0;
}
无名管道
无名管道 又名 管道 开发实际中说管道一般就是说的无名管道
概述:
作用于有血缘关系的进程中
属于:
半双工
补充
单工:指数据传输只支持数据在一个方向上传输
双工:指二台通讯设备之间,允许有双向的资料传输
全双工:允许二台设备间同时进行双向数据传输。一般的电话、手机就是全双工的系统,因为在讲话时同时也可以听到对方的声音。
半双工:允许二台设备间进行双向数据传输,但不能同时进行。因此同一时间只允许一设备传送资料,若另一设备要传送资料,需等原来传送资料的设备传送完成后再处理。
基于内存
特点:
1,数据只能从管道的一端写入,从另一端读出。
2,写入管道中的数据遵循先入先出的规则。
3,管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息等
4,管道在内存中对应一个缓冲区。不同的系统其大小不一定相同,liunx系统64kb
5,从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据
6,管道没有名字,只能在具有公共祖先的进程之间使用。
7,基于内存
8,半双工
补充:
如管道可用于一个进程和其子孙进程之间的通信。第一个进程创建管道,然后创建子进程,接着子进程再创建第一个进程的孙子进程。
管道通常用于两个兄弟进程之间的通信——它们的父进程创建了管道,并创建两个子进程。
总结:在当前进程中创建管道
pipe函数
作用:
创建无名管道
语法:
#include <unistd.h>
int pipe(int fd[2]);
参数:
fd 为 int 型数组的首元素地址,其存放了管道的文件描述符 fd[0]、fd[1]。
fd[0]为读而打开,fd[1]为写而打开管道。
0读1写
返回值:
成功:返回 0
失败:返回-1
示例:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <wait.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
// 让子进程A给子进程B发送消息
// 1,创建无名管道
int fd[2] = {0};
pipe(fd);
// 2,创建子进程
int i = 0;
for (i = 0; i < 2; i++)
{
int pid = fork();
if (pid == 0)
{
break;
}
}
// 3,分类
if (i == 0)
{
// 子进程A:发送消息给子进程B
// 因为子进程A是发送消息的,所有该进程中读的文件描述符无用,顾关闭
// 0,读,1,写
close(fd[0]);
char *info = "你好,我是子进程A\n";
write(fd[1], info, strlen(info));
close(fd[1]);
}
else if (i == 1)
{
// 子进程B:接收子进程A发送的消息
// 因为子进程B是接收消息的,所有其进程中写的文件描述符无用,顾关闭
close(fd[1]);
char buf[200] = {0};
// 注意:读会阻塞当前进程,直到管道中有数据可以读取
read(fd[0], buf, 200);
printf("子进程B:%s", buf);
close(fd[0]);
}
else if (i == 2)
{
// 当前进程:回收子进程
while (1)
{
if (waitpid(-1, NULL, WNOHANG) == -1)
{
break;
}
}
}
return 0;
}
示例2:无读有写,可以写入
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <wait.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
// 让子进程A给子进程B发送消息
// 1,创建无名管道
int fd[2] = {0};
pipe(fd);
// 2,创建子进程
int i = 0;
for (i = 0; i < 2; i++)
{
int pid = fork();
if (pid == 0)
{
break;
}
}
// 3,分类
if (i == 0)
{
// 子进程A:发送消息给子进程B
// 因为子进程A是发送消息的,所有该进程中读的文件描述符无用,顾关闭
// 0,读,1,写
close(fd[0]);
char *info = "你好,我是子进程A\n";
write(fd[1], info, strlen(info));
close(fd[1]);
}
else if (i == 1)
{
}
else if (i == 2)
{
// 当前进程:回收子进程
while (1)
{
if (waitpid(-1, NULL, WNOHANG) == -1)
{
break;
}
}
}
return 0;
}
示例3:有读无写,读端会阻塞
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <wait.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
// 让子进程A给子进程B发送消息
// 1,创建无名管道
int fd[2] = {0};
pipe(fd);
// 2,创建子进程
int i = 0;
for (i = 0; i < 2; i++)
{
int pid = fork();
if (pid == 0)
{
break;
}
}
// 3,分类
if (i == 0)
{
}
else if (i == 1)
{
// 子进程B:接收子进程A发送的消息
// 因为子进程B是接收消息的,所有其进程中写的文件描述符无用,顾关闭
close(fd[1]);
char buf[200] = {0};
// 注意:读会阻塞当前进程,直到管道中有数据可以读取
read(fd[0], buf, 200);
printf("子进程B:%s", buf);
close(fd[0]);
}
else if (i == 2)
{
// 当前进程:回收子进程
while (1)
{
if (waitpid(-1, NULL, WNOHANG) == -1)
{
break;
}
}
}
return 0;
}
示例4:缓存区大小测试与缓冲区内容已满继续写入会阻塞
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <wait.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
int fd[2] = {0};
pipe(fd);
int pid = fork();
if (pid == 0)
{
// 子进程写
close(fd[0]);
int i = 0;
while (1)
{
char buf[1024];
write(fd[1], buf, 1024);
i++;
printf("已写入%dkb\n", i);
}
}
else if (pid > 0)
{
wait(NULL);
}
return 0;
}
示例5:一个写入的信息只能被读取一次
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <wait.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
int fd[2];
pipe(fd);
int i = 0;
for (i = 0; i < 2; i++)
{
int pid = fork();
if (pid == 0)
{
break;
}
}
if(i == 0)
{
//写1次
close(fd[0]);
char *buf = "写入的内容\n";
write(fd[1],buf,strlen(buf));
close(fd[1]);
}
else if(i == 1)
{
//读两次
close(fd[1]);
char buf[100] = {0};
read(fd[0],buf,100);
printf("第一次读取到的内容:%s\n",buf);
read(fd[0],buf,100);
printf("第二次读取到的内容:%s\n",buf);
close(fd[0]);
}
else if(i == 2)
{
//回收子进程
while(waitpid(-1,NULL,WNOHANG) != -1);
}
return 0;
}
示例6:当所有进程中读端关闭,写端将收到SIGPIPE的信号
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <wait.h>
#include <sys/types.h>
void fun()
{
printf("收到SIGPIPE信号\n");
}
int main(int argc, char const *argv[])
{
int fd[2];
pipe(fd);
int i = 0;
for (i = 0; i < 2; i++)
{
int pid = fork();
if (pid == 0)
{
break;
}
}
if (i == 0)
{
signal(SIGPIPE,fun);
//写入
close(fd[0]);
while(1)
{
char *buf = "hello\n";
sleep(1);
write(fd[1],buf,strlen(buf));
}
close(fd[1]);
}
else if(i == 1)
{
//读一次
close(fd[1]);
for (int j = 0; j < 5; j++)
{
char buf[1024] = {0};
read(fd[0],buf,1024);
printf("读取到的内容:%s\n",buf);
}
close(fd[0]);
printf("读端关闭\n");
_exit(0);
}
else if(i == 2)
{
//回收
close(fd[0]);
while(waitpid(-1,NULL,WNOHANG) != -1);
}
return 0;
}
实现: ps -A | grep bash
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
//要求:使用代码实现 ps -A | grep bash
int fd[2] = {0}; // 0 读 1 写
pipe(fd);
int pid = vfork();
if (pid == 0)
{
close(fd[0]);
//ps -A
/*
因为ps -A最终的结果会输出的控制台
控制台输出对应的文件描述符为1
此时将我们管道的写端复制文件描述符为1
此时表示输出的文件描述1就是我们管道的写端
此时ps -A程序执行完毕后,将其输出的内容写入到了我们的管道中
*/
dup2(fd[1],1);
execlp("ps","./ps","-A",NULL);
close(fd[1]);
}
else if(pid > 0)
{
//grep bash
close(fd[1]);
/*
grep bash是从标准输入中读取数据
我们将标准输入改为我们管道的读端
此时grep bash就是我们管道的读端读取数据
管道读端的数据就是管道写段写入的数据
即ps -A输出的内容
*/
dup2(fd[0],0);
execlp("grep","./grep","bash",NULL);
close(fd[0]);
wait(NULL);
}
return 0;
}
有名管道:
概述
又名:命名管道(FIFO)
特点:
1、半双工,数据在同一时刻只能在一个方向上流动。
2、写入 FIFO 中的数据遵循先入先出的规则。
3、FIFO 所传送的数据是无格式的,这要求 FIFO 的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息等。
4、FIFO 在文件系统中作为一个特殊的文件而存在,但 FIFO 中的内容却存放在内存中。
5、管道在内存中对应一个缓冲区。不同的系统其大小不一定相同。
6、从 FIFO 读数据是一次性操作,数据一旦被读,它就从 FIFO 中被抛弃,释放空间以便写更多的数据。
7、当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。
8、FIFO 有名字,不相关的进程可以通过打开命名管道进行通信。
mkfifo 函数
作用:创建有名管道
语法
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数:
pathname:文件名
mode:文件操作模式,一般用0666(所有用户可读可写)
返回值:
成功:0
失败:-1,一般失败是因为存在与pathname名相同的文件
读写特点
1、open打开管道 不指定 O_NONBLOCK (阻塞)
1、open 以只读方式打开 FIFO 时,要阻塞到某个进程为写而打开此 FIFO
2、open 以只写方式打开 FIFO 时,要阻塞到某个进程为读而打开此 FIFO。
3、open 以只读、只写方式打开 FIFO 时会阻塞,调用 read 函数从 FIFO 里读数据时 read 也会阻塞。
4、通信过程中若写进程先退出了,则调用 read 函数从 FIFO 里读数据时不阻塞;若写进程又重新运行,则调用
read 函数从 FIFO 里读数据时又恢复阻塞。
5、通信过程中,读进程退出后,写进程向命名管道内写数据时,写进程也会(收到SIGPIPE 信号)退出。
6、调用 write 函数向 FIFO 里写数据,当缓冲区已满时 write 也会阻塞。
2、open打开管道 指定O_NONBLOCK (非阻塞)
1、先以只读方式打开:如果没有进程,已经为写而打开一个 FIFO, 只读 open 成功,并且 open 不阻塞。
2、先以只写方 式打开:如果没有进程,已经为读而打开一个 FIFO,只写 open 将出错返回-1。
3、read、write 读写命名管道中读数据时不阻塞。
4、通信过程中,读进程退出后, 写进程向命名管道内写数据时,写进程也会(收到SIGPIPE 信号)退出。
3、 注意:open函数以可读可写方式打开FIFO 文件时的特点:
1、open不阻塞。
2、调用read函数从FIFO里读数据时read会阻塞。
3、调用write函数向FIFO里写数据,当缓冲区已满时write也会阻塞
实现有名管道的聊天
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <wait.h>
// 实现 张十一 与 张优秀 之间的通信
int main(int argc, char const *argv[])
{
// 1.创建xxz给zy发信息的管道
mkfifo("./xxz_to_zy", 0666);
// 2.创建zy给xxz发信息的管道
mkfifo("./zy_to_xxz", 0666);
// 3.创建子进程 一个用来发信息 一个用来收信息
int i = 0;
for (i = 0; i < 2; i++)
{
int pid = fork();
if (pid == 0)
{
break;
}
}
//子进程1 用来发信息
if (i == 0)
{ //文件描述符 0标准输入 1标准输出 2标准错误
int fd = 0;
//如果有XXZ这个宏,当前用户就是 张优秀
#ifdef XXZ
fd = open("./xxz_to_zy", O_RDWR); //此时是张优秀给张十一发消息 以可读、可写的方式打开
#else
fd = open("./zy_to_xxz", O_RDWR); //此时是张十一给张优秀发消息
#endif
//开始发信息
while (1)
{
// 用来存放信息 大小为300字节
char info[300] = {0};
// 从标准输入中获取信息 最多获取300个字节
fgets(info, 300, stdin);
//打印展示
printf("我:%s", info);
//清除脏数据 也就是按的回车上去的\n
info[strlen(info) - 1] = 0;
//写进文件 消息
write(fd,info,strlen(info));
//给定结束的条件 使用 比较字符串是否相等
if(strcmp(info,"88") == 0 )
{
break;
}
}
close(fd); //关闭文件描述符
}
else if (i == 1)//子进程2 用来收信息
{
int fd = 0;
//接收信息
#ifdef XXZ
fd = open("./zy_to_xxz",O_RDWR); //读取 张十一发来的信息
#else
fd = open("./xxz_to_zy",O_RDWR); //读取 张优秀发来的信息
#endif
//开始读取信息
while(1)
{
// 用来存放信息 大小为300字节
char info[300] = {0};
read(fd,info,300);
//再定义宏用来决定谁来说话
#ifdef XXZ
printf("张优秀说:%s\n",info);
#else
printf("张十一说:%s\n",info);
#endif
//给定结束的条件 使用 比较字符串是否相等 这里是因为传过来的数据本身就是剔除了\n的 所以这里直接比较字符串是否相等即可
if(strcmp(info,"88") == 0 )
{
break;
}
}
close(fd); //关闭文件描述符
}
else if (i == 2)// 当前是父进程 用来回收子进程
{
while (waitpid(-1, NULL, WNOHANG) != -1);
}
return 0;
}
无名管道与有名管道的区别?
- 1 无名管道 只能在有血缘关系的进程之间通讯
有名管道 既可以在有血缘关系的进程间通讯也能在无血缘关系的进程间通信
- 2 无名管道没有对应的文件 有名管道有对应的文件
- 3 但是有名管道中对应的文件不会存储其管道中的数据 数据是存储在内存中