目录
一、什么是共享内存
二、共享内存的原理
三、使用共享内存实现进程间通信
3.1 shmget接口
3.1.1 key形参详解
3.2 释放共享内存
3.2.1 ipcs指令
3.2.2 ipcrm指令
3.2.3 shmctl接口
3.3 关联共享内存
3.4 去关联共享内存
3.5 使用共享内存进行进程间通信实例
四、共享内存的特性
4.1 共享内存的优点
4.2 共享内存的缺点
五、实例代码改进
一、什么是共享内存
共享内存区是最快的进程间通信(IPC)形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
二、共享内存的原理
要实现进程间通信的根本在于让不同的进程看到同一块份数据,共享内存也不例外
现在有两个毫不相干的进程A和B:
它们想要进行通信,就必须要看到同一份数据,所以操作系统会在物理空间上开辟一块空间,供这两个进程共同访问,这一块空间被称为共享内存:
共享内存被页表映射到进程地址空间的存储区域在共享区
想要释放共享内存也很简单,先取消两个进程页表对应的映射关系,就可以释放共享内存块了
但是系统中的进程都可以用共享内存进行通信,所以在任何一个时刻,可能有多个共享内存在被用来进行通信,导致系统中一定会存在很多共享内存同时存在
那OS要不要整体管理所有的共享内存呢?
当然是要的,所以共享内存,不仅仅是我们想的那样,只要在内存中开辟空间即可,系统也要为了管理共享内存,构建对应的描述共享内存的结构体对象!
即:共享内存=共享内存的内核数据结构+真正开辟的内存空间
管理共享内存的数据结构struct shmid_ds:
struct shmid_ds
{struct ipc_perm shm_perm; /* Ownership and permissions */size_t shm_segsz; /* Size of segment (bytes) */time_t shm_atime; /* Last attach time */time_t shm_dtime; /* Last detach time */time_t shm_ctime; /* Last change time */pid_t shm_cpid; /* PID of creator */pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */shmatt_t shm_nattch; /* No. of current attaches */...
};
struct ipc_perm
{key_t __key; /* Key supplied to shmget(2) */uid_t uid; /* Effective UID of owner */gid_t gid; /* Effective GID of owner */uid_t cuid; /* Effective UID of creator */gid_t cgid; /* Effective GID of creator */unsigned short mode; /* Permissions + SHM_DEST andSHM_LOCKED flags */unsigned short __seq; /* Sequence number */
};
三、使用共享内存实现进程间通信
废话不多说,我们直接来上操作
3.1 shmget接口
现在有一个系统接口shmget(包含在头文件<sys/ipc.h>和<sys/shm.h>中)来帮我们建立共享内存:
该函数有三个参数:
● key:这个共享内存段名字,要具备唯一性(通常使用ftok函数传入)
● size:共享内存大小(以字节为单位),但是OS实际开辟的空间大小是以Page页(4KB)为单位的,底层会将我们传入的空间大小向上取整为Page页的倍数并开辟;但是这不代表OS实际开辟了多少空间我们就可以用多少空间,实际使用空间的大小还是依据我们传入数据的大小,防止越界
● shmflg:由九个权限标志构成(它们的用法和创建文件时使用的mode模式标志是一样的),常用的标志位有IPC_CREAT和IPC_EXCL,另外我们还可以通过该参数设置我们所创建出的共享内存的权限(或上八进制方案)
IPC_CREAT:可以单独使用,单独使用就是创建一个共享内存,如果共享内存不存在,就创建之,如果已经存在,获取已经存在的共享内存并返回
IPC_EXCL:不能单独使用,一般都要配合IPC_CREAT使用(IPC_CREAT | IPC_EXCL:创建一个共享内存,如果共享内存不存在,就创建之,如果已经存在,则立马出错返回;这样可以保证创建成功时所对应的共享一定是最新的,一定没有被别的进程使用过)
该函数创建共享内存成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
3.1.1 key形参详解
操作系统中会存在很多共享内存,那如何让毫不相干的进程识别同一块共享内存空间呢?
我们可以给每个共享内存起名字,key该形参就可以表示共享内存段名字,但是名字可不是随便起的,要保证每一块共享内存的名字不相同我们可以使用ftok函数(包含在头文件<sys/types.h>和<sys/ipc.h>中):
该函数有两个形参:
● pathname:传入文件路径
● proj_id:传入工程id
对于这两个形参并没有强制性的要求,该函数会对传入的两个形参进行特定的算法计算后,返回一个冲突几率极小的值,该值可以作为系统接口shmget的key形参
该函数成功后,将返回生成的key_t值。失败时返回-1,并设置errno
这样子只要两个进程都调用ftok函数,传入相同的形参值就可以得到相同的值,以这个值我们就可以标定同一个共享内存,从而建立进程间的通信
3.2 释放共享内存
我们需要注意的是:当进程创建共享内存之后,当进程结束后,其创建的共享内存空间不会被自动释放,该空间的生命周期是随操作系统的(在下面我们来介绍释放共享内存的方法)
3.2.1 ipcs指令
我们在可以使用ipcs指令用于查看Linux进程间通信设施的状态,包括消息列表、共享内存和信号量的信息,下面是其命令选项:
-i,--id ID
详细显示指定资源 ID 的 IPC 信息。使用时需要指定资源类型,资源包括消息队列(-q)、共享内存(-m)和信号量(-s)
-h,--help
显示帮助信息
-V,--version
显示版本信息IPC 资源类型选项:
-q,--queues
显示活动的消息队列信息
-m,--shmems
显示活动的共享内存信息
-s, --semaphores
显示活动的信号量信息
-a,--all
显示系统内所有的IPC信息。命令的默认选项输出格式选项:当指定多个时,以最后一个为准。
-c,--creator
查看 IPC 的创建者和所有者
-l,--limits
查看 IPC 资源的限制信息
-p,--pid
查看 IPC 资源的创建者和最后操作者的进程 ID
-t,--time
查看最新调用 IPC 资源的详细时间。包括 msgsnd() 和 msgrcv() 对 message queues 的操作,shmat() 和 shmdt() 对shared memory 的操作,以及 semop() 对 semaphores 的操作
-u,--summary
查看 IPC 资源状态汇总信息显示大小单位控制选项:只对选项 -l, --limits 生效
-b,--bytes
以字节为单位显示大小
--human
以可读的格式显示大小
下面我们显示活动的共享内存信息:
(nattch表示该共享内存被几个进程关联着)
可以看到在我的系统中有一个key为0x1101120d,shmid为0的共享内存块, 下面我们来释放它 :
3.2.2 ipcrm指令
ipcrm可以删除指定 ID 的 IPC(Inter-Process Communication)对象,包括消息队列(message queue)、共享内存(shared memory)和信号量(semaphore),同时将与 IPC 对象关联的数据一并删除,下面是其命令选项:
-a, --all [shm | msg | sem]
删除所有 IPC 资源。当给定选项参数 shm、msg 或 sem,则只删除指定类型的 IPC 资源。注意:慎用该选项,否则可能会导致某些程序出于不确定状态
-M, --shmem-key SHMKEY
当没有进程与共享内存段绑定时,通过 SHMKEY 删除共享内存段
-m, --shmem-id SHMID
当没有进程与共享内存段绑定时,通过 SHMID 删除共享内存段
-Q, --queue-key MSGKEY
通过 MSGKEY 删除消息队列
-q, --queue-id MSGID
通过 MSGID 删除消息队列
-S, --semaphore-key SEMKEY
通过 SEMKEY 删除信号量
-s, --semaphore-id SEMID
通过 SEMID 删除信号量
-h, --help
显示帮助信息并退出
-V, --version
显示版本信息并退出
-v, --verbose
以冗余模式执行 ipcrm,输出 rpcrm 正在做什么
下面我们来删除上面的key为0x1101120d,shmid为0的共享内存块,我们使用-m选项加上要删除的shmid即可:
3.2.3 shmctl接口
我们在写代码创建共享内存后总不可能使用系统指令去删除,所以我们需要在代码中调用系统接口shmctl来掌控共享内存:
shmctl函数的有三个参数:
● shmid :共享内存标识符,即要控制的共享内存段的标识符。
● cmd :控制命令,用于指定要执行的操作,比如删除共享内存段、修改权限等。常用的命令包括:IPC_RMID (删除共享内存段)
IPC_SET (在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值)
IPC_STAT(在进程有足够权限的前提下,把shmid ds结构中的数据设置为共享内存的当前关联值)
● buf :指向 struct shmid_ds 结构的指针,用于存储共享内存段的信息。可以为 NULL ,表示不获取共享内存段的信息。
shmctl函数的返回值是一个整型值,表示函数执行的结果。如果函数执行成功,返回值为0;如果出现错误,返回值为-1,并设置 errno 来指示具体的错误原因。
3.3 关联共享内存
我们创建(获取)了共享内存,那该使两个进程联系起来呢?
我们可以将共享内存段关联到进程地址空间,这样子进程就可以使用所获得的进程地址空间中的地址访问共享内存了
我们调用系统接口shmat(包含在头文件<sys/types.h>和<sys/shm.h>中)来让进程挂接共享内存:
该函数需要3个参数:
● shmid:传入shmget返回的标识符。
● shmaddr:如果传入NULL,系统将自动选择一个合适的进程地址空间! 如果shmaddr不是NULL 并且没有指定SHM_RND,则此段连接到addr所指定的地址上,如果shmaddr非0 并且指定了SHM_RND 则此段连接到shmaddr -(shmaddr mod SHMLAB)所表示的地址上(SHM_RND命令的意思是取整,SHMLAB的意思是低边界地址的倍数,它总是2的乘方。该算式是将地址向下取最近一个 SHMLAB的倍数)。除非只计划在一种硬件上运行应用程序(这在当今是不大可能的),否则不用指定共享段所连接到的地址。所以一般应指定shmaddr为NULL,以便由内核选择地址。● shmflg:传入SHM_RDONLY,以只读方式连接此段;传入0以读写的方式连接此段
shmat返回值是返回创建的进程虚拟空间的地址 如果出错返回-1
3.4 去关联共享内存
那两个进程通过共享内存通信结束后,我们要取消进程与共享内存之间的关联,这里要用到另一个接口shmdt(包含在头文件<sys/types.h>和<sys/shm.h>中):
该接口有一个参数:
● shmaddr: 传入由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程去关联不等于删除共享内存段
3.5 使用共享内存进行进程间通信实例
下面我们使用两个进程:client和server来实现共享内存进行进程间通信:
common.hpp:
#ifndef __COMM_HPP__
#define __COMM_HPP__#include <iostream>
#include <cstring>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>#endif#define PATHNAME "."
#define PROJID 0x1111const int gsize = 4069; // 共享内存大小std::string To_Hex(int x) // 将十进制转为十六进制
{char buffer[64];snprintf(buffer, sizeof buffer, "0x%x", x);return buffer;
}const key_t GetKey() // 获取key值
{key_t k = ftok(PATHNAME, PROJID);if (k == -1){std::cerr << "error: " << errno << ": " << strerror(errno) << std::endl;exit(1);}return k;
}static int getshm(key_t key, int size, int flag) // 获取共享内存
{int k = shmget(key, size, flag);if (k == -1){std::cerr << "error: " << errno << ": " << strerror(errno) << std::endl;exit(1);}return k;
}int Create_shm(key_t key, int size) // 创建共享内存段
{umask(0);return getshm(key, size, IPC_CREAT | IPC_EXCL | 0666); //(IPC_CREAT | IPC_EXCL)以保证创建的共享内存段是最新的,|0666将创建的共享空间的权限让创建者有读写权
}int Obtain_shm(key_t key, int size) // 获取srever进程创建的共享内存段的标识码
{return getshm(key, size, IPC_CREAT);
}int Delete_shm(int shmid) // 删除共享内存
{return shmctl(shmid, IPC_RMID, nullptr);
}void *Attach_shm(int shmid) // 让进程关联上共享内存
{return shmat(shmid, nullptr, 0);
}void Datach_shm(void *addptr) // 解除进程关联的共享内存
{if (shmdt(addptr) == -1){std::cerr << "error: " << errno << ": " << strerror(errno) << std::endl;exit(1);}
}#define SERVER 0 // 服务段进程
#define CLIENT 1 // 客户端进程class Init_shm
{
public:Init_shm(int type): _type(type){_key = GetKey();if (type == SERVER)_shmid = Create_shm(_key, gsize);else_shmid = Obtain_shm(_key, gsize);_addptr = Attach_shm(_shmid);}void Get_shm_data(){struct shmid_ds ds;int ret = shmctl(_shmid, IPC_STAT, &ds);if (ret == -1)std::cerr << "error: " << errno << ": " << strerror(errno) << std::endl;else{std::cout << "create shm process pid: " << ds.shm_cpid << " ,proccess pid:" << getpid() << std::endl;std::cout << "shm shmid: " << To_Hex(ds.shm_perm.__key) << std::endl;}}void *Getptr(){return _addptr;}~Init_shm(){Datach_shm(_addptr);if (_type == SERVER)Delete_shm(_shmid);}private:int _type; // 进程类型key_t _key;int _shmid;void *_addptr; // 关联的共享内存在进程空间中的地址
};
client.cc:
#include "common.hpp"
#include <unistd.h>
int main()
{Init_shm shm(CLIENT);shm.Get_shm_data();void *ptr = shm.Getptr();char c = 'a';std::cout << "Start writing" << std::endl;while (c != 'z' + 1) // 写入数据{((char *)ptr)[c - 'a'] = c;((char *)ptr)[c - 'a' + 1] = '\0';++c;sleep(1);}std::cout << "Write End" << std::endl;return 0;
}
server.cc:
#include "common.hpp"int main()
{Init_shm shm(SERVER);shm.Get_shm_data();void *ptr = shm.Getptr();int n = 30;while (n--) // 读取数据{std::cout << "client message:" << (char *)ptr << std::endl;sleep(1);}return 0;
}
运行效果:
四、共享内存的特性
4.1 共享内存的优点
我们可以看到上面的实例代码,在数据的写入和读取的过程中并没有向管道通信一样调用write、read等系统接口,而是直接向对应的地址空间写入和读取
这样子使共享内存间的进程间的数据不用传送,而是直接访问内存,绕过了Linux的内核,加快了程序的效率。同时,它也不像匿名管道那样要求通信的进程有一定的父子关系
4.2 共享内存的缺点
下面我们单单运行serve进程:
可以看到即使共享内存中没有任何数据,但是该进程还是无脑的进行读取,但是在上期我们介绍的管道通信中,当管道中没有数据时,read函数会阻塞
所以共享内存没有任何保护机制(同步互斥),这使得我们在使用共享内存进行进程间通信时,往往要借助其他的手段来进行进程间的同步工作
五、实例代码改进
现在我们将上面的代码改进一下,让client进程先向共享内存中写入数据,写完了再通知server进程来读取:
对于该功能的实现我们要联系到管道通信,创建一个管道来告知server进程是否完成了写入任务(对于管道不熟悉的同学请看到这里:【Linux】进程间通信——管道):
common.hpp:
#pragma once#ifndef __COMM_HPP__
#define __COMM_HPP__#include <iostream>
#include <cstring>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>#endif#define PATHNAME "."
#define PROJID 0x1111const int gsize = 4069; // 共享内存大小std::string To_Hex(int x) // 将十进制转为十六进制
{char buffer[64];snprintf(buffer, sizeof buffer, "0x%x", x);return buffer;
}const key_t GetKey() // 获取key值
{key_t k = ftok(PATHNAME, PROJID);if (k == -1){std::cerr << "error: " << errno << ": " << strerror(errno) << std::endl;exit(1);}return k;
}static int getshm(key_t key, int size, int flag) // 获取共享内存
{int k = shmget(key, size, flag);if (k == -1){std::cerr << "error: " << errno << ": " << strerror(errno) << std::endl;exit(1);}return k;
}int Create_shm(key_t key, int size) // 创建共享内存段
{umask(0);return getshm(key, size, IPC_CREAT | IPC_EXCL | 0666); //(IPC_CREAT | IPC_EXCL)以保证创建的共享内存段是最新的,|0666将创建的共享空间的权限让创建者有读写权
}int Obtain_shm(key_t key, int size) // 获取srever进程创建的共享内存段的标识码
{return getshm(key, size, IPC_CREAT);
}int Delete_shm(int shmid) // 删除共享内存
{return shmctl(shmid, IPC_RMID, nullptr);
}void *Attach_shm(int shmid) // 让进程关联上共享内存
{return shmat(shmid, nullptr, 0);
}void Datach_shm(void *addptr) // 解除进程关联的共享内存
{if (shmdt(addptr) == -1){std::cerr << "error: " << errno << ": " << strerror(errno) << std::endl;exit(1);}
}#define SERVER 0 // 服务端进程
#define CLIENT 1 // 客户端进程class Init_shm
{
public:Init_shm(int type): _type(type){_key = GetKey();if (type == SERVER)_shmid = Create_shm(_key, gsize);else_shmid = Obtain_shm(_key, gsize);_addptr = Attach_shm(_shmid);}void Get_shm_data(){struct shmid_ds ds;int ret = shmctl(_shmid, IPC_STAT, &ds);if (ret == -1)std::cerr << "error: " << errno << ": " << strerror(errno) << std::endl;else{std::cout << "create shm process pid: " << ds.shm_cpid << " ,proccess pid:" << getpid() << std::endl;std::cout << "shm shmid: " << To_Hex(ds.shm_perm.__key) << std::endl;}}void *Getptr(){return _addptr;}~Init_shm(){Datach_shm(_addptr);if (_type == SERVER)Delete_shm(_shmid);}private:int _type; // 进程类型key_t _key;int _shmid;void *_addptr; // 关联的共享内存在进程空间中的地址
};#define NUM 1024std::string file_name = "./serve_fifo";class Init_fifo
{
public:Init_fifo(){umask(0);int ret;ret = mkfifo(file_name.c_str(), 0666); // 创建管道文件if (ret == -1){std::cout << "mkfifo error ,errno:" << strerror(errno) << std::endl;exit(1);}elsestd::cout << "created fifo successfully" << std::endl;}~Init_fifo(){unlink(file_name.c_str()); // 删除管道文件}
};
client.cc:
#include "common.hpp"int main()
{Init_shm shm(CLIENT);shm.Get_shm_data();void *ptr = shm.Getptr();char c = 'a';// 开始写入数据std::cout << "Start writing" << std::endl;while (c != 'z' + 1){((char *)ptr)[c - 'a'] = c;((char *)ptr)[c - 'a' + 1] = '\0';++c;sleep(1);}std::cout << "Write End" << std::endl;// 写入完毕int wfd;wfd = open(file_name.c_str(), O_WRONLY);if (wfd == -1){std::cout << "open error ,errno:" << strerror(errno) << std::endl;exit(1);}char buffer[NUM];buffer[0] = '1';ssize_t n = write(wfd, buffer, strlen(buffer)); // 向管道文件中传入信号if (n < 0){std::cout << "write error ,errno:" << strerror(errno) << std::endl;close(wfd);exit(1);}close(wfd);return 0;
}
server.cc:
#include "common.hpp"int main()
{Init_shm shm(SERVER);shm.Get_shm_data();Init_fifo client;int wfd;wfd = open(file_name.c_str(), O_RDONLY);if (wfd == -1){std::cout << "open error ,errno:" << strerror(errno) << std::endl;exit(1);}elsestd::cout << "open success" << std::endl;char buffer[NUM];ssize_t n = read(wfd, buffer, sizeof(buffer) - 1); // 等待client进程的信号if (n > 0){buffer[n] = '\0';if (buffer[0] == '1'){std::cout << "client write end" << std::endl;void *ptr = shm.Getptr();std::cout << "client message:" << (char *)ptr << std::endl;sleep(1);}}else if (n == 0){std::cout << "client quit" << std::endl;exit(1);}else{std::cout << "read error ,errno:" << strerror(errno) << std::endl;close(wfd);exit(1);}close(wfd);return 0;
}
运行效果: