18-IPC之共享内存

共享内存:共享内存的API与消息队列的API非常相似
与回顾管道、消息队列

 1.管道
  管道是OS在物理内存上开辟一段缓存空间,当进程通过read、write等API来共享读写这段空间时,就实现了进程间通信。

 2.消息队列
  消息队列是OS创建的链表,链表的所有节点都是保存在物理内存上的,所以消息队列这个链表其实也是OS在物理内存上所开辟的缓存,当进程调用msgsnd、msgrcv等API来   共享读写时,就实现了进程间通信。

3 .共享内存
  共享内存也逃不开同样的套路
  共享内存就是OS在物理内存中开辟一大段缓存空间,不过与管道、消息队列调用read、write、msgsnd、
  msgrcv等API来读写所不同的是,使用共享内存通信时,进程是直接使用地址来共享读写的
  当然不管使用那种方式,只要能够共享操作同一段缓存,就都可以实现进程间的通信

  不过如果直接使用地址来读写缓存时,效率会更高,但是如果是调用API来读写的话,中间必须经过重重
  的OS函数调用之后,直到调用到最后一个函数时,该函数才会通过地址去读写共享的缓存,中间的调用过程会降低效率。

  对于小数据量的通信来说,使用管道和消息队列这种使用API读写的通信方式很合适,但是如果进程涉及到
  超大量的数据通信时,必须使用“共享内存”这种直接使用地址操作的通信方式,如果使用API来读写的话,
  效率会非常的低

(2)共享内存的原理

  每个进程的虚拟内存只严格对应自己的那片物理内存空间,也就是说虚拟空间的虚拟地址,只和自己的那片物理内存空间的物理地址建立映射关系,

  和其它进程的物理内存空间没有任何的交集
  因此进程空间之间是完全独立的。
  mmap1.png

  


  共享内存的实现原理很简单,进程空间不是没有交集吗,让他们的空间有交集就可以了

  以两个进程使用共享内存来通信为例,实现的方法就是:
    (1)调用API,让OS在物理内存上开辟出一大段缓存空间。
    (2)让各自进程空间与开辟出的缓存空间建立映射关系

    就让虚拟地址和物理内存的实际物理地址建立一对一的对应关系,使用虚拟地址读写缓存时,虚拟地址最终
    是要转为物理地址的,转换时就必须参考这个映射关系

    总之建立映射关系后,每个进程都可以通过映射后的虚拟地址来共享操作实现通信了。、

    mmap2.png

    
  多个进程也能映射到同一片空间,然后数据共享

  不过当多个进程映射并共享同一个空间时,在写数据的时候可能会出现相互干扰,
  比如A进程的数据刚写了一半没写完,结果切换到B进程后,B进程又开始写,A的数据就被中间B的数据
  给岔开了这时往往需要加保护措施(信号量或者文件锁),让每个进程在没有操作时不要被别人干扰,等操作完以后,别的进程才能写数据。
3共享内存的使用步骤
(1)进程调用shmget函数创建新的或获取已有共享内存shm是share memory的缩写
(2)进程调用shmat函数,将物理内存映射到自己的进程空间
  说白了就是让虚拟地址和真实物理地址建议一一对应的映射关系
  建立映射后,就可以直接使用虚拟地址来读写共享的内存空间了
(3)shmdt函数,取消映射
(4)调用shmctl函数释放开辟的那片物理内存空间
  和消息队列的msgctl的功能是一样的,只不过这个是共享内存的
  多个进程使用共享内存通信时,创建者只需要一个,同样的,一般都是谁先运行谁创建,其它后运行的
  进程发现已经被创建好了,就直接获取共享使用,大家共享操作同一个内存,即可实现通信
4.共享内存的函数
  shmget函数
  (1)函数原型
  #include <sys/ipc.h>
  #include <sys/shm.h>
  int shmget(key_t key, size_t size, int shmflg);
  1)功能:创建新的,或者获取已有的共享内
  · 如果key值没有对应任何共享内存
    创建一个新的共享内存,创建的过程其实就是os在物理内存上划出(开辟出)一段物理内存空间出来
  · 如果key值有对应某一个共享内存
    说明之前有进程调用msgget函数,使用该key去创建了某个共享内存,既然别人之前就创建好了,
    那就直接获取key所对应的共享内存
2)返回值
(a)成功:返回共享内存的标识符,以后续操作
(b)失败:返回-1,并且errno被设置

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

3)参数
(a)key:用于生成共享内存的标识符
  可以有三种设置:
  · IPC_PRIVATE:指定这个后,每次调用shmget时都会创建一个新共享内存
  · 自己指定一个长整型数
  · 使用ftok函数,通过路径名和一个8位的整形数来生成key值,最常用的
(b)size:指定共享内存的大小,我们一般要求size是虚拟页大小的整数倍
  一般来说虚拟页大小是4k(4096字节),如果你指定的大小不是虚拟页的整数倍,也会自动帮你补成整数倍
(c)semflg:与消息队列一样
  指定原始权限和IPC_CREAT,比如0664|IPC_CREAT
  只有在创建一个新的共享内存时才会用到,否者不会用到

(2)代码演示
写一个例子程序,使用共享内存实现将A进程数据发送给B进程。
我这里只实现单向的通信,至于双向通信,后面会实现

(a)使用ipcs命令即可查看创建的共享内存:
  - a 或者 什么都不跟:消息队列、共享内存、信号量的信息都会显示出来
  - m:只显示共享内存的信息
  - q:只显示消息队列的信息
  - s:只显示信号量的信息
(b)共享内存的删除
  进程结束时,system v ipc不会自动删除,进程结束后,使用ipcs依然能够查看到
  · 方法1:重启OS,很麻烦
  · 方法2:进程结束时,调用相应的API来删除,后面再讲
  · 方法3:使用ipcrm命令删除
  - 删除共享内存
  + M:按照key值删除
    ipcrm -M key

  + m:按照标识符删除
    ipcrm -m semid

  - 删除消息队列
  + Q:按照key值删除
  + q:按照标识符删除
  - 删除信号量
  + S:按照key值删除
  + s:按照标识符删除

 shmat
(1)函数原型
  #include <sys/types.h>
  #include <sys/shm.h>
  void *shmat(int shmid, const void *shmaddr, int shmflg);
  1)功能
    将shmid所指向的共享内存空间映射到进程空间(虚拟内存空间),并返回影射后的起始
    地址(虚拟地址)有了这个地址后,就可以通过这个地址对共享内存进行读写操作
2)参数
  (a)shmid:共享内存标识符
  (b)shmaddr:指定映射的起始地址
    有两种设置方式
    1. 自己指定映射的起始地址(虚拟地址)
    我们一般不会这么做,因为我们自己都搞不清哪些虚拟地址被用了,哪些没被用。

    2. NULL:表示由内核自己来选择映射的起始地址(虚拟地址)。
    这是最常见的方式,也是最合理的方式,因为只有内核自己才知道哪些虚拟地址可用,哪些不可用
  (c)shmflg:指定映射条件
    · 0:以可读可写的方式映射共享内存,也就是说映射后,可以读、也可以写共享内存
    · SHM_RDONLY:以只读方式映射共享内存,也就是说映射后,只能读共享内存,不能写
    . 1:只写
  3)返回值
    (a)成功:则返回映射地址
    (b)失败:返回(void *)-1,并且errno被设置

shmdt函数
(1)函数原型
#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);

1)功能:取消建立的映射。
2)返回值:调用成功返回0,失败返回-1,且errno被设置
3)参数
  shmaddr:映射的起始地址(虚拟地址)
shmctl函数
(1)函数原型
#include <sys/ipc.h>
#include <sys/shm.h>

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

(a)功能:根据cmd的要求,对共享内存进行相应控制。
  比如:
  · 获取共享内存的属性信息
  · 修改共享内存的属性信息
  · 删除共享内存
  · 等等
  删除共享内存是最常见的控制。

(b)参数
· shmid:标识符。
· cmd:控制选项
- IPC_STAT:从内核获取共享内存属性信息到第三个参数(应用缓存)
- IPC_SET:修改共享内存的属性,修改方法与消息队列相同
- IPC_RMID:删除共享内存,不过前提是只有当所有的映射取消后,才能删除共享内存。
删除时,用不着第三个参数,所以设置为NULL

· buf
buf的类型为struct shmid_ds。
- cmd为IPC_STAT时
buf用于存储原有的共享内存属性,以供查看。

- cmd为IPC_SET时
buf中放的是新的属性设置,用于修改共享内存的属性

- 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 */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) :当前正在使用进程的PID*/
shmatt_t shm_nattch; /* No. of current attaches:映射数量,
* 标记有多少个进程空间映射到了共享内存上
* 每增加一个映射就+1,每取消一个映射就-1 */ 
...
};

struct ipc_perm,这个结构体我们在讲消息队列时已经讲过,这里不再重复讲。
struct ipc_perm 
{
key_t __key; /* Key supplied to shmget(2) */
uid_t uid; /* UID of owner */
gid_t gid; /* GID of owner */
uid_t cuid; /* UID of creator */
gid_t cgid; /* GID of creator */
unsigned short mode; /* Permissions + SHM_DEST andSHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};


4)、返回值
  调用成功0,失败则返回-1,并且errno被设置
(3)代码改进
  1)读共享内存的代码存缺陷
    

while(1)
{
  if(strlen((char *)shmaddr) != 0)
{
  printf("%s\n", (char *)shmaddr);
  bzero(shmaddr, SHM_SIZE);
}
}

(a)缺陷1:strlen函数只能用于判断字符串
  如果对方通过共享内存发送不是字符串,而是结构体、整形、浮点型数据,
  strlen将无法正确判断

(b)缺陷2:没有数据时,cpu会一直循环的判断
  这样会让cpu一直做好无意义的事情,非常浪费cpu资源。
2)改进
  保证写完后再读数据,当共享内存没有数据时,读进程休眠,当写进程把数据写完后,将读进程唤醒
  说白了就是多个进程在操作时,涉及到一个谁先谁后的问题,其实就是同步问题,所谓同步就是
  保持一个谁先谁后的统一步调

  这就好比我踩一个脚步你跟着踩一个脚步,统一踩脚步的步调,这就是同步,否者我踩我的,你踩
  你的,各自的步调不一致,这就是异步。

实现同步的方法:
(a)方法1:使用信号
  shm_upgrade.h

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

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define SHM_FILE "./shmfile"
#define SHM_SIZE 4096
#define FIFO_FILE "./fifo"

int shmid= -1;
void *shmaddr = NULL;
char buf[300]={0};

void print_err(char *estr){
    perror(estr);
    exit(-1);
}
void create_or_get_shm(void){
    int fd =0;
    key_t key =-1;
    fd =open(SHM_FILE,O_RDWR|O_CREAT,0664);
    if(fd == -1) print_err("open fail");
    key = ftok(SHM_FILE,'b');
    if(key == -1) print_err("ftok fail");

    shmid=shmget(key,SHM_SIZE,0664|IPC_CREAT);
    if(shmid == -1) print_err("shmget err");

}
int get_peer_pid(void){
    int ret =-1;
    int fifofd =-1;

    //创建有名管道文件
    ret =mkfifo(FIFO_FILE,0664);
    if(ret == -1 && errno != EEXIST) print_err("mkfifo err");
    printf("begin open fifo ....\n");
    //以只读方式打开管道
    fifofd = open(FIFO_FILE,O_RDONLY);//打开时阻塞
    if(fifofd == -1) print_err("open fifo fail");

    //读管道 获取"读共享内存进程" 的PID

    int peer_pid;
    printf("begin read fifo ....\n");
    ret = read(fifofd,&peer_pid,sizeof(peer_pid));
    if(ret ==-1) print_err("read fifo fail");
    printf("get peer_pid %d\n",peer_pid);
    return peer_pid;
}


void signal_fun(signum){
    if(SIGINT == signum){
        shmdt(shmaddr);
        shmctl(shmid,IPC_RMID,NULL);
        remove(SHM_FILE);
        remove(FIFO_FILE);
        exit(-1);
    }else if(SIGUSR1 == signum){
        printf("recv signum=%d\n",signum );
    }
    
}
void snd_self_pid(void){
    int ret =-1;
    int fifofd =-1;

    //创建有名管道文件
    ret =mkfifo(FIFO_FILE,0664);
    if(ret == -1 && errno != EEXIST) print_err("mkfifo err");

    //以只写方式打开管道文件
    fifofd = open(FIFO_FILE,O_WRONLY);
    if(fifofd == -1) print_err("open fifo fail");

    //获取当前进程的pid,使用有名管道发送给 写共享内存的进程
    pid_t pid=getpid();
    ret = write(fifofd,&pid,sizeof(pid));

    if(ret ==-1) print_err("write fifo fail");

}

shm1_upgrade.c

#include "shm.h"
int main(int argc, char const *argv[])
{
    
    signal(SIGINT,signal_fun);
    pid_t peer_pid;
    peer_pid =get_peer_pid();
    create_or_get_shm();
    //建立映射
    shmaddr=shmat(shmid,NULL,0);

    if(shmaddr == (void *) -1) print_err("shmaddr error");

    while(1){
        scanf("%s",buf);
        memcpy(shmaddr,buf,sizeof(buf));
        printf("send signal to %d\n",peer_pid );
        kill(peer_pid,SIGUSR1);
        sleep(1);
    }

    return 0;
}

shm2_upgrade.c  

#include "shm.h"
int main(int argc, char const *argv[])
{
    signal(SIGINT,signal_fun);
    //注册一个空捕获,用于唤醒pause函数
    signal(SIGUSR1,signal_fun);

    //使用有名管道,将当前进程的pid 发送给写共享内存的进程
    snd_self_pid();

    //创建获取共享内存
    create_or_get_shm();
    //建立映射
    shmaddr=shmat(shmid,NULL,0);

    if(shmaddr == (void *) -1) print_err("shmaddr error");

    while(1){
            pause();
            printf("recv data:%s\n",(char *)shmaddr);
            bzero(shmaddr,SHM_SIZE);
        
    }

    return 0;
}

 

 

(b)方法2:使用信号量实现

posted @ 2018-09-17 00:49  H&K  阅读(399)  评论(0编辑  收藏  举报