【Linux】进程间通信之管道
- 进程间通信
- 进程间通信目的
- 进程间通信的方式
- 管道(内核维护的缓冲区)
- 匿名管道(用于父子间进程间通信)
- 简单使用
- 阻塞状态读写特征
- 非阻塞状态读写特征
- 匿名管道特点
- 命名管道
- 匿名管道与命名管道的区别
进程间通信
进程之间具有独立性,进程间通信必定需要一块公共的区域用来作为信息的存放点,操作系统需要直接的或间接给通信进程双方提供内存空间。
如果内存空间是文件系统提供的,那么就是管道通信
如果OS提供的就是共享内存
通信的本质就是让不同的进程看到同一份内存空间
进程间通信目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程)
进程间通信的方式
管道: 匿名管道命名管道
System V:System V 消息队列;System V 共享内存;System V 信号量
POSIX:消息队列;共享内存;信号量;互斥量;条件变量;读写锁
管道(内核维护的缓冲区)
管道是基于文件系统的通信方式
任何一个文件包括两套资源:
1.file的操作方法
2.有属于自己的内核缓冲区,所以父进程和子进程有一份公共的资源:文件系统提供的内核缓冲区,父进程可以向对应的文件的文件缓冲区写入,子进程可以通过文件缓冲区读取,此时就完成了进程间通信,这种方式提供的文件称为管道文件。管道文件本质就是内存级文件,不需要IO。
匿名管道(用于父子间进程间通信)
通过父进程fork创建子进程,让子进程拷贝父进程中的文件描述符表,两个进程便能看到同一个管道文件,这个管道文件是一个内存级文件,并没有名字,所以被称为匿名管道
匿名管道中,父子进程必须关闭一个文件描述符,形成单向通信
简单使用
#include <unistd.h>
int pipe(int pipefd[2]);
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
成功时返回零,错误时返回 -1
#include <iostream>
#include <unistd.h>
#include <cassert>
#include <cstring>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
//父进程进行读取,子进程负责写入
int main()
{
//第一步:创建管道文件,打开读写端
//fds[0]读 fds[1]写
int fds[2];
int n=pipe(fds);
assert(n==0);
//fork
pid_t id=fork();
assert(id>=0);
if(id==0)
{
//子进程写入,先关闭读
close(fds[0]);
//子进程通信代码
const char* s="我是子进程,我正在给你发信息";
int cnt=0;
while (true)
{
cnt++;
char buffer[1024];//只有子进程能访问
//格式化写入
snprintf(buffer,sizeof(buffer),"chile->parent say: %s[%d][%d]",s,cnt,getpid());
//子进程向【fd[1]】不断写,父进程【fd[0]】延迟读,导致写阻塞
write(fds[1],buffer,strlen(buffer));
//cout<<"count"<<cnt<<endl;
//sleep(5);//延迟写,父进程read阻塞
//break;//写一次直接关闭读端,读端read返回0,
}
//出来还是子进程
close(fds[1]);//子进程关闭写端fd
cout<<"子进程关闭了自己的写端"<<endl;
exit(0);
}
//父进程读取
close(fds[1]);
//父进程通信代码
while(true)
{
//sleep(3000);//写端不断写,读端延迟读,写端阻塞
//sleep(2);//写端不断写,读端缓读,数据积累在一起被读出
char buffer[1024];
//如果管道中没有数据,读端在读,默认阻塞当前读进程由R->S状态
//cout<<"parent_read_begin..."<<endl;
ssize_t s= read(fds[0],buffer,sizeof(buffer)-1);//阻塞式调用
//cout<<"parent_read_end..."<<endl;
if(s>0)
{
buffer[s]=0;
cout<<"父进程读到了# "<<buffer<<" | my_pid: "<<getpid()<<endl;
}
else if(s==0)
{
//读到了文件末尾
cout<<"read: over "<<s<<endl;
break;
}
break;//读一次直接退出,父进程准备关闭读端,os会发送信号杀掉写进程
}
//读未关闭
// n=waitpid(id,nullptr,0);
// assert(n==id);
// close(fds[0]);
//关闭读端,程序无效
close(fds[0]);
cout<<"父进程关闭写端"<<endl;
int status=0;
n=waitpid(id,&status,0);
获取进程终止信号
cout<<"waitpid->"<<n<<" : "<<(status&0x7f)<<endl;
return 0;
}
阻塞状态读写特征
1)如果管道中没有数据,读取端进程再进行读取,会阻塞当前正在读取的进程
2)如果写入端写满了,再写就会对该进程进行阻塞,需要等待对方对管道内数据进行读取
3)如果写入进程关闭了写入fd,读取端将管道内的数据读完后read的返回值为0,正常退出
4)如果读关闭,写就没有意义了,操作系统会给写端发送13号信号SIGPIPE,终止写端。(信号参考)
非阻塞状态读写特征
创建的匿名管道默认是阻塞状态的,如果要设置成非阻塞状态,需要使用fcntl
第一种:写设置为非阻塞
1)读端不关闭,写端一直写,写满之后,wirte会返回-1,报错当前资源不可用
2)读端直接关掉,写端一直在写,当前进程会收到SIGPIPE信号,写端的程序直接被杀死,这种现象叫做管道破裂
第二种:读设置为非阻塞
1)写端不关闭,读端一直读,read调用返回-1,errno为EAGAIN
2)写端关闭,读端进行读,read是正常调用返回0,表示没有读到
匿名管道特点
1)只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道;
2)管道提供流式服务 ;
3)进程退出,管道释放,所以管道的生命周期随进程
4)内核会对管道操作进行同步与互斥
5)管道是半双工。需要双方通信时,需要建立起两个管道
6)管道的大小是64K(65536)
7)创建匿名管道返回的文件描述符属性默认是阻塞的
命名管道
上面说到,匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信,两个不相关的进程如果要通信,需要借助命名管道
创建命名管道
int mkfifo(const char *pathname, mode_t mode);
pathname:命名管道文件所在路径
mode:文件权限
删除命名管道
#include <unistd.h>
int unlink(const char *path);
命名管道可用于无血缘关系的进程间通信,两个进程均打开同一路径下的同名文件(看到同一份资源);命名管道也是一个内存级文件
comm.hpp(提供命名管道的创建和销毁方法)
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define NAMED_PIPE "./myname_pipe"
bool CreateFifo(const std::string &path)
{
umask(0);
int n=mkfifo(path.c_str(),0600);
if(n==0)
return true;
else
{
std::cout<<"errno: "<< errno <<"err string: "<<strerror(errno)<<std::endl;
return false;
}
}
void removeFifo(const std::string &path)
{
int n=unlink(path.c_str());
assert(n==0);
std::cout<<"removeFifo success ...."<<std::endl;
(void)n;//避免爆出waring:未引用的变量n
}
server.cc(读端,先运行)
#include "comm.hpp"
int main()
{
bool r=CreateFifo(NAMED_PIPE);
assert(r);
(void)r;
//先运行此文件,会在open时阻塞
//管道文件要求读写都要打开文件才能执行后面的功能
std::cout<< "server begin" <<std::endl;
int rfd=open(NAMED_PIPE,O_RDONLY);
std::cout<< "server end" <<std::endl;
if(rfd<0) exit(1);
//read
char buffer[1024];
while (true)
{
ssize_t s=read(rfd,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;
std::cout<<"cilent ->server# "<<buffer<<std::endl;
}
else if(s==0)
{
std::cout<<"cilent quit,me tool "<<std::endl;
break;
}
else
{
std::cout<<"err string: "<<strerror(errno)<<std::endl;
break;
}
}
close(rfd);
sleep(10);
removeFifo(NAMED_PIPE);
return 0;
}
client.c(写端)
#include "comm.hpp"
int main()
{
//如果先运行此文件,由于文件还未创建,无法写入
std::cout << "client begin" << std::endl;
int wfd=open(NAMED_PIPE,O_WRONLY);
std::cout<< "client end" <<std::endl;
if(wfd<0) exit(1);
//write
char buffer[1024];
while (true)
{
std::cout<<"Please Say#";
fgets(buffer,sizeof buffer,stdin);
if(strlen(buffer)>0)
buffer[strlen(buffer)-1]=0;
ssize_t n=write(wfd,buffer,strlen(buffer));
assert(n==strlen(buffer));
(void)n;
}
close(wfd);
return 0;
}
匿名管道与命名管道的区别
1)匿名管道由pipe函数创建并打开。
2)命名管道由mkfifo函数创建,打开用open
3)FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义