目录
无论如何,进程间通信的本质是:先让不同的进程,看到同一份资源
1.共享内存直接原理
1.先申请内存
2.再将内存挂接到进程地址空间
如果要释放内存则正好相反。
对于共享内存我们也需要管理,因此也需要一个结构体,先描述再组织。
申请和挂接的操作都是操作系统来执行的,如果是进程执行,那么由于进程独立性的需求,这块空间就应该是你一个人的。
1.创建/获取共享内存
可是,我们怎么保证 让不同的进程看到同一个共享内存呢?
你怎么知道这个共享内存存在还是不存在呢?
key!!来标识
接下来我们来谈谈key
1.key是一个数字,这个数字是几,不重要。关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识
2.我们第一个进程可以通过key创建共享内存,对于第二个以及之后的进程,只要拿着同一个key就可以和第一个进程看到同一个共享内存了。
3.对于一个已经创建好的共享内存。key在哪呢?key在描述这个共享内存的对象中,其中包含着该共享内存的各属性
4.我们创建共享内存的时候必须先获取一个key。(怎么获取?也是通过接口获取)
5.key本质上类似于路径,都是用于标识唯一性
我们通过这样一个接口来创建一个key。这个接口并不会去内核里查找哪些key没被占用帮我们申请之类的,它只是一套算法,它根据特定算法通过pathname和proj_id来计算出一个key值,即相同的pathname和proj_id得出的是同一个key,而这两个参数都是由我们自由指定的。
由于上述原因,key值可能会出现冲突,因此共享内存的创建也可能因此失败。那么我们为什么要用户来获取key值呢?由系统获取不是更好吗?
由系统获取自然可以保持高度的差异性,但是哪些进程要通信是需要用户指定的,操作系统它不知道哪些进程需要通信。况且如果想要把系统获取的这个key值传给另一个进程那就需要进程间通信,而我们现在正在解决的也正是进程间通信问题,会产生冲突。(我们的确可以使用管道来让这些进程进行通信,但是这样共享内存就不是一个独立的进程间通信方案了)
key与shmid
key用来在操作系统中标识共享内存的唯一性,只在申请或者获取内存的时候使用
shmid用来在用户层面标识共享内存的唯一性,我们在命令行的删除共享内存等操作就是用shmid
(小知识:共享内存的生命周期是随内核的,用户不主动关闭,共享内存就会一直存在,除非内核重启或者用户释放,其实和malloc有些相似)
ipcs -m查询共享内存
对于这里的perms(权限),我们可以在创建共享内存的时候一起设定
删除则是ipcrm -m (+)
再谈共享内存的大小
我们在获取共享内存的时候设置是多少它就是多少。但是我们最好设置成4096(字节)的整数倍,因为系统获取内存是以4kb为单位的,这里系统实际上申请了4096* 2的大小,但是因为我们需要的是4097大小,所以分配给我们的就是4097了
2.共享内存的挂接与去挂接
第一个参数就是我们之前获取到的,在用户层标识共享内存唯一性的shmid
第二个参数是共享内存挂接的位置,我们可以手动设置,也可以设置成nullptr,让系统自行决定,挂接的位置确定后会通过返回值返回(可以联想到malloc的返回值,对于malloc,其实它申请空间是申请虚拟地址,如果后面发现物理内存并没有被建立,那么就会缺页中断,然后才申请)
第三个参数是关于权限的设置,我们可以手动设置,让666权限的共享内存变成只读或只写等权限,也可以设置成0,挂接的时候使用共享内存创建时的权限。
去挂接更为简单,传入的参数就是我们挂接时候的返回值。 这里和与malloc对应的free很像,free也是只需要传入一个起始地址就可以了。我们要去关联,必须知道起始地址和共享内存的大小,这里不需要我们传入大小那么就说明,一定有用户层看不见的地方储存了大小并且让shmdt去关联的时候能看见,由于共享内存有专门管理的结构,我们能够想到共享内存的大小一定也会被存储。而malloc则是会多申请一些空间来记录用以维护这块空间,里面包括了空间的大小
3.删除共享内存
第一个参数不必多说
第三个参数就是管理共享内存的结构体,我们可以通过这个接口获取,修改结构体内的内容
第二个参数是一个选项,可以由我们选择获取或者修改结构体内容等
在删除的时候我们并不关心这个结构体,所以第三个参数可以设置为nullptr。
4.共享内存的特点
1.共享内存没有同步互斥之类的保护机制
(因此我们可以借用管道的同步功能加以限制,来保证共享内存也能够进行同步)
2.共享内存是所有进程间通信中速度最快的 为什么?因为它拷贝少。对于已经挂接上的进程,这块共享内存就是它的一块空间,和自己malloc出来以后使用的空间没有任何区别。我们可以对比以下管道,管道需要我们标准输入把内容输到管道里,然后write到内核缓冲区,接着再调用read,最后显示到标准输出。write和read的过程都需要分别进行一次拷贝。
3.共享内存内部的数据,由用户自己来维护。
5.共享内存的属性
6.编码实现
comm.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include<iostream>
#include<string>
#include<cstdlib>
#include<string.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include"log.hpp"
using namespace std;
const int size = 4096;
const string pathname="/home/myh";
const int proj_id = 0x666;
Log log;
key_t GetKey()
{
key_t k = ftok(pathname.c_str(),proj_id);
if(k < 0)
{
log(Fatal,"ftok error: %s", strerror(errno));
exit(1);
}
log(Info,"ftok success, key is : 0x%x", k);
return k;
}
int GetShareMemHelper(int flag)
{
key_t k = GetKey();
int shmid = shmget(k , size ,flag);
if(shmid < 0)
{
log(Fatal,"create share memory error: %s",strerror(errno));
exit(2);
}
log(Info,"create share memory success, shmid: %d",shmid);
return shmid;
}
int CreateShm()
{
return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
int GetShm()
{
return GetShareMemHelper(IPC_CREAT);
}
#endif
log.hpp(插件,非核心代码)
#pragma once
#include <iostream>
#include<time.h>
#include<stdarg.h>
#include <fcntl.h>
#include<unistd.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
class Log
{
public:
Log()
{
printMethod = Screen;
path = "./log/";
}
void Enable(int method)
{
printMethod = method;
}
std::string levelToString(int level)
{
switch(level)
{
case Info: return "Info";
case Debug: return "Debug";
case Warning: return "Warning";
case Error :return "Error";
case Fatal :return "Fatal";
default: return "None";
}
}
// void logmessage(int level,const char *format, ...)
// {
// time_t t = time(nullptr);//时间戳
// struct tm *ctime = localtime(&t);//用时间戳得到一个结构体,可以从里面取年月日时分秒
// char leftbuffer[SIZE];
// snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
// ctime->tm_year+1900, ctime->tm_mon+1,ctime->tm_mday,
// ctime->tm_hour, ctime->tm_min, ctime->tm_sec);//把字符串存到leftbuffer里面
// va_list s;
// va_start(s, format);
// char rightbuffer[SIZE];
// vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了
// //格式 默认部分(左)+自定义部分(右)
// char logtxt[SIZE*2];
// snprintf(logtxt, sizeof(logtxt),"%s %s\n", leftbuffer, rightbuffer);
// printLog(level, logtxt);//暂时打印
// }
void printLog(int level, std::string logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std:: endl;
break;
case Onefile:
printOneFile("LogFile" ,logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
default:
break;
}
}
void printOneFile(const std:: string logname, const std::string logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY|O_CREAT|O_APPEND, 0666);//LogFile
if(fd < 0)return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string logtxt)
{
std::string filename = "LogFile";
filename += ".";
filename += levelToString(level);//LogFile.Debug/Warning/Fatal
printOneFile(filename, logtxt);
}
~Log()//这里析构只是为了让类看起来完整
{
}
void operator()(int level,const char *format, ...)
{
time_t t = time(nullptr);//时间戳
struct tm *ctime = localtime(&t);//用时间戳得到一个结构体,可以从里面取年月日时分秒
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year+1900, ctime->tm_mon+1,ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);//把字符串存到leftbuffer里面
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了
//格式 默认部分(左)+自定义部分(右)
char logtxt[SIZE*2];
snprintf(logtxt, sizeof(logtxt),"%s %s\n", leftbuffer, rightbuffer);
printLog(level, logtxt);
}
private:
int printMethod;
std :: string path;
};
makefile
.PHONY:all
all:processa processb
processa:processa.cc
g++ -o $@ $^ -g -std=c++11
processb:processb.cc
g++ -o $@ $^ -g -std=c++11
.PHONY:clean
clean:
rm -f processa processb
processa.cc
#include"comm.hpp"
extern Log log;
int main()
{
int shmid = CreateShm();
log(Debug, "create shm done");
sleep(5);
char * shmaddr = (char*)shmat(shmid ,nullptr ,0);
log(Debug, "attach shm done");
sleep(5);
//ipc code
while(true)
{
//一旦有人把数据写到共享内存,其实我们立马就能看到了
//不需要经过系统调用,直接就能看到数据了
cout <<" client say@ " << shmaddr << endl;//直接访问共享内存
sleep(1);
}
shmdt(shmaddr);
log(Debug,"detach shm done, shmaddr: 0x%x",shmaddr);
sleep(5);
shmctl(shmid, IPC_RMID, nullptr);
log(Debug, "destroy shm done, shmaddr:0x%x", shmaddr);
sleep(5);
return 0;
}
processb.cc
#include"comm.hpp"
extern Log log;
int main()
{
int shmid = GetShm();
log(Debug, "create shm done");
sleep(5);
char * shmaddr = (char*)shmat(shmid ,nullptr ,0);
log(Debug, "attach shm done");
sleep(5);
//ipc code
while(true)
{
//char buffer[1024];我们可以再弄一个缓冲区,但其实这是多此一举,我们可以直接用这块共享内存
//cout << "Please Enter@ ";
//fgets(buffer, sizeof(buffer), stdin);
//memcpy(shmaddr, buffer, strlen(buffer) + 1);//fgets会把获取的字符串后面加个\0,+1是为因为我们这里把消息看成字符串格式,特意加的。
cout << "Please Enter@ ";
fgets(shmaddr,4096, stdin);//直接用这块内存
//一旦有了共享内存,挂接到自己的地址空间中,你直接把他当成你的内存空间来用即可
//不需要调用系统调用
}
shmdt(shmaddr);
log(Debug,"detach shm done, shmaddr: 0x%x",shmaddr);
sleep(5);
return 0;
}
2.消息队列直接原理
1.与共享内存接口的对比
(两种通信方式都为SystemV标准)
共享内存接口
消息队列接口
我们看到,消息队列和共享内存中很大一部分接口都是类似的,下面这个是消息队列特有的收发信息的接口
发消息,第一个参数就是要指定发给谁,第二个参数是你要发送的数据块的起始地址,第三个是数据块的大小,最后一个参数我们和共享内存中说的一样,可以设成nullptr
收消息多了的部分是数据块的类型。
我们收发数据时,要自行定义相应的数据块,
同样,我们的指令能查共享内存,也同样能查消息队列
ipcs -q 查
ipcm - q msgid 删除
实际上信号量的管理对象结构也类似
2.ipc在内核中数据结构的设计
在操作系统中,所有 ipc资源,全部都被整合在ipc模块中
系统中有一个数组将这些结构管理起来,这个数组的下标就是我们前面所说的shmid xxxid!
(这个数组不隶属于进程,是操作系统层面上维护的一个数组)(数组下标线性增加)
共享内存 消息队列 信号量他们的管理结构的第一项都是同样的数据结构,因此这个数据结构的地址可以统一地存储到数组中 ,而这个数据结构的起始地址也正好是整个管理结构的起始地址,因此我们只需要进行强转即访问这个管理结构中的其它部分。关于操作系统如何辨别并根据类别将数据结构正确地强转,操作系统它自己是可以辨别的,例如添加一个标志位等等。
这一管理形态我们可以看成是c语言中实现的多态。
3.信号量基本原理
1.补充知识
如下有一个场景,当我们的a 正在写入,写入了一部分,就被b拿走了导致双方发和收的数据不完整,这就是数据不一致问题。(管道有原子性以及自己的互斥机制,因此不会出现数据不一致问题)
1.a,b看到的同一份资源,这份资源叫做共享资源,但是如果不加以保护,会导致数据不一致问题
2.我们可以通过加锁来实现互斥访问(任何时刻,只允许一个执行流访问共享资源,这就是互斥)
3.共享的,任何时刻只允许ig执行流访问的资源被称为临界资源----一般是内存空间(管道就是临界资源,管道的缓冲区,不就是一段内存空间吗)。
4.举例:我们有100行代码,但是只有5-10行代码才是在在访问临界资源的代码,这样的代码我们称之为临界区
解释一个现象:多进程,多线程,并发打印
显示器上的消息:1.错乱的2.混乱的3.和命令行混在一起
为什么会这样呢? 显示器它实际上也是文件,我们向显示器写入,实际上先要向显示器的缓冲区写入数据,然后操作系统才把我们的数据刷新到显示器上。当我们有多个进程尝试着对一个显示器进行打印时,前提是他们都要看到这个显示器资源,那么显示器资源就是共享资源了,当各个进程进行打印时,你打印你的我打印我的,彼此之间并没有互斥或者同步保护机制,所以会产生数据不一致问题
2.理解信号量
信号量的本质就是一个计数器,类似与int count = n。
它被用于描述临界资源中资源数量的多少。
下面有一个故事:
现在有一个电影院,里面有100个座位,因此放出了100张票
当我们想去看电影,我们需要先买票--------买票的本质就是对资源的预定机制
此时我们需要一个票数计数器,每卖出一张票,这个计数器就要减1------即放映厅里面的资源就减1
票数的计数器减到0之后,就说明票已经被申请完毕了
现在我们有若干临界资源
假如里面有15份资源,那我们int count = 15
我们想要去访问资源,就要先申请,每次成功申请资源count 就会减1,count等于0时,就说明资源已经被申请完了。
这个临界资源计数器就被我们称为信号量
1.申请计数器成功,就表示我们有访问这一资源的权限了
2.我们申请了资源,但是并不代表我们访问了资源,通过计数器申请资源,是对资源的预定机制
3.计数器可以有效保证进入共享资源的执行流数量(我们计数器初始为15时,就绝对不可能让十六个执行流进入)
4.所以每一个执行流,它想要访问共享资源中的一部分的时候,不是直接访问,而是先申请计数器资源。如同要看电影先买票!
我们再回归到上面提及的例子,如果电影院放映厅里只有一个座位呢?那我们就只需要一个值为1的计数器
也就是说只有一个人能抢到,只有一个人能进放映厅看电影,可以理解为这期间只有一个执行流在访问临界资源。这就是互斥!
我们把值只能为1,0两态的计数器叫做二元信号量-----本质就是一个锁
凭什么让计数器为1呢? 因为资源为1! 其实就是将临界资源看成一个整体,不把它分成多份。整体申请整体释放
思考:我们要访问临界资源,先要申请信号量计数器资源。要申请必须要先看到它,那么信号量计数器,不也是共享资源吗?
信号量计数器要保护临界资源的访问安全,必须先保证自己的安全。
这个整数减1是安全的吗?
是不安全的,我们这里count--,在c语言上是一条语句,变成汇编就成了多条汇编语句
1.cnt变量的内容,内存->cpu寄存器
2.cpu内进行操作
3.将计算结果写回cnt变量的内存位置
可是我们知道,进程在运行的时候是可以随时被切换的。这个过程就可能出现问题,这里的问题后续再谈
信号量
申请信号量,本质是对计数器进行--。即P操作
释放资源,释放信号量,本质是对计数器进行++操作,即V操作
申请和释放操作(PV操作)原子的!----要么不做要么做完,是两态的。没有“正在做”这样的概念
3.总结
1.信号量本质上一把计数器,PV操作是原子的
2.执行流申请资源,必须先申请信号量资源,得到信号量之后才能访问临界资源
3.信号量是0.1两态的被称为二元信号量,就是互斥功能
4.申请信号量的本质就是对临界资源的预定机制
信号量凭什么是进程间通信中的一种?
1.因为通信不仅仅是通信数据,互相协同也是通信的一种
2.要协同,本质也是通信,信号量首先要被所有的通信进程看见。