Fork me on GitHub

IPC——共享内存

概述

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

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

共享内存也逃不开同样的套路。共享内存就是OS在物理内存中开辟一大段缓存空间,不过与管道、消息队列调用read、write、msgsnd、msgrcv等API来读写所不同的是,使用共享内存通信时,进程是直接使用地址来共享读写的。

当然不管使用那种方式,只要能够共享操作同一段缓存,就都可以实现进程间的通信。

 

信号、管道、消息队列、共享内存对比

信号:非精确通信

管道:无名管道只用于亲缘进程,命名管道克服了这一缺点。但是这两种管道方式还是不适合网状通信

消息队列:克服了无名管道只用于亲缘进程,无名/命名管道 网状通信若的缺点。但是不能实现大规模数据的通信。

共享内存:继承了消息队列的优点,还克服了其缺点。支持大规模数据通信。

为啥共享内存比消息队列效率高?

前面这4中IPC,其本质都是操作OS提供的一段虚拟内存(在引入虚拟内存机制的情况下),虚拟内存最终还是被OS映射到真实物理内存。信号、管道、消息队列 都要调用各种API,在到达内存之前,经过了多次函数调用,直到最后一个函数,该函数才会通过地址去读写共享的缓存。层层调用势必会严重降低效率。而共享内存就没有这么多麻烦,直接使用地址来读写内存,效率高,那是必须的!

 

共享内存原理

注:紫线表述有误,共享内存不会占据进程全部虚拟地址空间

每个进程的虚拟内存只严格对应自己的那片物理内存空间,也就是说虚拟空间的虚拟地址,只和自己的那片物理内存空间的物理地址建立映射关系,和其它进程的物理内存空间没有任何的交集,因此进程空间之间是完全独立的。

以两个进程使用共享内存来通信为例,实现的方法就是:

(1)调用API,让OS在物理内存上开辟出一大段缓存空间。
(2)让各自进程空间与开辟出的缓存空间建立映射关系

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

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

当然是可以的。不过当多个进程映射并共享同一个空间时,在写数据的时候可能会出现相互干扰,比如A进程的数据刚写了一半没写完,结果切换到B进程后,B进程又开始写,A的数据就被中间B的数据给岔开了。这时往往需要加保护措施,让每个进程在没有操作时不要被别人干扰,等操作完以后,别的进程才能写数据。

 

共享内存的使用步骤

①进程调用shmget函数创建新的或获取已有共享内存。shm是share memory的缩写。

②进程调用shmat函数,将物理内存映射到自己的进程空间。即让虚拟地址和真实物理地址建立一 一对应的映射关系。建立映射后,就可以直接使用虚拟地址来读写共享的内存空间了。

③shmdt函数,取消映射

④调用shmctl函数释放开辟的那片物理内存空间和消息队列的msgctl的功能是一样的,只不过这个是共享内存的。

多个进程使用共享内存通信时,创建者只需要一个,同样的,一般都是谁先运行谁创建,其它后运行的进程发现已经被创建好了,就直接获取共享使用,大家共享操作同一个内存,即可实现通信。

 

API

shmget

函数原型 

#include <sys/ipc.h>
#include <sys/shm.h>                 
int shmget(key_t key, size_t size, int shmflg);

功能

创建新的,或者获取已有的共享内存

如果key值没有对应任何共享内存:创建一个新的共享内存,创建的过程其实就是OS在物理内存上划出(开辟出)一段物理内存空间出来。

如果key值有对应某一个共享内存:说明之前有进程调用msgget函数,使用该key去创建了某个共享内存,既然别人之前就创建好了,那就直接获取key所对应的共享内存。

参数

key:用于生成共享内存的标识符,使用方法参考消息队列的key

size:指定共享内存的大小,我们一般要求size是虚拟页大小的整数倍。一般来说虚拟页大小是4k(4096字节),如果你指定的大小不是虚拟页的整数倍,也会自动帮你补成整数倍。

semflg:与消息队列一样。指定原始权限和IPC_CREAT,比如 0664|IPC_CREAT。只有在创建一个新的共享内存时才会用到,否者不会用到。

返回值

成功:返回共享内存的标识符,以后续操作

失败:返回-1,并且errno被设置。

 

shmat

函数原型

#include <sys/types.h>
#include <sys/shm.h>               
void *shmat(int shmid, const void *shmaddr, int shmflg); 

功能

将shmid所指向的共享内存空间映射到进程空间(虚拟内存空间),并返回影射后的起始地址(虚拟地址)。有了这个地址后,就可以通过这个地址对共享内存进行读写操作。

参数

shmid:共享内存标识符。

shmaddr:指定映射的起始地址,有两种设置方式

①自己指定映射的起始地址(虚拟地址)。我们一般不会这么做,因为我们自己都搞不清哪些虚拟地址被用了,哪些没被用。

②NULL:表示由内核自己来选择映射的起始地址(虚拟地址)。这是最常见的方式,也是最合理的方式,因为只有内核自己才知道哪些虚拟地址可用,哪些不可用。

shmflg:指定映射条件。

0:以可读可写的方式映射共享内存,也就是说映射后,可以读、也可以写共享内存。

SHM_RDONLY:以只读方式映射共享内存,也就是说映射后,只能读共享内存,不能写。

返回值

成功:则返回映射地址

失败:返回(void *)-1,并且errno被设置。

 

shmdt

函数原型 

#include <sys/types.h>
#include <sys/shm.h>                
int shmdt(const void *shmaddr);   

功能

取消建立的映射。

参数

shmaddr:映射的起始地址(虚拟地址)。

返回值

调用成功返回0,失败返回-1,且errno被设置。

 

shmctl

函数原型 

#include <sys/ipc.h>
#include <sys/shm.h>           
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

 功能

根据cmd的要求,对共享内存进行相应控制。

比如:

获取共享内存的属性信息
修改共享内存的属性信息
删除共享内存
等等

删除共享内存是最常见的控制。

参数

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 */
};

返回值

调用成功0,失败则返回-1,并且errno被设置。

 

共享内存示例代码

单向通行

p1.c

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

#define SHM_FILE "./shmfile"

#define SHM_SIZE 4096

int shmid = -1;    

char buf[300] = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222\
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2222222222"};

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 fail");
    
    //read(fd, &shmid, sizeof(shmid));
}

int main(void)
{

    void *shmaddr = NULL;

    /* 创建、或者获取共享内存 */
    create_or_get_shm();

    //建立映射
    shmaddr = shmat(shmid, NULL, 0);
    if(shmaddr == (void *)-1) print_err("shmat fail");    
    
    while(1)
    {
        memcpy(shmaddr, buf, sizeof(buf));
        sleep(1);
        
    }
    
    return 0;
}
View Code

p2.c

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

#define SHM_FILE "./shmfile"

#define SHM_SIZE 4096

int shmid = -1;    

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 fail");
    
    //read(fd, &shmid, sizeof(shmid));
}

int main(void)
{

    void *shmaddr = NULL;

    /* 创建、或者获取共享内存 */
    create_or_get_shm();

    //建立映射
    shmaddr = shmat(shmid, NULL, 0);
    if(shmaddr == (void *)-1) print_err("shmat fail");    
    
    while(1)
    {
        if(strlen((char*)shmaddr)!=0)
        {
            printf("%s\n", (char *)shmaddr);
            bzero(shmaddr, SHM_SIZE);
        }
    
    }
    
    return 0;
}
View Code

  这种写法,在Ctrl+C硬件中断进程后,并不会删除共享内存。下面代码改进这一问题

p1.c

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



#define SHM_FILE "./shmfile"

#define SHM_SIZE 4096

int shmid = -1;
void *shmaddr = NULL;    



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 fail");

    //write(fd, &shmid, sizeof(shmid));
}

char buf[300] = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222\
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2222222222"};

void signal_fun(int signo)
{
    shmdt(shmaddr);
    shmctl(shmid, IPC_RMID, NULL);
    remove("./fifo");
    remove(SHM_FILE);
    
    exit(-1);    
}

int get_peer_PID(void)
{
    int ret = -1;
    int fifofd = -1;

    /* 创建有名管道文件 */
        ret = mkfifo("./fifo", 0664); 
        if(ret == -1 && errno != EEXIST) print_err("mkfifo fail");
    
    /* 以只读方式打开管道 */
    fifofd = open("./fifo", O_RDONLY);
        if(fifofd == -1) print_err("open fifo fail");

    /* 读管道,获取“读共享内存进程”的PID */
        int peer_pid;
        ret = read(fifofd, &peer_pid, sizeof(peer_pid));
        if(ret == -1) print_err("read fifo fail");

    return peer_pid; 
}

int main(void)
{
    int peer_pid = -1;

    /* 给SIGINT信号注册捕获函数,用于删除共享内存、管道、文件等 */
    signal(SIGINT, signal_fun);


    /* 使用有名管道获取读共享内存进程的PID */
    peer_pid = get_peer_PID();
    
    
    /* 创建、或者获取共享内存 */
    create_or_get_shm();
    
    //建立映射
    shmaddr = shmat(shmid, NULL, 0);
    if(shmaddr == (void *)-1) print_err("shmat fail");    
    
    while(1)
    {
        memcpy(shmaddr, buf, sizeof(buf));
        kill(peer_pid, SIGUSR1);
        sleep(1);
        
    }
    
    return 0;
}
View Code

p2.c

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


#define SHM_FILE "./shmfile"

#define SHM_SIZE 4096

int shmid = -1;
void *shmaddr = NULL;    



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 fail");
    
    //read(fd, &shmid, sizeof(shmid));
}

void signal_fun(int signo)
{
    if(SIGINT == signo)
    {
        shmdt(shmaddr);
        shmctl(shmid, IPC_RMID, NULL);
        remove("./fifo");
        remove(SHM_FILE);

        exit(-1);
    }
    else if(SIGUSR1 == signo)
    {
        
    }
}

void snd_self_PID(void)
{
    int ret = -1;
    int fifofd = -1;

    /* 创建有名管道文件 */
    mkfifo("./fifo", 0664); 
    if(ret == -1 && errno != EEXIST) print_err("mkfifo fail");
    
    /* 以只写方式打开文件 */
    fifofd = open("./fifo", O_WRONLY);
    if(fifofd == -1) print_err("open fifo fail");
    
    /* 获取当前进程的PID, 使用有名管道发送给写共享内存的进程 */
    int pid = getpid();
    ret = write(fifofd, &pid, sizeof(pid));//发送PID
    if(ret == -1) print_err("write fifo fail");
}

int main(void)
{

    /*给SIGUSR1注册一个空捕获函数,用于唤醒pause()函数 */
    signal(SIGUSR1, signal_fun);
    signal(SIGINT, signal_fun);

    /* 使用有名管道,讲当前进程的PID发送给写共享内存的进程 */
    snd_self_PID();    


    /* 创建、或者获取共享内存 */
    create_or_get_shm();


    //建立映射
    shmaddr = shmat(shmid, NULL, 0);
    if(shmaddr == (void *)-1) print_err("shmat fail");    
    
    while(1)
    {
        pause();
        printf("%s\n", (char *)shmaddr);
        bzero(shmaddr, SHM_SIZE);
    }
    
    return 0;
}
View Code

双向通信

 

 

posted @ 2018-07-30 21:14  克拉默与矩阵  阅读(487)  评论(0编辑  收藏  举报