2024年10月mmap内存映射(内存映射:小块内存申请brk和申请大块内存的Mmap分析)

 更新时间:2024-10-12

  ⑴mmap内存映射(内存映射:小块内存申请brk和申请大块内存的Mmap分析

  ⑵内存映射:小块内存申请brk和申请大块内存的Mmap分析

  ⑶本文为内存部分最后一篇,介绍内存映射。内存映射不仅是物理内存和虚拟内存间的映射,也包括将文件中的内容映射到虚拟内存空间。这个时候,访问内存空间就能够访问到文件里面的数据。而仅有物理内存和虚拟内存的映射,是一种特殊情况。本文首先分析用户态在堆中申请小块内存的brk和申请大块内存的mmap,之后会分析内核态的内存映射机制vmalloc,kmap_atomic,swapper_pg_dir以及内核态缺页异常。

  ⑷用户态调用malloc()会分配堆内存空间,而实际上则是完成了一次用户态的内存映射,根据分配空间的大小,内存映射对应的系统调用主要有brk()和mmap()(当然我们也可以直接调用mmap()来映射文件)。对小块内存(小于K,C标准库使用brk()来分配,也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。而大块内存(大于K,则直接使用内存映射mmap()来分配,也就是在文件映射段找一块空闲内存分配出去。这两种方式,自然各有优缺点。

  ⑸brk()方式的缓存,可以减少缺页异常的发生,提高内存访问效率。不过,由于这些内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。

  ⑹mmap()方式分配的内存,会在释放时直接归还系统,所以每次mmap()都会发生缺页异常。在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大。这也是malloc()只对大块内存使用mmap()的原因。

  ⑺brk()系统调用为sys_brk()函数,其参数brk是新的堆顶位置,而mm-》brk是原堆顶位置。该函数主要逻辑如下

  ⑻在do_brk_flags()中,调用find_vma_links()找到将来的vm_area_struct节点在红黑树的位置,找到它的父节点、前序节点。接下来调用vma_merge(),看这个新节点是否能够和现有树中的节点合并。如果地址是连着的,能够合并,则不用创建新的vm_area_struct了,直接跳到out,更新统计值即可;如果不能合并,则创建新的vm_area_struct,添加到anon_vma_chain链表中,也加到红黑树中。

  ⑼大块内存的申请通过mmap系统调用实现,mmap既可以实现虚拟内存向物理内存的映射,也可以映射文件到自己的虚拟内存空间。映射文件时,实际是映射虚拟内存到物理内存再到文件。

  ⑽这里主要调用ksys_mmap_pgoff()函数,这里逻辑如下:

  ⑾vm_mmap_pgoff()函数调用do_mmap_pgoff(),实际调用do_mmap()函数。这里get_unmapped_area()函数负责寻找可映射的区域,mmap_region()负责映射该区域。

  ⑿更多Linux内核视频教程文本资料免费领取后台私信【内核大礼包】自行获取。

  ⒀首先来看看寻找映射区的函数get_unmapped_area()。

  ⒁mmap_region()首先会再次检测地址空间是否满足要求,然后清除旧的映射,校验内存的可用性,在一切均满足的情况下调用vma_link()将新创建的vm_area_struct结构挂在mm_struct内的红黑树上。

  ⒂vma_link()本身是__vma_link()和__vma_link_file()的包裹函数

  ⒃其中__vma_link()主要是链表和红黑表的插入,这属于基本数据结构操作,不展开讲解。

  ⒄而__vma_link_file()会对文件映射进行处理,在file结构体中成员f_mapping指向address_space结构体,该结构体中存储红黑树i_mmap挂载vm_area_struct。

  ⒅至此,我们完成了用户态内存的映射,但是此处仅在虚拟内存中建立了新的区域,尚未真正访问物理内存。物理内存的访问只有在调度到该进程时才会真正分配,即发生缺页异常时分配。

  ⒆用户态缺页异常??一旦开始访问虚拟内存的某个地址,如果我们发现,并没有对应的物理页,那就触发缺页中断,调用do_page_fault()。这里的逻辑如下

  ⒇find_vma()为红黑树查找操作,在此不做展开描述,下面重点看看handle_mm_fault()。这里经过一系列校验之后会根据是否是大页而选择调用hugetlb_fault()或者__handle_mm_fault()

  ⒈__handle_mm_fault()完成实际上的映射操作。这里涉及到了由pgd,pg,pud,pmd,pte组成的五级页表,页表索引填充完后调用handle_pte_fault()创建页表项。

  ⒉handle_pte_fault()处理以下三种情况:

  ⒊对于匿名页映射,流程如下:

  ⒋映射文件do_fault()函数调用了fault函数,该函数实际会根据不同的文件系统调用不同的函数,如ext文件系统中vm_ops指向ext_file_vm_ops,实际调用ext_filemap_fault()函数,该函数会调用filemap_fault()完成实际的文件映射操作。

  ⒌file_map_fault()主要逻辑为:

  ⒍前文提到了我们会通过主动回收或者被动回收的方式将物理内存已映射的页面回收至硬盘中,当数据再次访问时,我们又需要通过do_swap_page()将其从硬盘中读回来。do_swap_page()函数逻辑流程如下:查找swap文件有没有缓存页。如果没有,就调用swapin_readahead()将swap文件读到内存中来形成内存页,并通过mk_pte()生成页表项。set_pte_at将页表项插入页表,swap_free将swap文件清理。因为重新加载回内存了,不再需要swap文件了。

  ⒎通过以上步骤,用户态的缺页异常就处理完毕了。物理内存中有了页面,页表也建立好了映射。接下来,用户程序在虚拟内存空间里面可以通过虚拟地址顺利经过页表映射的访问物理页面上的数据了。页表一般都很大,只能存放在内存中。操作系统每次访问内存都要折腾两步,先通过查询页表得到物理地址,然后访问该物理地址读取指令、数据。为了加快映射速度,我们引入了TLB(TranslationLookasideBuffer,我们经常称为快表,专门用来做地址映射的硬件设备。它不在内存中,可存储的数据比较少,但是比内存要快。所以我们可以想象,TLB就是页表的Cache,其中存储了当前最可能被访问到的页表项,其内容是部分页表项的一个副本。有了TLB之后,我们先查块表,块表中有映射关系,然后直接转换为物理地址。如果在TLB查不到映射关系时,才会到内存中查询页表。

  ⒏和用户态使用malloc()类似,内核态也有相应的内存映射函数:vmalloc()可用于分配不连续物理页(使用伙伴系统,kmem_cache_alloc()和kmem_cache_create()使用slub分配器分配小块内存,而kmalloc()类似于malloc(),在分配大内存的时候会使用伙伴系统,分配小内存则使用slub分配器。分配内存后会转换为虚拟地址,保存在内核页表中进行映射,有需要时直接访问。由于vmalloc()会带来虚拟连续页和物理不连续页的映射,因此一般速度较慢,使用较少,相比而言kmalloc()使用的更为频繁。而kmem_cache_alloc()和kmem_cache_create()会分配更为精准的小内存块用于特定任务,因此也比较常用。

  ⒐相对于用户态,内核态还有一种特殊的映射:临时映射。内核态高端内存地区为了节省空间会选择临时映射,采用kmap_atomic()实现。如果是位有高端地址的,就需要调用set_pte通过内核页表进行临时映射;如果是位没有高端地址的,就调用page_address,里面会调用lowmem_page_address。其实低端内存的映射,会直接使用__va进行临时映射。

  ⒑kmap_atomic()发现没有页表的时候会直接创建页表进行映射。而vmalloc()只分配了内核的虚拟地址。所以访问它的时候,会产生缺页异常。内核态的缺页异常还是会调用do_page_fault(),最终进入vmalloc_fault()。在这里会实现内核页表项的关联操作,从而完成分配,整体流程和用户态相似。

  ⒒至此,我们分析了内存物理地址和虚拟地址的映射关系,结合前文页的分配和管理,内存部分的主要功能就算是大致分析清楚了,最后引用极客时间中的一幅图作为总结,算是全部知识点的汇总。

  ⒓page_fault

  ⒔MMKV通过mmap内存映射文件来进行读写操作的,这是其效率高于普通IO的原因。传统的read首先将文件内容从硬盘拷贝到内核空间的一个缓冲区,然后再将这些数据拷贝到用户空间,这个过程中,实际上完成了。mmap将文件直接映射到用户空间,所以中断处理函数根据这个映射关系,直接将文件从硬盘拷贝到用户空间,只进行了,因此,mmap内存映射的效率要比read/write效率高。既然MMKV使用的内存映射优于IO,为什么还要使用IO?首先要明白,直接将文件映射到虚拟内存,意味着没有数据没有缓存在内核缓存空间,而是直接读到了用户空间,而系统的IO和内核缓存搭配可以使得部分的文件使用效率更高。(OS会根据局部性原理在一次read()系统调用的时候预读取更多的文件数据到内核空间缓冲区中,这样当下一次read()系统调用的时候发现要读取的数据已经存在于内核空间缓冲区中的时候只要直接拷贝数据到用户空间缓冲区中即可,无需再进行一次低效的磁盘I/O操作,且磁盘的大小要远远超过内存而且mmap映射的文件是大于一个内存页大小的(,并且是。也就是说两个方式都是有优缺点的,所以不存在代替这个说法,只能通过分析其场景而选择不同的方式。protobuf是google开源的一个序列化框架,类似xml,json,最大的特点是基于二进制,比SharedPreferences使用的传统的XML表示同样一段内容要短小得多。同样这也不能说明Protobuf优于XML,关于Protobuf的更多内容如下:标准protobuf和SharedPreferences一样,每次写入kv对象都必须全量写入。也就是写入之前将所有数据加载到内存中,然后判断新增的key是否已经存在,完成更新或增加后在全部写入文件。MMKV中采用增量更新的方式处理protobuf,当需要写入kv对象时,不论是新增还是更新都将其直接加入文件的末尾,这样大大增加了写入效率。上面的做法必然带来两个问题.必然导致同一key值会有新旧若干份数据,最新的数据在最后。.文件大小会增长得不可控。同一个key不断更新的话,是可能耗尽几百M甚至上G空间。针对第一个问题,在读取时,针对同一个key使用后读入的value替换之前的值,就可以保证数据是最新有效的。针对第二个问题,有上文可知MMKV的文件必然是稍大于(一个内存页的大小)的倍数,当写入的数据小于k时,可以继续写入,因为本身文件大小就已经略大于k了,有点很小的浪费,当写入数据超过k的倍数后,进行文件重整、key排重,尝试序列化保存排重结果;排重后空间还是不够用的话,将文件在增加k,直到空间足够。文件系统、操作系统都有一定的不稳定性,MMKV使用crc校验确保数据有效性,关于crc校验,可以参考:这一步官方有详尽的说明,如下:多进程设计与实现

  ⒕mmap即memorymap,也就是内存映射。mmap操作提供了一种机制,让用户程序直接访问设备内存,这种机制,相比较在用户空间和内核空间互相拷贝数据,效率更高。在要求高性能的应用中比较常用。mmap映射内存必须是页面大小的整数倍,面向流的设备不能进行mmap,mmap的实现和硬件有关。

  ⒖映射条件:mmap()必须以PAGE_SIZE为单位进行映射,而内存也只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行内存对齐,强行以PAGE_SIZE的倍数大小进行映射。

  ⒗头文件:《sys/mman.h》

  ⒘函数原型:void*mmap(void*start,size_tlength,intprot,intflags,intfd,off_toffset);intmunmap(void*start,size_tlength);

  ⒙参数说明:start:映射区的开始地址,设置为时表示由系统决定映射区的起始地址。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_BIT//将映射区放在进程地址空间的低GB,MAP_FIXED指定时会被忽略。当前这个标志只在x-平台上得到支持。MAP_POPULATE//为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。MAP_NONBLOCK//仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。fd:有效的文件描述词。一般是由open()函数返回,其值也可以设置为-,此时需要指定flags参数中的MAP_ANON,表明进行的是匿名映射。offset:被映射对象内容的起点。

  ⒚返回值成功执行时,mmap()返回被映射区的指针,munmap()返回。失败时,mmap()返回MAP_FAILED,munmap返回-。errno被设为以下的某个值EAES:访问出错EAGAIN:文件已被锁定,或者太多的内存已被锁定EBADF:fd不是有效的文件描述词EINVAL:一个或者多个参数无效ENFILE:已达到系统对打开文件的限制ENODEV:指定文件所在的文件系统不支持内存映射ENOMEM:内存不足,或者进程已超出最大内存映射数量EPERM:权能不足,操作不允许ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志SIGSEGV:试着向只读区写入SIGBUS:试着访问不属于进程的内存区

  ⒛适用场景:mmap的适用场景实际上非常受限,在如下场合下可以选择使用mmap机制:

  [原创]深入剖析mmap原理-从三个关键问题说起

  对于mmap,您是否能从原理上解析以下三个问题:

  要解决这些疑问,可能还需要在操作系统层面多了解。本文将尝试通过这些问题深入剖析,希望通过这篇文章,能使大家对mmap有较深入的认识,也能在存储引擎的设计中,有所参考。

  最近在研发分布式日志存储系统,这是一个基于Raft协议的自研分布式日志存储系统,Logstore则是底层存储引擎。

  Logstore中,使用mmap对数据文件进行读写。Logstore的存储结构简化如下图:

  Logstore使用了SegmentsFiles+IndexFiles的方式存储Log,SegmentFile是存储主体,用于存储Log数据,使用定长的方式,默认每个M,IndexFile主要用于SegmentFile的内容检索。

  Logstore使用mmap的方式读写SegmentFile,SegmentsFiles的个数,主要取决于磁盘空间或者业务需求,一般情况下,Logstore会存储T~T的数据。

  我们先看看什么是mmap。

  在《《深入理解计算机系统》》这本书中,mmap定义为:Linux通过将一个虚拟内存区域与一个磁盘上的对象(object)关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memorymapping)。

  在Logstore中,mapping的对象是普通文件(SegmentFile)。

  我们先来简单看一下mapping一个文件,mmap做了什么事情。如下图所示:

  假设我们mmap的文件是FileA,在调用mmap之后,会在进程的虚拟内存分配地址空间,创建映射关系。

  这里值得注意的是,mmap只是在虚拟内存分配了地址空间,举个例子,假设上述的FileA是G大小

  在mmap之后,查看mmap所在进程的maps描述,可以看到

  由上可以看到,在mmap之后,进程的地址空间feead-fead被分配,并且map到FileA,fead减去feead,刚好是(ps:这里是整个文件做mapping)

  在Linux中,VM系统通过将虚拟内存分割为称作虚拟页(VirtualPage,VP)大小固定的块来处理磁盘(较低层)与上层数据的传输,一般情况下,每个页的大小默认是字节。同样的,物理内存也被分割为物理页(PhysicalPage,PP),也为字节。

  上述例子,在mmap之后,如下图:

  在mmap之后,并没有在将文件内容加载到物理页上,只上在虚拟内存中分配了地址空间。当进程在访问这段地址时(通过mmap在写入或读取时FileA,若虚拟内存对应的page没有在物理内存中缓存,则产生“缺页“,由内核的缺页异常处理程序处理,将文件对应内容,以页为单位()加载到物理内存,注意是只加载缺页,但也会受操作系统一些调度策略影响,加载的比所需的多,这里就不展开了。(PS:再具体一些,进程在访问feead这个进程虚拟地址时,MMU通过查找页表,发现对应内容未缓存在物理内存中,则产生“缺页“)

  缺页处理后,如下图:

  我认为从原理上,mmap有两种类型,一种是有backend,一种是没有backend。

  这种模式将普通文件做memorymapping(非MAP_ANONYMOUS),所以在mmap系统调用时,需要传入文件的fd。这种模式常见的有两个常用的方式,MAP_SHARED与MAP_PRIVATE,但它们的行为却不相同。

  MAP_SHARED

  这个方式我认为可以从两个角度去看:

  MAP_PRIVATE

  这是一个copy-on-write的映射方式。虽然他也是有backend的,但在写入数据时,他会在物理内存copy一份数据出来(以页为单位),而且这些数据是不会被回写到文件的。这里就要注意,因为更新的数据是一个副本,而且不会被回写,这就意味着如果程序运行时不主动释放,若更新的数据超过可用物理内存+swapspace,就会遇到OOMKiller。

  无backend通常是MAP_ANONYMOUS,就是将一个区域映射到一个匿名文件,匿名文件是由内核创建的。因为没有backend,写入/更新的数据之后,若不主动释放,这些占用的物理内存是不能被释放的,同样会出现OOMKiller。

  到这里,这个问题就比较好解析了。我们可以将此问题分离为:

  --虚拟内存是否会出问题:

  回到上述的“mmap在进程虚拟内存做了什么“,我们知道mmap会在进程的虚拟内存中分配地址空间,比如G的文件,则分配G的连续地址空间。那究竟可以maping多少呢?在位操作系统,寻址范围是^,除去一些内核、进程数据等地址段之外,基本上可以认为可以mapping无限大的数据(不太严谨的说法)。

  --物理内存是否会出问题回到上述“mmap的分类“,对于有backend的mmap,而且是能回写到文件的,映射比内存+swap空间大是没有问题的。但无法回写到文件的,需要非常注意,主动释放。

  MAP_NORESERVE是mmap的一个参数,MAN的说明是“Donotreserveswapspaceforthismapping.Whenswapspaceisreserved,onehastheguaranteethatitispossibletomodifythemapping.“。

  场景A:物理内存+swapspace:G,映射文件G,使用一个进程进行mmap,成功后映射后持续写入数据场景B:物理内存+swapspace:G,映射文件G,使用两个进程进行mmap,成功后映射后持续写入数据

  从上述测试可以看出,从现象上看,NORESERVE是绕过mmap的校验,让其可以mmap成功。但其实在RESERVE的情况下(序列),从测试结果看,也没有保障。

  mmap的性能经常与系统调用(write/read做对比。

  我们将读写分开看,先尝试从原理上分析两者的差异,然后再通过测试验证。

  我们先来简单讲讲write系统调用写文件的过程:

  再来简单讲讲使用mmap时,写入文件流程:

  系统调用会对性能有影响,那么从理论上分析:

  下面我们对两者进行性能测试:

  场景:对G的文件进行顺序写入(go语言编写)

  每次写入大小|mmap耗时|write耗时---------------|-------|--------|--------|byte|.s|》s|bytes|.s|.s|bytes|.s|.s|bytes|.s|.s|bytes|.s|.s|bytes|.s|.s|bytes|.s|.s|bytes|.s|.s

  可以看到mmap在byte写入时已经基本达到最大写入性能,而write调用需要在(也就是一个pagesize)时,才能达到最大写入性能。

  从测试结果可以看出,在写小数据时,mmap会比write调用快,但在写大数据时,反而没那么快(但不太确认是否go的slicecopy的性能问题,没时间去测C了)。

  测试结果与理论推导吻合。

  我们还是来简单分析read调用与mmap的流程:

  从图中可以看出,read调用确实比mmap多一次copy。因为read调用,进程是无法直接访问kernelspace的,所以在read系统调用返回前,内核需要将数据从内核复制到进程指定的buffer。但mmap之后,进程可以直接访问mmap的数据(pagecache)。

  从原理上看,read性能会比mmap慢。

  接下来实测一下性能区别:

  场景:对G的文件进行顺序读取(go语言编写)(ps:为了避免磁盘对测试的影响,我让G文件都缓存在pagecache中)

  每次读取大小|mmap耗时|write耗时---------------|-------|--------|--------|byte|.ms|》s|bytes|.ms|.ms|bytes|.ms|.ms|bytes|.ms|.ms|bytes|.ms|.ms|bytes|.ms|.ms|bytes|.ms|.ms|bytes|.μs|.ms

  由上可以看出,在read上面,mmap比write的性能差别还是很大的。测试结果与理论推导吻合。

  对mmap的深入了解,能帮助我们在设计存储系统时,更好地进行决策。比如,假设需要设计一个底层的数据结构是B+Tree,node操作以Page单位的单机存储引擎,根据上述推论,写入使用系统调用,而读取使用mmap,可以达到最优的性能。而LMDB就是如此实现的。

  ty中的零拷贝是怎么实现的

  ty是Java语言中一个高性能的网络通信框架,零拷贝又是这个框架的特色之一,它是如何实现的呢?

  在计算机中完成一次数据传输,一般需要经过两个阶段。第一步,操作系统把数据从本地硬盘或网卡拷贝到内核空间的内存;第二步,应用程序再把数据从系统内核空间的内存拷贝到用户空间的内存;接下来才是应用程序中的数据处理工作。

  先来看几个名词。DMA(DirectMemoryAess直接存储器访问,将数据从一个地址空间复制到另一个地址空间。当CPU初始化这个传输动作后,传输动作本身是由DMA控制器(DMAC来完成的。也就是说在数据传输期间,系统可以并行执行其他任务。CPU拷贝,是由CPU直接处理的数据的传送,数据拷贝时一直占用CPU资源。

  从上图中可以看出,传统的IO读写流程,包括次用户态和内核态的切换,次上下文切换,次的数据拷贝,次CPU拷贝,次DMA拷贝。

  什么是零拷贝?拷贝,是指数据从一个存储区域复制到另一个存储区域。零,表示次数为,复制的次数为,也就是数据不需要从一个存储区域复制到另一个存储区域。

  为什么需要零拷贝?零拷贝,就是指从系统内核空间的内存到用户空间的内存,不需要采用传统方式的数据复制。而是将系统内核空间的内存和用户空间的内存实现关联映射(mmap内存映射机制,从而省去了数据传输过程中的复制。

  mmap(memorymap内存映射机制,简单来说就是将文件/设备映射到内存中,进程可以通过读写内存的方式,实现对mmap文件的操作。零拷贝并不是完全没有拷贝,而是减少了数据拷贝的次数。

  零拷贝在ty中的三种实现。.使用堆外内存,也叫直接内存(DirectMemory。ty的接收和发生都是使用Directbuffer,对应系统底层的mmap机制,直接使用堆外内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。.提供了组合buffer对象(positeByteBuf,可以聚合多个ByteBuffer对象,用户只需要像操作一个ByteBuffer一样操作组合ByteBuffer,避免了传统通过内存拷贝的方式将几个buffer合并成一个大buffer,不需要内存拷贝。.文件传输采用TransferTo方法,它可以直接将文件缓冲区的数据发送到目标channel,避免了传统通过循环write方式导致的内存拷贝问题。

  通过整理可以发现,ty的零拷贝并不是完全不拷贝,而是减少了CPU拷贝,也就是数据从系统内核空间的内存到用户空间内存的拷贝。DMA拷贝还是存在的,毕竟它是操作系统所做的事情,不属于应用程序的操作范围。在ty中,目前有三种方式实现的零拷贝。第一种使用堆外内存。第二种,positeByteBuf组合buffer对象。第三种,文件传输采用TransferTo方法。

  预留一块m的内存,在函数a中通过mmap映射预留的内存,并运行会导致溢出吗

  mmap函数是unix/linux下的系统调用。当存在客户-服务程序中复制文件时候,其数据流如下,要经历四次数据复制,开销很大。eeabffdaedcfcadf-x.png如果采用共享内存的方式,那么将大大优化IO操作,数据流变成了如下,数据只复制两次:edeeefdfefbbefd-x.png映射文件或设备到内存中,取消映射就是munmap函数。语法如下:*mmap(void*addr,size_tlength,intprot,intflags,intfd,off_toffset);intmunmap(void*addr,size_tlength);该函数主要用途有三个:l??将普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,用内存读写取代I/O读写,以获得较高的性能;l??将特殊文件进行匿名内存映射,为关联进程提供共享内存空间;l??为无关联的进程间的Posix共享内存(SystemV的共享内存操作是shmget/shmat)我们来看下函数的入参选择:??参数addr:指向欲映射的内存起始地址,通常设为NULL,代表让系统自动选定地址,映射成功后返回该地址。??参数length:代表将文件中多大的部分映射到内存。??参数prot:映射区域的保护方式。可以为以下几种方式的组合:PROT_EXEC映射区域可被执行PROT_READ映射区域可被读取PROT_WRITE映射区域可被写入PROT_NONE映射区域不能存取??参数flags:影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED或MAP_PRIVATE。MAP_FIXED如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此。MAP_SHARED对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。MAP_PRIVATE对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copyonwrite)对此区域作的任何修改都不会写回原来的文件内容。MAP_ANONYMOUS建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。MAP_DENYWRITE只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。MAP_LOCKED将映射区域锁定住,这表示该区域不会被置换(swap)。??参数fd:要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-。??参数offset:文件映射的偏移量,通常设置为,代表从文件最前方开始对应,offset必须是分页大小的整数倍。如下图内存映射文件的示例。aacfcdfbad.png...共享映射修改共享内存中的文件内容:#include#include#include#include#include#include#includeintmain(intargc,char**argv){intfd,nread,i;structstatsb;char*mapped;if(argc《=){printf(“%s:Needfilepath!

  “,argv=’’;/*导致段错误*/munmap(p_map,pagesize*);return;

  内存映射mmap和共享内存

  内存映射指的是将:进程中的个虚拟内存区域&个磁盘上的对象,使得二者存在映射关系。当然,也可以多个进程同时映射到一个对象上面。

  进程在读写磁盘的时候,大概的流程是:以write为例:进程(用户空间-》系统调用,进入内核-》将要写入的数据从用户空间拷贝到内核空间的缓存区-》调用磁盘驱动-》写在磁盘上面。

  使用mmap之后进程(用户空间--》读写映射的内存--》写在磁盘上面。

  (这样的优点是避免了频繁的进入内核空间,进行系统调用,提高了效率

  共享内存是一种ipc的方式,用于进程通信。共享内存位于进程空间的栈和堆之间。一般默认的大小是M。

  【深入浅出Linux】关于mmap的解析

  看这篇文章之前需要知道一个概念

  虚拟内存系统通过将虚拟内存分割为称作虚拟页(VirtualPage,VP)大小固定的块,一般情况下,每个虚拟页的大小默认是字节。同样的,物理内存也被分割为物理页(PhysicalPage,PP),也为字节。

  在LINUX中我们可以使用mmap用来在进程虚拟内存地址空间中分配地址空间,创建和物理内存的映射关系。

  映射关系可以分为两种、文件映射磁盘文件映射进程的虚拟地址空间,使用文件内容初始化物理内存。、匿名映射初始化全为的内存空间。

  而对于映射关系是否共享又分为、私有映射(MAP_PRIVATE)多进程间数据共享,修改不反应到磁盘实际文件,是一个copy-on-write(写时复制的映射方式。、共享映射(MAP_SHARED)多进程间数据共享,修改反应到磁盘实际文件中。

  因此总结起来有种组合、私有文件映射多个进程使用同样的物理内存页进行初始化,但是各个进程对内存文件的修改不会共享,也不会反应到物理文件中

  私有匿名映射mmap会创建一个新的映射,各个进程不共享,这种使用主要用于分配内存(malloc分配大内存会调用mmap)。例如开辟新进程时,会为每个进程分配虚拟的地址空间,这些虚拟地址映射的物理内存空间各个进程间读的时候共享,写的时候会copy-on-write。

  共享文件映射多个进程通过虚拟内存技术共享同样的物理内存空间,对内存文件的修改会反应到实际物理文件中,他也是进程间通信(IPC)的一种机制。

  共享匿名映射这种机制在进行fork的时候不会采用写时复制,父子进程完全共享同样的物理内存页,这也就实现了父子进程通信(IPC).

  这里值得注意的是,mmap只是在虚拟内存分配了地址空间,只有在第一次访问虚拟内存的时候才分配物理内存。在mmap之后,并没有在将文件内容加载到物理页上,只上在虚拟内存中分配了地址空间。当进程在访问这段地址时,通过查找页表,发现虚拟内存对应的页没有在物理内存中缓存,则产生“缺页“,由内核的缺页异常处理程序处理,将文件对应内容,以页为单位()加载到物理内存,注意是只加载缺页,但也会受操作系统一些调度策略影响,加载的比所需的多。

  因为物理内存是有限的,mmap在写入数据超过物理内存时,操作系统会进行页置换,根据淘汰算法,将需要淘汰的页置换成所需的新页,所以mmap对应的内存是可以被淘汰的(若内存页是“脏“的,则操作系统会先将数据回写磁盘再淘汰。这样,就算mmap的数据远大于物理内存,操作系统也能很好地处理,不会产生功能上的问题。

  从图中可以看出,mmap要比普通的read系统调用少了一次copy的过程。因为read调用,进程是无法直接访问kernelspace的,所以在read系统调用返回前,内核需要将数据从内核复制到进程指定的buffer。但mmap之后,进程可以直接访问mmap的数据(pagecache)。

  测试结果来源于:深入剖析mmap-从三个关键问题说起

  读性能分析场景:对G的文件进行顺序写入

  可以看到mmap在byte写入时已经基本达到最大写入性能,而write调用需要在(也就是一个pagesize)时,才能达到最大写入性能。从测试结果可以看出,在写小数据时,mmap会比write调用快,但在写大数据时,反而没那么快。

  写性能分析场景:对G的文件进行顺序读取(为了避免磁盘对测试的影响,G文件都缓存在pagecache中

  由上可以看出,在read上面,mmap的性能还是非常好的。

  对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。

  实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。

  提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。同时,如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据。

  可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。

  文件如果很小,是小于字节的,比如字节,由于内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。虽然被映射的文件只有字节,但是对应到进程虚拟地址区域的大小需要满足整页大小,因此mmap函数执行后,实际映射到虚拟内存区域的是个字节,~的字节部分用零填充。因此如果连续mmap小文件,会浪费内存空间。

  如果更新文件的操作很多,会触发大量的脏页回写及由此引发的随机IO上。所以在随机写很多的情况下,mmap方式在效率上不一定会比带缓冲区的一般写快。

  写内存算是访问内存吗

  当用户访问用户空间的这段地址范围时,实际是访问设备内存。在linux上电时,并不会为外设地址空间建立页表。但我们知道,linux访问内存使用的都是虚拟地址,因此如果想访问外设的寄存器(一般包括数据寄存器、控制寄存器与状态寄存器,需要在驱动初始化中将外设所处的物理地址映射为虚拟地址,使用ioremap接口可以实现该功能。ioremap&ioremap_nocacheioremap和ioremap_nocache实现相同,使用场景为映射devicememory类型内存。同时不使用cache(devicememory本身就没有cacheable这个属性),即CPU的读写操作直接操作设备内存。ioremap_cachedioremap_cached用来映射memorytype为normalmemory的设备,同时使用cache,这会提高内存的访问速度,提高系统的性能。ioremap_wc&ioremap_wtioremap_wc用来映射memorytype为normalmemory的设备,同时不使用cache。I/O内存访问流程request_mem_regionioremaprwiounmaprelease_mem_region二、设备地址映射到用户空间一般情况下,用户空间是不能够直接访问设备的。mmap可实现这个功能。mmap通过将设备内存映射到用户空间的一段内存上,这样,当用户访问用户空间的这段地址范围时,实际是访问设备内存。这样在每次访问时,节省了用户空间和内核空间的复制过程。无论是普通文件还是设备文件,读写都是基于系统的虚拟文件系统接口,普通文件为了保护磁盘,避免频繁读写,还引入带缓冲页机制,通过read/write/ioctl访问文件时,都需经历“用户到内核”的内存拷贝过程,然后才将文件内容写入磁盘。通过mmap方法,将文件(包括设备文件映射到用户进程虚拟内存空间,代替read/write/ioctl的访问方式,此时内存拷贝过程只有“用户空间到虚拟内存空间”,省去了“用户到内核”的拷贝过程,在数据量大的情况下能显著提升读写效率。因此,mmap也称为“零拷贝”(zerocopy技术。caddr_t*mmap(void*start,size_tlength,intprot,intflags,intfd,off_toffset);fd为文件描述符,一般由open返回。fd也可指定为-,并指定flags参数中的MAP_ANON,表示匿名映射。length指映射的字节数,从offset开始计算;prot指定访问权限;start指定文件被映射到用户空间的起始地址,一般设为NULL,由内核指定改地址;函数返回值为映射到用户空间的地址。mmap过程、在虚拟内存中查找一块VMA、将这块VMA进行映射、如果设备驱动程序或文件系统的file_operation定义了mmap接口,则调用它;、将VMA插入进程的VMA链表中进程在映射空间的对共享内容的修改不会实时同步写回到磁盘文件中,只有调用munmap()函数释放映射后才会执行同步操作。mmap机制提供msync()函数,用于手动同步修改内容到磁盘源文件。linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域,由于每个不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。各个vm_area_struct结构使用链表或者树形结构链接,方便进程快速访问,如下图所示:在这里插入图片描述vm_area_struct结构中包含区域起始和终止地址以及其他相关信息,同时也包含一个vm_ops指针,其内部可引出所有针对这个区域可以使用的系统调用函数。这样,进程对某一虚拟内存区域的任何操作需要用要的信息,都可以从vm_area_struct中获得。mmap函数就是要创建一个新的vm_area_struct结构,并将其与文件的物理磁盘地址相连。三、devmem原理“/dev/mem”设备“/dev/mem”是linux系统的一个虚拟字符设备,无论是标准linux系统还是嵌入式linux系统,都支持该设备。“/dev/mem”设备是内核所有物理地址空间的全映像,这些地址包括:物理内存(RAM空间物理存储(ROM空间cpu总线地址cpu寄存器地址外设寄存器地址,GPIO、定时器、ADC“/dev/mem”设备通常与“mmap”结合使用,可将指定内存映射到用户空间。类似的还有/dev/kmem设备,kernel看到的虚拟内存的全镜像。可以用来访问kernel的内容。devmem命令原理应用程序通过mmap函数实现对/dev/mem驱动中mmap方法的使用,映射了设备的内存到用户空间,实现对这些物理地址的读写操作。类似的有devkmem命令,通过mmap函数实现对/dev/kmem驱动中mmap方法的使用,映射了设备的内核空间到用户空间,实现对这些物理地址的读写操作。四、malloc原理malloc的工作原理可执行文件加载到内存中的时候,就给栈和堆划分了固定大小的空间。使用vm_area_struct结构体指明了一个连续区域的头地址和尾地址。malloc函数分配内存主要是使用brk和mmap系统调用brk():小于k在堆段分配malloc的内存,将堆顶的指针brk往上推;mmap():大于k是在堆和栈之间(文件映射区域找分配一块空闲的虚拟内存,malloc系统调用后,并没有实际分配物理内存。这时候读虚拟内存地址,返回值是;第一次写的时候,发生缺页中断,才会实际分配物理内存,建立虚拟内存与物理内存的映射关系。缺页中断malloc的空间没有实际分配的情况下,在写的时候会报缺页中断。实际上:进程线性地址空间里的页面不必常驻内存,在执行一条指令时,如果发现他要访问的页没有在内存中(即存在位为,那么停止该指令的执行,并产生一个页不存在的异常,对应的故障处理程序可通过从外存加载该页的方法来排除故障,之后,原先引起的异常的指令就可以继续执行,而不再产生异常。当一个进程发生缺页中断的时候,进程会陷入内核态,执行以下操作:、检查要访问的虚拟地址是否合法、查找/分配一个物理页、填充物理页内容(读取磁盘,或者直接置,或者啥也不干、建立映射关系(虚拟地址到物理地址重新执行发生缺页中断的那条指令如果第步,需要读取磁盘,那么这次缺页中断就是majflt,否则就是minflt。如何查看进程发生缺页中断的次数:ps-omajflt,minflt-Cprogrammajflt代表majorfault,中文名叫大错误,minflt代表minorfault,中文名叫小错误。这两个数值表示一个进程自启动以来所发生的缺页中断的次数。malloc的free前面知道,通过移动brk申请的内存,存放在进程的堆区域中。free是由运行库实现,它只是在已分配的堆块前面加一个可用标志,并不实际释放内存,不论是物理内存还是进程的堆空间。在下次的malloc时,这块空间可能被重用。如果进程的堆空间出现较多的碎片(这是逻辑地址中的碎片,运行库的堆管理例程会移动/合并碎片,此时可能会出现物理内存的释放/重新分配。而对于brk指针,只有它指向的那片内存被free的时候才会下移。比如先malloc了一个A,然后malloc了一个B。free掉A之后,brk是不会下移的;free掉B的时候brk才会下移。

  Android跨进程通信-mmap函数

  通过mmap或者内存共享的LinuxIPC机制

  直接将同一段内存映射到数据发送进程和数据接收进程的用户空间,这样数据发送进程只需要将数据拷贝到共享的内存区域,数据接收进程就可以直接使用数据了。

  mmap是一个很重要的函数,它可以实现共享内存,但并不像SystemV和Posix的共享内存存粹的只用于共享内存,mmap()的设计,主要是用来做文件的映射的,它提供了我们一种新的访问文件的方案。

  mmap函数的使用非常简单,我们来看一下

  常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制,这种机制会造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。

  常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制,这种机制会造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。

  而使用mmap操作文件中,由于不需要经过内核空间的数据缓存,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。

  mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程,因此mmap效率很高。

  mmap()使用非常频繁,看过Android系统源码的人,肯定看到过大量的地方使用mmap()函数,比如上面提到的匿名共享内存的使用就使用到了mmap来映射/dev/ashmem里的文件。

  这里我再介绍一种mmap()在Android系统上的使用场景,mmap的设计目的就是为了让文件的访问更有效率,所以当APK进行安装时,为了更高效的读取APK包里面的文件,同样也用到了mmap函数。

  Dalvik在安装应用时,需要加载dex文件,然后进行odex优化处理,优化函数为dvmContinueOptimization,我们看一下他的大致实现。

  可以看到,dvmContinueOptimization函数中对dex文件的加载便用了mmap内存映射函数。

您可能感兴趣的文章:

相关文章