内存映射为何能提升IO读取速率?

本文详细介绍了Linux的mmap()函数,它是一种内存映射文件的方法,可实现文件磁盘地址和进程虚拟地址的映射。使用mmap()能减少数据拷贝次数、提升性能,还可用于进程间通信。文章阐述了其使用方法、原理,并总结了优缺点,如不适合变长文件等。

ap技术,也就是内存映射,直接将磁盘文件数据映射到内核缓冲区,这个映射的过程是基于DMA引擎拷贝的,同时用户缓冲区是跟内核缓冲区共享一块映射数据的,建立共享映射之后,就不需要从内核缓冲区拷贝到用户缓冲区了。光是这一点,就可以避免一次拷贝了,但是这个过程中还是会用户态切换到内核态去进行映射拷贝,接着再次从内核态切换到用户态,建立用户缓冲区和内核缓冲区的映射。接着把数据通过Socket发送出去,还是要再次切换到内核态。接着直接把内核缓冲区里的数据拷贝到Socket缓冲区里去,然后再拷贝到网络协议引擎里,发送出去就可以了,最后切换回用户态。减少一次拷贝,但是并不减少切换次数,一共是4次切换,3次拷贝。mmap技术是主要在RocketMQ里来使用的,RocketMQ底层主要就是基于mmap技术来提升了磁盘文件的读写,性能。

什么是 mmap()

mmap, 从函数名就可以看出来这是memory map, 即地址的映射, 是一种内存映射文件的方法, (其他的还有mmap()系统调用,Posix共享内存,以及系统V共享内存,这些我们有机会在后续的文章讨论,今天的男主角是mmap),将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。

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


为什么使用 mmap()

Linux通过内存映像机制来提供用户程序对内存直接访问的能力。内存映像的意思是把内核中特定部分的内存空间映射到用户级程序的内存空间去。也就是说,用户空间和内核空间共享一块相同的内存。这样做的直观效果显而易见:内核在这块地址内存储变更的任何数据,用户可以立即发现和使用,根本无须数据拷贝。举个例子理解一下,使用mmap方式获取磁盘上的文件信息,只需要将磁盘上的数据拷贝至那块共享内存中去,用户进程可以直接获取到信息,而相对于传统的write/read IO系统调用, 必须先把数据从磁盘拷贝至到内核缓冲区中(页缓冲),然后再把数据拷贝至用户进程中。两者相比,mmap会少一次拷贝数据,这样带来的性能提升是巨大的。

使用内存访问来取代read()和write()系统调用能够简化一些应用程序的逻辑。
在一些情况下,它能够比使用传统的I/O系统调用执行文件I/O这种做法提供更好的性能。
原因是:

  1. 正常的read()或write()需要两次传输:一次是在文件和内核高速缓冲区之间,另一次是在高速缓冲区和用户空间缓冲区之间。使用mmap()就不需要第二次传输了。对于输入来讲,一旦内核将相应的文件块映射进内存之后,用户进程就能够使用这些数据了;对于输出来讲,用户进程仅仅需要修改内核中的内容,然后可以依靠内核内存管理器来自动更新底层的文件。
  2. 除了节省内核空间和用户空间之间的一次传输之外,mmap()还能够通过减少所需使用的内存来提升性能。当使用read()或write()时,数据将被保存在两个缓冲区中:一个位于用户空间,另个一位于内核空间。当使用mmap()时,内核空间和用户空间会共享同一个缓冲区。此外,如果多个进程正在同一个文件上执行I/O,那么它们通过使用mmap()就能够共享同一个内核缓冲区,从而又能够节省内存的消耗。

如何使用mmap()

 
  1. #include <sys/mman.h>

  2.  
  3. void *mmap (void *addr, size_t length, int prot, int flags, int fd, off_t offset);

  4.  

Arguments Describes (参数描述)

  • 参数addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。
  • len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。
  • prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。
  • flags由以下几个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。offset参数一般设为0,表示从文件头开始映射。
  • 参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。
  • offset参数一般设为0,表示从文件头开始映射, 代表偏移量。

Return Value (返回值)

函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。


两种映射方式


1. 基于文件的映射:

适用于任何进程之间, 此时,需要打开或创建一个文件,然后再调用mmap(), 典型调用代码如下:

 
  1. ...

  2. fd = open (name, flag, mode);

  3. if(fd<0)

  4. {

  5. printf("error!\n");

  6. }

  7.  
  8. /* 这块内存可读可写可执行 */

  9. ptr = mmap(NULL, len , PROT_READ|PROT_WRITE|PROT_EXEC, MAP_SHARED , fd , 0);

这样用户进程就可以像读取内存一样读取文件了,效率非常高。


2. 匿名映射

匿名映射是一种没用对应文件的一种映射,是使用特殊文件提供的匿名内存映射:
一个匿名映射没有对应的文件,这种映射的分页会被初始化为0。可以把它看成是一个内容总是被初始化为0的虚拟文件映射,比如在具有血缘关系的进程之间,如父子进程之间, 当一个进程调用mmap().之后又调用了fork(), 之后子进程会继承(拷贝)父进程映射后的空间,同时也继承了mmap()的返回地址,通过修改数据共享内存里的数据, 父子进程够可以感知到数据的变化,这样一来,父子进程就可以通过这块共享内存来实现进程间通信。

 
  1.  
  2. /* 例如一些网络套接字进行共享*/

  3. ptr = mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0);

  4.  
  5. pid = fork();

  6. switch (pid)

  7. {

  8. case pid < 0:

  9. printf ("err\n");

  10. case pid = 0:

  11. /* 使用互斥的方式访问共享内存 */

  12. lock(ptr)

  13. 修改数据;

  14. unlock(ptr);

  15. case pid > 0:

  16. /* 使用互斥的方式访问共享内存 */

  17. lock(ptr)

  18. 修改数据;

  19. unlock(ptr);

  20. }

  21.  

mmap 具体原理

/* 摘自网络 有修改*/

mmap内存映射的实现过程,总的来说可以分为三个阶段:


(一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域

  1. 进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
  2. 在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址
  3. 为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化
  4. 将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中

(二)调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系

  1. 为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。
  2. 通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数。
  3. 内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。
  4. 通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。

(三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝

注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。

  1. 进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。
  2. 缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。
  3. 调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。
  4. 之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程(是不是有点像写时复制技术呢,哦,这篇博客拖了好久了)。

注:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。


mmap()优缺点总结

mmap()的优点:

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

  2. 实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。
    mmap映射的页和其它的页并没有本质的不同.
    所以得益于主要的3种数据结构的高效,其页映射过程也很高效:
    (1) radix tree,用于查找某页是否已在缓存.
    (2) red black tree ,用于查找和更新vma结构.
    (3) 双向链表,用于维护active和inactive链表,支持LRU类算法进行内存回收.

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

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

 

mmap()的缺点:

  1. 对变长文件不适合.
  2. 如果更新文件的操作很多,mmap避免两态拷贝的优势就被摊还,最终还是落在了大量的脏页回写及由此引发的随机IO上. 所以在随机写很多的情况下,mmap方式在效率上不一定会比带缓冲区的一般写快.

 

原文转载自:https://blog.youkuaiyun.com/Jiangtagong/article/details/109393789

为了对W25Q256闪存芯片进行内存映射读写操作,需要理解其特性和操作流程。W25Q256是一款由Winbond生产的256Mbit的串行闪存芯片,支持通过四线SPI(QSPI)接口进行高速数据访问。内存映射读写操作通常指的是将外部存储器映射到处理器的地址空间中,使得访问外部存储器就像访问内部RAM一样方便。 ### 内存映射读取操作 1. **初始化QSPI接口**:首先需要配置微控制器的QSPI接口以匹配W25Q256的要求。这包括设置正确的时钟频率、模式(CPOL和CPHA),以及启用内存映射功能。对于STM32系列MCU,可以通过CubeMX工具配置QSPI接口,并生成初始化代码。 2. **配置内存映射模式**:W25Q256支持多种读取模式,包括单线、双线和四线SPI模式。为了实现内存映射读取,应选择四线SPI模式(QPI),因为这种模式提供了最高的数据传输速率。配置过程中需要发送特定的命令序列来进入QPI模式。 3. **映射地址计算**:确定W25Q256在系统内存映射中的起始地址。这通常涉及到设置QSPI控制器中的基地址寄存器。一旦配置完成,就可以通过直接访问该地址范围内的任何位置来读取W25Q256的内容。 ### 内存映射写入操作 虽然W25Q256支持内存映射读取操作,但它并不直接支持内存映射写入。这是因为写入操作涉及到更多的控制步骤,包括擦除和编程操作,这些操作不能像简单的内存写入那样快速完成。因此,写入操作通常需要通过DMA或者中断服务程序来实现,具体步骤如下: 1. **擦除操作**:在写入新数据之前,必须先擦除目标区域。W25Q256支持按扇区(4KB)、块(32KB或64KB)或整个芯片擦除。擦除操作前需要确保目标区域未被保护。 2. **编程操作**:擦除完成后,可以开始编程操作。W25Q256支持页编程,每页大小为256字节。编程时需要注意不要超过当前页的边界,否则可能会导致数据损坏。 3. **状态检查**:每次写入或擦除操作后,都需要检查状态寄存器以确认操作是否成功完成。这可以通过发送读取状态寄存器命令来实现。 ### 示例代码 以下是一个简化的示例代码,展示如何使用STM32 HAL库初始化QSPI接口并执行基本的读取操作。请注意,实际应用中可能需要更多的错误处理和配置选项。 ```c #include "stm32h7xx_hal.h" // QSPI handle declaration QSPI_HandleTypeDef hqspi; void MX_QSPI_Init(void) { hqspi.Instance = QUADSPI; hqspi.Init.ClockPrescaler = 1; // Adjust according to your system clock and desired QSPI frequency hqspi.Init.FifoThreshold = 4; hqspi.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_HALFCYCLE; hqspi.Init.FlashSize = 24; // 256Mbit = 32MB, log2(32*1024*1024) = 25, but some devices use 24 hqspi.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_1_CYCLE; hqspi.Init.ClockMode = QSPI_CLOCK_MODE_0; hqspi.Init.FlashID = QSPI_FLASH_ID_1; hqspi.Init.DualFlash = QSPI_DUALFLASH_DISABLE; if (HAL_QSPI_Init(&hqspi) != HAL_OK) { Error_Handler(); } } // Function to read data from W25Q256 using memory-mapped mode uint8_t ReadFromW25Q256(uint32_t address) { // Assuming the QSPI is already configured and memory-mapped return *(__IO uint8_t*)(QSPI_BASE_ADDR + address); } ``` 请注意,上述代码仅为示例,实际使用时需要根据具体的硬件配置和需求进行调整。此外,写入操作较为复杂,需要额外的步骤来确保数据正确性和设备安全。[^1]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值