文章目录
- 前言
- 1、三个问题
- 1-1、什么是通信?
- 1-2、为什么要有通信
- 1-3、怎么进行通信?
- 1-4、进程间通信分类
- 2、管道
- 2-1、匿名管道
- 2-1-1、理解通信本质问题
- 2-1-2、进一步理解管道
- 2-1-3、代码实现
- pipe函数
- 2-1-4、读写特征
- 2-1-5、管道的特点(重点)
- 2-1-6、基于匿名管道的设计(重点)
- 2-2、命名管道
- 2-2-1、创建一个命名管道
- 2-2-2、匿名管道与命名管道的区别
- 2-2-3、命名管道的打开规则
- 2-2-4、命名管道的样例
- 3、system V共享内存
- 3-1、共享内存的原理
- 3-2、共享内存的概念
- 3-3、认识共享内存的接口
- 3-3-1、shmget接口
- 3-3-2、ftok接口
- 3-3-3、再谈key
- 3-3-4、补充接口
- 3-4、IPC的特点(system V版本进程间通信特点)
- 3-5、共享内存特点
- 3-5-1、优点
- 3-5-2、题目(小难点)
- 3-5-3、缺点
- 3-6、共享内存测试代码
- 4、总结
前言
.cpp、.cc和.cxx都是用来表示C++文件的!
三者没有区别
//纯数字没有任何意义,必须有类型才有意义
//int a =10;编译器推导+隐式类型转换
//100;字面值
//10u 无符号整数
//10L
//10.0f
//10
1、三个问题
1-1、什么是通信?
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止 时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
以上4点都是进程间通信
1-2、为什么要有通信
我们到目前为止学习的代码都是单进程的,不管是C++还是linux都是一个进程。但是,我们一定会遇到多个进程协同完成某种业务(场景)
,这个时候就需要由通信来处理
cat file | grep 'hello'
1-3、怎么进行通信?
管道——基于文件系统
System V进程间通信——只能在本地通信,也就是同一台机器
POSIX进程间通信——可以跨主机通信
1-4、进程间通信分类
管道
匿名管道
pipe 命名管道
System V IPC
System V 消息队列
System V 共享内存
System V 信号量
POSIX IPC
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
上面就是我们进行通信的方法(我们这里学习的通信方法是主流的),但是由于System V进程间通信只能在本地进行通信,所以我们只学习一部分该内容,主要学习的还是POSIX进程间通信
2、管道
什么是管道
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
管道分为:匿名管道和命名管道
我们先来学习一下匿名管道,学完匿名管道之后,命名管道就很简单了
这里要注意:有人问为什么管道不是双向的。管道管道,既然叫管道双向不是更好吗?
这是因为:早期科学家先研究出管道这项技术,然后再取名为管道。先有技术,后有名字。所以,管道是单向的!
最简单的例子:家里的水龙头,只会从水源地向家里来水.
2-1、匿名管道
我们知道父进程fork创建子进程处了task_struct(进程控制块PCB)会拷贝以外,struct_files_struct(叫做用户打开文件表
)也会被拷贝。这些拷贝的数据是内核数据结构,但是文件是属于文件系统的,他不会进行拷贝。所以父子进程的文件描述符表都指向同一个文件
2-1-1、理解通信本质问题
我们两个进程a、b进行通信,一定要有一块空间是我们两个进程都能够找到/看到的,不然通信没有办法完成。这个进程既不属于a也不属于b,因为不管空间属于谁,都属于该进程的私有空间。但是,进程具有独立性,所以不能与另一个进程进行通信
那么,这个空间只能由OS来提供,这样就可以进行通信了!
结论1:OS需要直接或者间接给通信双方/多方进程提供“内存空间”
结论2:要通信的进程必须看到一份公共资源(这份公共资源由OS提供)
结论3:不同的通信种类:本质上就是:公共资源是由哪一个模块提出来的!
举个例子:结论2的公共资源由操作系统中的文件系统提供,那么就叫做管道通信
;如果是System V和POSIX提供的,那么就叫做System V或者POSIX进程间通信!
这就是为什么通信成本不低的原因。要进行通信,先要做好两件事情:
1、让通信的所有进程看到同一份资源(OS内部的不同模块提出,就形成不同的进程间通信),因为进程具有独立性,再进程内部创建公共资源,其他进程看不到!!!
2、进行通信
所以,进行小结:
这种基于文件系统的管道,就叫做管道文件。管道文件是一个内存级的文件。管道文件是进程在内存里面就进行了通信,速度很快。如果采用普通文件,把数据写到磁盘,再从磁盘读出来,这就很慢了
我们总是把磁盘文件加载到内存,然后操作系统创建struct_file对象,再形成对应的内核缓冲区…
但是,OS就算不打开磁盘内部的文件,也是可以生成struct_file结构体对象,然后形成内核缓冲区的!所以,管道是不需要进行磁盘刷新的! 我们说的管道文件是种内存级文件!
那么,我们任何让两个进程看到同一个管道文件呢?
1、父进程打开一个文件
2、fork创建子进程
但是,我们这种管道文件是没有起名字的!(父子进程不是通过文件名来查找的)。所以这类内存级管道文件就叫做匿名文件
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
2-1-2、进一步理解管道
站在文件描述符角度-深度理解管道
这里父进程打开文件必须是分别以读和写两种方式打开文件!因为父进程只有一种操作,子进程继承父进程也只有一种操作,这就不能称之为管道了。
然后,我们要分别关闭父子进程不需要的操作端口,比如父进程只负责写,子进程只负责读,那么就关闭父进程的读端和子进程的写端。具体场景具体操作。
父子进程都不关端口也是可以的,但是,我们所没有关闭的端口可能被其他人使用,那样就会产生危害了!
到目前为止我们也得出来了小结论:
匿名管道:能够用来进行父子进程之间进行进程间通信(具有血缘关系的进程都可以采用匿名管道)
2-1-3、代码实现
pipe函数
pipe 创建一个管道
所以,要使用管道,调用一些pipe函数接口就行
样例:
#include <iostream>
#include <unistd.h>
#include <cassert>
//#include <assert.h>//对比于assert.h和stdio.h我们写成cassert和cstdio更好——去掉.h,在头文件前面加上c
using namespace std;
int main()
{
int fds[2];
int ret = pipe(fds);
assert(ret==0);
//0,1,2是输入输出错误流,那pipe的结果哪一个是读,哪一个是写呢?
cout<<"fds[0]"<<fds[0]<<endl;
cout<<"fds[1]"<<fds[1]<<endl;
return 0;
}
接下来我们继续拓展:
#include <iostream>
#include <unistd.h>//#include <assert.h>//对比于assert.h和stdio.h我们写成cassert和cstdio更好——去掉.h,在头文件前面加上c
#include <cstdio>
#include <cstring>
#include <string>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()//父进程读取,子进程写入
{
//1、创建管道,打开读写端
int fds[2];
int ret = pipe(fds);
assert(ret==0);
//2、fork子进程
pid_t id = fork();
assert(id >= 0);
const char *s = "我是子进程,我正在给你发消息";
int cnt=0;
if(id==0)//子进程
{
//子进程通信代码
close(fds[0]);
while(1)
{
char buffer[1024];
++cnt;
snprintf(buffer,sizeof (buffer),"child->parent say: %s[%d][%d]",s,cnt,getpid());
write(fds[1],buffer,strlen(buffer));//不考虑\0作为结尾
sleep(1);//每隔1s向管道写一次
}
close(fds[1]);
exit(-1);
}
//父进程通信代码
close(fds[1]);
while(1)
{
char buffer[1024];
ssize_t n = read(fds[0],buffer,sizeof(buffer)-1);
if(n>0)
buffer[n]=0;
cout << "Get Message# " << buffer << " | my pid: " << getpid() << endl;
}
ret = waitpid(id,nullptr,0);
assert(ret == id);
close(fds[0]);
// 0,1,2是输入输出错误流,那pipe的结果哪一个是读,哪一个是写呢?
// [0]: 读取,嘴巴,读书的
// [1]: 写入,钢笔,写的
// cout << "fds[0]" << fds[0] << endl;
// cout << "fds[1]" << fds[1] << endl;
return 0;
}
这就叫做管道(匿名管道)
2-1-4、读写特征
上面例子的改进:
#include <iostream>
#include <unistd.h> //#include <assert.h>//对比于assert.h和stdio.h我们写成cassert和cstdio更好——去掉.h,在头文件前面加上c
#include <cstdio>
#include <cstring>
#include <string>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main() // 父进程读取,子进程写入
{
// 1、创建管道,打开读写端
int fds[2];
int ret = pipe(fds);
assert(ret == 0);
// 2、fork子进程
pid_t id = fork();
assert(id >= 0);
const char *s = "我是子进程,我正在给你发消息";
int cnt = 0;
if (id == 0) // 子进程
{
// 子进程通信代码
close(fds[0]);
while (1)
{
char buffer[1024];
++cnt;
snprintf(buffer, sizeof(buffer), "child->parent say: %s[%d][%d]", s, cnt, getpid());
write(fds[1], buffer, strlen(buffer)); // 不考虑\0作为结尾
// sleep(1);//每隔1s向管道写一次
// sleep(5);//5s写一次
cout << "count: " << cnt << endl; // 统计缓冲区的容量
// sleep(50);
// break;//写入一行消息,直接退出写端
}
close(fds[1]); // 退出写端,关闭文件描述符
cout << "子进程关闭自己的写端" << endl;
//sleep(10000);
exit(-1);
}
// 父进程通信代码
close(fds[1]);
while (1)
{
sleep(2);//写端写一行数据退出,关闭文件描述符。读端等待两秒,读取到0就退出
char buffer[1024];
// cout << "AAAAAAAAAAAAAAAAAAAAAA" << endl;
// 如果管道中没有了数据,读端在读,默认会直接阻塞当前正在读取的进程!
ssize_t n = read(fds[0], buffer, sizeof(buffer) - 1); // 写入速度慢,读取会卡在read这里,等待写端继续写入
// cout << "BBBBBBBBBBBBBBBBBBBBBB" << endl;
if (n > 0)
{
buffer[n] = 0;
cout << "Get Message# " << buffer << " | my pid: " << getpid() << endl;
}
else if (n == 0)//写端关闭
{
// 读到文件结尾
cout << "read: " << s << endl;
break;
}
//else
//{
break;//写端一直写,读端关闭
//}
}
close(fds[0]);//提前关闭文件描述符
cout << "父进程关闭读端" << endl;
int status = 0;
ret = waitpid(id, &status, 0);
assert(ret == id);
cout <<"pid->"<< ret << " : "<< (status & 0x7F) << endl;
// ret = waitpid(id, nullptr, 0);
// assert(ret == id);
// close(fds[0]);
// 0,1,2是输入输出错误流,那pipe的结果哪一个是读,哪一个是写呢?
// [0]: 读取,嘴巴,读书的
// [1]: 写入,钢笔,写的
// cout << "fds[0]" << fds[0] << endl;
// cout << "fds[1]" << fds[1] << endl;
return 0;
}
根据上面的例子,加以改进,我们可以得出结论:
1、读取速度慢,写入速度快——写端直接写满管道(管道是有大小的),发送写端阻塞,等待读端读取(读端一次性读取1024字节,不是一行一行读取的)(读慢,写快)
2、读取速度快,写入速度慢——读取会在read处阻塞等待,等待写端写入,然后进行读取(由于写端过慢,所以写端写一行,读端就读一行)(读快,写慢)
3、写入操作关闭,读取读到0——写端关闭了,读端读取完管道剩余数据,读到0就停止(写关闭,读到0)
4、读取操作关闭,OS会终止写端(给进程发送信号,终止写端)
2-1-5、管道的特点(重点)
1、管道的生命周期随进程退出而销毁。管道基于文件而产生,随着打开文件的进程关闭而销毁
2、管道可以使具有血缘关系的进程进行通信,常用于父子进程
3、管道是面向字节流的(网络知识)——比如:我们写了很多数据,但是读取的时候,读端不管我们写入数据的类型(字符、字符串或者其他格式),只按照读端最大读取字节数来读
4、半双工——单向通信(特殊概念)。数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
5、互斥与同步机制——对共享资源进行保护的一种方案
这个机制进程是没有的,因为进程具有独立性,所以互相不知道有对方的存在。
但是对于管道来说,读端读完了等写端写继续读,写端写满了等读端读完了继续写,好像管道的读端和写端都很照顾对方,这就是互斥与同步机制
所以
sleep 1000 | sleep 2000
这两个是兄弟进程!
2-1-6、基于匿名管道的设计(重点)
任务:我们将我们的任务均衡的下发/分配给每一个子进程,让子进程进行
负载均衡
的操作(单机版)
简单来说就是:父进程随机给任意一个子进程,发送任意的任务,然后子进程执行
#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <cassert>
#include <ctime>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;
#define MakeRand() srand((unsigned long)time(nullptr) ^ getpid() ^ 0x171237 ^ rand() % 1234) // 获取随机数
#define C_NUM 5 //创建子进程个数
typedef void (*func_t)(); // 函数指针类型
void downLoadTask() /我们自己模拟出来的任务
{
std::cout << getpid() << ": 下载任务\n"
<< std::endl;
sleep(1);
}
void ioTask()
{
std::cout << getpid() << ": IO任务\n"
<< std::endl;
sleep(1);
}
void flushTask()
{
std::cout << getpid() << ": 刷新任务\n"
<< std::endl;
sleep(1);
}
void LoadTaskFunc(std::vector<func_t> *out)
{
assert(out); // 判断数组不为空,也就是任务表不能为空
out->push_back(downLoadTask);
out->push_back(ioTask);
out->push_back(flushTask);
} ///
class SubEp // 我们将这个结构体放到数组里面,那么父进程通过任意管道对进程的操作就变成了对数组下标对应的操作
{
public:
SubEp(pid_t subId, int writeFd)
: subId_(subId), writeFd_(writeFd)
{
char my_buffer[1024];
snprintf(my_buffer, sizeof(my_buffer), "process-%d[pid(%d)-fd(%d)]", num++, subId_, writeFd_); // 查看num的值,子进程pid和文件描述符
name_ = my_buffer;
}
public: // 这里为了方便正文代码部分的调用,就把成员变量设置为public的
static int num;
std::string name_; // 进程的名字
pid_t subId_; // 子进程pid
int writeFd_; // 管道的写端,很重要,父进程靠写端给子进程发消息
};
int SubEp::num = 0;
// 这里定义num是因为我们创建子进程的时候,子进程的id值是波动的,导致我们这里为进程取名字的时候每次都不一样,所以加个num,每次初始化为0,然后++保证创建的name是可以一样的
int recvtask(int readfd)
{
int code = 0;
int n = read(readfd, &code, sizeof(code));
if (n == sizeof(int))
return code;
else if (n <= 0)
return -1;
else
return 0;
}
void SendTask(const SubEp &my_id, int fm_num) // 将任务发送给进程
{
cout << "send task num: " << fm_num << " send to -> " << my_id.name_ << endl;
int num = write(my_id.writeFd_, &fm_num, sizeof(fm_num));
assert(num == sizeof(int));
(void)num;
}
void creatSubPoints(std::vector<SubEp> *subs, std::vector<func_t> &funcMap)
{
for (int i = 0; i < C_NUM; i++)
{
int fds[2]; // 获取文件描述符,fds[0]是读端,fds[1]是写端
int n = pipe(fds); // 创建匿名管道
assert(n == 0);
(void)n; // 这里是因为debug模式下assert生效,但是在release模式下assert失效了。这个时候n没有被使用就报警告,变量没有被使用,这里强转是表示使用了变量,避免警告
pid_t id = fork(); // 创建子进程
if (id == 0)
{
// 子进程, 进行处理任务
close(fds[1]); // 子进程关闭写端
while (true) // 子进程一直做下面的事情
{
// 1、子进程要获取命令码,如果没有发送,应该阻塞
int my_code = recvtask(fds[0]); // 从读端获取命令码
// 2、完成任务
if (my_code >= 0 && my_code < funcMap.size())
funcMap[my_code]();
else if (my_code == -1 || my_code == 0) // 其实这里不可能等于0
break;
// else
// cout << "my_code error!" << endl;
}
exit(0);
}
close(fds[0]);
SubEp sub(id, fds[1]); // 这里创建对象
subs->push_back(sub); // 将创建好的对象尾插到数组里面,方便正文进行操作
}
}
void MyidLoadBash(const std::vector<SubEp> &subs, const std::vector<func_t> &funcMap, int count)
{
int Id_num = subs.size(); // 子进程个数
int fm_num = funcMap.size();
bool quit = (count == 0) ? true : false;
while (true)
{
// 1、选择一个进程——> std::vector<SubEp> ——> index下标
int subidx = rand() % Id_num;
// 2、选择一个任务——> std::vector<func_t> ——> index下标
int fmidx = rand() % fm_num;
// 3、将任务发给进程
SendTask(subs[subidx], fmidx); // 将任务fmidx发送给数组中的具体进程
sleep(1);
if (!quit)
{
--count;
if (count == 0)
break;
}
}
// write quit -> read 0
for (int i = 0; i < Id_num; i++)
close(subs[i].writeFd_); // 关闭父进程读端,进行waitpid();
}
void waitMyid(std::vector<SubEp> Myid)
{
int num = Myid.size();
for (int i = 0; i < num; ++i)
{
waitpid(Myid[i].subId_, nullptr, 0);
cout << "wait sub process success ...: " << Myid[i].subId_ << endl;
}
}
int main()
{
MakeRand(); // 生成随机数
// 1. 建立子进程并建立和子进程通信的信道, 这里是有bug的,但是不影响我们后面编写,我们代码写完了然后讲这个bug
// 1.1加载方发表
std::vector<func_t> funcMap; // 创建一个表格,表格里面存放着我们要执行的任务
LoadTaskFunc(&funcMap);
// 1.2 创建子进程,并且维护好父子通信信道
std::vector<SubEp> subs; // 这样父进程通过管道对子进程发送消息就简化成为了对数组下标的处理
creatSubPoints(&subs, funcMap); // 将创建管道和进程封装为一个函数接口,然后把任务表也加载过去
// 2. 走到这里就是父进程, 控制子进程
int Count = 3; // 0: 永远进行
MyidLoadBash(subs, funcMap, Count);
// 3. 回收子进程信息
waitMyid(subs);
return 0;
}
我们上面说了,这段代码是有bug的,但是bug不会影响我们上面的代码,接下来我们就来讲述一下这个bug
大家动手画一下就明白了,简单来说就是:
父进程每次创建一个新的子进程,新的子进程就会继承前面所有子进程的写端!
我们上面的处理方法本质上是倒着关闭子进程的写端,接下来就来看看如何正着来关闭写端:
void creatSubPoints(std::vector<SubEp> *subs, std::vector<func_t> &funcMap)
{
std::vector<int> deleteFd;///这个数组存放着上一个子进程的写端
for (int i = 0; i < C_NUM; i++)
{
int fds[2]; // 获取文件描述符,fds[0]是读端,fds[1]是写端
int n = pipe(fds); // 创建匿名管道
assert(n == 0);
(void)n; // 这里是因为debug模式下assert生效,但是在release模式下assert失效了。这个时候n没有被使用就报警告,变量没有被使用,这里强转是表示使用了变量,避免警告
pid_t id = fork(); // 创建子进程
if (id == 0)
{
for(int i = 0; i < deleteFd.size(); i++) close(deleteFd[i]);//清除上一个子进程的读端
// 子进程, 进行处理任务
close(fds[1]); // 子进程关闭写端
while (true) // 子进程一直做下面的事情
{
// 1、子进程要获取命令码,如果没有发送,应该阻塞
int my_code = recvtask(fds[0]); // 从读端获取命令码
// 2、完成任务
if (my_code >= 0 && my_code < funcMap.size())
funcMap[my_code]();
else if (my_code == -1 || my_code == 0) // 其实这里不可能等于0
break;
// else
// cout << "my_code error!" << endl;
}
exit(0);
}
close(fds[0]);
SubEp sub(id, fds[1]); // 这里创建对象
subs->push_back(sub); // 将创建好的对象尾插到数组里面,方便正文进行操作
deleteFd.push_back(fds[1]);/将该进程的写端存放到数组里面
}
}
2-2、命名管道
有了上面匿名管道的基本知识,命名管道学起来十分简单!
命名管道就可以让两个毫不相干的进程进行交互
2-2-1、创建一个命名管道
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
$ mkfifo filename
命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);
创建命名管道:
int main(int argc, char *argv[])
{
mkfifo("p2", 0644);
return 0;
}
2-2-2、匿名管道与命名管道的区别
匿名管道由pipe函数创建并打开。
命名管道由mkfifo函数创建,打开用open
FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完 成之后,它们具有相同的语义。
2-2-3、命名管道的打开规则
如果当前打开操作是为读而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO O_NONBLOCK
enable:立刻返回失败,错误码为ENXIO
2-2-4、命名管道的样例
makefile:
.PHONY:all
all:server client
server:server.cc
g++ -o $@ $^ -std=c++11 -g
client:client.cc
g++ -o $@ $^ -std=c++11 -g
.PHONY:clean
clean:
rm -f server client
comm.hpp:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define NAMED_PIPE "/tmp/mypipe.106"
bool createFifo(const std::string &path)
{
umask(0);
int n = mkfifo(path.c_str(), 0600);
if (n == 0)
return true;
else
{
std::cout << "errno: " << errno << " err string: " << strerror(errno) << std::endl;
return false;
}
}
void removeFifo(const std::string &path)
{
int n = unlink(path.c_str());
assert(n == 0); // debug , release 里面就没有了
(void)n;
}
client.cc:
#include "comm.hpp"
int main()
{
std::cout << "client begin" << std::endl;
int wfd = open(NAMED_PIPE, O_WRONLY);
std::cout << "client end" << std::endl;
if(wfd < 0) exit(1);
//write
char buffer[1024];
while(true)
{
std::cout << "Please Say# ";
fgets(buffer, sizeof(buffer), stdin); // abcd\n
if(strlen(buffer) > 0) buffer[strlen(buffer)-1] = 0;//去掉我们输入时敲的回车
ssize_t n = write(wfd, buffer, strlen(buffer));
assert(n == strlen(buffer));
(void)n;
}
close(wfd);
return 0;
}
server.cc:
#include "comm.hpp"
int main()
{
bool r = createFifo(NAMED_PIPE);
assert(r);
(void)r;
std::cout << "server begin" << std::endl;
int rfd = open(NAMED_PIPE, O_RDONLY);
std::cout << "server end" << std::endl;
if(rfd < 0) exit(1);
//read
char buffer[1024];
while(true)
{
ssize_t s = read(rfd, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;
std::cout << "client->server# " << buffer << std::endl;
}
else if(s == 0)
{
std::cout << "client quit, me too!" << std::endl;
break;
}
else
{
std::cout << "err string: " << strerror(errno) << std::endl;
break;
}
}
close(rfd);
// sleep(10);
removeFifo(NAMED_PIPE);
return 0;
}
这样我们在client下面输入就能够打印到server界面中
3、system V共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到
内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
3-1、共享内存的原理
第一步:申请一块物理内存空间(共享内存)
第二步:将创建好的内存分别映射到多个进程的进程地址空间(进程和共享内存挂接)
第三步:取消进程和内存的映射关系(去关联),并且释放内存(释放共享内存)
理解:
1、进程间通信是专门设计的(说明有专门的接口),用来IPC(进程间通信的简称)。C语言的malloc等操作只能让自己看到堆区上面的空间,不能让其他人看到同一份空间,所以行不通。
2、共享内存是一种通信方式,所有想通信的进程都可以用
3、OS中一定会存在很多的共享内存
共享内存示意图
3-2、共享内存的概念
通过让不同的进程,看到同一个内存块的方式,就叫做:共享内存
3-3、认识共享内存的接口
linux老套路了,先把makefile给写好
makefile:
//我们需要用到shm_client.cc和shm_server.cc两个文件,然后编译链接生成shm_client shm_server
.PHONY:all
all:shm_client shm_server
shm_client:shm_client.cc
g++ -o $@ $^ -std=c++11
shm_server:shm_server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f shm_client shm_server
我们搜先需要创建一份内存
3-3-1、shmget接口
我们一一介绍这些参数:
shmflg:最常用的选项有两个
如果shmflg给0,那么就表示选项IPC——CREAT
size:共享内存的大小
返回值:shmget操作成功之后,会返回一个共享内存的标识符,这个标识符就是一个数字,这个数字就是数组的下标。但是,在不同的操作系统下这个数组下标是不同的。并且这个标识符与文件是两套不同的,所以我们用这个很少,我们只需要知道,将这个数字当做标识符就可以了,未来我们想要对共享内存做一些操作,通过这个标识符就能够完成了
key:我们在调用完shmget接口之后,能够得到一个标识符,通过这个标识符才能够判断两个进程是不是看到的是同一块共享内存,但是在没有调用完shmget之前,我们怎么能够保证两个或者多个进程看到的是同一块共享内存呢?
所以,key是什么不重要,重要的是,它能够进行唯一标识性最重要(和人的身份证号码一样,号码是多少其实没有什么用,重要的是号码是唯一的,它代表你这个人)!
3-3-2、ftok接口
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
pathname:路径
proj_id:项目标识符,也就是项目id
将路径名和项目标识符(也就是项目id)进行整合,然后转化为一个IPC的key,也就是一个唯一的数字,然后将这个数字返回
所以,ftok接口生成一个唯一的数字(标识符),然后返回给shmget函数中的key
那么,是不是我们pathname和proj_id两个参数一样,就能够得到一模一样的key,key一样shmget返回的标识符(数字)也就一样,这就可以确定多个进程看到的是同一份共享内存了!!!
那么我们先来见见猪跑:
comm.hpp:
#ifndef _COMM_HPP_
#define _COMM_HPP
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<cerrno>
#include<cstring>
#include<cstdlib>
using namespace std;
using std::cout;
using std::endl;
#define PATHNAME "." //确定多个进程采用相同的路径
#define PROJ_ID 0x55 //确定多个进程采用相同的项目id(标识符)
key_t GetKey()
{
key_t k = ftok(PATHNAME,PROJ_ID);
if(k < 0)
{
//cin,cout,cerr -> stdin,stdout,stderr -> 0,1,2
//strerror将错误码转化为错误码的描述
cerr << errno << ":" << strerror(errno) << endl;//像文件标识符2打印,也就是标准错误输出
exit(1);
}
return k;
}
#endif
shm_server.cpp:
#include "comm.hpp"
int main()
{
key_t k = GetKey();
printf("0x%x\n",k);
return 0;
}
shm_client.cpp:
#include "comm.hpp"
int main()
{
key_t k = GetKey();
printf("0x%x\n",k);
return 0;
}
3-3-3、再谈key
我们C语言的malloc开辟堆空间会多开辟一些空间,这些空间用来记录此次开辟空间的一系列数据,方便os做管理
那么同理,我们的共享内存也是需要被os管理起来的!先描述,再组织
共享内存 = 物理内存块 + 共享内存的相关属性
那么,我们在创建共享内存的时候,怎么保证共享内存在系统中是唯一的呢?
这就要用到key值了
我们只要保证其他进程看到同一个key值,那么就可以保证多个进程看到的是同一个共享内存了
那么key值在什么地方呢?
struct shm{
key_t k;//k在某一个共享内存的相关属性里面,我们只需要查找到key即可
}
key我们创建出来了,是要设置进共享内存相关属性当中的!用来表示该共享内存在内核中的唯一性
这里key和shmid,为什么有了key还要shmid呢?直接返回key不行吗?与前面的fd和inode一样
是为了宏观上面的解耦。举个例子,我们都有自己的身份证号,为什么到了学校不用身份证号而是用名字,为什么到了企业用的是工号,而不是名字。就是为了不全部耦合在一起,不会互相产生影响
所以,底层用key,上层用shmid,这样没有强耦合,操作系统将底层代码等数据修改了不会影响上层数据
3-3-4、补充接口
shmget函数
功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
shmat函数
功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr -
(shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
shmdt函数
功能:将共享内存段与当前进程脱离
原型
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
shmctl函数
这里的控制包括了删除等一系列操作
功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
3-4、IPC的特点(system V版本进程间通信特点)
IPC的特点:共享内存的生命周期是随os的,不是随进程的!,这是所有system v版本通信的特性
查看IPC资源:
ipcs -m/-q/-s
删除共享内存:
ipcrm -m shmid
3-5、共享内存特点
3-5-1、优点
所有的进程间通信速度最快,能大大减少数据拷贝次数
3-5-2、题目(小难点)
相同的代码,通过共享内存和管道,经过键盘输入,显示器输出,分别要进行几次拷贝呢?
管道:
共享内存:
共享内存直接充当了进程通信的缓冲区!!!
当然!如果考虑特殊场景,比如:键盘输入数据就是要经过缓冲区特殊处理然后输出,这样我们就必须定义一个缓冲区出来来处理数据;这样就要加一次拷贝(+1);后面也可能从缓冲区拿出来要经过特殊处理,那又要加一次拷贝了(+1).特殊场景特殊对待!
3-5-3、缺点
共享内存是没有给我们进行同步和互斥操作的(后面会讲),也就是说没有对数据进行保护
那么有没有什么方法可以避免共享内存的缺点呢?
我们可以通过共享内存+管道(匿名和命名都可以)的方法来实现
补充:如果s服务端读完数据,也可以建立一根管道,发送字符等数据,告诉c用户端,我已经读完了共享内存的数据了,你可以继续写了
3-6、共享内存测试代码
shm_client.cc
用户端口:
#include "comm.hpp"
int main()
{
key_t k = GetKey();
printf("key : 0x%x\n",k);
int shmid = create_shm(k);
printf("shmid : %d\n",shmid);//第一次成功之后,后面都会报文件已存在的错误
sleep(5);
//挂接成功
char* start = (char*)attchshm(shmid);
printf("attch start : %p\n",start);
//进行通信
//sleep(5);
while(true)
{
//以前拿数据的方法:定义一个buffer,通过read一个个读取出来
printf("client say : %s",start);
//获取共享内存的内核结构!!
struct shmid_ds ds;
shmctl(shmid,IPC_STAT,&ds);
printf("获取属性: size: %d, pid: %d, myself: %d, key: 0x%x",\
ds.shm_segsz, ds.shm_cpid, getpid(), ds.shm_perm.__key);
sleep(1);
}
/这里直接删除共享内存太粗暴了,我们进行去关联操作即可,剩下的操作交给os就行
//去关联
detachshm(start);
sleep(10);//一定要让server最后推出,所以休眠时间长一点
//先去关联,10s后,删除共享内存
//删除共享内存
delshm(shmid);//谁创建共享内存,谁就来删除
return 0;
}
shm_server.cc
服务端口:
#include "comm.hpp"
#include <unistd.h>
int main()
{
key_t k = GetKey();
printf("key : 0x%x\n",k);
int shmid = get_shm(k);
printf("shmid : %d\n",shmid);
//sleep(5);
char* start = (char*)attchshm(shmid);
//这里的start就是我们获取的共享内存,也相当于我们用户自己定义的缓冲区
printf("attch start : %p\n",start);
//sleep(5);
const char* message = "hello server,my name is client,我正在和你通信!";
pid_t id = getpid();
int cnt = 1;
// //以前的写法
// char buffer[2048];
// while(true)
// {
// snprintf(buffer,sizeof(buffer),"%s[pid:%d][消息编号:%d]",message,id,cnt++);
// memcpy(start,buffer,strlen(buffer)+1);
// //将我们buffer里面准备好的消息拷贝到start里面就相当于发送了,因为server看得到
// }
//既然你都是共享内存了,我直接把消息打到statr里面server不就自动看到了?
while(true)
{
sleep(1);
//snprintf会给我们默认添加\n !!!!!!!!
snprintf(start,MAX_SIZE,"%s[pid:%d][消息编号:%d]",message,id,cnt++);
}
detachshm(start);
//sleep(5);
//不需要删除,由server删除
return 0;
}
comm.hpp
各个函数的实现:
#ifndef _COMM_HPP_
#define _COMM_HPP
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#include<cerrno>
#include<cstring>
#include<cstdlib>
using namespace std;
using std::cout;
using std::endl;
#define PATHNAME "." //确定多个进程采用相同的路径
#define PROJ_ID 0x55 //确定多个进程采用相同的项目id(标识符)
#define MAX_SIZE 4096//共享内存块的大小,单位是字节
key_t GetKey()
{
key_t k = ftok(PATHNAME,PROJ_ID);
if(k < 0)
{
//cin,cout,cerr -> stdin,stdout,stderr -> 0,1,2
//strerror将错误码转化为错误码的描述
cerr << errno << ":" << strerror(errno) << endl;//像文件标识符2打印,也就是标准错误输出
exit(1);
}
return k;
}
int get_shmflg(key_t k,int flag)
{
int shmid = shmget(k,MAX_SIZE,flag);
if(shmid < 0)
{
cerr << errno <<" : "<<strerror(errno)<<endl;
exit(2);
}
return shmid;
}
int create_shm(key_t k)//创建共享内存块
{
return get_shmflg(k,IPC_CREAT | IPC_EXCL | 0600);//这个0600是权限的意思,是ipsc -m中perms需要的
}
int get_shm(key_t k)//获取共享内存块
{
return get_shmflg(k,IPC_CREAT);//这里也可以是0,0也表示获取。这就相当于if else if 和else,这里的0就是else,匹配获取
}
void delshm(int shmid)
{
if(shmctl(shmid,IPC_RMID,nullptr) == -1);//删除失败
{
cerr << errno<<" : "<< strerror(errno) << endl;
}
}
void* attchshm(int shmid)//挂接函数
{
//纯数字没有任何意义,必须有类型才有意义
//int a =10;编译器推导+隐式类型转换
//100;字面值
//10u 无符号整数
//10L
//10.0f
//10
void* mm = shmat(shmid,nullptr,0);//64位系统指针大小是8个字节,int是4个字节,精度丢失
if((long long)mm < 0)
{
cerr << errno<<" : "<< strerror(errno) << endl;
exit(3);
}
return mm;
}
void detachshm(void* start)
{
if(shmdt(start) == -1)
{
cerr << errno<<" : "<< strerror(errno) << endl;
exit(4);
}
}
#endif
共享内存的大小,一般建议是4KB的整数倍
系统分配共享内存是以4KB为单位的!—— 内存划分内存块的基本单位Page
#define MAX_SIZE 4097 ———— 内核给你的会向上取整, 内核给你的,和你能用的,是两码事
所以,我们不要使用4097这种数据大小,采用4096这种整数倍大小就行
4、总结
本期共享内存并没有讲完,但是内容已经非常丰富了,要理解管道和共享内存不是一次性就解决的事情,所以剩下的进程间通信的内容我留到了下一节再来详谈
最后重复一遍上面的共享内存大小知识点:
共享内存的大小,一般建议是4KB的整数倍
系统分配共享内存是以4KB为单位的!—— 内存划分内存块的基本单位Page
#define MAX_SIZE 4097 ———— 内核给你的会向上取整, 内核给你的,和你能用的,是两码事
所以,我们不要使用4097这种数据大小,采用4096这种整数倍大小就行!!