💓博主CSDN主页:杭电码农-NEO💓
⏩专栏分类:Linux从入门到精通⏪
🚚代码仓库:NEO的学习日记🚚
🌹关注我🫵带你学更多操作系统知识
🔝🔝
进程间通信
- 1. 前言
- 2. 进程间通信的方法
- 3. 管道的简单介绍
- 4. 匿名管道
- 5. 命名管道
- 6. 总结以及拓展
1. 前言
众所周知,进程运行是具有独立性的,
想要进程间进行通信就要打破这种
独立性,而进程间通信的本质其实是
让不同的进程看见同一份资源!
本章重点:
本篇文章会介绍进程间通信中常见
的几种方式,并且着重讲解匿名管道
和命名管道的这两种通信手段的原理
和代码的实现.
2. 进程间通信的方法
首先通信种类分为三大类:
管道
system V
POSIX
当然网络通信的本质其实也是进程
间的通信,但是本篇文章的重点是
利用管道通信!!!
3. 管道的简单介绍
首先要回答什么是管道,在学习
Linux指令时我们使用过竖划线 |
也就是管道,把从一个进程连接
到另一个进程的数据流称为“管道”
在管道通信中,管道的本质其实就是
一个被系统打开的文件,然而用管道
进行通信的本质就是让不同的进程
看见相同的资源(文件)
并且管道是单向通信的
,即一个进程
不能同时从一个管道中读取和写入,
读取和写入要对应两个不同的管道!
4. 匿名管道
对于匿名管道来说,通常用于父子
进程之间的通信,在父进程使用
pipe创建好管道后,再使用fork创建
子进程,此时子进程会将父进程的
文件描述符表给拷贝过来,也就天然
的拥有这两个管道文件了!
并且此时我们会面临两个问题:
- 读取方将文件关闭了会发送什么?
- 读取方在文件中没有数据时会干什么?
读端关闭后,写端进程会终止
当管道无数据时读端会阻塞
有了上面的经验后,现在可以编码验证一下:
int main()
{
//创建管道
int pipefd[2]={0}; //0下标表示读取端,1下标表示写入端
int n = pipe(pipefd);
assert(n!=-1);
(void)n;
#ifdef DEBUG//条件编译
cout<<"[0]: "<<pipefd[0]<<" "<<"[1]: "<<pipefd[1]<<endl;
#endif
//创建子进程
pid_t id = fork();
assert(id!=-1);
if(id==0)//子进程, 构建单向通信
{
close(pipefd[1]);
char buffer[1024];
while(1)
{
ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;
cout<<"father# "<<buffer<<endl;
}
else//read的返回值等于0代表父进程的管道文件已经close了
{
cout<<"写入结束,子进程退出";
break;
}
}
exit(0);
}
//父进程写入,子进程读取
close(pipefd[0]);
string str = "我在给子进程发信息";
int count=0;
char send_buffer[1024];
while(count<=5)
{
//构建一个变化的字符串
snprintf(send_buffer, sizeof(send_buffer),"%s[%d]: %d",str.c_str(),getpid(),count++);//往缓冲区里写入数据
//写入到管道中
write(pipefd[1],send_buffer,sizeof(send_buffer));
sleep(1);
}
close(pipefd[1]);
pid_t ret = waitpid(id,NULL,0);
assert(ret > 0);
return 0;
}
总结管道的特点:
- 管道常用于父子进程间的通信
- 管道是面向字节流的服务
- 管道是基于文件的,管道的生命周期随进程
- 管道是单向通信的
- 写快读慢,写满管道后不能再写了
- 写慢读快,管道没有数据时,读端要等待
- 写端关闭,读端会读到0,标识结束
- 读端关闭,写端继续写会终止进程
5. 命名管道
匿名管道的一个限制就是必须是在
有血缘关系的进程间才能通信,使用
命名管道可以解决这个问题
匿名管道和命名管道的区别:
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
命名管道的打开规则:
如果当前打开操作是为读而打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
- O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
- O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
命名管道简单通信:
读端代码:
#define SIZE 1024
#define MODE 0666
string ipcpath = "./fifo.ipc";
static void getmessage(int fd)
{
//编写通信代码
char buffer[SIZE];
while(1)
{
memset(buffer,'\0',sizeof(buffer));
ssize_t s = read(fd,buffer,sizeof(buffer)-1);
if(s>0)//读取成功
{
cout<<"["<<getpid()<<"] "<<"client say: "<<buffer<<endl;
}
else if(s==0)//写端关闭,并且读到end
{
cerr<<"["<<getpid()<<"]"<<"read end"<<endl;
break;
}
else//读取失败
{
perror("read");
break;
}
}
}
int main()
{
//创建管道文件
int n = mkfifo(ipcpath.c_str(),MODE);
if(n<0)
{
perror("mkfifo");
exit(1);
}
//文件操作
int fd = open(ipcpath.c_str(),O_RDONLY);//这里必须等待client进程将此管道文件打开后才能执行下面的代码
if(fd<0)
{
perror("open");
exit(2);
}
for(int i=0;i<4;i++)
{
int id = fork();
if(id==0)
{
getmessage(fd);
exit(0);
}
}
close(fd);
return 0;
}
写端代码:
#define SIZE 1024
#define MODE 0666
string ipcpath = "./fifo.ipc";
int main()
{
//client不用自己创建管道文件,只需获取文件即可
int fd = open(ipcpath.c_str(),O_WRONLY);
if(fd<0)
{
perror("open");
exit(1);
}
//通信过程
string buffer;
while(1)
{
cout<<"please Enter message: ";
getline(cin,buffer);
write(fd,buffer.c_str(),buffer.size());
}
close(fd);
return 0;
}
6. 总结以及拓展
虽然匿名管道和命名管道已经包含了
对于有血缘关系和无血缘关系的进程
的通信,但是毕竟调用read和write这些
系统调用接口会不断的在用户态和内核
态进行切换,比较消耗资源,所以管道不是
最好的!
拓展: 为什么读端关闭后,写端进程会终止?
这是因为当读端关闭后,写端再使用
write操作会产生SIGPIPE信号,此进
程收到此信号进而会终止进程,可以
使用捕捉信号的方式验证这一结论