内存共享

每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它;它使得多个进程可以访问同一块内存空间,是最快的可用IPC(由于数据不用传送,直接在内存中操作)形式。这是针对其他通信机制运行效率较低而设计的。它往往与其他通信机制,如信号量结合使用,以达到进程间的同步及互斥。

共享内存的原理

在Linux中,每个进程都有属于自己的进程控制块(PCB)地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射(内存映射),通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存

共享内存的相关函数:

创建共享内存

int shmget(key_t key, size_t size, int shmflg);
//[参数key]:由ftok生成的key标识,标识系统的唯一IPC资源。 一个非零整数,两个进程需保持一致,即两个进程之间通信的钥匙。为0时建立新的共享内存对象;
//[参数size]:需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页是4k字节,为了避免内存碎片,我们一般申请的内存大小为页的整数倍。
//[参数shmflg]: 同 open 函数的 mode 参数,设置文件访问权限,这里额外多出一个 IPC_CREAT 和 IPC_EXCL,可与 mode 进行或操作。IPC_CREAT 表示共享区不存在则创建,IPC_EXCL 和 IPC_CREAT 共同使用,表示共享区已存在则返回错误。如 0644 | IPC_CREAT
//[返回值]:成功时返回一个新建或已经存在的的共享内存标识符,取决于shmflg的参数。失败返回-1并设置错误码。

挂接共享内存

void *shmat(int shmid, const void *shmaddr, int shmflg);
//[参数shmid]:共享存储段的标识符。
//[参数*shmaddr]:shmaddr = 0,则存储段连接到由内核选择的第一个可以地址上(推荐使用)。
//[参数shmflg]:若指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写方式连接此段。
//[返回值]:成功返回共享存储段的指针(虚拟地址),并且内核将使其与该共享存储段相关的shmid_ds结构中的shm_nattch计数器加1(类似于引用计数);出错返回-1。

去关联共享内存(将之前用shmat函数连接好的共享内存区脱离目前的进程)

int shmdt(const void *shmaddr);
//[参数*shmaddr]:连接以后返回的地址。
//[返回值]:成功返回0,并将shmid_ds结构体中的 shm_nattch计数器减1;出错返回-1。

销毁共享内存

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//[参数shmid]:共享存储段标识符:
	//IPC_STAT 状态查询;
	//IPC_SET 在权限允许下,将共享内存状态更新为 buf 中的数据;
    //IPC_RMID 删除共享内存
//[参数*buf]:设置为NULL即可。
//[返回值]:成功返回0,失败返回-1。

共享内存的简易实现

读写两个进程,读端先运行,对共享内存写入数据;写端对共享内存读数据,然后销毁内存。

读端:

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


int main()
{
    char data[] = "hello i am writer!";
    key_t key=(key_t) 1234;
    int size = 4096;
    char *shmaddr = NULL;
    int shmid = shmget(key,size,0666|IPC_CREAT);//创建共享内存
    if(shmid < 0)
    {
        perror("shmget fail:");
        return -1;
    }
    shmaddr = shmat(shmid,0,0);//挂载共享内存
    if(NULL == shmaddr)
    {
        perror("shmat fail:");
        return -1;
    }
    memset(shmaddr,0,size);
    memcpy(shmaddr,&data,strlen(data));//共享内存写入数据
    if(shmdt(shmaddr) < 0)//共享内存去关联
    {
        perror("shmdt fail:");
        return -1;
    }
    printf("write--: %s :--ok!\n",data);
    return 0;
}

写端:

#include<stdio.h>
#include<errno.h>
#include<sys/ipc.h>
#include<sys/shm.h>

int main()
{
    key_t key = (key_t)1234;
    int size = 4096;
    int shmid;
    char *shmaddr = NULL;
    shmid = shmget(key,size,0644|IPC_CREAT);//创建共享内存
    if(shmid < 0)
    {
        perror("shmget fail:");
        return -1;
    }
    shmaddr = shmat(shmid,0,0);//挂载共享内存
    if(NULL == shmaddr)
    {
        perror("shmaddr fail:");
        return -1;
    }
    printf("从共享内存中读到:%s\n",(char *)shmaddr);
    if(shmdt(shmaddr) < 0)//共享内存去关联
    {
        perror("shmdt fail:");
        return -1;
    }
    if(shmctl(shmid,IPC_RMID,0) < 0)//删除共享内存空间
    {
        perror("shmctl rm id fail:");
        return -1;
    }
    return 0;
}

运行效果

第二次由于关闭了共享内存,所以读不到内容

内存映射

 进程A和进程B都将该页映射到自己的地址空间,当进程A第一次访问该页中的数据时,它生成一个缺页终端,内核此时读入这一页到内存并更新页表使之指向它,以后,当进程B访问同一页面而出现缺页中断时,该页已经在内存,内核只需要将进程B的页表登记项指向次页即可。

内存映射的系统调用

调用形式:

常用函数:

将文件映射到内存

void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

[参数]

  • start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址。

  • length:映射区的长度。//长度单位是 以字节为单位,不足一内存页按一内存页处理

  • prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起

    PROT_EXEC //页内容可以被执行
    PROT_READ //页内容可以被读取
    PROT_WRITE //页可以被写入
    PROT_NONE //页不可访问

  • flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体。
    MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。
    MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
    MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
    MAP_DENYWRITE //这个标志被忽略。
    MAP_EXECUTABLE //同上
    MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
    MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。
    MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。
    MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。
    MAP_ANON //MAP_ANONYMOUS的别称,不再被使用。
    MAP_FILE //兼容标志,被忽略。
    MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
    MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
    MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。

  • fd:有效的文件描述词。一般是由open()函数返回,其值也可以设置为-1,此时需要指定flags参数中的MAP_ANON,表明进行的是匿名映射。

  • off_toffset:被映射对象内容的起点。

[返回值]
成功执行时,mmap()返回被映射区的指针。失败时,mmap()返回MAP_FAILED[其值为(void *)-1]。
释放存储映射区

int munmap(void* start,size_t length);

[返回值]
成功执行时,munmap()返回0。失败时,munmap返回-1。
磁盘文件内容与共享内存区中的内容同步

int msync ( void * addr, size_t len, int flags);

[参数]

  • addr:文件映射到进程空间的地址;
  • len:映射空间的大小;
  • flags:刷新的参数设置,可以取值MS_ASYNC/ MS_SYNC/ MS_INVALIDATE
    取值为MS_ASYNC(异步)时,调用会立即返回,不等到更新的完成;
    取值为MS_SYNC(同步)时,调用会等到更新完成之后返回;
    取MS_INVALIDATE(通知使用该共享区域的进程,数据已经改变)时,在共享内容更改之后,使得文件的其他映射失效,从而使得共享该文件的其他进程去重新获取最新值;

配合信号量等进程同步方式,可以实现进程间同步信号量链接

Logo

更多推荐