深入探究Linux mmap的实现方式 (linux mmap 实现)

在Linux系统中,mmap是一种常用的内存映射方式。通过mmap,可以将一个文件或者一个设备映射到进程的地址空间,使得进程可以像访问普通内存一样访问文件或设备。mmap方法的实现方式十分复杂,本文将从实现角度。

一、Linux mmap的基本概念

mmap是Linux内核提供的一种I/O操作接口,可以将文件或设备映射到进程的地址空间,使得进程可以像访问普通内存一样访问文件或设备。mmap可以将一个文件或设备的整个实体映射到地址空间,也可以根据需要进行部分映射。mmap一般用于需要频繁访问文件或设备的应用程序,可以大大提高I/O性能。

mmap主要通过vma(Virtual Memory Area)描述地址空间中的映射区域。vma结构体包括映射区域的起始地址、大小、访问权限、映射的对象(文件或设备)等信息。在Linux系统中,每个进程都有自己的地址空间,由多个vma描述。

二、Linux mmap的实现方式

Linux mmap的实现方式十分复杂,下面将详细介绍Linux mmap的实现原理。

1. mmap系统调用的实现

在用户进程调用mmap系统调用时,Linux内核会调用sys_mmap函数。sys_mmap函数首先对参数进行检查,然后调用do_mmap函数实现具体的映射操作。

do_mmap的实现过程如下:

(1)调用vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)函数找到当前进程地址空间中addr处的vma。

(2)判断当前的vma是否与要映射的地址重叠,如果重叠则返回错误。

(3)调用get_unmapped_area函数找到当前进程地址空间中一段未映射的区域。

(4)调用vm_area_struct *vm_area_alloc(struct mm_struct *mm)函数为新的vma分配一块地址空间,将映射信息存储到vma结构体中。

(5)调用do_mmap_pgoff函数实现将文件映射到地址空间中,具体实现会在下面介绍。

2. 文件的映射

在调用do_mmap_pgoff函数时,首先需要根据文件的inode号在内存中建立与文件对应的页表项(Page Table Entries,PTE)。Linux内核中的PTE有两种类型:软件PTE和硬件PTE。软件PTE由内核管理,记录一个vma所映射的虚拟地址和实际物理地址之间的映射关系。硬件PTE由CPU的内存管理单元(Memory Management Unit,MMU)管理,记录一个进程虚拟地址和实际物理地址之间的映射关系。

建立好PTE后,就可以将文件内容映射到进程的地址空间中。具体步骤如下:

(1)调用file->f_op->mmap函数获取文件的页表(Page Table)。

(2)在vma结构体中寻找相应的PTE记录,如果找到了,则表示之前已经将文件的部分或者全部内容映射到该进程的地址空间中。

(3)如果没有找到相应的PTE记录,则需要创建一个新的PTE,将文件内容映射到相应的虚拟地址中。

(4)通过vm_insert_page函数将新的PTE插入到进程的页表中。

(5)通过page_cache_release函数释放文件的缓存。

映射完成后,Linux内核会根据vma的属性设置相应的属性,比如可读可写可执行等。

3. 延迟映射

在Linux系统中,为了提高页表的访问效率,采用了一种称为延迟映射(Lazy Mapping)的策略。延迟映射指的是在访问一个虚拟地址时,才会真正将相应的内存页映射到物理内存上,而不是在mmap时就将整个文件映射到物理内存中。

延迟映射的优点在于可以大大减少内存的消耗,因为不是所有的文件都被频繁访问,对于那些不常访问的文件,延迟映射可以将其在内存中定位但不占用实际内存。

4. 文件页管理

在Linux系统中,为了避免频繁访问磁盘,会针对常用的文件页面缓存。

常用的方法是,调用算法来实现文件页的管理。最常见的算法是LRU(Least Recently Used)算法,即最近最少使用算法,根据页表项的最近使用时间来判断哪些页应该被淘汰。其他的算法包括FIFO(First In First Out)、LFU(Least Frequently Used)等。

五、

本文详细介绍了Linux mmap的实现方式。mmap的实现涉及到面向对象编程、内存管理、硬件特性等多方面的知识。掌握Linux mmap的实现方式对于Linux系统的内核开发和应用开发都非常重要。

相关问题拓展阅读:

linux共享内存和mmap的区别

mmap的机制如:就是在磁盘上建立一个文件,每个进程存储器里面,单独开辟一个空陆升间来进行映射。如果多进程的话,那么不会对实际的磨悉耐物理存储器(主存)消耗太大。

shm的机制:每个进程的共享内存都直接映射到实际物理存储器里面。

1、mmap保存到实际硬盘,实瞎春际存储并没有反映到主存上

2、shm保存到物理存储器(主存),实际的储存量直接反映到主存上。

使用上看:如果分配的存储量不大,那么使用shm;如果存储量大,那么使用shm。

以上内容来源,

  共享内存的创建

  根据理论:

  1. 共享内存允许两个或多个进程共享一给定的存储区,因为数据不需要来回复制,所以是最快的一种进程间通信机制。共享内存可以通过mmap()映射普通文件(特殊情况下还可以采用匿名映射)机制实现,也可以通过系统V共享内存机制实现。应用接口和原理很简单,内部机制复杂。为了实现更安全通信,往往还与信号灯等同步机制共同使用。

  mmap的机制如:就是在磁盘上建立一个文件,每个进程存储器里面,单独开辟一个空间来进行映射。如果多进程的话,那么不会对实际的物理存储器(主存)消耗太大。

  shm的机制:每个进程的共享内存都直接映射到实际物理存储器里面。

  结论:

  1、mmap保存到实际硬盘,实际存储并没有反映到主存上。优点:储存量可以很大(多于主存)(这里一个问题,需要高手解答,会不会太多拷贝到主存里面???);缺点:进程间读取和写入速度要比主存的要慢。

  2、shm保存到物理存储器(主存),实际的储存量直接反映到主存上。优点,进程间访问速度(读写)比磁盘要快;缺点,储存量不能非常大(多于主存)

  使用上看:如果分配的存储量不大,那么使用shm;如果存储量大,那么使用shm。

  参看百度:

  mmap就是一个文件操作

  看这些百度的描述:

  mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。 成功执行时,mmap()返回被映射区的指针,munmap()返回0。失败时,mmap()返回MAP_FAILED,munmap返回-1。errno被设为以下的某个值 EACCES:访问出错EAGAIN:文件已被锁定,或者太多的内存已被锁定EBADF:fd不是有效的文件描述词EINVAL:一个或者多个参数无效 ENFILE:已达到系统对打开文件的限制ENODEV:指定文件所在的文件系统不判团支持内存映射ENOMEM:内存不足,或者进程已超出更大内存映射数量 EPERM:权能不足,操作不允许ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标毁启志SIGSEGV:试着向只读区写入 SIGBUS:试着访问不属于进程的内存区参数fd为即将映射到进程空间的文件描述字,

  一般由open()返回,同时,fd可以指定为-1,此时须指定 flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)

  相关文章参考:

  mmap函数是unix/linux下的系统调用,来看《Unix Netword programming》卷二12.2节有详细介绍。

  mmap系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文纤冲如件的操作。而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。

  mmap系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再 调用read(),write()等操作。mmap并不分配空间, 只是将文件映射到调用进程的地址空间里, 然后你就可以用memcpy等操作写文件, 而不用write()了.写完后用msync()同步一下, 你所写的内容就保存到文件里了. 不过这种方式没办法增加文件的长度, 因为要映射的长度在调用mmap()的时候就决定了.

  简单说就是把一个文件的内容在内存里面做一个映像,内存比磁盘快些。

  基本上它是把一个档案对应到你的virtual memory 中的一段,并传回一个指针。

  重写总结:

  1、mmap实际就是操作“文件”。

  2、映射文件,除了主存的考虑外。shm的内存共享,效率应该比mmap效率要高(mmap通过io和文件操作,或“需要写完后用msync()同步一下”);当然mmap映射操作文件,比直接操作文件要快些;由于多了一步msync应该可以说比shm要慢了吧???

  3、另一方面,mmap的优点是,操作比shm简单(没有调用比shm函数复杂),我想这也是许多人喜欢用的原因,包括nginx。

  缺点,还得通过实际程序测试,确定!!!

  

  修正理解(这也真是的,这个网站没办法附加;只能重写了):

  今天又细心研究了一下,发现百度这么一段说明:

  2、系统调用mmap()用于共享内存的两种方式:

  (1)使用普通文件提供的内存映射:适用于任何进程之间;此时,需要打开或创建一个文件,然后再调用mmap();典型调用代码如下:

  fd=open(name, flag, mode);

  if(fd

  …

  ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 通过mmap()实现共享内存的通信方式有许多特点和要注意的地方,我们将在范例中进行具体说明。

  (2)使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间;由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。注意,这里不是一般的继承关系。一般来说,子进程单独维护从父进程继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。

  看了一下windows“内存映射文件”:

  内存映射文件与虚拟内存有些类似,通过内存映射文件可以保留一个地址空间的区域,同时将物理存储器提交给此区域,只是内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,而非系统的页文件,而且在对该文件进行操作之前必须首先对文件进行映射,就如同将整个文件从磁盘加载到内存。由此可以看出,使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,这意味着在对文件进行处理时将不必再为文件申请并分配缓存,所有的文件缓存操作均由系统直接管理,由于取消了将文件数据加载到内存、数据从内存到文件的回写以及释放内存块等步骤,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。另外,实际工程中的系统往往需要在多个进程之间共享数据,如果数据量小,处理方法是灵活多变的,如果共享数据容量巨大,那么就需要借助于内存映射文件来进行。实际上,内存映射文件正是解决本地多个进程间数据共享的最有效方法。

  

  这里再总结一次:

  1、mmap有两种方式,一种是映射内存,它把普通文件映射为实际物理内存页,访问它就和访问物理内存一样(这也就和shm的功能一样了)(同时不用刷新到文件)

  2、mmap可以映射文件,不确定会不会像windows“内存映射文件”一样的功能,如果是,那么他就能映射好几G甚至好几百G的内存数据,对大数据处理将提供强大功能了???

  3、shm只做内存映射,和mmap之一个功能一样!只不过不是普通文件而已,但都是物理内存。

共享内存允许两个或多个进程共享一给定的存储区,因为数据不需要来回复制,所以是最快的一种进程间通信机制。共享内存可以通过mmap()映射普通文件

(特殊情况下还可以采用匿名映射)机制实现,也可以通过系统V共享内存机制实现。应用接口和原理很简单,内部机制复杂。为了实现更安全通信,往往还与信号灯等同步机制共同使用。

mmap的机制如:就是在磁盘上建立一个文件,每个进程存储器里面,单独开辟一个空间来进纯团行映射。如果多进程的话拍戚,那么不会对实际的物理存储器(主存)消耗太大。

shm的机制:每个进程的共享内存都直接映射到实际物理存储器里面。

1、mmap保存到实际硬盘,实际存储并没有反映到主存上。优点:储存量可以很大(多于主存);缺点:进程间读取和写入速度要比主存的要慢。

2、shm保存到物理存储器(主存),实际的储存量直接反映到主存上。优点,进程间访问速度(读写)比袭裤陵磁盘要快;缺点,储存量不能非常大(多于主存)

Linux的IPC机制(三):Binder

正如上一章所说, 跨进程通信是需要内核空间做支持的. 传统的 IPC 机制如 管道, Socket, 都是内核的一部分, 因此通过内核支持来实现进程间通信自然是没问题的.

但是 Binder 并不是 Linux 系统内核的一部分, 那怎么办呢, 这得益于 Linux 的动态内核可加载模块 (Loadable Kernel Module, LKM)的铅厅机制

这样 Android 系统就可以通过动态添加一个内核模块运行在内核空间, 用户进程进程之间通过这个内核模块作为桥梁来实现通信.

那么在 Android 系统中用户进程之间是如何通过这个内核模块 (Binder Driver)来实现通信的呢? 显然不是和上一章的传统 IPC 通信一样,进行两次 copy 了, 不然Binder 也不有在性能方面的优势了.

Binder IPC 机制中设计到的内存映射通过 mmap() 来实现, mmap() 是操作系统中一种内存映射的方法.

内存映射能减少数据 copy 的次数, 实现用户空间和内核空间的高效互动. 两个空间各自的修改也能直接反应在映射的内存区域, 从而被对方空间及时感知. 也正因为如此, 内存映射能够提供对进程间通信的支持.

Binder IPC 正是基于内存映射( mmap() ) 来实现的, 但是 mmap() 通常是用在有物理介质的文件系统上的.

比如进程中的用户区域是不能直接和物理设备打交道的, 如果想要把磁盘上的数据读取到进程的用户区域, 需要两次 copy (磁盘 -> 内核空间 -> 用户空间). 通常在这种场景下 mmap() 就能发挥作用, 通过在物理介质和用户空间之间建立映射, 减少数据的 copy 次数, 用内存读写代替 I/O 读写, 提高文件读取效率.

而 Binder 并不存在物理介质, 因此 Binder 驱动使用 mmap() 并不是为了在物理介质和用户空间之间映射, 而是用来在内核空间创建数据接收的缓存空间.

一次完整的 Binder IPC 通信过程通常是这样:

这样就完成了一次进程间通信

如下图:

介绍完 Binder IPC 的底层通信原理, 接下来我们看看实现层面是如何设计的

一次完成的进程间通信必然至少包含两个进程, 通常我们称通信的双方分别为客户端进程(Client) 和服务端进程(Server), 由于进程隔离机制的存在, 通信双方必然需要借助 Binder 来实现.

BInder 是基于 C/S 架构. 是由一些列组件组尺雹成. 包括 Client, Server, ServiceManager, Binder 驱动.

Binder 驱动就如如同路由器一样, 是整个通信的核心. 驱动负责进程之间 Binder 通信的建立 / 传递, Binder 引用计数管理, 数据包在进程之间的传递和交互等一系列底层支持.

ServiceManager 作用是将字符形式的 Binder 名字转化成 Client 中对该 Binder 的引用, 使得 Client 能够通过 Binder 的名字获得对 Binder 实体的引用陵激帆.

注册了名字的 Binder 叫实名 Binder, 就像网站一样除了 IP 地址以外还有自己的网址.

Server 创建了 Binder, 并为它起一个字符形式, 可读易记的名字, 将这个 BInder 实体连同名字一起以数据包的形式通过 Binder 驱动 发送给 ServiceManager, 通知 ServiceManager 注册一个名字为 “张三”的 Binder, 它位于某个 Server 中, 驱动为这个穿越进程边界的 BInder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用, 将名字以及新建的引用打包传给 ServiceManager, ServiceManager 收到数据后从中取出名字和引用填入查找表.

ServiceManager 是一个进程, Server 又是一个另外的进程, Server 向 ServiceManager 中注册 BInder 必然涉及到进程间通信. 当实现进程间通信又要用到进程间通信, 这就好像蛋可以孵出鸡的前提确实要先找只鸡下蛋! Binder 的实现比较巧妙, 就是预先创造一只鸡来下蛋. ServiceManager 和其他进程同样采用 Binder 通信, ServiceManager 是 Server 端, 有自己的 Binder 实体, 其他进程都是 Client, 需要通过这个 Binder 的引用来实现 Binder 的注册, 查询和获取. ServiceManager 提供的 Binder 比较特殊, 它没有名字也不需要注册. 当一个进程使用 BINDERSETCONTEXT_MGR 命令将自己注册成 ServiceManager 时 Binder 驱动会自动为它创建 Binder 实体(这就是那只预先造好的那只鸡). 其实这个 Binder 实体的引用在所有 Client 中都固定为 0 , 而无需通过其他手段获得. 也就是说, 一个 Server 想要向 ServiceManager 注册自己的 Binder 就必须通过这个 0 号引用和 ServiceManager 的 Binder 通信. 这里说的 Client 是相对于 ServiceManager 而言的, 一个进程或者应用程序可能是提供服务的 Server, 但是对于 ServiceManager 来说它仍然是个 Client.

Server 向 ServiceManager 中注册了 Binder 以后, Client 就能通过名字获得 Binder 的引用. Client 也利用保留的 0 号引用向 ServiceManager 请求访问某个 Binder. 比如,Client 申请访问名字叫”张三”的 Binder 引用. ServiceManager 收到这个请求后从请求数据包中取出 Binder 名称, 在查找表里找到对应的条目, 取出对应的 Binder 引用, 作为回复发送给发起请求的 Client. 从面相对象的角度看, Server 中的 Binder 实体现在有两个引用: 一个位于 ServiceManager 中, 一个位于发起请求的 Client 中. 如果后面会有更多的 Client 请求该 Binder, 系统中就会有更多的引用指向这个 Binder, 就像 Java 中一个对象有多个引用一样.

我们已经解释清楚 Client, Server 借助 Binder 驱动完成跨进程通信的实现机制了, 但是还有个问题需要弄清楚, 比如 A 进程想要 B 进程中的某个对象(object) 是如何实现的呢, 毕竟它们属于不同的进程, A 进程没办法直接使用 B 进程中的 object.

前面我们说过跨进程通信的过程都有 Binder 驱动的参与, 因此在数据流经 Binder 驱动的时候 Binder 驱动会对数据做一层转换.

我们在 Client端,向 ServiceManager 获取具体的 Server 端的 Binder 引用的时候,会首先进过 Binder 驱动,Binder 驱动它并不会把真正的 Server 的 Binder 引用返回给 Client 端,而是返回一个代理的 java 对象,该对象具有跟 Server 端的 Binder 引用相同的方法签名,这个对象为 ProxyObject,他具有跟 Server 的 Binder 实例一样的方法,只是这些方法并没有 Server 端的能力,这些方法只需要把请求参数交给 Binder 驱动即可. 对于 Client 端来说和直接调用 Server 中的方法是一样的.

了解了上面之后, 我们大致可以推算出 Binder 的通信过程

1. 注册 ServiceManager

2. 注册 Server

3. Client 获取 Server 的 Binder 引用

4. Client 与 Server 通信

关于linux mmap 实现的介绍到此就结束了,不知道你从中找到你需要的信息了吗 ?如果你还想了解更多这方面的信息,记得收藏关注本站。


数据运维技术 » 深入探究Linux mmap的实现方式 (linux mmap 实现)