一、进程间通讯的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
进程间通讯的本质:让不同的进程看到同一份资源(一般由OS提供),以此保证进程的独立性
二、进程间通信分类
- 管道:1.匿名管道 2.命名管道
- System V IPC:1.System V 消息队列 2.System V 共享内存 3.System V 信号量
- POSIX IPC:1.消息队列 2.共享内存 3.信号量 4.互斥量 5.条件变量 6.读写锁
三、 管道
管道的4种情况
1、如果管道没有数据了,读端必须等待,直到有数据为止(写端写入数据)
2、如果管道被写满了,写端必须等待,直到有空间为止(读端读走数据)
3、写端关闭,读端一直读,读端会读到文件的结尾,read返回值为0
4、读端关闭,写端一直写,OS会直接杀掉写端进程,通过向目标进程发送SIGPIPE(13)
管道的5种特性
1.匿名管道,可以允许具有血缘关系的进程之间进行通信,常用于父子,仅限于此,即没有血缘关系的进程无法用匿名管道进行通信
2.匿名管道,默认给读写端要提供同步的机制
3.面向字节流的
4.管道的生命周期随进程的,即进程结束,管道会自动关闭,当然手动关闭也行
5.管道是单向通信
注意:读端和写端必须同时被打开,不然两个进程会相互等待,直到双方都打开为止
如何理解管道的同步机制?这个目前可以理解为两个进程的通信大体上呈现出一种回合制的感觉,即写一句,读一句 / 写完了之后开始读 / 管道写满了,读出一些,然后再写,然后再读....
如何理解面向字节流?这个目前可以理解为我们的数据是以字节为单位进行传输的,就比如我们传一个int类型的整数,在读取的时候可以读出一个int类型的整数,也可以读出4个char类型的字符
1、匿名管道
1、指令级的使用
我们在命令行上敲的 | 就是对匿名管道的使用
通过上面这个简单的例子,我们就能看出匿名管道是在有血缘关系的进程间进行通信的(上面的命令只是为了验证这一点,并没有啥特别的作用)
2、代码级的使用
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端
返回值:成功返回0,失败返回错误代码函数原理如下
使用方法如下
#include <unistd.h>
#include <iostream>
#include <cassert>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdlib>
#include <cstdio>
using namespace std;
#define MAX 1024
int main()
{
//1、建立管道
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n == 0);
//cout<<pipefd[0]<<" "<<pipefd[1]<<endl;
cout << "I am father ,pid:" << getpid() << endl;
//2、创建子进程
pid_t id = fork();
if(id < 0)
{
perror("fork");
return -1;
}
char buffer[MAX] = { 0 };
//3、关闭多余的fd,形成单向通信
//父读,子写
if(id == 0)//子进程
{
close(pipefd[0]);
char message[MAX] = { 0 };
int cnt = 0;
while(1)
{
snprintf(message, MAX, "hello father, my pid:%d, cnt:%d", getpid(), cnt++);
write(pipefd[1], message, strlen(message));
cout << "writing ..." << endl;
}
// close(pipefd[1]);//可以手动关闭写端,也可以等进程结束,让OS帮你关闭
exit(0);
}
//父进程
close(pipefd[1]);
while(true)
{
ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1);//如果写端没有关闭,管道中没有数据会在这里阻塞,如果写端关闭,将管道中的数据读完就会返回0
if(n > 0){
cout << "child say:" << buffer << endl;
}else if(n == 0){
cout << "read end" << endl;
break;
}
}
close(pipefd[0]);
pid_t ret = waitpid(id, nullptr, 0);
if(ret == id)
{
cout<<"wait success"<<endl;
}
return 0;
}
(对于匿名管道的4种情况和一些特性,有兴趣可以在自己的电脑上验证一下,这里就不演示了,想验证啥就在上面的代码上进行适当修改即可)
2、命名管道
1、指令级的使用
简单演示一下用法
2、代码级的使用
代码如下
//shared.h
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#define FILENAME ".fifo"
//server.cc
#include "shared.h"
bool Makefifo()
{
int n = mkfifo(FILENAME,0666);
if(n < 0)
{
std::cout << "errno: " << errno << ",errstring: " << strerror(errno) << std::endl;
return false;
}
return true;
}
int main()
{
Start:
int rfd = open(FILENAME, O_RDONLY);
if(rfd < 0)
{
std::cout << "open failed" << std::endl;
if(Makefifo()) goto Start;
else return 0;
}
char buffer[1024] = { 0 };
while(1)
{
ssize_t n = read(rfd, buffer, sizeof(buffer)-1);
if(n > 0)
{
buffer[n] = 0;//添加\0
std::cout << "Client say# "<< buffer << std::endl;
}
else if(n == 0)
{
std::cout << "Client quit, sercer quit too!" << std::endl;
break;
}
}
return 0;
}
//client.cc
#include "shared.h"
int main()
{
int wfd = open(FILENAME, O_WRONLY);
if(wfd < 0)
{
std::cout << "open failed" << std::endl;
return 0;
}
std::string message;
while(1)
{
std::getline(std::cin,message);
write(wfd, message.c_str(), message.size());
}
return 0;
}
总结:管道这种通信方式本质就是在复用文件相关的函数,没有啥新的知识点,在一定层度上说明复用的强大(新瓶装旧酒还是很管用的)
ulimit 用来设置和查看系统资源限制,如下
从上面这张图我们也能看出管道文件的大小为512*8=4096字节,但是实际上不是,感兴趣的可以去测试一下自己的管道文件的大小,测试方法:写端边写边打印次数,每次写入1个字节的数据,读端sleep(较大的数),直到写满为止,看打印了多少次