目录
1 管道
1.1 管道是什么
1.1 匿名管道通信
1.2 父子进程通信
1.3 匿名管道实现多进程文件的写入读取
1.4 命名管道
2 共享内存
1 管道
1.1 管道是什么
管道顾名思义,他就是一个像是连通器一样的东西,原本不存在联系的东西之间建立起一定的关联。那么在Linux当中,管道可以作为文件之间流通数据的工具。
怎么理解呢?举一个简单的例子,我们通过一个命令得到了数据,但是我们不希望这个数据直接被显示在显示器上面,我们希望做一定的处理再显示。这个时候管道就起作用了,当我们拿到了前一个命令的数据流,然后通过管道传送给另一个命令,另一个命令再对这一份数据做处理,从而达到我们的预期。
当然,我相信大家对于管道还是不太理解,那么请结合我下面的配图:
在Linux当中,管道的表示就是( | )表示,从图中我们可以看到原本ls/ll是显示我们的目录下的内容,但是我们通过管道加上了一个wc -l的指令,发生了什么样的改变呢?输出变为了3,而wc -l命令的作用就是为了显示某一个数据当中有多少行数据,原本wc -l是不能单独执行的,不过在管道的作用下,ls的数据流向了wc命令,此时wc命令就有了可以统计的对象,最终输出了我们的3。
那么上述管道的作用也很清晰明了了,它在文件之间的位置如下:
那么看到上图,可以得知管道是一个单独的东西,那么他到底是怎么工作的呢?他会不会和我们的其它文件一样会在磁盘当中创建属于它自己的inode、数据块呢?
答案是否定的,管道不会有自己的数据块,他是一块由操作系统维护的缓存区,也就是说他不会有磁盘写入的过程,原因如下:
因为我们的管道是一个协助进程之间沟通的工具,那么这就表明了它的生命周期最多就是进程能够存活的时间,之后就会被杀掉,对于进程来说他还有属于自己的代码需要被存储起来,那么管道还剩下什么?他什么都不剩下,只有一堆没有意义的数据,所以无论对于我们来说,还是OS,管道在进程结束通信之后,它本身不具有任何的实际价值,所以它是不需要有数据需要被写入磁盘当中的;
还有就是对于管道来说,它本身需要维持两个进程之间的通信的高效性,如果需要及那个数据写道磁盘当中,这无疑拖慢了整个通信的高效性。
1.1 匿名管道通信
前面所提到的知识都只是开胃小菜,接下来才是真正重要的地方。毕竟如果管道只是处于一个命令级的操作那它的操作上限也就这样了。所以让我们来看看如何在代码中搭建出一个管道。
首先,我们需要一个创建管道的系统函数pipe(),它里面的参数是一个能够存两个整型数据的数组,我这里直接给你们说了,这两个整数分别是管道以读方式打开的fd和管道以写方式打开的fd。如果有小伙伴不理解fd是什么,可以移步到博主写的《文件系统》这篇博客当中,相信能帮到你。
并且这个参数是一个输出型参数,什么意思呢?也就是我们只需要传入这个数组就行,里面的数据由这个函数内部帮我们处理了。例如如果我们刚创建进程就用它为我们创建一个管道,那么它里面存的数据一定是3和4。不信我们可以来看看。
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
using namespace std;
int main()
{
int piped[2];
int ret = pipe(piped);
if(ret == -1)
{
cout << "创建管道失败" << endl;
exit(1);
}
cout << piped[0] << " " << piped[1] << endl;
return 0;
}
效果图如上,与博主所说的3、4没有任何的区别。
1.2 父子进程通信
那么此时我们已经得到了管道的写端和读端,那么此时我们可以做到一个什么样的事情呢?博主决定自己设计一个小项目:子进程写数据让父进程可以看到。因为有写时拷贝的存在,所以正常情况下,子进程写入的数据与父进程没有任何关联了,但是通过管道可以让他们两个联系起来。
代码:
#include<iostream>
#include<unistd.h>
#include<cerrno>
#include<cstring>
#include<string>
#include<sys/types.h>
#include<sys/wait.h>
//第一种通信方式为管道通信,即两个进程之间的数据通过管道进行交互
//为了保证进程之间的独立性,所以想要通信必须要让两个进程看到同一份代码资源
//匿名管道实际上也是一个文件,所以它也有对应的文件描述符,通过系统接口
//int pipe(int fd[2])来帮助我们实现
//fd[2]实际就是一个数组,用来存管道的读端和写端,分别是fd[0]\fd[1]
//成功之后会返回0,失败返回退出码
//通过父子进程间的继承关系,利用匿名管道进行通信,父进程读取内容,子进程写入内容
int main()
{
//创建一个管道文件
int pipefd[2] = {0};
int n = pipe(pipefd);
if(n != 0)
{
//错误判断,如果管道创建失败了,那么通信也不用进行了,直接exit
std::cout << "errno:" << errno << ':' << strerror(errno) << std::endl;
return 1;
}
//对应打开的文件描述符应该是3和4
std::cout << "pipefd[0]: " << pipefd[0] << std::endl;
std::cout << "pipefd[1]: " << pipefd[1] << std::endl;
//子进程创建
pid_t id = fork();
if(id == -1)
{
//子进程创建错误,通信无法成功,直接exit
std::cout << "errno:" << errno << ':' << strerror(errno) << std::endl;
return 2;
}
if(id == 0)
{
//子进程会继承父进程打开的文件描述符数组,当然也有对应的文件描述符
//子进程关闭读端
close(pipefd[0]);
//开始通信
const std::string namestr = "hello, I am sub process";
int cnt = 1;
char buffer[1024];
//写端一直写入
// while(true)
//写端控制写入
while(cnt < 5)
{
//写入信息进入buffer缓冲区当中
snprintf(buffer,sizeof(buffer),"%s,计数器:%d,我的PID,%d",namestr.c_str(),cnt++,getpid());
//然后将文件刷新到pipefd[1]这个写端当中
write(pipefd[1],buffer,strlen(buffer));
sleep(1);
}
//子进程执行完成自己的任务直接退出子进程就行
close(pipefd[1]);
exit(0);
}
//父进程执行位置
//父进程应该关闭写端
close(pipefd[1]);
char buffer[1024];
int cnt = 0;
while(cnt < 10)
{
//对于文件的读取来说,如果写端没有关闭,那么读端会一直等待,如果写端退出了,读端才有可能读到0
int n = read(pipefd[0],buffer,sizeof(buffer) - 1);
cnt++;
if(n > 0)
{
buffer[n] = '\0';
std::cout << "我是父进程,子进程给了我信息:" << buffer << std::endl;
}
else if(n == 0)
{
std::cout << "我是父进程,我读到了文件的结尾" << std::endl;
break;
}
else{
std::cout << "我是父进程,我读取异常了" << std::endl;
break;
}
}
//提前关闭读端,会出现异常情况
// close(pipefd[0]);
int status = 0;
waitpid(id,&status,0);
std::cout << "status: " << WEXITSTATUS(status) << ", single: " << (status & 0x7F) << std::endl;
close(pipefd[0]);
return 0;
}
运行效果图:
如上图,就是实现了我们的小项目咯,也就完成了我们父子进程的通信,但是大家有没有注意到一件事情?那就是最后的信号为什么会出现13,这很明显表示我们的进程并不是正确的退出的啊?还有就是这个13号信息是什么呢?
看到上图中博主画出来的部分,13号异常信号——SIGPIPE,也就是管道出现了问题,有什么问题?此时就引出了我们的4种管道的场景了:
1. 如果read读取完数据,此时的状态。
2. 如果writer将管道写完了,还能继续写吗?
3. 如果我们关闭了写端,再通过读端读取数据会怎么样?
4. 如果我们关闭了读端,写端继续会怎么样?
问题1:
如果read读取完数据,那么此时该进程的运行状态会卡在读取数据这一步当中,并不会进行下一步操作,所以,如果写端一直不写,读端也会一直在这个地方等待。
问题2:
如果writer将管道写完了,写端就不能再写数据了,我在最开始时就已经提过了,管道是一个由OS帮忙维护的缓存区,那么这也就表示了我们对于它的权限是很小的,只能是OS为我们开辟多少空间,我们就能用多少空间,当然这里的使用的空间其实是指的单次输入的数据大小不能操作系统为我们提供的大小。
那么什么叫做单次写入呢?并不是说我写了一个数据那就叫一次写入,如果我们不读取数据,那么后续的写入都会被纳入第一次写入当中。一般来说OS会为我们开辟的管道缓存大小为65535大小,也就是2的16次方-1,当然,在不同的OS下开辟的空间是有可能不同的。
问题3:
如果我们关闭了写端,再通过读端读取数据此时会怎么样呢?其实当写端关闭的时候,此时的写端就会有相应的信息可以收到,在问题一当中我们说过,如果读端读取完数据,那么他会卡到读取数据这个位置,但是写端如果被关闭了,此时的读端就不会卡住了,而是会读取到文件结尾,read同时也会返回0,这个时候读端就不会再读了。注意,这并不表示读端也被关闭了,只表示它不会再读取任何的数据了,关闭文件的操作任然需要我们认为操作。
问题4:
如果我们关闭了读端,写端继续会怎么样?关闭了读端,也就表示了写端写入的数据是无意义的,既然我们都认为写端的数据是无意义的了,那么对于OS来说,它更不会让写端一直占据资源,他会直接杀死还在写入这个文件。然后会为我们返回一个信号13 SIGPIPE,也就是我之前的小项目当中所展示的那样。
有兴趣的小朋友可以试着去把这个问题解决了,根据我所讲的这四种场景。
1.3 匿名管道实现多进程文件的写入读取
2.2当中的项目只是我们的父子进程之间的通信,那么此时要求变了,我希望能够实现一个项目,父进程可以指定一个子进程进行通信,它有多个子进程。
代码:(库文件部分)
#ifndef __MYPIPE_H__
#define __MYPIPE_H__
#include<iostream>
#include<unistd.h>
#include<vector>
#include<string>
#include<cstring>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
//重命名一个函数指针,表示我们的任务
typedef void(*func_t)();
//不同的任务
void NetworkLog()
{
cout << "正在执行一个网络任务……" << endl;
}
void MySQLLog()
{
cout << "正在执行一个MySQL任务……" << endl;
}
void PrintLog()
{
cout << "正在执行一个打印日志任务……" << endl;
}
#define COMMAND_LOG 0
#define COMMAND_MYSQL 1
#define COMMAND_REQUEST 2
//任务的管理
class Task
{
public:
Task()
{
//将所有的任务都添加进来
funcs.push_back(PrintLog);
funcs.push_back(MySQLLog);
funcs.push_back(NetworkLog);
}
//根据不同的命令执行不同的任务
void Excute(int command)
{
if(command >= 0 && command < funcs.size())
funcs[command]();
}
private:
vector<func_t> funcs;
};
//对每一对父子进程之间的通信进行管理
class proPipe
{
static int number;
public:
proPipe(int id, int fd)
:_id(id),_write_fd(fd)
{
//构建不同风格的进程名字
char namebuffer[64];
snprintf(namebuffer,sizeof(namebuffer),"process-%d[%d : %d]",number++,_write_fd,_id);
processName = namebuffer;
}
//访问成员方法
int get_fd() const
{
return _write_fd;
}
pid_t get_id() const
{
return _id;
}
string get_process_name() const
{
return processName;
}
private:
int _write_fd;
pid_t _id;
string processName;
};
int proPipe::number = 0;
#endif
代码:(主程序部分)
#include"mypipe.h"
#define PRO_NUM 5
Task t;
//命令执行
void waitCommand()
{
//一直保持通信当中,因为这里是子进程执行的事情,与父进程没有任何关系,等待的只有子进程
while(true)
{
//规定读取命令只有4个字节
int command = 0;
int n = read(0,&command,sizeof(int));
//成功读取到命令
if(n == sizeof(int))
{
t.Excute(command);
}
else if(n == 0)
{
cout << "父进程的写端已经关闭,所以我也要关闭了:" << getpid() << endl;
break;
}
else
{
break;
}
}
}
//创建多个进程和多个管道,建立链接
void CreateProcesses(vector<proPipe>& pro_pipe)
{
//用来存之前被打开的写端
vector<int> fds;
for(int i = 0; i < PRO_NUM; ++i)
{
int pipefd[2] = {0};
int n = pipe(pipefd);
if(n == -1)
{
cout << "errno:" << errno << endl;
exit(1);
}
pid_t id = fork();
if(id == -1)
{
cout << "errno:" << errno << endl;
exit(2);
}
if(id == 0)
{
//关闭的写端是父进程哪里来的
//for(auto& fd : fds) close(fd);
for(size_t i = 0; i < fds.size(); ++i)
{
close(fds[i]);
}
//这里是关闭自己这一次得到的写端位置
close(pipefd[1]);
//输入重定向,因为是不同的进程,所以不会相互之间影响
dup2(pipefd[0],0);
//下发命令
waitCommand();
close(pipefd[0]);
exit(0);
}
//关闭读端
close(pipefd[0]);
//保留写端,记录子进程的pid
pro_pipe.push_back(proPipe(id,pipefd[1]));
fds.push_back(pipefd[1]);
}
}
//界面显示
int ShowBoard()
{
cout << "############################################" << endl;
cout << "###### 0. Log_Task 1. MySQL_Taks ######" << endl;
cout << "###### 2. Network_log 3. exit ######" << endl;
cout << "############################################" << endl;
cout << "请选择#";
int command = 0;
cin >> command;
return command;
}
//进程控制
void CtrlProcess(vector<proPipe>& pro_pipe)
{
//int num = 0;
int cnt = 0;
while(true)
{
int command = ShowBoard();
if(command == 3) break;
if(command < 0 || command > 2) continue;
int index = cnt++;
cnt %= pro_pipe.size();
cout << "进程为:" << pro_pipe[index].get_process_name() << " | 处理任务:" << command << endl;
write(pro_pipe[index].get_fd(),&command,sizeof(command));
sleep(1);
}
}
void CtrlProcess2(vector<proPipe>& pro_pipe)
{
int num = 0;
while(true)
{
//选择任务
int command = rand() % 3;
//选择进程
int index = rand()%pro_pipe.size();
// printf("%d,%d",pro_pipe[index].get_id(),pro_pipe[index].get_fd());
// cout << pro_pipe[index].get_id() << ", " << pro_pipe[index].get_fd() << endl;
//这里的输出需要刷新一下缓冲区,否则子进程的数据会去覆盖父进程输出的数据
cout << pro_pipe[index].get_process_name() << ":" ;
fflush(stdout);
//sleep(1);
//分配任务
write(pro_pipe[index].get_fd(),&command,sizeof(command));
sleep(1);
}
}
//回收所有资源
void WaitProcess(vector<proPipe>& pro_pipe)
{
for(size_t i = 0; i < pro_pipe.size();++i)
{
cout << "父进程让子进程退出:" << pro_pipe[i].get_id() << endl;
close(pro_pipe[i].get_fd());
waitpid(pro_pipe[i].get_id(),nullptr,0);
cout << "父进程回收了子进程:" << pro_pipe[i].get_id() << endl;
}
}
int main()
{
vector<proPipe> pro_pipe;
//1. 先进行构建控制结构, 父进程写入,子进程读取
CreateProcesses(pro_pipe);
// 2. 输入命令控制进程实现不同的任务
CtrlProcess(pro_pipe);
// 3. 处理所有的退出问题
WaitProcess(pro_pipe);
return 0;
}
运行结果:
上图中,我们模拟了实际工程当中的父进程通过管道通信操作子进程完成某种资源的作用,这里的工作自是我简易表示的。如执行MySQL任务、网络任务等等。
博主并不打算对这个项目细讲,如果有特别感兴趣的可以私信博主,博主只分享这里面值得注意的部分。
//用来存之前被打开的写端
vector<int> fds;
//关闭的写端是父进程哪里来的
//for(auto& fd : fds) close(fd);
for(size_t i = 0; i < fds.size(); ++i)
{
close(fds[i]);
}
这一部分代码的作用我相信大家也能够理解,那就是为了保证子进程就是为了读取数据,那么就需要关闭他对应的写端。这我相信大家都能理解,大家不能理解的是,为什么我要用一个vector去存所有的写端呢?我之前不是已经关了一遍了吗?难道我还要重复的关,这不是有病吗?我每一次不就打开了两个文件吗?
这么想的朋友一定是忘了一个知识了,那就是子进程会继承父进程的文件描述符表,我问你,我们在子进程当中关闭了一个文件,对父进程有任何影响吗?不会,因为OS会保证进程之间的独立性,此时子进程关闭的是它自己打开的文件,而不是父亲的。但是!我们下一次创建子进程继承的数据是谁的?父进程!这表示了什么?这表示上一个子进程的写端被完美的继承到了下一个进程当中,哦豁,这不糟了吗?这不就表示我在这个子进程实际是可以给另外一个子进程发信息的,这不扯蛋了吗?所以我们需要通过循环的方式关闭从父进程继承来的所有写端。
如下图所示:
如果我们不用循环的方式进行管道连接:
博主并没有把第二张图画完,看起来太乱了,但是也足也表示了,可以看到我们的第二个子进程是有第一个子进程的写端的,第三个子进程是有前两个进程的写端的,这就会导致整个通讯乱套。
可能大家对于这个问题并没有实际的感受,但是如果看到下方的代码呢?
//回收所有资源
void WaitProcess(vector<proPipe>& pro_pipe)
{
for(size_t i = 0; i < pro_pipe.size();++i)
{
cout << "父进程让子进程退出:" << pro_pipe[i].get_id() << endl;
close(pro_pipe[i].get_fd());
waitpid(pro_pipe[i].get_id(),nullptr,0);
cout << "父进程回收了子进程:" << pro_pipe[i].get_id() << endl;
}
}
首先,我们在主进程当中关闭了对子进程的写端,但是我们真的关闭完了吗?我们的第二个子进程当中不是还有第一个子进程的写端吗?那么也就表示了子进程不会读取到文件结尾,也就是说,子进程并不会自己结束,那么再然后,我们等待子进程退出资源,我们能够等到吗?不能,子进程一直等着别人给他写东西呢,那么我们的程序还能正常的退出吗?不能!程序会一直卡在这个位置。难受不?如果不知道的话,这个bug估计得找一天。
匿名管道得使用规则:
对于匿名通道来说,一般只会出现于具有血缘关系的进程当中,它们能够通信的实际,其实就是子进程能够得到父进程创建的管道fd,通过这个fd,父子进程才能看到同一份资源。
1.4 命名管道
有匿名管道,那么对应的肯定是有一个东西叫做命名管道,命名管道的作用又是什么?又怎么创建它呢?
在Linux当中可以通过mkfifo函数创建一个命名管道。
这个玩意有啥用?别急,容我再创建一个窗口才能看出它的作用:
首先在该窗口当中我输入了echo命令,然后重定向到了fifo文件当中,正常来说应该会直接执行,但是事实是这样吗?并不是,界面一直在等待,那么我们在到另外一个窗口当中输出看一下是什么情况呢?
我们再另外一个界面查看fifo当中的数据,直接就帮助我们将另一个界面的数据输出了。
并且,此时看到另外一个界面,有什么问题?没有卡住了,继续执行了,
并且我们的fifo文件并没有大小,而且我们再次去cat fifo就不会出现上一次的数据了,也就表示了这个文件并不会为我们保存一个实际的数据,和匿名管道一样,只是一个缓存,只不过这一次的管道他又属于直接的名字了,也有自己的inode了,但是还是没有数据块。
那么我们在代码中可以怎么实现这个功能呢?如果实现了这样的需求,那岂不是我们以后的通信就不只是局限于父子进程了吗?
那么首要的事情就是了解系统接口:
该函数的两个参数分别是命名管道文件的地址加名字,后面的mode表示创建这个管道文件的权限是多少,相信大家也很熟悉了。
通过这个函数,我们就能创建一个实际的文件在我们的指定路径当中,之后的进程就可以通过这个文件进行通信。
对于这个文件的操作就和普通的文件操作是一模一样的,只需要一端通过写的方式打开,一段通过读得方式打开,就能进行通信。
那么基于此,我们就能实现两个进程基于命名管道得通信:所以博主决定实现两种功能,第一种是输入通过回车提示另一边接收,第二种是同步跟随我的输入。
代码:(库文件)
#pragma once
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string>
#include<string.h>
#include<cerrno>
std::string fifoname = "./fifo";
unsigned int Mode = 0666;
代码:(服务端)
#include"shared.h"
//创建管道文件
void creat_pipe_file()
{
umask(0);
//创建一个管道文件
int fi = mkfifo(fifoname.c_str(), Mode);
if(fi == -1)
{
std::cout << errno << " : " << strerror(errno) << std::endl;
exit(1);
}
(void)fi;
std::cout << "已经创建好了一个命名管道文件,准备通信" << std::endl;
}
//打开管道文件
int open_pipe_file()
{
//用读方式打开管道文件
int fd = open(fifoname.c_str(),O_RDONLY);
if(fd == -1)
{
std::cout << errno << " : " << strerror(errno) << std::endl;
exit(2);
}
std::cout << "服务端已经以读方式打开了管道文件" << std::endl;
return fd;
}
void read_pipe_data(int fd)
{
char buffer[1024];
bool adjust_style = false;
//读取数据
while(true)
{
buffer[0] = '\0';
int n = read(fd,buffer,sizeof(buffer)-1);
if(n > 0)
{
//读取到了数据
buffer[n] = '\0';
//换行显示模式
if(n >= 2)
{
std::cout << "client# "<< buffer << std::endl;
}
//跟随模式
else
{
if(!adjust_style) {adjust_style = true; continue; }
printf("%c",buffer[0]);
fflush(stdout);
}
}
else if(n == 0)
{
//写端关闭
std::cout << "客户端退出,所以我也要退出了" << std::endl;
break;
}
else
{
//异常错误
std::cout << errno << " : " << strerror(errno) << std::endl;
break;
}
}
}
int main()
{
//创建一个管道文件
creat_pipe_file();
//打开管道文件
int fd = open_pipe_file();
//读取数据
read_pipe_data(fd);
//关闭文件
close(fd);
//取消与管道的关联
unlink(fifoname.c_str());
return 0;
}
代码:(客户端)
#include"shared.h"
//显示方式
int show_board()
{
int n = 0;
while(true)
{
std::cout << "请选择换行显示还是跟随模式:(1 or 2) # ";
std::cin >> n;
if(n == 1 || n == 2)
break;
}
return n;
}
//换行显示模式
bool enter_display(int fd)
{
//写入数据
char buffer[1024];
char* msg = fgets(buffer,sizeof(buffer),stdin);
assert(msg);
(void)msg;
//消去我们按下的回车键
buffer[strlen(buffer)-1] = '\0';
if(strcasecmp(buffer,"quit") == 0)
{
std::cout << "收到退出信号,准备退出" << std::endl;
return false;
}
ssize_t n = write(fd,buffer,sizeof(buffer)-1);
assert(n >= 0);
(void)n;
return true;
}
//跟随显示模式
bool follow_display(int fd)
{
system("stty raw");
int c = getchar();
system("stty -raw");
//自己设置一个退出条件
if(c == '!')
{
std::cout << "收到退出信号,准备退出" << std::endl;
return false;
}
ssize_t n = write(fd,&c,sizeof(char));
assert(n >= 0);
(void)n;
return true;
}
//打开管道文件
int open_pipe_file()
{
int fd = open(fifoname.c_str(),O_WRONLY);
if(fd == -1)
{
std::cout << errno << " : " << strerror(errno) << std::endl;
exit(2);
}
return fd;
}
//写入文件
void write_pipe_data(int fd,int ret)
{
while(true)
{
if(ret == 1)
{
if(!enter_display(fd))
break;
}
//跟随显示模式
else
{
if(!follow_display(fd))
break;
}
}
}
//客户端
int main()
{
//以写方式打开文件
int fd = open_pipe_file();
//显示模式选择
int ret = show_board();
//写入数据进入文件当中
write_pipe_data(fd,ret);
//关闭文件
close(fd);
return 0;
}
运行效果:
模式1:输入(quit)退出
模式2:输入(!)退出
上面的程序并没有什么特别厉害的东西,用到得知识也就是对文件得操作还有对命名管道得创建,所以我也相信大家能够看懂和实现。当然如果有小伙伴感兴趣,随时私信博主哦。
2 共享内存
讲了这么一大部分关于管道的内容,相信大家也看疲劳的,来点更疲劳的,哈哈。关于共享内存呢,大家可以先建立一个概念,那就是这个共享内存他就是一个数组,我们在使用的时候可以直接把他当作一个数组使用。
我们知道进程互相独立,而想要让他们进行通信必须要让他们看到同一份资源,但是这一份资源又不可能在任何一个程序当中,否则就不能保证进程之间的独立性了,所以说,这个共享内粗你的存储位置只能在哪里呢?真实物理内存,说起真实的物理内存,这一个空间就又是OS帮我们维护的了,那么这就表示了什么呢?表示了共享内存很可能在系统当中不止有1个,可能有多个,而多个就需要对这个共享内存进行管理咯。怎么管理呢?先描述在组织,这里面具体的细节博主就不多讲了,对朋友们压力太大,对博主压力也太大。
首先通过ipcs -m可以看到系统当中的共享内存,如下图:
此时,我们并没有开启任何的程序,也没有自己创建任何的共享内存,系统当中就有这样的共享内存,朋友们的机器里面可能没有,我的是自己修改了一下。
那么系统中存在共享内存且没有任何进程在使用,他表示了什么?表示了共享内存的生命周期与进程脱离了,进程创建了它,不代表他就会跟随进程死掉。并且我告诉大家,如果我们已经存在了共享内存再创建同样的共享内存是不行的,这一点我后面在为大家讲解。
通过ipcrm -m shmid号 就可以删除这个共享内存。我这里就不删除了。
还是老规矩,既然我们能够通过命令的方式控制共享内存,那么对应的,它是一定在用户层提供了系统接口的。
如下:
申请一个共享内存,第二个参数的意思是创建的大小,第三个参数是创建的共享内存的权限和IPC_CREAT和IPC_EXEL两个位图变量。
IPC_CREAT:这个位表示,如果没有共享内存那就创建它,如果有了,那么我就获取它的shmid,对比于我们文件的fd。
IPC_EXEL:该位一般与IPC_CREAT一起使用,表示如果没有共享内存那就创建它,如果有了那就直接报错,也就是加上了这个位,就只能是自己创建的共享内存才正确。
第一个参数key是什么?这个是共享内存自己搞出来的一套标识这个共享内存唯一的标志,它通过函数ftok函数创建,如下:
这两个参数其实都可以随意设置,但是博主一般会把第一个参数设置为我们项目的路径,第二个博主真就是随心所欲了。通过这个函数可以返回一个唯一key指,此时我们就能够通过这个返回的key去创建共享内存了。
那么创建好了共享内存之后我们需要做什么呢?肯定是关联它咯,当然两个进程都是需要关联的。关联函数如下:
该函数会返回这个共享内存的首地址,拿到了首地址,也就表示了我们有了控制这个地址资本,当两个进程都连接上了,这个时候也就能够相互通信了。
其中的第二个参数和第三个参数我们都填入空即可,对于我们来说他并不重要,因为我们已经创建好了共享内存,不需要再来创建了。
这个时候我就来讲解一个为什么两个进程能够通信吧,如图:
每一个进程都有属于自己的进程地址空间,这个地址空间当中有一个区域叫做共享区,位于堆区和栈区之间,共享区当中有一个地址通过页表映射能够找到实际物理空间当中的共享内存,另一个进程也是如此,此时两个进程就能看到同一份资源,这也就表明了两个进程之间能够通信了。
通信完毕之后就需要取消关联:
取消关联的函数如下:和shmat共用头文件
我们将这个共享内存在该进程中的地址拿去去关联就行,每一个进程都有权力去去关联,如果在某一个进程还连接着这个共享内存时,然后把这个共享内存删除,此时该共享内存不会被关闭,只有等到我们的进程关闭之后,这个共享内存才会真正的被删除,也就是我之前展示的图当中有的那个dest标志,正常情况下是没有的。
最后就是删除共享内存,对于删除来说,正常情况下都是谁创建谁去删除,不要让其他进程代为控制,很容易出现文件。函数如下:
它里面的参数分别是shmid,以及我们要进行的操作方式,还有就是如果我们想要看这个共享内存在系统中的结构是什么就可以传一个共享内存对象的指针给他,他会将数据填完返回给你。当然如果我们要删除直接传入空就好,没必要看他:如果一定要看的话,用他的结构体构建对象:
第二个参数命令方式,常用的有IPC_STAT也就是可以让你查看到共享内存具体数据的命令。而我们的删除命令就是IPC_RMID。
以上就完成了我们的共享内存的创建-连接-通信-去连接-删除的过程。那么我想要像命名管道一样实现一个两个进程之间的通信也变得很简单了。如下:
代码:(库文件)博主用类封装起来了,以后直接创建一个对象就能使用共享内存了。
#pragma once
#include<iostream>
#include<sys/types.h>
#include<sys/ipc.h>
#include<cstdio>
#include<cstring>
#include<string>
#include<sys/shm.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#define PATHNAME "."
int gsize = 4096;
int PROID = 0x1111;
//获取key值
int getKey()
{
key_t k = ftok(PATHNAME,PROID);
if(k == -1)
{
std::cout << errno << " : " << strerror(errno) << std::endl;
exit(1);
}
return k;
}
//转化为十六进制
std::string tohex(int k)
{
char buffer[64];
snprintf(buffer,sizeof(buffer),"0x%x",k);
return buffer;
}
//共享内存帮助函数
static int shm_helper(key_t k, int size, int flag)
{
umask(0);
int shmid = shmget(k,gsize,flag);
if(shmid == -1)
{
std::cout << errno << " : " << strerror(errno) << std::endl;
exit(2);
}
return shmid;
}
//创建共享内存
int create_shm(key_t k)
{
return shm_helper(k,gsize,IPC_CREAT | IPC_EXCL | 0666);
}
//获取共享内存
int get_shm(key_t k)
{
return shm_helper(k,gsize,IPC_CREAT);
}
//删除共享内存
void del_shm(int shmid)
{
shmctl(shmid,IPC_RMID,NULL);
}
//关联共享内存
char* link_shm(int shmid)
{
char* shmaddr = (char*)shmat(shmid,NULL,0);
return shmaddr;
}
//取消关联
void detach_shm(char* shmaddr)
{
shmdt(shmaddr);
}
#define SERVER 0
#define CLIENT 1
//通过封装类将整个连接过程合并
class shm_conected
{
public:
shm_conected(int user)
:_user(user)
{
_key = getKey();
//查看是服务端还是客户端
if(_user == SERVER)
{
_shmid = create_shm(_key);
}
else
{
_shmid = get_shm(_key);
}
_shmaddr = link_shm(_shmid);
}
const char* get_shmaddr() const
{
return _shmaddr;
}
char* get_shmaddr()
{
return _shmaddr;
}
~shm_conected()
{
detach_shm(_shmaddr);
if(_user == SERVER)
{
del_shm(_shmid);
}
}
private:
char* _shmaddr;
int _shmid;
key_t _key;
int _user;
};
代码:(服务端)
#include"sharedfile.h"
//换行输出形式
void Enter_display()
{
shm_conected shm1(SERVER);
char* shmaddr = shm1.get_shmaddr();
int size = gsize;
while(true)
{
//shmaddr[0] = '\0';
fgets(shmaddr,size,stdin);
shmaddr[strlen(shmaddr)-1] = '\0';
if(strcmp(shmaddr,"quit") == 0)
break;
}
std::cout << "退出通信" << std::endl;
}
//完整的连接过程
void common_link()
{
//1.创建一个关键key值,用于申请我们的共享内存
int k = getKey();
std::cout << "key: " << tohex(k) << std::endl;
//2.创建一个共享内存
int shmid = create_shm(k);
std::cout << shmid << std::endl;
//3. 关联共享内存
char* shmaddr = link_shm(shmid);
sleep(3);
//4. 通信
char val = 'A';
int pos = 0;
while(val <= 'Z')
{
shmaddr[pos++] = val++;
sleep(1);
}
//5. 取消关联
detach_shm(shmaddr);
//6. 删除共享内存
del_shm(shmid);
}
int main()
{
Enter_display();
return 0;
}
代码:(用户端)
#include"sharedfile.h"
void Enter_display()
{
shm_conected shm1(CLIENT);
char* shmaddr = shm1.get_shmaddr();
while(true)
{
if(shmaddr[0] == '\0') continue;
std::cout << shmaddr << std::endl;
if(strcmp(shmaddr,"quit") == 0) break;
shmaddr[0] = '\0';
}
std::cout << "退出通信" << std::endl;
}
//完整的连接过程
void common_link()
{
int k = getKey();
std::cout << "key: " << tohex(k) << std::endl;
int shmid = get_shm(k);
std::cout << shmid << std::endl;
char* shmaddr = link_shm(shmid);
int n = 0;
while(n < 35)
{
n++;
std::cout << shmaddr << std::endl;
sleep(1);
}
detach_shm(shmaddr);
}
int main()
{
Enter_display();
return 0;
}
运行结果:
如上就完成了我们对共享内存的通信了,上面的代码大家有兴趣可以看一下。
以上就是博主对于这一片知识的全部理解了,希望能够帮助到大家。