13 共享内存
13.1 基本概念
共享内存就是将内存进行共享,允许多个不相关的进程访问同一个逻辑内存, 直接将一块裸露的内存放在需要数据传输的进程面前。
共享的内存需要进程自己去维护好,如同步、互斥等工作,共享内存是属于临界资源,在某一时刻最多只能有一个进程对其操作(读/写数据), 共享内存一般不能单独使用,而要配合信号量、互斥锁等协调机制,让各个进程在高效交换数据的同时, 不会发生数据践踏、破坏等意外。
总的来说共享内存有以下特点:
- 共享内存是进程间通信中效率最高的方式之一。
- 共享内存是出于多个进程之间通讯的考虑,而预留的的一块内存区,因此共享内存是以传输数据为目的的。
- 共享内存允许两个或更多进程访问同一块内存,一个进程改变这块地址中的内容, 其它进程都会察觉到这
- 共享内存无同步无互斥。
共享内存的优缺点:
- 优点:使用共享内存进行进程间的通信非常方便,函数接口简单,数据的共享使进程间的数据不用传送, 而是直接访问内存,加快了程序的效率。同时,它也不像匿名管道那样要求通信的进程有一定的“血缘”关系, 只要是系统中的任意进程都可以对共享内存进行读写操作。
- 缺点:共享内存没有提供同步的机制,这使得我们在使用共享内存进行进程间通信时, 往往要借助其他的手段(如信号量、互斥量等)来进行进程间的同步工作。
13.2 shmget()创建共享内存函数
内核提供了shmget()函数的创建或获取一个共享内存对象,并返回共享内存标识符。函数原型如下:
int shmget(key_t key, size_t size, int shmflg);
参数说明:
- key:标识共享内存的键值,可以有以下取值:
- 0 或 IPC_PRIVATE。当key的取值为IPC_PRIVATE,则函数shmget()创建一块新的共享内存; 如果key的取值为0,而参数shmflg中设置了IPC_PRIVATE这个标志,则同样将创建一块新的共享内存。
- 大于0的32位整数:视参数shmflg来确定操作。
- size:共享内存的大小,所有的内存分配操作都是以页为单位
- shmflg:表示创建的共享内存的模式标志参数,在真正使用时需要与IPC对象存取权限mode(如0600)进行“|”运算来确定共享内存的存取权限。 msgflg有多种情况:
- IPC_CREAT:如果内核中不存在关键字与key相等的共享内存,则新建;如果存在,返回标识符。
- IPC_EXCL:如果内核中不存在键值与key相等的共享内存,则新建;如果存在这样的共享内存则报错。
- SHM_HUGETLB:使用“大页面”来分配共享内存,所谓的“大页面”指的是内核为了提高程序性能,对内存实行分页管理时,采用比默认尺寸(4KB)更大的分页,以减少缺页中断。Linux内核支持以2MB作为物理页面分页的基本单位。
- SHM_NORESERVE:不在交换分区中为这块共享内存保留空间。
- 返回值:shmget()函数的返回值是共享内存的ID。
当调用shmget()函数失败时将产生错误代码,有如下取值:
- EACCES:指定的消息队列已存在,但调用进程没有权限访问它
- EEXIST:key指定的消息队列已存在,而msgflg中同时指定IPC_CREAT和IPC_EXCL标志
- EINVAL:创建共享内存时参数size小于SHMMIN或大于SHMMAX。
- ENFILE:已达到系统范围内打开文件总数的限制。
- ENOENT:给定的key不存在任何共享内存,并且未指定IPC_CREAT。
- ENOMEM:内存不足,无法为共享内存分配内存。
- EACCES:没有权限。
12.3 shmat()映射函数
如果一个进程想要访问这个共享内存,那么需要将其映射到进程的虚拟空间中, 然后再访问
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明:
- shmid:共享内存ID,通常是由shmget()函数返回的。
- shmaddr:如果不为NULL,则系统会根据shmaddr来选择一个合适的内存区域, 如果为NULL,则系统会自动选择一个合适的虚拟内存空间地址去映射共享内存。
- shmflg:操作共享内存的方式:
- SHM_RDONLY:以只读方式映射共享内存。
- SHM_REMAP:重新映射,此时shmaddr不能为NULL。
- NULLSHM:自动选择比shmaddr小的最大页对齐地址。
shmat()函数调用成功后返回共享内存的起始地址。
共享内存的映射有以下注意的要点:
- 共享内存只能以只读或者可读写方式映射,无法以只写方式映射。
- shmat()第二个参数shmaddr一般设为NULL,让系统找寻合适的地址。但当其确实不为空时, 那么要求SHM_RND在shmflg必须被设置,系统将会选择比shmaddr小而又最大的页对齐地址(即为SHMLBA的整数倍)作为共享内存区域的起始地址。 如果没有设置SHM_RND,那么shmaddr必须是严格的页对齐地址。
12.4 shmdt()解除映射函数
shmdt()函数与shmat()函数相反,是用来解除进程与共享内存之间的映射的,解除映射后,该进程不能再访问
int shmdt(const void *shmaddr);
参数说明:
- shmaddr:映射的共享内存的起始地址。
shmdt()函数调用成功返回0,如果出错则返回-1,并且将错误原因存于error中。
虽然shmdt()函数很简单,但是还是有注意要点的:该函数并不删除所指定的共享内存区, 而只是将先前用shmat()函数映射好的共享内存脱离当前进程,共享内存还是存在于物理内存中。
13.5 shmctl()获取或设置属性函数
内核提供了shmctl()用于获取或者设置共享内存的相关属性。函数原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数说明:
- shmid:共享内存标识符。
- cmd:函数功能的控制命令,其取值如下:
- IPC_STAT:获取属性信息,放置到buf中。
- IPC_SET:设置属性信息为buf指向的内容。
- IPC_RMID:删除这该共享内存。
- IPC_INFO:获得关于共享内存的系统限制值信息。
- SHM_INFO:获得系统为共享内存消耗的资源信息。
- SHM_STAT:与IPC_STAT具有相同的功能,但shmid为该SHM在内核中记录所有SHM信息的数组的下标, 因此通过迭代所有的下标可以获得系统中所有SHM的相关信息。
- SHM_LOCK:禁止系统将该SHM交换至swap分区。
- SHM_UNLOCK:允许系统将该SHM交换至swap分。
- buf:共享内存属性信息结构体指针,设置或者获取信息都通过该结构体,shmid_ds结构如下:
注意:选项SHM_LOCK不是锁定读写权限,而是锁定SHM能否与swap分区发生交换。 一个SHM被交换至swap分区后如果被设置了SHM_LOCK,那么任何访问这个SHM的进程都将会遇到页错误。 进程可以通过IPC_STAT后得到的mode来检测SHM_LOCKED信息。
struct shmid_ds {
struct ipc_perm shm_perm; /* 所有权和权限 */
size_t shm_segsz; /* 共享内存尺寸(字节) */
time_t shm_atime; /* 最后一次映射时间 */
time_t shm_dtime; /* 最后一个解除映射时间 */
time_t shm_ctime; /* 最后一次状态修改时间 */
pid_t shm_cpid; /* 创建者PID */
pid_t shm_lpid; /* 后一次映射或解除映射者PID */
shmatt_t shm_nattch; /* 映射该SHM的进程个数 */
...
};
其中权限信息结构体如下:
struct ipc_perm {
key_t __key; /* 该共享内存的键值key */
uid_t uid; /* 所有者的有效UID */
gid_t gid; /* 所有者的有效GID */
uid_t cuid; /* 创建者的有效UID */
gid_t cgid; /* 创建者的有效GID */
unsigned short mode; /* 读写权限 + SHM_DEST + SHM_LOCKED 标记 */
unsigned short __seq; /* 序列号 */
};