1. 什么是system V
System V IPC(Interprocess Communication,进程间通信)是Unix系统中一种经典的进程间通信机制,由AT&T在System V.2版本中引入,并广泛应用于Linux等现代操作系统中。它通过三种核心机制实现进程间的同步、数据传递和资源共享。
在Linux中支持了这种标准,专门设计了IPC通信模块。
2. 共享内存原理
这里我们先来宏观的认识一下什么是共享内存。假设有俩个进程ab,如果它们需要进行进程间通信,除了我们之前说的命名管道,还可以通过共享内存的方式实现。我们之前再谈动态库加载原理的时候说过进程的地址空间的堆栈之间有一块共享区,用来记录动态库的虚拟地址。其实,在这块空间中,还用一片区域->共享内存区。进程ab在物理内存中申请同一块区域,然后各自通过页表映射建立物理内存和共享内存区之间的关系。至此,两个进程就可以看到同一份资源了!以上就是我们对共享内存的宏观认识,还有许多细节没有说。
> 我们刚刚所说的所有工作都涉及内核数据结构和磁盘,这些工作都由操作系统完成,我们使用相应的系统调用完成上面的工作。
> 在实际情况中,可能有多组进程在进行进程间通信,那势必有多个共享内存,有的是被创建,有的正在打开,有的正在关闭……所以,在内核中势必也会有描述共享内存的结构体对象,也势必会有管理这些共享内存的内核数据结构!
3. shmget[share memory get]
shmget是我们用来获取共享内存的接口:
> 参数size是用来设置我们创建共享内存的大小的,很好理解。
> 第三个参数是共享内存标记位, 有两个选项:IPC_CREAT、IPC_EXCL。
IPC_CREAT:只带这一个选项表示,如果目标共享内存不存在则创建并打开共享内存,否则就直接打开已近存在的目标共享内存。
IPC_EXCL:该选项单独使用无任何意义,必须和IPC_CREAT一起使用,使用是表示如果目标内存不存在则创建并打开该共享内存,如果存在,则会直接报错!从解释上来看该选项是保证我们创建一个全新的共享内存。
> 不过,这里还有许多问题:我们怎么知道一个共享内存到底是否存在,并且如何保证两个进程打开同一个共享内存呢?
> 这就由第一个参数key来标识共享内存的唯一性了!这个key不是由内核直接生成,而是让用户来构建并传入给操作系统的。这是为什么呢???
> 假如,进程a在内存中创建了一个共享内存区,内核直接生成对应放id给管理该区域的结构体对象。进程b需要和进程a进行进程间通信,那么进程b就需要拿到这个id来找到同一块共享内存。但是,id也是数据,如果进程a可以把id给到进程b,它们不就已经可以进程间通信了吗!!!所以,由内核自己生成是做不到共享内存的!
> 但是,如果这两个进程其中一方在创建共享内存时就做好约定,在用户层规定一个key,传给操作系统,让这个key来唯一标记这个共享内存。有朝一日,另一个进程就可以通过这个key找到目标共享内存来完成进程间通信!
> 其实,这个key原理不就是我们用路径和管道文件名找同一个管道文件一样的吗。
> 好了,明白了上面的原理,我们就来说说key如何给定。理论上来说,这个key我们可以随便给,但是,为了减少key之间的冲突,系统给了我们一个生成key的接口ftok()。
这个接口会根据用户传入的字符串和id整合形成一个key并返回给用户,正常情况下,我们也是使用这个接口来生成key。 这里有一个问题:为什么操作系统不这样设置呢->提供一个系统调用,在内核遍历已有的key,生成一个不存在的key返回给用户,这样做不是更好吗?但是,如果其他用户同时生成key,这些key会不会一样呢?
> 最后,我们再来说一下shmget的返回值:
如果一个共享内存被创建成功,会返回一个整形来唯一标记这个共享内存。这个整形是给用户看到的,而key则是给操作系统来寻找共享内存的。这就好比我们之前学文件时,打开一个文件时,系统通过file*的指针找到文件,却返回一个整形文件描述符fd给用户使用。
4. 共享内存demon代码编写
4.1 预备代码的编写过程
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstdio>
#include <fcntl.h>
#include <unistd.h>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>#define ERR_EXIT(m) \do \{ \perror(m); \exit(EXIT_FAILURE); \} while (0)const std::string gpathname = ".";
int gproj_id = 0x66;
int g_size = 1024;
int g_default_id=-1;// 共享内存
class shm
{
public:shm() : _shm_id(g_default_id), _size(g_size) {}// 创建共享内存void create_shm(){// 获取keykey_t k = ftok(gpathname.c_str(), gproj_id);if (k < 0){// 获取key失败ERR_EXIT("ftok");}printf("key:0x%0x\n", k);// 创建共享内存_shm_id = shmget(k, _size, IPC_CREAT | IPC_EXCL);if (_shm_id < 0){// 创建共享内存失败ERR_EXIT("shmget");}printf("_shm_id:%d\n", _shm_id);}~shm() {}private:int _shm_id;int _size;
};
./server运行之后,我们果然创建共享内存成功了,也看到了相应的key和id。
不过,当我们结束进程,再次运行时,却发现共享内存创建失败了。这说明之前的共享内存并没有随进程的结束而释放,事实上,共享内存的生命周期是随系统的,我们关闭系统重启之后,共享内存便释放了。
我们可以用命令:ipcs -m 来查看系统内的共享内存情况。【ipcs即查看系统进程间通信资源,-m则标识查看共享内存部分】。
如果我们想要删除一个共享内存资源,可用命令ipsrm -m +shmid
接下来,我们来看看代码级别怎么删除共享内存资源的,这里我们再认识一个接口shmctl:
该系统调用是用来控制共享内存资源的,包括删除,查看共享内存资源属性等。第二个参数用来控制我们想要对共享内存做和控制,而第三个参数我们暂时用不到,直接设为nullptr即可。
要想释放目标共享内存,我们仅需将第二个参数设为IPC_RMID即可。好了,下面我们就来实现destroy接口。
// 释放共享内存资源void destroy(){if (_shm_id == g_default_id){// 没有创建共享内存资源,无需释放return;}int n = shmctl(_shm_id, IPC_RMID, nullptr);if (n < 0){ERR_EXIT("shmctl");}printf("共享内存释放成功! shmid->%d\n", _shm_id);}
有了上面的铺垫,我们就可以来学习如何使用共享内存来完成进程间通信了。首先,我们需要将我们创建的共享内存和我们的进程关联【本质就是让物理地址和进程虚拟地址空间完成映射】。这由系统完成,所以由对应的系统调用帮我们完成工作->shmat【at->attach】:
第一个参数很好理解,第二个参数用来设置固定虚拟地址映射,在我们应用层开发一般不需要关心这个参数,设为nullptr即可,第三个参数是用来标识我们的共享内存资源的权限的,我们一般设置为0,则系统会使用缺省权限->只读和只写。最后,如果成功完成内存和进程地址空间的关联,则该接口会返回系统选定虚拟地址映射共享内存的起始虚拟地址,这个返回值和我们使用的malloc返回值是一样的,使用上也没什么区别,我们这样就很好理解了。
好了,接下来我们就设计一个接口来完成共享内存和进程的关联:
//进程和共享内存完成关联void attach(){_start_addr=shmat(_shm_id,nullptr,0);if((long long)_start_addr<0){ERR_EXIT("shmat");}printf("进程关联成功!_start_addr->%p\n",_start_addr);}
诶?为什么我们在关联共享内存时报错了呢,这个报错说明我们的行为被决绝了。其实,共享内存和文件在某些方面有些类似。共享内存也有对应的读写执权限,所以我们在创建共享内存时,需要设定相应的权限,做法也是和文件权限一样。
执行结果:
现在,服务端创建对应的共享内存并完成地址空间的映射。那么,客户端也需要相应的接口获取同一份共享内存并进行地址空间的映射。创建和删除共享内存的工作在客户端也就不需要做了!
具体做法如下图所示:
我们做完以上工作之后,就可以开始进程间通信了。做法非常简单,将我们返回的虚拟地址用指针指向,把共享内存当做长字符串来使用就可以了。
4.2 共享内存优缺点
最后,我们再来总结一下:我们发现在使用共享内存实现进程间通信的读写的时候,并没有使用系统调用,而我们在使用管道的时候,切切实实的使用了系统调用。很好理解,因为共享内存的读写是在进程的虚拟地址空间上面操作的【这属于用户层】,所以共享区属于用户层可以让用户直接使用,而管道读写则是在内核文件缓冲区完成的,这属于系统层。
共享区也是因此有以下的优点:
> 共享区是进程间通信中,速度最快的方式:映射之后,内容直接被双方进程看到,不需要进行系统调用来获取和写入。
不过,共享内存机制的优点也是牺牲了很多换来的:第一点就是共享内存没有像管道那样的同步机制,服务端开启后就一直读,如果客户端不开启呢??而管道文件一方开始读后,如果另一方没有写,则读取放就会同步阻塞并不开始读取,知道另一方写入。另一点就是共享内存没有保护机制,这里的保护不是指对共享内存的保护,而是对读写数据的保护。如果写入端对自己写入的内容在读取时有特定的读取要求【比如一次读多少字节,一次读一整句话等等】,目前的共享内存就无能为力了,因为读取方不停的读,就无所谓读取格式的要求了,它也正因此是进程间通信中最快的方式。
那么,我们目前有没有什么方案可以将我们的数据保护起来呢?当然有了,我们可以同时用管道把这两个进程关联起来。而这个管道的作用就是用来做服务端等待wait,客户端唤醒wake!假设客户端自己写入两个字符后,服务端成对的读取打印结果。那我在客户端写入成对字符后,通过管道写入来唤醒【唤醒的方式随便啦】服务端,服务端通过是否收到管道信号来决定是否读取共享内存中的数据。如此一来,我们就通过管道,讲我们的共享内存的读写控制起来了!话不多说,直接上代码!
客户端和服务端之间的通信逻辑:
结果如下:
4.3 共享内存去关联
上面,我们在释放共享内存的时候是直接shmctl删除的,但是我们再删除之前还遗漏了一步,就是去关联!在释放共享内存之前,我们还应该将关联该共享内存的所有进程去关联。这里我们使用shmdt接口即可,使用也很简单,传入虚拟地址空间的其实地址即可。
修改以下部分即可:
4.3 共享内存大小问题
然后,我们再来说一下创建共享内存时的大小问题,实际上,共享内存的大小一定是4kb的整数倍的 。但是如果我们创建大小时,给定的不是4kb的整数被,那么系统则会向上取整开辟对应大小的共享内存空间。不过,我们用ipcs -m 命令查看到的大小还是我们自己定义的大小!
4.4 获取共享内存属性
在最后的最后,我们再来见一见共享内存是如何被系统管理起来的。
内核中,有一种结构体对象shmid_ds,该结构体对象记录了共享内存的一些属性,其中有一个结构体ipc_perm里面记录了共享内存的key!
我们使用shmctl传入IPC_STAT参数即可查看到先关的属性!
5. 源码
5.1 comm.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstdio>
#include <fcntl.h>
#include <unistd.h>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>#define FIFO_FILE "fifo"
#define FIFO_PATH "."const std::string gpathname = ".";
int gproj_id = 0x66;
int g_size = 4097;
int g_default_id = -1;
int gmode = 0666;#define USER "user"
#define CREATER "creater"#define ERR_EXIT(m) \do \{ \perror(m); \exit(EXIT_FAILURE); \} while (0)
5.2 Shm.hpp
#include "comm.hpp"// 共享内存
class shm
{
private:// 创建共享内存void create_help(int flag){// 创建共享内存// _shm_id = shmget(k, _size, IPC_CREAT | IPC_EXCL | gmode);_shm_id = shmget(_key, _size, flag);if (_shm_id < 0){// 创建共享内存失败ERR_EXIT("shmget");}printf("共享内存创建成功!_shm_id->%d\n", _shm_id);}// 创建共享内存void create_shm(){create_help(IPC_CREAT | IPC_EXCL | gmode);}// 获取共享内存void get_shm(){create_help(IPC_CREAT);}// 释放共享内存资源void destroy(){if (_shm_id == g_default_id){// 没有创建共享内存资源,无需释放return;}// 去关联detach();if (_user_type == CREATER){int n = shmctl(_shm_id, IPC_RMID, nullptr);if (n < 0){ERR_EXIT("shmctl");}printf("共享内存释放成功! shmid->%d\n", _shm_id);}}// 进程和共享内存去关联void detach(){int n = shmdt(_start_addr);if (n < 0)ERR_EXIT("shmdt");printf("共享内存去关联成功!\n");}// 进程和共享内存完成关联void attach(){_start_addr = shmat(_shm_id, nullptr, 0);if ((long long)_start_addr < 0){ERR_EXIT("shmat");}printf("进程关联成功!_start_addr->%p\n", _start_addr);}public:shm(const std::string &pathname, int projid, const std::string &user_type): _shm_id(g_default_id),_size(g_size),_user_type(user_type){// 获取key_key = ftok(pathname.c_str(), projid);if (_key < 0){// 获取key失败ERR_EXIT("ftok");}printf("成功获取key!key->0x%0x\n", _key);if (user_type == CREATER)create_shm();else if (user_type == USER)get_shm();else{}attach();}~shm(){if (_user_type == CREATER)destroy();}void *get_start_addr(){return _start_addr;}int get_size(){return _size;}// 获取共享内存相关属性void get_property(){struct shmid_ds tmp;int n = shmctl(_shm_id, IPC_STAT, &tmp);if (n < 0)ERR_EXIT("shmctl");printf("shm_segsz->%ld\n", tmp.shm_segsz);printf("key->0x%0x\n", tmp.shm_perm.__key);}private:key_t _key;int _shm_id;int _size;void *_start_addr;std::string _user_type;
};
5.3 name_fifo.hpp
#include "comm.hpp"// 命名管道类
class name_fifo
{
public:// 构造函数name_fifo(const std::string &path, const std::string &name): _path(path),_name(name){umask(0);_fifo_name = _path + "/" + _name;// 创建命名管道int n = mkfifo(_fifo_name.c_str(), 0666);if (n < 0){// 命名管道创建失败// perror("命名管道创建失败\n");ERR_EXIT("mkfifo");}else{std::cout << "管道创建成功!" << std::endl;}}// 析构函数~name_fifo(){// 删除管道文件int n = unlink(_fifo_name.c_str());if (n < 0){// perror("删除管道文件失败!\n");ERR_EXIT("unlink");}else{std::cout << "管道删除成功!" << std::endl;}}private:std::string _path;std::string _name;std::string _fifo_name;
};// 命名管道操作类
class fifo_opre
{
public:fifo_opre(const std::string &path, const std::string &name): _path(path),_name(name),_fd(-1){_fifo_name = _path + "/" + _name;}void open_for_read(){// 创建成功->服务端打开管道->读取内容_fd = open(_fifo_name.c_str(), O_RDONLY);if (_fd < 0){// 命名管道打开失败// perror("命名管道打开失败\n");ERR_EXIT("open");}else{std::cout << "管道打开成功!" << std::endl;}}void open_for_write(){// 打开管道文件_fd = open(_fifo_name.c_str(), O_WRONLY);if (_fd < 0){// 命名管道打开失败// perror("命名管道打开失败\n");ERR_EXIT("open");}else{std::cout << "管道打开成功!" << std::endl;}}bool wait(){char c;int num = read(_fd, &c, 1);if (num > 0)return true;return false;}bool wake(){char c = 'a';int num = write(_fd, &c, 1);if (num > 0)return true;return false;}void Close(){if (_fd > 0)close(_fd);}~fifo_opre(){}private:std::string _path;std::string _name;std::string _fifo_name;int _fd;
};
5.4 server.cc
#include "comm.hpp"
#include "Shm.hpp"
#include "name_fifo.hpp"int main()
{// 服务端先创建关联共享内存shm s(gpathname, gproj_id, CREATER);// // 创建命名管道// name_fifo nf(FIFO_PATH, FIFO_FILE);// char *mem = (char *)s.get_start_addr();// // 命名管道读取端// fifo_opre fifo_reader(FIFO_PATH, FIFO_FILE);// fifo_reader.open_for_read();// // 服务端读取// while (true)// {// // 如果等待成功则读取// if (fifo_reader.wait())// {// printf("%s\n", mem);// }// else// break;// }// // 关闭命名管道// fifo_reader.Close();s.get_property();return 0;
}
5.5 client.cc
#include "comm.hpp"
#include "Shm.hpp"
#include "name_fifo.hpp"int main()
{// 客户端获取共享内存shm s(gpathname, gproj_id, USER);char *mem = (char *)s.get_start_addr();// 客户端打开管道写入唤醒fifo_opre fifo_writer(FIFO_PATH, FIFO_FILE);fifo_writer.open_for_write();int index = 0;for (char c = 'A'; c <= 'Z'; index += 2, c++){sleep(1);mem[index] = c;mem[index + 1] = c;fifo_writer.wake();}return 0;
}
5.6 Makefile
.PHPNY:all
all:client server
client:client.ccg++ -o $@ $^ -std=c++11
server:server.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f client server fifo