进程间通信-内存映射的原理与共享内存

子进程与父进程的继承

子进程继承父进程的

用户号UIDs和用户组号GIDs
环境Environment
堆栈
共享内存
打开文件的描述符
执行时关闭(Close-on-exec)标志
信号处理程序
存储映射区
进程组号
当前工作目录
根目录
文件方式创建屏蔽字
资源限制
控制终端

子进程独有的

进程号PID
不同的父进程号
自己的文件描述符和目录流的拷贝
子进程不继承父进程的进程正文(text),数据和其他锁定内存(memory locks)
不继承异步输入和输出

父进程和子进程拥有独立的地址空间和PID参数

写时复制,fork,vfork与线程在Linux下的实现

写时复制

fork()->exec() 读取可执行文件并将其载入地址空间然后运行,这样的话,页根本就没有被写入,就没必要进行实际上的复制了

fork 开销:复制页表,创建PID.

fork

forkj,vfor,__clone->clone ->do_fork->copy_process

copy_process

  1. dup_task_struct创建内核栈,此时父PID与子PID相同
  2. 父子分离,清零与初始化
  3. 保证不会投入运行
  4. allloc_pid 分配新的PID
  5. clone_flags继承该继承的,线程的话就是共享该共享的
  6. 回到do_fork函数,唤醒子进程并运行他,这是为了避免其写入(见虚拟内存管理的文章)

注意几乎是子进程先执行

vfork

不拷贝父进程的页表项,其余与fork相同

线程在Linux下的实现

就是一个进程,有自己普通的task_struct,只不过共享了相同的资源而已clone_flags

共享地址空间,文件系统资源,fds,信号处理程序


存储映射I/O

Linux通过将一个虚拟内存区域(也在磁盘)与一个硬盘上的文件关联起来,以初始化这个虚拟内存区域的内容.于是当从缓冲区读取数据的时候,就相当于读取文件,向缓冲区写入数据就会自动写入文件,这样的话,就不用使用read/write执行I/O了!!!

主要使用函数:

 #include <sys/mman.h>

       void *mmap(void *addr, size_t len, int prot, int flags,
           int fildes, off_t off);
           

具体见man文档.

必须先打开该文件.若文件是只读打开的,就不能设置 PROT_WRITE

flags表示是共享对象还是匿名对象还是私有对象:

  • MAP_SHARED  共享对象,提供了POSIX共享内存,对其操作就相当于修改文件
  • MAP_PRIVATE  私有对象。对该内存段的修改不会反映到映射文件(写时复制)
  • MAP_ANNO   匿名对象

文件的长度尽量与页的长度相同
在这里插入图片描述
有关的信号是SIGSEGVSIGBUS.前者一般是表示权限,后者表示访问的某个部分不存在.

更改权限:

 #include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);//addr必须是系统页长的整数倍

强制性写回映射文件:

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

解除映射:

int munmap(void *addr, size_t len);

在解除后,对MAP_PRIVATE的修改会被丢弃.

实例:使用mmap函数实现cp


#include <fcntl.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>

using namespace std;

#define SS (1024 * 1024 * 1024)

void err(const char *str)
{
    perror(str);
    exit(-1);
}
int main(int argc, char *argv[])
{
    int fdin = open(argv[1], O_RDONLY);
    if (fdin < 0)
        err("open");
    int fdout = open(argv[2], O_RDWR | O_CREAT | O_TRUNC,0666);
    if (fdout < 0)
        err("open");
    struct stat sbuf;
    if (fstat(fdin, &sbuf) < 0)
        err("fstat");
    //如果不设置,对虚拟存储区的第一次访问就会产生 SIGBUS 信号
    if (ftruncate(fdout, sbuf.st_size) < 0)
        err("ftruncate");

    off_t fsz = 0;
    size_t copysz;

    while (fsz < sbuf.st_size)
    {
        if ((sbuf.st_size - fsz) > SS)
            copysz = SS;
        else
            copysz = sbuf.st_size - fsz;

        void *src = mmap(0, copysz, PROT_READ, MAP_SHARED, fdin, fsz);
        if (src == MAP_FAILED)
            err("11122");

        void *dst = mmap(0, copysz, PROT_READ | PROT_WRITE, MAP_SHARED, fdout, fsz);
        if (dst == MAP_FAILED)
            err("11122");
        memcpy(dst, src, copysz);
        munmap(src, copysz);
        munmap(dst, copysz);
        fsz += copysz;
    }
    exit(0);
}

共享内存

概述与特点

共享内存是进程间通信中最快的方式。共享内存允许两个或更多进程访问同一块内存,就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。

实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

Linux的2.2.x内核支持多种共享内存方式,如mmap()系统调用,Posix共享内存,以及System V 共享内存。

在这里插入图片描述

内核怎样保证各个进程寻址到同一个共享内存区域的内存页面(深入理解共享内存,重点)

页缓存

缓存的是内存页面,但是缓存中的页来自对文件的读写,所以还不如直接看作是对于最近访问过得文件的数据块的一种缓存!!!,避免对磁盘开销大的访问(局部性原理)

OS会根据内存使用情况,动态调整!!!

read()->页缓存(物理块的一部分(几页大小))->磁盘

write的操作:

  • 使得缓存失效,直接写到磁盘
  • 更新缓存+更新磁盘
  • 写回:见缓存的文章(目前使用的策略)
缓存的回收

Linux内核用的是改良版的LRU,两个链表,一个活跃链表,一个非活跃链表,实现LRU的算法策略见另一篇文章

struct address_space对象 :用于管理物理文件(struct inode)映射到内存的页面(struct page)的

页缓存中的页可能包含了多个不连续的物理磁盘块.现在让我们来思考一个问题:就下面的这个结构,如何找到对应的物理块磁盘块???

(页大小4k,块大小512KB)

在这里插入图片描述
OK,这个就是我们要讲的这个结构的作用了.

假设有进程创建了多个 vm_area_struct 都指向同一个文件,那么这个 vm_area_struct 对应的页高速缓存只有一份。也就是磁盘上的文件缓存到内存后,它的虚拟内存地址可以有多个,但是物理内存地址却只能有一个。

在这里插入图片描述

i_map 帮助内核找到关联的被缓存的文件

find_get_page( address_space ,偏移量)从而找到

重要

所有的I/O操作必然都是通过页缓存来进行的. 运行机制和缓存的运行机制如出一辙!!!
在这里插入图片描述

address_space_operations 就是用来操作该文件映射到内存的页面,比如把内存中的修改写回文件、从文件中读入数据到页面缓冲等

  1. page cache及swap cache中页面的区分:一个被访问文件的物理页面都驻留在page cache或swap cache中,一个页面的所有信息由struct page来描述。struct page中有一个域为指针mapping ,它指向一个struct address_space类型结构。page cache或swap cache中的所有页面就是根据address_space结构以及一个偏移量来区分的。

  2. 文件与address_space结构的对应:一个具体的文件在打开后,内核会在内存中为之建立一个struct inode结构,其中的i_mapping域指向一个address_space结构。这样,一个文件就对应一个address_space结构,一个address_space与一个偏移量能够确定一个page cache 或swap cache中的一个页面。因此,当要寻址某个数据时,很容易根据给定的文件及数据在文件内的偏移量而找到相应的页面。

在这里插入图片描述

  1. 进程调用mmap()时,只是在进程空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并没有建立进程空间到物理页面的映射。因此,第一次访问该空间时,会引发一个缺页异常。

  2. 对于共享内存映射(普通文件,MAP_SHARED)情况,缺页异常处理程序首先在swap cache中寻找目标页(符合address_space以及偏移量的物理页),如果找到,则直接返回地址;如果没有找到,则判断该页是否在交换区(swap area),如果在,则执行一个换入操作;如果上述两种情况都不满足,处理程序将分配新的物理页面,并把它插入到page cache中。进程最终将更新进程页表。

共享对象(MAP_SHARED)->swap cache ->找到返回,没找到去swap 分区找->如果在,换入->如果不在,就配新的物理页面,更页表,查page cache

注:对于映射普通文件情况(匿名对象),缺页异常处理程序首先会在page cache中根据address_space以及数据偏移量寻找相应的页面。如果没有找到,则说明文件数据还没有读入内存,处理程序会从磁盘读入相应的页面,并返回相应地址,同时,进程页表也会更新。

表示很大的质疑!!!!!

  1. 所有进程在映射同一个共享内存区域时,情况都一样,在建立线性地址与物理地址之间的映射之后,不论进程各自的返回地址如何,实际访问的必然是同一个共享内存区域对应的物理页面。
    注:一个共享内存区域可以看作是特殊文件系统shm中的一个文件,shm的安装点在交换区上。

与普通IPC的对比

以下面“进程A从文件f中读取数据,进行加工之后,将数据传递给进程B”这种场景为例,若使用其他的IPC形式,我们至少需要以下步骤:

1. 从文件f中复制数据到进程A的内存中;
2. 加工数据;
3. 将加工好的数据通过系统调用拷贝到内核空间中;
4. 进程B得知有数据发来,从内核空间将加工好的数据拷贝到进程B的内存中;
5. 进程B使用数据

而我们若使用共享内存,则至少需要以下三个步骤:

1. 从文件f中复制数据到共享内存区域中;
2. 加工数据;
3. 进程B使用数据

注意事项

  • 多个进程之间对一个给定存储区访问的互斥。若一个进程正在向共享内存区写数据,则在它做完这一步操作前,别的进程不应当去读、写这些数据。(一般使用信号量去进行同步)
  • 当再也没有进程需要使用这个共享内存块的时候,必须有一个(且只能是一个)进程负责释放这个被共享的内存页面。

System V 的大致实现

共享内存实际上就是进程通过调用shmget(Shared Memory GET 获取共享内存)来分配一个共享内存块,然后每个进程通过shmat(Shared Memory Attach 绑定到共享内存块),将进程的虚拟地址空间指向共享内存块中。 随后需要访问这个共享内存块的进程都必须将这个共享内存绑定到自己的地址空间中去。当一个进程往一个共享内存快中写入了数据,共享这个内存区域的所有进程就可用都看到其中的内容。

实例

还是创建两个进程,在 A 进程中创建一个共享内存,并向其写入数据,通过 B 进程从共享内存中读取数据。

writer.cpp

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <iostream>
using namespace std;

const int BUFSIZE = 512;

int main(void)
{
    //1. 创建 key
    key_t key = ftok("/dev/shm/myshm2", 2016);
    cout << key << endl;
    if (key == -1)
    {
        perror("ftok ");
    }
    //2.创建共享内存
    int shmid = shmget(key, BUFSIZE, IPC_CREAT | 0666);
    //3.映射到这块共享内存
    void *shmaddr = shmat(shmid, NULL, 0);

    //4. 写入数据到共享内存
    printf("start   writing !!!!!\n");
    bzero(shmaddr, BUFSIZE); //清空共享内存 
    strcpy((char *)shmaddr, "刘生玺最帅@@@@@@\n");

    return 0;
}

reader.cpp

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <iostream>
using namespace std;

const int BUFSIZE = 512;

int main(void)
{
    //1. 创建 key
    key_t key = ftok("/dev/shm/myshm2", 2016);
    cout << key << endl ;
    if (key == -1)
    {
        perror("ftok ");
    }

    system("ipcs -m"); //查看共享内存

    //2.找到对应的共享内存
    int shmid = shmget(key, BUFSIZE, IPC_CREAT | 0666);

    //3.映射到这块共享内存
    void *shmaddr = shmat(shmid, NULL, 0);

    //4. 读共享内存区数据
    printf("data = [%s]\n", shmaddr);

    //5.分离共享内存和当前进程
    int ret = shmdt(shmaddr);
    if (ret < 0)
    {
        perror("shmdt");
        exit(1);
    }
    else
    {
        printf("deleted shared-memory\n");
    }

    //删除共享内存
    shmctl(shmid, IPC_RMID, NULL); //IPC_RMID:删除。(常用 )

    system("ipcs -m"); //查看共享内存

    return 0;
}
  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值