进程管道
进程间通信的目的
数据传输:一个进程需要将它的数据发送给另一个进程。
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发送了某种事件(如子进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如debug调试进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时直到它的状态改变。
进程间通信的发展
管道
System V进程间通信(可以实现本地通信)
POSIX进程间通信(可以实现跨网络通信)
进程间通信分类
管道:
匿名管道pipe
命名管道
System V IPC:
System V 消息队列
System V 共享内存
System V 信号量
POSIX IPC:
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
什么是管道?
管道是Unix中最古老的进程间通信的形式。
我们把一个进程连接到另一个进程的一个数据流称为一个“管道”。
其本质是一个伪文件(管道实为内核使用环形队列机制,借助内核缓冲区4k实现);
其中,who命令和wc命令都是两个程序,当它们运行起来后就变成了两个进程,who进程通过标准输出将数据打到“管道”当中,wc进程再通过标准输入从“管道”当中读取数据,至此便完成了数据的传输,who命令用于查看当前云服务器的登录用户(一行显示一个用户),wc -l是计算行数的一个命令,说明当前只有一个用户在线,
匿名管道pipe函数
pipe函数用于创建匿名管道,pipe函数的函数原型如下:
int pipe(int pipefd[2]);
pipe函数的参数是一个输出型参数,所以用户需要定义两个数组,并传过来,数组pipefd用于返回两个指向管道读端和写端的文件描述符:
管道的特点(重要)
由两个文件描述符引用,一个表示读端,一个表示写端;
管道提供流式服务,规定数据从管道的写端流入管道,从读端流出。
管道是单向通信的,如果想要使用双向通信,那就创建两个管道吧;
管道通信,通常用来进行具有“血缘”关系的进程,进行进程间通信。常用于父子通信,因为具有了血缘关系,fork创建子进程后才能看到同一份资源—pipe函数打开管道,并不清楚管道的名字,称之为匿名管道。
在管道通信中,写入的次数,和读取的次数,不是严格匹配的,读写次数的多少没有强相关。——表现:面向字节流。
管道具有一定的协同能力,让读端和写端能够按照一定的步骤进行通信——自带同步进制。
一般而言,进程退出,管道就会被释放,所以管道的生命周期随进程。
一般而言,内核会对管道操作进行同步与互斥;
管道是半双工的,数据只能向一个方向流动
管道的读写规则
当没有数据可读时
O_NONBLOCK disable: read调用阻塞,即进程暂停执行,一直等待有数据来为止;
O_NONBLOCK enable: read调用返回-1,errno值为EAGAIN。
当管道满的时候
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据;
O_NONBLOCK enable: 调用返回-1,errno值为EAGAIN。
如果所有管道写端对应的文件描述符被关闭,则read返回0,表面读到文件结尾;
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性(即要么全做,要么都不做)。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
管道的四种场景
数据一旦被读走,便不再管道中存在,不可反复读取。如果我们读取完毕了所有的管道数据,如果对方不写入,我们就只能等待。
如果我们写端将管道写满了,我们就不能再写了。
如果我关闭了写端,读取完管道数据后,再读,read就会返回0,表明读到了文件结尾。
如果写端一直写,读端关闭,那么写端继续写就没有意义了,OS不会维护无意义,低效率的事情,OS会杀死一直在写入的进程!通过信号来终止进程:13)SIGPIPE
下列代码演示:子进程通过写端写数据到管道,父进程读取管道数据。
实例代码:
#include<iostream>
#include<unistd.h>
#include<cerrno>
#include<string.h>
#include<string>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
//父进程需要以读写的方式打开管道文件,父进程和子进程之间进行相互通信,父进程打开的管道文件地址是会被子进程继承下来的
//所以它们指向的是同一份管道文件
//【一定要让不同的进程看到同一份资源】
//1.创建管道
int pipefd[2] = {0};//将其初始化为0
int n = pipe(pipefd);//pipefd是一个输出型参数
if(n<0)
{
std::cout<<"pipe error, "<<errno<<":"<<strerror(errno)<<std::endl;
}
//pipe函数创建管道文件,将数组pipefd初始化成3和4,并返回,因为0,1,2已被占用
std::cout<<"pipefd[0]: "<<pipefd[0]<<std::endl;//3->读端
std::cout<<"pipefd[1]: "<<pipefd[1]<<std::endl;//4->写端
//2.创建子进程
pid_t id = fork();
assert(id!=-1);//正常应该应用判断,我这里就断言
if(id==0) //子进程
{
//3.关闭不需要的fd,让父进程进行读取,让子进程进行写入
//我们想让子进程进行写入,所以关闭的是pipefd[0]读,保留pipefd[1]写
close(pipefd[0]);
//4.开始通信--结合某种场景---子进程将数据写入管道
const std::string namestr = "嗨!我是子进程";
int cnt = 1;
char buffer[1024];
while(true)
{
//先向buffer字符缓冲区格式化写入数据
snprintf(buffer,sizeof(buffer),"%s,计数器:%d,我的PID:%d\n",namestr.c_str(),cnt++,getpid());
//然后再将缓冲区的数据写到管道里
write(pipefd[1],buffer,strlen(buffer));
sleep(1);
}
close(pipefd[1]);
//子进程执行完退出
exit(0);
}
//父进程
//3.为实现单向通信,需要关闭不需要的文件描述符fd
close(pipefd[1]);
//4.开始通信--结合某种场景---父进程从管道中读取数据
char buffer[1024]={0};
while(true)
{
//父进程先把管道里的数据读出来,放到缓冲区中
int n = read(pipefd[0],buffer,sizeof(buffer)-1);
if(n>0)
{
buffer[n]='\0';
std::cout<<"我是父进程,子进程给我的消息是:"<<buffer<<std::endl;
}
else if(n==0)
{
std::cout<<"我是父进程,我读到文件结尾了"<<std::endl;
break;
}
else
{
std::cout<<"我是父进程,我读异常了"<<std::endl;
}
}
close(pipefd[0]);//关闭管道读端
//父进程等待子进程
waitpid(id,&status,0);
std::cout<<"sig: "<<(status & 0x7) << std::endl;//如果子进程一直写,但是父进程读取一段时间后退出
//OS就会杀掉子进程
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构