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信号,此进
程收到此信号进而会终止进程,可以
使用捕捉信号的方式验证这一结论
🔎 下期预告:共享内存通信方式 🔍