前言
进程之间除了使用管道来进行通信,还能通过申请共享内存的方式使多个进程访问同一片共享资源。下面介绍System V标准下的共享内存是如何使进程间通信的。
共享内存的原理
其实每一个进程的虚拟空间分布中都有一块区域专门用来存放公共资源,这个区域就是“数据共享区‘。
值得注意的是,这个数据共享区里的数据不属于任何一个进程,而是被多个进程共享。当一个进程连接到共享内存区域时,操作系统会将这块共享内存段映射到进程的虚拟地址空间中,使得进程可以直接访问这块共享内存。共享内存的生命周期是随操作系统的,也就意味着,如果我们不主动删除共享内存,即使创建共享内存的进程已经终止,该共享内存也会一直存在,直到重启或者用指令删除。
所以使用共享内存进行通信有以下步骤:
- 创建共享内存
- 连接共享内存
- 访问共享内存
- 分离共享内存
- 删除共享内存。
下面将一一介绍上述步骤。
创建共享内存区域
进程可以通过调用shmget函数来创建一个共享内存标识符,并且指定内存大小和权限等参数。该标识符类似与文件描述符fd,是操作系统给用户使用的,用来标识共享内存区域。
关于getshm函数:
#include<sys/types.h> #include<sys/ipc.h> #include<sys/shm.h> int shmget(key_t key,size_t size,int shmflag);
key
是一个键值,用于唯一标识共享内存区域。通常使用ftok
函数来生成一个键值。size
是要创建的共享内存的大小,一般建议是4096的整数倍。shmflg
是权限标志,用来指定共享内存的权限和创建的方式,和打开文件的系统调用类似。- 创建成功返回一个非负整数(shmid),否则返回-1.
key和shmget返回值的区别
简单来说,key是一个键值,用来标识共享内存。而shmget的返回值即shmid主要用于操作共享内存的标识符。一个是用户在创建时交给操作系统的,另一个则是创建后操作系统给用户来操作的。就好像我们的文件系统中fd与inode的区别。一个文件没有被打开的时候,需要通过inode来找到并打开,而打开之后给用户一个描述符fd用于后续对文件的操作。
参数shmflag
shmflag是一个位图结构,通过按位|
来传递多个选项信息。常见的选项有:
IPC_CREAT
:如果指定了这个选项,并且键值key对应的共享内存区不存在,则创建一个新的共享内存区。如果已经存在,就返回这个共享内存标识符。IPC_EXCL
:与IPC_CREAT一起使用时,如果指定的共享内存区存在,shmget就返回错误。如果不存在,就创建一个。IPC_EXCL不单独使用。- 一般来说,创建共享内存的进程需要使用
IPC_CREAT|IPC_EXCL
选项,因为这样创建出来的共享内存一定是最新的,就是目前没有其它进程使用的。而想使用这个已经被创建出来的共享内存的进程就使用IPC_CREAT
.
参数key
我们可以通过ftok函数来获取一个key
#include<sys/types.h> #include<sys/ipc.h> key_t ftok(const char*pathname,int proj_id);
ftok函数可以根据一个已存在的文件路径pathname和一个整型的proj_id来转换成一个key。我们可以将pathname和proj_id的组合看成是一个规则,同样的规则生成的key相同。也就意味着,当我们创建shm之后,后面要是有进程想使用这个shm,我们就可以通过同样的pathname和proj_id的组合来获得同一个key,用同一个key获得的shmid也就是一样的。这样就能让不同的进程对同一个共享内存操作了!(通信的前提)
共享内存数据结构
为了管理这些共享内存,操作系统对这些共享内存做了描述并组织。下面给出共享内存是如何被描述的:
struct shmid_ds { struct ipc_perm shm_perm; /* operation perms */ int shm_segsz; /* size of segment (bytes) */ __kernel_time_t shm_atime; /* last attach time */ __kernel_time_t shm_dtime; /* last detach time */ __kernel_time_t shm_ctime; /* last change time */ __kernel_ipc_pid_t shm_cpid; /* pid of creator */ __kernel_ipc_pid_t shm_lpid; /* pid of last operator */ unsigned short shm_nattch; /* no. of current attaches */ unsigned short shm_unused; /* compatibility */ void *shm_unused2; /* ditto - used by DIPC */ void *shm_unused3; /* unused */ };
结构体shmid_ds其实就是一个共享内存的所有属性集合。包括了共享内存的创建时间、权限、修改时间、挂接数等。操作系统把这些shmid_ds集合在一起管理,所有的shmid_ds组合在一起就形成了一个数组。于是对共享内存的管理就变成了对shmid_ds数组的管理。而shmget函数的返回值shmid其实就是这个数组的下标。
用指令查询共享内存
使用ipcs -m
指令可以查看所有的共享内存的信息。
同样,我们也可以使用ipcrm -m shmid
来删除标识符为shmid的共享内存。
连接共享内存
我们已经知道如何创建一个共享内存了,那么该如何使用共享内存呢?我们需要将创建的共享内存段连接到进程的虚拟地址空间,并通过页表建立与物理内存空间的映射 。这样进程就能使用共享内存了。如何连接共享内存呢?可以使用shmat
函数。
#include <sys/types.h> #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid
,共享内存的用户级标识符。shmaddr
,用于指定shm映射到进程虚拟地址空间的位置。如果设置为NULL,系统会自动找一个合适的位置来加载shm。如果不为空,且shmlag选项包括了SHM_RND,系统就会在地址shmaddr处加载shm.shmflag
,用于设置连接shm的一些属性,是一个位图,可以表示多个选项。
- 如果shmat调用成功,返回shm加载到进程地址空间的起始位置,否则返回(void*)-1
分离共享内存
既然我们能通过shmat连接一个shm,也能”卸载“这个shm。
通过shmdt
函数分离shm:
#include<sys/types.h> #include<sys/shm.h> int shmdt(const void* shmaddr);
shmaddr
即进程地址空间中的shm的起始地址
删除共享内存
既然能创建一个共享内存,同样也能删除一个共享内存。
使用shmctl
函数来释放shm.
#include<sys/ipc.h> #include<sys/shm.h> int shmctl(int shmid,int cmd,struct shmid_ds *buf);
cmd
表示对共享内存的操作。具体操作有以下几种:
IPC_STAT
:将与shmid相关的内核级数据结构shmid_ds的数据拷贝到buf中IPC_SET
:将buf指向的shmid_ds的值拷贝到shmid的数据中
IPC_RMID
:删除共享内存
buf
一个指向shimd_ds结构的指针
用共享内存模拟Server与Client的通信
Shm.hpp
用来描述封装共享内存的操作集和属性集
#ifndef __SHM_HPP__ #define __SHM_HPP__ #include <iostream> #include <sys/types.h> #include <sys/shm.h> #include <sys/ipc.h> #include <string> #include <cstring> #include <unistd.h> using namespace std; const int gCreater = 1; const int gUser = 2; const string pathname = "/home/tsx/linux/24-5-2/myShm/4.shm"; const int proj_id = 0x66; const size_t gShmSize = 4096; class Shm { private: key_t GetKey() { // 获得key key_t k = ftok(_pathname.c_str(), _proj_id); if (k < 0) { perror("ftok"); } return k; } int GetShm(key_t key, int size, int flag) { // 通过系统调用获得shm int shmid = shmget(key, size, flag); if (shmid < 0) { perror("shmget"); } return shmid; } string PrintWho() { if (_who == 1) return "gCrearter"; else if (_who == 2) return "gUser"; else return "None"; } void *AttachShm() { if (_shmaddr != nullptr) { DetachShm(); } void *shmaddr = shmat(_shmid, nullptr, 0); if (shmaddr == nullptr) { perror("shmat"); } cout << "who: " << PrintWho() << " attachshm :" << endl; return shmaddr; } void DetachShm() { if (_shmaddr == nullptr) { return; } shmdt(_shmaddr); cout << "who: " << PrintWho() << " deattachshm :" << endl; } void DeleteShm() { if (_who == gCreater) { int res = shmctl(_shmid, IPC_RMID, nullptr); if (res == -1) { perror("shmclt"); } } } public: Shm(const string &pathname, const int proj_id, int who) : _pathname(pathname), _proj_id(proj_id), _who(who), _shmaddr(nullptr) { // 获得_key _key = GetKey(); // 创建shm if (_who == gCreater) { GetShmForCreater(); } else if (_who == gUser) { GetShmForUse(); } _shmaddr = AttachShm(); // 连接 } ~Shm() { DetachShm(); // 分离连接shm DeleteShm(); // 删除shm } bool GetShmForCreater() { // server创建shm _shmid = GetShm(_key, gShmSize, IPC_CREAT | IPC_EXCL | 0666); if (_shmid < 0) return false; cout << "server成功创建shm" << endl; return true; } void InitShmaddr() { if (_shmaddr) { memset(_shmaddr, 0, sizeof gShmSize); } } void *GetShmaddr() { return _shmaddr; } bool GetShmForUse() { _shmid = GetShm(_key, gShmSize, IPC_CREAT | 0666); if (_shmid < 0) return false; cout << "client成功获得shm" << endl; return true; } private: key_t _key; int _shmid; int _id; string _pathname; int _who; int _proj_id; void *_shmaddr; }; #endif
NamePipe.hpp
封装了命名管道的属性集和方法集,在共享内存中使用命名管道可以保证数据的同步。
#include <iostream> #include <string> #include <cstdio> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> using namespace std; const string comm_path = "/home/tsx/linux/24-5-2/myShm/myfifo"; #define Creater 1 // 身份码,1表示创建管道者,2表示使用者 #define User 2 #define Defaultfd -1 #define Read_Mode O_RDONLY #define Write_Mode O_WRONLY #define BaseSize 4096 // 默认读管道的数据大小 class NamePipe { private: void CreatFifo() { int res = mkfifo(_fifo_path.c_str(), 0777); if (res != 0) { perror("mkfifo"); } } bool OpenNameFifo(int mode) { _fd = open(_fifo_path.c_str(), mode); if (_fd < 0) { cout << _fd << endl; return false; } return true; } public: NamePipe(const string &path, int who) // 构造函数,根据身份码来决定谁创建管道 : _fifo_path(path), _id(who), _fd(Defaultfd) { if (_id == Creater) { CreatFifo(); // 创建管道 cout << "creat a fifo" << endl; } } // 读写操作 bool OpenForWrite() { return OpenNameFifo(Write_Mode); } bool OpenForRead() { return OpenNameFifo(Read_Mode); } int ReadNamePipe(string &out) { // 向管道中读取数据 char buffer[BaseSize]; int n = read(_fd, buffer, sizeof(buffer) - 1); if (n > 0) { buffer[n] = '\0'; out = buffer; } return n; } int WriteNamePipe(string in) { // 像管道中写数据 return write(_fd, in.c_str(), in.size()); } ~NamePipe() // 析构,删除管道文件,关闭文件 { if (_id == Creater) { int res = unlink(_fifo_path.c_str()); // 删除管道文件 if (res != 0) { perror("unlink"); } } if (_fd != Defaultfd) { // 关闭文件 close(_fd); } } void Print() { cout << _fd << " " << _fifo_path << " " << _id << endl; } private: int _fd; // FIFO描述fu const string _fifo_path; // 管道路径 int _id; // 身份码 };
Server.cpp
接收数据,当命名管道中有数据的时候才会读共享内存的数据。
#include "shm.hpp" #include "NamePipe.hpp" #include <iostream> using namespace std; int main() { // 创建共享内存x Shm shm(pathname, proj_id, gCreater); char *shmaddr = (char *)shm.GetShmaddr(); shm.InitShmaddr(); sleep(3); // 创建命名管道 NamePipe fifo(comm_path, Creater); fifo.OpenForRead(); while (true) { string message = ""; fifo.ReadNamePipe(message); cout << "shm memory content: " << shmaddr << endl; // 读取共享内存中的数据 } return 0; }
Client.cpp
发送数据,先用命名管道给读端发送,提示共享内存已经被写入了数据。
#include "shm.hpp" #include "NamePipe.hpp" #include <iostream> using namespace std; int main() { // 创建共享内存 Shm shm(pathname, proj_id, gUser); // shm.InitShmaddr(); char *shmaddr = (char *)shm.GetShmaddr(); sleep(3); // 创建命名管道 NamePipe fifo(comm_path, User); fifo.OpenForWrite(); char ch = 'A'; while (ch <= 'Z') { shmaddr[ch - 'A'] = ch; string message = "weakup"; fifo.WriteNamePipe(message); cout << "add: " << ch << " to Server" << endl; sleep(2); ch++; } return 0; }
总结
共享内存是进程通信最快的方法,因为没有额外的拷贝过程,直接对内存进行读取。此外,共享内存并不保证通信的同步,数据在读取之后不会自动清空释放,也就意味着无论何时只要想读就能读。所以很多时候,为了保证数据的同步性,即写入端进程写入数据后才让读端进程读取数据,我们可以让管道做一个提示作用。当有数据要写入的时候,就让管道发出一个提示,读端读到提示之后才从shm中读取数据。否则,读端就会堵塞等待。
此外,根据共享内存的特性,通信双方都可以同时向shm中读或者写数据。这也就意味着共享内存通信的模式是全双工的。