彻底理解mmap()

彻底理解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()

 #include <sys/mman.h>

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

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(), 典型调用代码如下:

...
fd = open (name, flag, mode);
if(fd<0)
{
	printf("error!\n");
}
        
/* 这块内存可读可写可执行 */
ptr = mmap(NULL, len , PROT_READ|PROT_WRITE|PROT_EXEC, MAP_SHARED , fd , 0); 

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


2. 匿名映射

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


/* 例如一些网络套接字进行共享*/
ptr = mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 

pid = fork();
switch (pid)
{
	case pid < 0:
		printf ("err\n");
	case pid = 0:
     	/* 使用互斥的方式访问共享内存 */
	 	lock(ptr)
	 	修改数据;
	 	unlock(ptr);
	case pid > 0:
	 	/* 使用互斥的方式访问共享内存 */
	 	lock(ptr)
	 	修改数据;
	 	unlock(ptr);
}


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方式在效率上不一定会比带缓冲区的一般写快.

参考内容

认真分析mmap:是什么 为什么 怎么用

共享内存实现原理

linux内核空间与用户空间信息交互方法

Linux IPC之内存映射mmap()

周明德,保护方式下的80386及其编程,清华大学出版社,1993

感谢

<think>好的,我现在需要帮用户理解零拷贝文件传输的概念和原理,并且给出通俗的解释和工作原理。首先,我得回忆一下自己之前学过的相关知识,确保理解正确,然后结合用户提供的引用内容来组织回答。 用户给出的引用内容里提到了几个关键点:零拷贝并不是零次复制,而是减少不必要的数据拷贝次数。比如,用户提到,传统的数据传输需要两次拷贝,从网卡到内核再到用户空间,而零拷贝技术可以消除这两次拷贝。引用中还有关于网络服务器CPU资源消耗主要在数据拷贝上的问题,零拷贝技术能解决这个问题。 首先,我需要用通俗的语言解释零拷贝。可能用快递包裹的比喻比较合适,比如包裹不需要经过多个中转站直接送到目的地,减少中间的搬运次数。这样用户更容易理解。 接下来,要分步骤说明传统文件传输的问题。传统的流程应该是:磁盘→内核缓冲区→用户缓冲区→socket缓冲区→网卡。这里涉及到四次上下文切换和两次CPU拷贝,两次DMA拷贝。这样效率低,因为用户态和内核态的切换以及数据复制消耗资源。 然后,说明零拷贝技术如何优化这个过程。比如使用sendfile系统调用,或者内存映射(mmap),减少拷贝次数。比如mmap将内核缓冲区和用户缓冲区共享,省去一次拷贝;sendfile更进一步,连用户态都不需要参与,直接在内核完成数据到socket的传输,甚至可以利用DMA gather技术让网卡直接从内核缓冲区取数据,实现零次CPU拷贝。 需要注意的是,用户引用中提到零拷贝并不是完全没有复制,而是减少不必要的复制次数。比如可能还有DMA的拷贝,但CPU不再参与复制,从而节省CPU资源。这点需要明确,避免用户误解。 然后,可能还需要提到零拷贝的应用场景,比如高性能服务器、大文件传输、视频流媒体等,这样用户知道在什么地方使用该技术。 最后,生成相关问题。根据用户之前的示例,可能需要问及零拷贝与传统拷贝的区别、实现方式、应用场景、优缺点等。但需要确保问题符合用户的学习路径,帮助深入理解。 在回答过程中,需要引用用户提供的引用内容,比如引用[3]提到两次数据拷贝消耗80%的CPU资源,零拷贝解决这个问题。引用[1]和[2]纠正了零拷贝不等于零复制的错误说法,所以要在回答中强调这一点,确保概念正确。 另外,检查是否有技术细节错误,比如DMA的作用,上下文切换的次数,sendfile和mmap的区别等。确保信息准确,避免误导用户。 可能还需要比较不同的零拷贝技术,比如mmap和sendfile的区别,以及DMA gather的功能,这样用户能全面了解不同层次的优化。 总结下来,回答的结构应该是:通俗解释→传统传输的问题→零拷贝的解决方案(分步骤)→技术实现(如sendfile)→优点和应用场景→相关问题。同时,正确引用用户提供的资料,确保信息可靠。</think>### 零拷贝文件传输的通俗解释及工作原理 #### 通俗解释 零拷贝文件传输可以理解为**“快递包裹直达”**的优化过程。想象你网购的商品需要经过多个中转站才能到达手中,而零拷贝技术相当于让商品直接从仓库发到你家,跳过了所有中间环节。虽然实际运输过程中仍有物理移动(数据传输),但减少了不必要的分拣和搬运(数据复制),从而大幅提升效率[^3]。 --- #### 传统文件传输的问题 传统方式传输文件(如从磁盘发送到网络)需要经过以下步骤: 1. **磁盘 → 内核缓冲区**(DMA拷贝) 2. **内核缓冲区 → 用户缓冲区**(CPU拷贝) 3. **用户缓冲区 → Socket缓冲区**(CPU拷贝) 4. **Socket缓冲区 → 网卡**(DMA拷贝) 此过程涉及**4次上下文切换**(用户态↔内核态)和**2次CPU拷贝**,导致CPU资源被大量消耗在数据搬运上(占服务器CPU资源的80%[^3])。 --- #### 零拷贝的核心原理 零拷贝通过**减少数据复制次数**和**避免用户态参与**来优化性能,具体技术包括: 1. **`mmap`内存映射** - 将内核缓冲区与用户缓冲区**映射到同一块内存**,省去第2步的CPU拷贝。 - 但仍需第3步的CPU拷贝(内核→Socket缓冲区)。 2. **`sendfile`系统调用** - 文件直接在内核空间从磁盘传输到Socket缓冲区,**完全跳过用户态**。 - 进一步减少到**2次DMA拷贝** + **0次CPU拷贝**。 3. **DMA Gather(分散-聚集)** - 网卡直接从内核缓冲区读取数据,彻底消除所有CPU拷贝,实现真正的“零拷贝”[^3]。 --- #### 技术对比 | 技术 | CPU拷贝次数 | 上下文切换次数 | |--------------|-------------|----------------| | 传统方式 | 2 | 4 | | `mmap` | 1 | 4 | | `sendfile` | 0 | 2 | --- #### 应用场景 1. 高频网络通信(如HTTP服务器、消息队列) 2. 大文件传输(视频流、云存储) 3. 高并发场景(减少CPU资源竞争) ---
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值