进程间通信之共享内存SHM

1. system-v IPC简介

  消息队列、共享内存和信号量统称为system-V IPC,V是罗马数字5,是UNIX的AT&T分支中的一个版本,一般习惯称之为IPC对象。这些对象的操作接口比较相似,在系统中它们都使用一种名为key的值也唯一标识,而且它们是“可持续”资源–它们被创建以后,不会因为进程的退出而消息,而会持续存在,除非调用特殊的函数或者命令来删除。
 进程每次“打开”一个IPC对象,就会获得一个表征这个对象的ID,进而再使用这个ID来操作这个对象。IPC对象的key是唯一的,但是ID是可变的。key类似文件的路径名,ID类似文件的描述符
系统IPC对象

2. 函数ftok()函数介绍

  IPC对象的键值key是怎么产生的呢?理论上它就是一个整数,一般用函数ftok()函数来产生,函数ftok()的接口规范如下:

函数项描述函数项说明
函数功能获取一个当前未用的IPC的key
头文件#include <sys/types.h>#include <sys/ipc.h>
原型key_t ftok(const char *pathname, int proj_id);
参数1pathname一个合法的路径,比如/目录或者./当前目录
参数2proj_id工程ID,非0,仅仅使用低8位,通常传入一个unsigned char
返回值成功:返回合法未用的键值失败:返回-1

这个函数需要注意以下几点:

  1. 如果两个参数相同,那么产生的key值也相同
  2. 第一个参数一般取进程所在的目录,因为一个项目中需要通信的几个进程通常会出现在一个工程目录中
  3. 如果同一个目录中的进程需要使用超过一个IPC对象,可以通过第2个参数来标识
  4. 系统中只有一套key标识,也就是说,不同类型的IPC对象也不能重复

可以使用如下命令来查看或者删除系统中的IPC对象

  • 查看消息队列:ipcs -q
  • 查看共享内存:ipcs -m
  • 查看信号量:ipcs -s
  • 查看所有IPC对象:ipcs -a
  • 删除指定的消息队列:ipcs -q MSG_ID 或者 ipcrm -Q msg_key
  • 删除执行的共享内存:ipcs -m SHM_ID 或者 ipcrm -M shm_key
  • 删除指定的信号量:ipcs -s SEM_ID 或者 ipcrm -S sem_key

IPC对象

3. 共享内存SHM介绍

  共享内存是效率最高的IPC,因为它摒弃了内核这个“代理人”,直截了当地将一块裸漏的内存暴漏在需要进行通信的进程面前,让进程自己来操作这块内存,这样做的代价是:这些进程必须小心谨慎地操作这块裸露的共享内存,做好诸如同步、互斥等工作,毕竟现在操作系统已经不参与该过程了,一切需要进程们自己动手。也是因为这个原因,共享内存一般不能单独使用,而要配合信号量、互斥锁等协调机制,让各个进程在高下率交换数据的同时,不会发生数据践踏、破坏等意外
 共享内存的思想很朴素,进程与进程之间之间虚拟内存本来是相互独立的,不能互相访问,但是可以通过某种方式,使得相同的一块物理内存多次映射到不同的进程虚拟空间之中,这样的效果就相当于多个进程的虚拟内存空间部分重叠在一起,如下图所示:
共享内存逻辑图
 如上图所示,但进程1向其虚拟内存空间区域1写如数据时,进程2就能同时在其虚拟内存空间的区域2看见这些数据,中间没有经过任何转发,效率极高

使用共享内存的一般步骤包括:

  1. 获取共享内存对象的ID
  2. 将共享内存映射至本进程的虚拟内存空间的某个区域
  3. 当不再使用时,解除映射关系
  4. 当没有任何进程使用这块共享内存时,删除它

4. 共享内存SHM相关接口函数

获取共享内存ID的函数原型:

int shmget(key_t key, size_t size, int shmflg);

共享内存的映射函数原型:

void *shmat(int shmid, const void *shmaddr, int shmflg);

共享内存的解除映射函数原型:

int shmdt(const void *shmaddr);

shmaddr参数:

  • 映射函数shmat()的shmaddr参数: a. 如果为NULL,则系统会自动选择一个合适的 b.如果不为NULL,则系统会根据shmaddr来选择一个合适的内存区域
  • 解除映射函数shmdt()的shmaddr参数:共享内存的首地址

shmflg参数:

  • SHM_RDONLY:以只读方式映射共享内存
  • SHM_REMAP:重新映射,此时shmaddr不能为NULL
  • SHM_RND:自动选择比shmaddr小的最大页对齐地址

需要注意以下几点:

  1. 共享内存只能以只读或者可读可写方式映射,无法以只写方式映射
  2. shmat()第2个参数shmaddr一般设置为NULL,让系统自动寻找合适的地址。但shmaddr确实不为NULL时,那么要求SHM_RND在shmflag必须被设置,这样的话系统会选择比shmaddr小而又最大的页对齐地址(也就是SHMLBA的整数倍)当作共享区域的起始地址,如果没有设置SHM_RND,那么shmflag必须时严格的页对齐地址。总之,将shmaddr设置为NULL是最为明智的做法,因为这样简单,也具有移植性
  3. 解除映射之后,进程将不再被允许访问共享内存

获取或者设置共享内存相关属性的函数原型:

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmid参数:共享内存ID

cmd参数:

  • IPC_STAT:获取属性信息,放到buf中
  • IPC_SET:设置属性信息为buf中指定的信息
  • IPC_RMID:将共享内存标记为“即将移除状态”,此时还未移除
  • IPC_LOCK:禁止系统将SHM交换至swap分区

5. 共享内存SHM代码示例

下面的示例代码展示了进程Jack如何通过共享内存SHM向进程Rose发送一段数据的过程。在Rose收到数据后,将数据打印出来,给Jack发送一个信号通知Jack将该SHM删除

Jack向共享内存中发送数据

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHMSZ   1024

#define PROJ_PATH "."
#define PROJ_ID   33

int shmid;

//信号来了,执行删除共享内存ID的动作
void rmid(int sig){
    shmctl(shmid, IPC_RMID, NULL);
}

int main(int argc, char ** argv){
    signal(SIGINT, rmid);       //捕捉一个信号,捕捉后的动作为执行函数指针指向的函数
    key_t key = ftok(PROJ_PATH, PROJ_ID);   //获取IPC的Key
    shmid = shmget(key, SHMSZ, IPC_CREAT | 0666);
    char *p = (char *)shmat(shmid, NULL, 0);    //将该共享内存映射到当前进程中,如果第二个参数为NULL,则系统指定返回地址
    bzero(p, SHMSZ);
    pid_t pid = getpid();   //Jack将自身的PID信息放入到SHM的前4个字节
    printf("Current Pid = %d\n", pid);
    memcpy(p, &pid, sizeof(pid_t));
    fgets(p+sizeof(pid_t), (SHMSZ-sizeof(pid_t)), stdin);   //从键盘输入数据到共享内存中
    pause();            //等待Rose的信号去删除SHM
    shmdt(p);           //解除共享内存映射
    return 0;
}

Rose从共享内存中读数据

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHMSZ   1024

#define PROJ_PATH "."
#define PROJ_ID   33

int main(int argc, char** argv){
    key_t key = ftok(PROJ_PATH, PROJ_ID);       //获取IPC对象的key
    int shmid = shmget(key, SHMSZ, 0666);       //获取共享内存的id
    char *p = shmat(shmid, NULL, 0);            //将共享内存映射到当前进程
    printf("From Jack : %s\n", p+sizeof(pid_t));    //从共享内存中读取Jack发来的消息
    pid_t jack_pid = *((pid_t *)(p));           //获取jack的进程PID
    kill(jack_pid, SIGINT);                     //给Jack发送信号
    shmdt(p);                                   //解除映射
    return 0;
}

Jack向共享内存中写数据

Rose从共享内存中读数据

 上面是一个比较粗糙的示例,有一个要求是,必须先让写程序运行,而且必须要输入数据,然后读程序才能运行,否则读程序不能读取写程序的数据(如果先运行读程序,会发生段错误,如下图所示)
如果先运行读程序
 从代码中可以看到,读程序读完数据之后,需要使用信号通知写程序,因此写程序必须将自己的进程号PID写入共享内存的头4个字节,这样的方式,让人觉得非常笨拙,事实上对SHM的多进程或者多线程同步和互斥的工作,一般并不是采用信号量来协调的,有更好用的工具,比如信号量

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

博可睿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值