目录
一进程间通信的介绍
1进程为什么要通信
不同进程之间需要某种协同,所以如何协同的前提条件:通信------通信的数据是有类别的:通知就绪的:单纯的要给我传递数据的;控制相关信息的数据的......
而事实上,进程之间具有独立性(进程=内核数据结构+代码和数据)通信的选择??
2进程如何通信
a.可以直接让进程之间进行进行通信,但成本可能会高一点
b.通信的前提:让不同的进程,看到同一份**(OS)资源(同一块内存)**
而OS要这样做:
1.一定是某个进程需要进行通信的需求,让OS创建一份资源;
2.OS要如何知道要进行通信了------用户知道:所以OS要提供系统调用来满足用户;
OS创建资源的不同,意味着有很多的系统调用,也就是说:进程间通信有不同的种类!!
3进程的常见方式
管道 匿名管道pipe 命名管道
System V IPC System V 消息队列 System V 共享内存 System V 信号量
POSIX IPC 消息队列 共享内存 信号量
互斥量 条件变量 读写锁
二进程间通信的方式
1匿名管道
1.1原理
理解OS内部进程与文件之间的关系:
进程对文件进行操作:
进程要在files结构体 在2号下标之后(0,1,2默认是打开的)添加指向对文件进行read或者write的方法的指针,指向对应的file结构体去执行方法:不同方法的file结构体只是里面的对文件的操作(如read或者write)不同,其他的如文件内容与属性,缓冲区...这些资源是每个file结构体所共享的,不用在单独去再创建一份出来;
我们要实现父进程read,子进程write:父进程创建出子进程,把父进程的task_struct和files结构体里面的内容拷贝一份给子进程,但关于file结构体就不用再拷贝了??
前面我们不是说进程之间具有独立性吗?
因为files结构体所指向的struct file以及后面的文件的属性,内容,这些都是文件系统相关的,关我什么事!
把父进程的files里的4号下标给关了,子进程的files里的3号下标给关了:一个只读,一个只写,就完成了父进程与子进程的单向通信!
正因为这种通信实现简单,才有了管道这一概念的出现!
a.为什么父子进程会向同一个显示器终端打印数据?
1.父进程会默认打开三个流:stdin,stdout,stderr;在创建子进程后,子进程通过继承也打开;
2.显示器也是文件,父子进程在往显示器文件里写数据,数据就会在同一个缓冲区里,通过刷新显示到显示器上
b.子进程主动ciose(0,1,2),会影响父进程继续使用显示器文件吗?
在struct file中会包含着引用计数变量:子进程关闭只是在此基础上进行--;只有减到0时file才会被释放:所以子进程的close不影响父进程的使用
c.既然后面父进程要关闭files中4号下标(关闭文件的写端),先关闭好不好?
如果这样做的话:子进程继承下来的files里的4号也是关闭的,但我们是想让它来写操作!!
1.2实现匿名管道
先来了解创建匿名管道的系统调用:
1.父进程创建管道:
cpp
int main()
{
// 1. 创建管道
int pipefd[2];
int n = pipe(pipefd); // 输出型参数,rfd, wfd
//n不为0创建失败
if (n != 0)
{
std::cerr << "errno: " << errno << ": "
<< "errstring : " << strerror(errno) << std::endl;
return 1;
}
//...
}
2.父进程fork出子进程
cpp
// pipefd[0]->0->r(嘴巴 - 读) pipefd[1]->1->w(笔->写)
//2.创建子进程
pid_t id = fork();
if (id == 0)
{
std::cout << "子进程关闭不需要的fd了, 准备发消息了" << std::endl;
sleep(1);
// 子进程 --- write
//关闭不需要的fd
close(pipefd[0]);
SubProcessWrite(pipefd[1]);//实现子进程写如管道的函数
close(pipefd[1]);//写完进行关闭
exit(0);
}
3.父进程关闭fd[pipe[0]],子进程关闭fd[pipe[1]]:
cpp
std::cout << "父进程关闭不需要的fd了, 准备收消息了" << std::endl;
sleep(1);
// 父进程 --- read
// 3. 关闭不需要的fd
close(pipefd[1]);
FatherProcessRead(pipefd[0]);//从管道读的函数实现
std::cout << "5s, father close rfd" << std::endl;
sleep(5);
close(pipefd[0]);
//等待子进程退出
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
std::cout << "wait child process done, exit sig: " << (status & 0x7f) << std::endl;
std::cout << "wait child process done, exit code(ign): " << ((status >> 8) & 0xFF) << std::endl;
}
既然父进程在后面要关闭pipe[0]:在最开始的时候为什么不先把它关掉呢?
为了让子进程继承~
可以不关吗? -> 可以:但建议关,防止误写~
完整代码
cpp
#include <iostream>
#include <string>
#include <cerrno> // errno.h
#include <cstring> // string.h
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
const int size = 1024;
std::string getOtherMessage()
{
static int cnt = 0;
std::string messageid = std::to_string(cnt); // stoi -> string -> int
cnt++;
pid_t self_id = getpid();
std::string stringpid = std::to_string(self_id);
std::string message = "messageid: ";
message += messageid;
message += " my pid is : ";
message += stringpid;
return message;
}
// 子进程进行写入
void SubProcessWrite(int wfd)
{
int pipesize = 0;
std::string message = "father, I am your son prcess!";
int n = 10;
while (n--)
{
sleep(1);
std::cerr << "+++++++++++++++++++++++++++++++++" << std::endl;
std::string info = message + getOtherMessage(); // 子进程发给父进程的消息
write(wfd, info.c_str(), info.size()); // 不需要写入\0
}
std::cout << "child quit ..." << std::endl;
}
// 父进程进行读取
void FatherProcessRead(int rfd)
{
char inbuffer[size]; //变长数组->c99标准
while (true)
{
sleep(1);
// std::cout << "-------------------------------------------" << std::endl;
ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1);//保留一个位置填'\0'
if (n > 0)
{
inbuffer[n] = 0; // == '\0'
std::cout << inbuffer << std::endl;
}
else if (n == 0)
{
// 如果read的返回值是0,表示写端直接关闭了,我们读到了文件的结尾
std::cout << "client quit, father get return val: " << n << " father quit too!" << std::endl;
break;
}
else if (n < 0)
{
std::cerr << "read error" << std::endl;
break;
}
}
}
int main()
{
// 1. 创建管道
int pipefd[2];
int n = pipe(pipefd); // 输出型参数,rfd, wfd
if (n != 0)
{
std::cerr << "errno: " << errno << ": "
<< "errstring : " << strerror(errno) << std::endl;
return 1;
}
// pipefd[0]->0->r(嘴巴 - 读) pipefd[1]->1->w(笔->写)
// 2. 创建子进程
pid_t id = fork();
if (id == 0)
{
std::cout << "子进程关闭不需要的fd了, 准备发消息了" << std::endl;
sleep(1);
// 子进程 --- write
// 3. 关闭不需要的fd
close(pipefd[0]);
// if(fork() > 0) exit(0);
SubProcessWrite(pipefd[1]);
close(pipefd[1]);
exit(0);
}
std::cout << "父进程关闭不需要的fd了, 准备收消息了" << std::endl;
sleep(1);
// 父进程 --- read
// 3. 关闭不需要的fd
close(pipefd[1]);
FatherProcessRead(pipefd[0]);
std::cout << "5s, father close rfd" << std::endl;
sleep(5);
close(pipefd[0]);
//等待子进程退出
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
std::cout << "wait child process done, exit sig: " << (status & 0x7f) << std::endl;
std::cout << "wait child process done, exit code(ign): " << ((status >> 8) & 0xFF) << std::endl;
}
return 0;
}
以上是用一个管道实现简单的单向通信,如果要实现双向通信可以选择用两个管道来实现
2管道的4种情况和5种特征
特征:a.匿名管道:只用来进行有血缘关系的进程之间的通信,如:父子进程
1.让父进程一直读,子进程写完后休眠5s:
cpp
// 子进程进行写入
void SubProcessWrite(int wfd)
{
std::string message = "father, I am your son prcess!";
while (true)
{
std::cerr << "+++++++++++++++++++++++++++++++++" << std::endl;
std::string info = message + getOtherMessage();
write(wfd, info.c_str(), info.size());
sleep(5);//休眠5s
}
}
// 父进程进行读取
void FatherProcessRead(int rfd)
{
char inbuffer[size];
while (true)
{
//sleep(1);//永远在读
ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1);
if (n > 0)
{
inbuffer[n] = 0; // == '\0'
std::cout << inbuffer << std::endl;
}
else if (n == 0)
{
std::cout << "client quit, father get return val: " << n << " father quit too!" << std::endl;
break;
}
else if (n < 0)
{
std::cerr << "read error" << std::endl;
break;
}
}
}
刚开始:父进程读到子进程的第一条消息: (之后父进程阻塞)
过了5s之后才读到第二条消息:(继续阻塞...)
进程之间进行通信,可能会让不同进程(并发)进行访问的:带来数据不一致问题(子进程本来要写入整体的"abc1234":写到abc的时候就被父进程读走了)
但在上面我们所演示的现象是:子进程写一条,父进程读一条;
因为管道本质上也是文件,子进程休眠的过程,父进程在阻塞着,等待子进程的写入
我们可以得出:
b.管道内部:自带进程之间同步的机制(同步:多执行流执行代码时,有明显的顺序性)
也看到了第一种情况:
1.如果管道内部为空&&读的fd没有关闭:读取条件不具备读进程进行阻塞
2.父进程不读了,子进程不断进行写入:
cpp
// 子进程进行写入
void SubProcessWrite(int wfd)
{
int pipesize = 0;
std::string message = "father, I am your son prcess!";
while (true)
{
char ch='A';
write(wfd,&ch,1);
pipesize++;
std::cout<<"pipesize:"<<pipesize<<std::endl;
}
}
// 父进程进行读取
void FatherProcessRead(int rfd)
{
char inbuffer[size];
while (true)
{
sleep(500);//不读了
ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1);
if (n > 0)
{
inbuffer[n] = 0; // == '\0'
std::cout << inbuffer << std::endl;
}
else if (n == 0)
{
std::cout << "client quit, father get return val: " << n << " father quit too!" << std::endl;
break;
}
else if (n < 0)
{
std::cerr << "read error" << std::endl;
break;
}
}
}
结果:子进程写到65535(=65KB)时,子进程阻塞:管道被写满了
2.管道被写满&&读的fd不去读且不关闭,管道被写满,写进程会被阻塞
3. 父进程一直读,子进程写一条信息后就退出了:
cpp
// 子进程进行写入
void SubProcessWrite(int wfd)
{
int pipesize = 0;
std::string message = "father, I am your son prcess!";
while (true)
{
char ch='A';
write(wfd,&ch,1);
break; // 关闭写端
}
std::cout<<"child quit..."<<std::endl;
}
// 父进程进行读取
void FatherProcessRead(int rfd)
{
char inbuffer[size];
while (true)
{
// 一直读
ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1);
if (n > 0)
{
inbuffer[n] = 0; // == '\0'
std::cout << inbuffer << std::endl;
}
std::cout << "get return val:" << n << std::endl;
}
}
结果:父进程会死循环地进行读取
3管道一直在读&&写端关闭了wfd,读端会读到返回值0,表示读到了文件结尾
4.父进程读端关闭,子进程一直写:
cpp
// 子进程进行写入
void SubProcessWrite(int wfd)
{
int pipesize = 0;
std::string message = "father, I am your son prcess!";
// 一直写
while (true)
{
sleep(1);
std::string info = message + getOtherMessage(); // 子进程发给父进程的消息
write(wfd, info.c_str(), info.size()); // 不需要写入\0
sleep(1);
}
std::cout << "child quit..." << std::endl;
}
// 父进程进行读取
void FatherProcessRead(int rfd)
{
char inbuffer[size];
while (true)
{
ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1);
if (n > 0)
{
inbuffer[n] = 0; // == '\0'
std::cout << "father read:" << inbuffer << std::endl;
}
else if (n == 0)
{
std::cout << "client quit, father get return val: " << n << " father quit too!" << std::endl;
break;
}
else if (n < 0)
{
std::cerr << "read error" << std::endl;
break;
}
sleep(1);
break; // 读端关闭
}
}
我们在实现的时候让子进程进行写入,目的就在这:便于观察现象
当父进程关闭读端时,OS认为这个管道是broken pipe(没有了读端,往管道里写入就是浪费时间),对写的进程发送SIGPIPE(13)信号来终止掉进程:所以子进程的exit sig为13
4.rfd直接关闭,写端一直写入,写端进程会被操作系统使用13号信号杀掉,相当于进程异常
进程退出后,管道也就被OS释放了:c管道(文件)的生命周期是随进程的
d管道在通信时,是面向字节流的(读取与写入不是一一匹配的)
e管道的通信方式,是一种特殊的半双工模式(双方聊天你一句我一句的交谈)
在管道里:只要每次写入的信息不超过4096个字节,写进去的数据就是安全的(原子的)
管道在指令中的符号:| 代表的就是匿名管道:同时连接(启动)多个进程;
他们是:兄弟关系,对应的父进程是:~bash
3进程池
主逻辑:实现父进程给子进程派发任务,让子进程去执行相应的任务即可
1.创建一个Channel对象
cpp
class Channel
{
public:
Channel(const int wfd, const string &ChannelName, const pid_t ChannelId)
: _wfd(wfd), _ChannelName(ChannelName), _ChannelId(ChannelId)
{}
~Channel()
{
}
//关闭读端
void CloseWfd()
{
close(_wfd);
}
//等待子进程
void WaitChild()
{
pid_t id=waitpid(_ChannelId,nullptr,0);
if(id>0)
{
cout<<"wait seccess"<<endl;
}
}
//获取private变量
int Getwfd() {return _wfd;}
string GetChannelName() {return _ChannelName;}
pid_t GetChannelId() {return _ChannelId;}
private:
int _wfd;
string _ChannelName;
//管道对应子进程的Id
pid_t _ChannelId;
};
2.任务表的初始化,挑选任务(父进程)与执行任务(子进程):
cpp
#define TaskNum 3
typedef void(*task_t)();
task_t Task[TaskNum];
void Print()
{
cout<<"I an a print task"<<endl;
}
void DownLoad()
{
cout<<"I an a download task"<<endl;
}
void Fllush()
{
cout<<"I an a Fllush task"<<endl;
}
void InitTask()
{
srand(time(nullptr));
Task[0]=Print;
Task[1]=DownLoad;
Task[2]=Fllush;
}
//父进程随机选择任务
int SlectTask()
{
int tasknum=rand()%TaskNum;
return tasknum;
}
//执行任务
void ExcuteTask(int Num)
{
if(Num<0||Num>TaskNum-1)
{
cout<<"excute task fail"<<endl;
return;
}
else
{
Task[Num]();
}
}
3创建信道(管道)与子进程
cpp
//读到父子进程发来的任务并执行
void work()
{
while(true)
{
int Read_Task=0;
ssize_t n=read(0,&Read_Task,sizeof(Read_Task));
if(n==sizeof(int))
{
cout<<"child will excute task"<<endl;
ExcuteTask(Read_Task);
cout<<"-----------------------"<<endl;
}
else if(n==0)
{
cout<<"child quie"<<endl;
break;
}
}
}
void CreateChannelAndSub(int num, vector<Channel> * channels, task_t task)//work的重命名
{
for(int i=0;i<num;i++)
{
int pipefd[2]={0};
int n=pipe(pipefd);
if(n<0) exit(-1);
pid_t id=fork();
if(id==0)
{
//child->读
close(pipefd[1]);
//关闭不同管道的读端(后面创建的子进程会有不同管道的读端要进行循环关闭)
if(!channels->empty())
{
for(int i=0;i<(*channels).size();i++)
{
(*channels)[i].CloseWfd();
}
}
//回调函数
//将读端重写到标准输入端,task()不用传pipefd[0]啦
dup2(pipefd[0],0);
task();
close(pipefd[0]);
exit(-1);
}
//father->写与初始化
string channelsName="channel"+to_string(i);
//关闭写端
close(pipefd[0]);
channels->push_back(Channel(pipefd[1],channelsName,id));
}
}
4父进程发送任务(往管道里写)
cpp
int SlectPipe(int channel_size)
{
//分配管道是依次的,保证每个子进程都有任务执行
static int n=0;
int channel=n;
n++;
n%=channel_size;
return channel;
}
void SandTask(int Task_Num,int Pipe_Num,vector<Channel>& Channels)
{
//写任务码
write(Channels[Pipe_Num].Getwfd(),&Task_Num,sizeof(Task_Num));
}
void ctrlProcessOnce(vector<Channel>& channels)
{
sleep(1);
//选任务
int Task_Index=SlectTask();
//选管道
int Pipe_Index=SlectPipe(channels.size());
//发送任务
SandTask(Task_Index,Pipe_Index,channels);
}
void ctrlProcess(vector<Channel>&channels, int time=-1)
{
if(time==-1)
{
while(true)
{
ctrlProcessOnce(channels);
}
}
else
{
while(time--)
{
ctrlProcessOnce(channels);
}
}
}
5回收管道与子进程
cpp
void CleanUpChannel(vector<Channel>& channels)
{
for(auto& Channel:channels)
{
Channel.CloseWfd();
Channel.WaitChild();
}
}
完整代码
cpp
//TestPipePool.cc
#include <iostream>
#include<string>
#include<vector>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
#include"Task.hpp"
class Channel
{
public:
Channel(const int wfd, const string &ChannelName, const pid_t ChannelId)
: _wfd(wfd), _ChannelName(ChannelName), _ChannelId(ChannelId)
{}
~Channel()
{
}
void CloseWfd()
{
close(_wfd);
}
void WaitChild()
{
pid_t id=waitpid(_ChannelId,nullptr,0);
if(id>0)
{
cout<<"wait seccess"<<endl;
}
}
int Getwfd() {return _wfd;}
string GetChannelName() {return _ChannelName;}
pid_t GetChannelId() {return _ChannelId;}
private:
int _wfd;
string _ChannelName;
pid_t _ChannelId;
};
void CreateChannelAndSub(int num, vector<Channel> * channels, task_t task)
{
for(int i=0;i<num;i++)
{
int pipefd[2]={0};
int n=pipe(pipefd);
if(n<0) exit(-1);
pid_t id=fork();
if(id==0)
{
//child->读
close(pipefd[1]);
//关闭继承的读端
if(!channels->empty())
{
for(int i=0;i<(*channels).size();i++)
{
(*channels)[i].CloseWfd();
}
}
//回调函数
//将读端重写到标准输入端就不用传值调用task()了
dup2(pipefd[0],0);
task();
close(pipefd[0]);
exit(-1);
}
//father
string channelsName="channel"+to_string(i);
close(pipefd[0]);
channels->push_back(Channel(pipefd[1],channelsName,id));
}
}
int SlectPipe(int channel_size)
{
static int n=0;
int channel=n;
n++;
n%=channel_size;
return channel;
}
void SandTask(int Task_Num,int Pipe_Num,vector<Channel>& Channels)
{
//写任务码
write(Channels[Pipe_Num].Getwfd(),&Task_Num,sizeof(Task_Num));
}
void ctrlProcessOnce(vector<Channel>& channels)
{
sleep(1);
//选任务
int Task_Index=SlectTask();
//选管道
int Pipe_Index=SlectPipe(channels.size());
//发送任务
SandTask(Task_Index,Pipe_Index,channels);
}
void ctrlProcess(vector<Channel>&channels, int time=-1)
{
if(time==-1)
{
while(true)
{
ctrlProcessOnce(channels);
}
}
else
{
while(time--)
{
ctrlProcessOnce(channels);
}
}
}
void CleanUpChannel(vector<Channel>& channels)
{
/* int num=channels.size()-1;
while(num)
{
channels[num].CloseWfd();
channels[num--].WaitChild();
} */
//注意多个读端的问题
for(auto& Channel:channels)
{
Channel.CloseWfd();
Channel.WaitChild();
}
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " processnum" << std::endl;
return 1;
}
int num = stoi(argv[1]);
//函数指针数组
InitTask();
vector<Channel> channels;
// 1. 创建信道和子进程
CreateChannelAndSub(num, &channels, work);
// 2. 通过channel控制子进程
ctrlProcess(channels, 5);
// 3. 回收管道和子进程. a. 关闭所有的写端 b. 回收子进程
CleanUpChannel(channels);
return 0;
}
//Task.hpp 函数的声明与实现可以放到一起
#include<iostream>
using namespace std;
#include<time.h>
#define TaskNum 3
typedef void(*task_t)();
task_t Task[TaskNum];
void Print()
{
cout<<"I an a print task"<<endl;
}
void DownLoad()
{
cout<<"I an a download task"<<endl;
}
void Fllush()
{
cout<<"I an a Fllush task"<<endl;
}
void InitTask()
{
srand(time(nullptr));
Task[0]=Print;
Task[1]=DownLoad;
Task[2]=Fllush;
}
int SlectTask()
{
int tasknum=rand()%TaskNum;
return tasknum;
}
void ExcuteTask(int Num)
{
if(Num<0||Num>TaskNum-1)
{
cout<<"excute task fail"<<endl;
return;
}
else
{
Task[Num]();
}
}
void work()
{
while(true)
{
int Read_Task=0;
ssize_t n=read(0,&Read_Task,sizeof(Read_Task));
if(n==sizeof(int))
{
cout<<"child will excute task"<<endl;
ExcuteTask(Read_Task);
cout<<"-----------------------"<<endl;
}
else if(n==0)
{
cout<<"child quie"<<endl;
break;
}
}
}
4命名管道
命名管道的原理与匿名管道的差不多:
只不过由父子进程 之间的通信变为两个毫不相干的进程之间进行通信
4.1原理
两个不相关进程之间的通信:一个读,一个写:在对应的files结构体添加r的fd,w的fd:分别指向不同的file结构体执行不同的方法(但属性集合与操作集是共享的前面说过),指向同一个缓冲区进行实现;但如果要满足两个进程的需求:缓冲区里的数据要进行多次的刷新(OS不做如任何浪费时间的事),就得把刷新的通道给关闭掉,两个进程的通信在缓冲区里完成
但两个毫不相关的进程,怎么保证打开了同一个文件呢?
每一个文件,都有文件路径(唯一性):而这个文件就得是特殊文件即:(命名)管道文件!
4.2实现命名管道
思路:创建一个Server(服务)端:负责读Client的内容,一个Client(客户):负责写消息
1.写一个namepipe类:类中成员:pipe路径,fd文件描述符,id表示身份;将所有要实现的方法函数(创建pipe,读pipe,写pipe...)共同写在这个类中(优雅)
cpp
//NamePipe.hpp
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <fcntl.h>
using namespace std;
#define PCreater 0
#define PUser 1
#define Read O_RDONLY
#define Write O_WRONLY
#define Size 1024
class NamePipe
{
public:
NamePipe(int id)
: _id(id)
{
// server要进行创建管道文件
if (_id == PCreater)
{
int n = mkfifo(_path.c_str(), 0666);
if (n != 0) exit(-1);
cout<<"Creater Create Pipe..."<<endl;
}
}
bool OpenFifoForWrite()
{
return OpenFifo(Write);
}
int WriteFifo(const string &in)
{
return write(_fd, in.c_str(), in.size());
}
bool OpenFifoForRead()
{
return OpenFifo(Read);
}
int ReadFifo(string *out)
{
char OutBuffer[Size];
ssize_t n = read(_fd, OutBuffer, sizeof(OutBuffer) - 1);
if (n > 0)
{
OutBuffer[n] = '\0';
*out = OutBuffer;
}
return n;
}
~NamePipe()
{
if (_id == PCreater)
{
int n = unlink(_path.c_str());
if (n != 0)
cout << "unlink fail:" << errno << ":" << strerror(errno) << endl;
cout << "Clean Fifo Suecss..." << endl;
}
if (_fd != -1)
close(_fd); // 关文件描述符
}
private:
bool OpenFifo(int flags)
{
_fd = open(_path.c_str(), flags);
if (_fd == -1)
{
cout << "open fail" << errno << ":" << strerror(errno) << endl;
return false;
}
return true;
}
const string _path = "./myfifo";
int _id; // 身份
int _fd = -1; // 文件描述符
};
2.Server进行读pipe
cpp
//Server.cc
#include "NamePipe.hpp"
int main()
{
NamePipe server(PCreater);
if (server.OpenFifoForRead())
{
cout << "Server Create Pipe..." << endl;
sleep(3);
while (true)
{
string message;
int n = server.ReadFifo(&message);
if (n > 0)
{
cout << "Client say: " << message << endl;
}
else if (n == 0)
{
cout << "Client quit,Server quit" << endl;
break;
}
else
{
cout << "Read fail" << endl;
break;
}
}
}
return 0;
}
3.Clinet端进行写pipe
cpp
//Client.cc
#include "NamePipe.hpp"
int main()
{
NamePipe client(PUser);
// write
while (client.OpenFifoForWrite())
{
string message;
cout << "Please Write Word >" << " ";
getline(cin, message);
int n = client.WriteFifo(message);
if (n == -1)
{
cout << "write fail" << endl;
break;
}
}
return 0;
}
5共享内存
除了使用管道进行通信外,还有别的设计者从0开始,重新设计出一套(本地)通信的方案:
System V IPC:共享内存,消息队列,信号量;但在后面有了网络后,这些方案就逐渐处于淘汰的边缘了:在本文中只讲一下共享内存的原理与使用
5.1原理
通信的本质是让不同的进程看到同一份资源:为了让不同的进程看到,OS在物理内存中开辟一段内存空间,通过不同进程的页表映射到对应的虚拟地址空间中的共享区中;进程通过地址空间也就能拿到共享内存的起始虚拟地址,也就能让不同的进程访问到同一块内存空间进行通信啦
理解:
1.以上讲的内容,要明白这些事情都是OS做的
2.OS不知道用户创建进程A,进程B是要什么时候进行通信的:所以务必要提供系统调用
3.AB,CD,EF...共享内存存在系统中是可以存在多份,供不同个数,不同进程进行同时通信!
4.这样说OS就要对共享内存进行管理------先描述,在组织;
说明:它不是一段简单的内存空间,也要有描述并管理共享内存的数据结构和匹配的算法
5.共享内存 = 内存空间(数据) + 共享内存的属性
5.2代码实现
先来认识一下系统调用:
shmget的第一个参数:key_t key:key_t ftok的返回值;
为什么要让用户自己生成一个key值?
共享内存被创建出来要有一个自己的标识符来表示:方便OS对后续的管理;
共享内存被进程A创建出来,进程B要进行通信,它怎么知道共享内存被创建出来了?
这就要有一个规定(规则)来标识共享内存创建的标志:用户生成的key值
有了key值,进程B就能知道共享内存被创建出来,从而进行访问啦!!
shmget的第三个参数:int shmflg:有系统提供选项来组合使用:(身份不同,参数不同)
我们要知道:共享内存是不随进程的结束而自动释放的,一直存在直到系统重启;
关于生命周期:共享内存随内核的,文件随进程的:不是一样的!!
开辟好共享内存后,要进行挂接才能进行使用:
1.实现出共享内存的整个生命周期的过程(类封装)
cpp
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <cstring>
using namespace std;
#define SCreater 0
#define SUser 1
const int ShmSize = 4096;
class shm
{
public:
// 获取key值
key_t GetKey()
{
key_t key = ftok(_pathname, _proj_id);
return key;
}
// 获取shm
int GetShm(key_t key, size_t size, int shmflg)
{
if (_id == SCreater)
cout << "Shm Creating..." << endl;
else if (_id == SUser)
cout << "Get Shm..." << endl;
sleep(2);
return shmget(key, size, shmflg);
}
// 获取shm地址
void *GetShmadd()
{
return _shmadd;
}
// 将当前进程进行挂接到shm上
void* AttachShm()
{
void* shmadd = shmat(_shmid, nullptr, 0);
if (shmadd == nullptr)
{
perror("shmat");
}
if (_id == SUser)
cout << "User" << " Attach Shm..." << endl;
else if (_id == SCreater)
cout << "Creater" << " Attach Shm..." << endl;
sleep(2);
return shmadd;
}
// 取消挂接
void ShmDeleteTouch()
{
if (_shmadd == nullptr)
return;
shmdt(_shmadd);
if (_id == SCreater)
cout << "Creater Decth Shm..." << endl;
else if (_id == SUser)
cout << "User Decth Shm..." << endl;
sleep(2);
}
// 初始化
shm(int id)
: _id(id)
{
_key = GetKey();
if (_id == SCreater)
_shmid = GetShm(_key, ShmSize, IPC_CREAT | IPC_EXCL | 0666); // 加了excl存在就报错
else if (_id == SUser)
_shmid = GetShm(_key, ShmSize, IPC_CREAT | 0666); // 第二个参数以4096*n来设置
// 进行挂接
_shmadd = AttachShm();
}
~shm()
{
ShmDeleteTouch(); // 取消挂接
if (_id == SCreater)
{
int n = shmctl(_shmid, IPC_RMID, nullptr);
cout << "Shm Delete..." << endl;
sleep(2);
}
}
// 共享区清空并初始化
void Zero()
{
if (_shmadd)
{
memset(_shmadd, 0, ShmSize);
}
}
private:
const char *_pathname = "/root/Code/Shm"; // 当前路径
int _proj_id = 0x66; // 随便设置
key_t _key; // 用户设置
int _shmid; // 系统提供
int _id; // 身份
void *_shmadd = nullptr; // 共享内存地址
};
2.客户端负责写,服务端负责读:
cpp
//Server.cc
int main()
{
//读
shm Server(SCreater);
char * shmadd=(char*)Server.GetShmadd();
while(true)
{
cout<<"Client say:"<<shmadd<<endl;
sleep(1);
}
return 0;
}
//Client.cc
int main()
{
shm Client(SUser);
Client.Zero();//写之前先清空
char *shmadd=(char*)Client.GetShmadd();
char ch='A';
while(ch!='G')
{
shmadd[ch-'A']=ch;
ch++;
sleep(2);
}
return 0;
}
执行代码:
我们会发现:
1.Server端在Client端退出后(不写了)还在继续读,而使用命名管道则不会这样
2.Server端在读的时候是不管Client端写没写完成就进行读取,而管道会继续阻塞等待
说明:1.共享内存不提供如何的保护机制;2.造成数据不一致问题
5.3优化
解决上面的问题,我们引入管道来解决:(用前面写好的命名管道代码)
cpp
//Server.cc
#include "shm.hpp"
#include "NamePipe.hpp"
int main()
{
//1.创建共享内存
shm Shm(SCreater);
char *shmaddr = (char *)Shm.GetShmadd();
// 2. 创建管道(提供保护机制)
NamePipe fifo(PCreater);
fifo.OpenFifoForRead();
while (true)
{
std::string tmp;//不重要
int n=fifo.ReadFifo(&tmp);// 没有写入就阻塞
if(n==0) break;//Client端没写数据了就结束read(只是方便当前Server最后的回收)
std::cout << "Shm Memory : " << shmaddr << std::endl;
}
return 0;
}
//Client.cc
#include "shm.hpp"
#include "NamePipe.hpp"
int main()
{
//1.创建共享内存
shm Shm(SUser);
//写数据前先清空
Shm.Zero();
char *shmaddr = (char *)Shm.GetShmadd();
// 2.打开管道
NamePipe fifo(PUser);
fifo.OpenFifoForWrite();
char ch = 'A';
while (ch <= 'G')
{
shmaddr[ch - 'A'] = ch;
std::cout << "Add " << ch++ << " Into Shm Memory" << std::endl;
std::string tmp="wake up";//让Server read数据
fifo.WriteFifo(tmp);
sleep(1);
}
return 0;
}
总结:
1.创建共享内存时,共享内存不通过如何的保护机制:会产生数据不一致问题;
但我们创建好共享内存后,直接使用即可,不需要使用如何的系统调用(而管道需要调用read,write等系统调用来实现);这也说明:
2.共享内存是所有进程IPC中最快的:它大大减少了拷贝次数!!
6.消息队列(了解)
6.1原理
原理:一个进程进行接受,另一个发送有类型的数据块的方式:
进程A拿队列中B的数据块,进程B拿队列中A的数据块
6.2接口
因为与共享内存都是System V IPC的通信版本,设计接口时都是大差不差的:
在发送和收消息时要自定义结构体类型:
7信号量
7.1五大概念渗透
a.多个执行流(进程)能看到的一份资源------共享资源->共享内存
b.共享资源数据不一致问题;要进行保护:被保护的资源------临界资源
c.通过互斥(同步)的方式来保护临界资源;互斥------任何时刻只能有一个进程在访问资源
d.访问资源:(程序员)通过代码来访问:代码------访问共享资源 +不访问共享资源
e.所谓的对资源进行保护------本质上是对访问共享资源的代码进行保护(加锁)
f.共享资源------临界区;非共享资源------非临界区
7.2信号量理解
信号量(信号灯):保护临界资源(Code)
a.共享内存不整体使用时:
OS会划分为一个一个的数据块,进程要访问数据块之前要先申请信号量,申请完后这个数据块就只有一个进程能访问并进行使用:
而信号量根据块数的多少来确定初始值:有人来申请就进行--操作:它本质上相当于计数器
这就相当于电影院的购票机制:
有多少个座位,在购票系统里count就初始化多少:有人完成购票操作就继续count--;
我是在购票后还是到电影院坐下后,这个位置才是我的?------当然是在购票完成后,这个座位就是属于我的!!(我们国民都是有素质的群众)
在这里:电影院------共享内存;购票------申请数据块(临界资源)进行申请(购票)的本质:对临界资源的预定机制
但我们最担心的还是申请的块数 > 总共资源的块数(100张电影票卖出去了102张)
这就需要来让执行流(购票系统)与资源(座位)进行一一对应(程序员实现)暂时不关心
b,共享资源整体使用时
比如:电影院的超级VIP,只有一个座位:购票完成后只有购票者有权利使用
这不就是说:对整体资源的使用,不就是说资源只有一个吗?
这个过程不就是我们在上面说的互斥方式吗?
这种信号量为1 or 0:我们称之为二元信号量
而上面的情况的信号量我们称之为多元信号量
信号量本质上是一个计数器:那我们可不可以创建一个全局变量:gcount来替代它呢?不能!!
1.全局变量不能被所有进程看到(父子进程即使看到了,修改它也会发生写时拷贝)
2.gcount++,不是原子的(多线程重点叙述)
7.3信号量操作
信号量作为进程间通信的其中一种方式:和共享内存,消息队列一样:必须先让不同的进程看到同一个信号量(计数器);这也意味着:信号量本身也是共享资源!
信号量是用来保护临时资源的安全的:这是不是你自己得是安全的啊!!
信号量--:安全性通过P操作来保证:信号量++:安全性通过V操作来保证
也可以说:PV操作共同来保证信号量是原子的!!(简单理解:原子的就是它只在意结果)
三OS管理进程间通信
由于共享内存,消息队列,信号量这三个都是基于System V版本说出现的三个进程间通信的方式,那么:这三个在OS内部是如何管理的呢?
在这三个方式删除(回收)的系统接口手册中,我们发现:这三个的结构体名字是一样的(会不会是巧合?);结构体里第一个储存都是IPC_Prem结构体:
我们往OS内部去探索发现:OS用一个kern_ipc_perm结构体数组来共同管理:
在ipc_id中,用一个重要的成员变量:struct kern_ipc_pern p[]->结构体柔性数组(可变长)
在这个数组里储存着shm,smg,sem的IPC_Prem结构体的地址!!
用指令删除shm,smg,sem用到的是shmid:这个shmdid对应的是kern_ipc_pern p 数组下标!
要访问类型就要先强转成对应的类型在用->进行使用其它成员~
但是怎么知道它是什么类型呢??
在结构体数组中有成员mode来标明对应是什么类型!!
以上便是进程间通信的所以内容:有问题在评论区指出,感激不尽!!