欢迎光临散文网 会员登陆 & 注册

一篇带你深入理解mmap 0拷贝技术(从这五点入手~)

2022-05-30 14:57 作者:补给站Linux内核  | 我要投稿

1、前言

  • 环境:处理器架构:arm64内核源码:linux-5.11ubuntu版本:20.04.1代码阅读工具:vim+ctags+cscope

  • 我们知道,linux系统中用户空间和内核空间是隔离的,用户空间程序不能随意的访问内核空间数据,只能通过中断或者异常的方式进入内核态,一般情况下,我们使用copy_to_user和copy_from_user等内核api来实现用户空间和内核空间的数据拷贝,但是像显存这样的设备如果也采用这样的方式就显的效率非常底下,因为用户经常需要在屏幕上进行绘制,要消除这种复制的操作就需要应用程序直接能够访问显存,但是显存被映射到内核空间,应用程序是没有访问权限的,如果显存也能同时映射到用户空间那就不需要拷贝操作了,于是字符设备中提供了mmap接口,可以将内核空间映射的那块物理内存再次映射到用户空间,这样用户空间就可以直接访问不需要任何拷贝操作,这就是我们今天要说的0拷贝技术。

  • 下面是正常情况下用户空间和内核空间数据访问图示:

【文章福利】小编推荐自己的Linux内核技术交流群:【891587639】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100名进群领取,额外赠送一份价值699的内核资料包(含视频教程、电子书、实战项目及代码)    

2.、体验一下

  • 首先我们通过一个例子来感受一下:

  • 驱动代码:

注:驱动代码中使用misc框架来实现字符设备,misc框架会处理如创建字符设备,创建设备等通用的字符设备处理,我们只需要关心我们的实际的逻辑即可(内核中大量使用misc设备框架来使用字符设备操作集如ioctl接口,像实现系统虚拟化kvm模块,实现安卓进程间通信的binder模块等)。 0copy_demo.c

应用代码:test.c

Makefile文件:

编译驱动代码和应用代码,然后拷贝到qemu中运行:

  • 执行了以上步骤可以发现最终内核中出现了我在应用程序中写入的“hello world!!!“ 字符串,应用程序也能成功读取到(当然本文讲解的0拷贝实现的驱动接口是mmap,而我们读取使用的是read接口,里面我们用copy_to_user来实现的,当然我们可以直接操作mmap映射的内存不需要任何拷贝操作)。

  • 查看应用程序的内存映射发现,/dev/misc_dev设备被映射到了ffff8aa91000-ffff8aa92000这段用户空间地址范围,而且权限为rw-s(可读可写共享)。

  • 写到这里可能大家还是有点不明白那我来解释下:

  1. 用户空间不能直接访问内核空间数据(不能直接读写),一旦访问发生缺页异常,产生段错误,必须通过read这样的接口来访问,而read这样的接口会通过系统调用的方式写入到内核态,然后通过copy_to_user这样的内核api来拷贝内核空间数据到用户空间之后才能正常访问。

  2. 通过mmap这种方式之后,用户进程可以直接访问这块内存,memcpy访问的也只不过是用户空间地址,由于访问的时候已经分配好了物理页面和建立好了物理页到虚拟页的映射,所有不会发生缺页异常,也不会发生用户态到内核态的陷入动作。

  3. 用户态进程正常访问内核态数据需要首先通过系统调用等方式陷入内核,进行数据拷贝,然后再次回到用户态,用户态和内核态直接的进出需要进行上下文切换,需要2次上下文切换,需要一定的开销,而mmap映射好之后以后访问都不需要进行上下文切换。

  4. mmap映射这种方法由于物理页面通过页面共享更加节省内存,而用户态和内核态内存拷贝需要两份物理页面。

3、实现原理

  • 我们发现通过mmap映射之后,我们在应用程序中可以直接读写这段内存,不需要任何用户空间和内核空间的拷贝动作,大大提高了内存访问效率,那么就是是如何实现的呢?下面我们来揭开它神秘的面纱:

  • 实现0拷贝功不可没的是mmap接口中的remap_pfn_range内核api,它将内核空间映射的物理内存重新映射到了用户空间,下面我们来看这个函数的实现:remap_pfn_range函数参数如下:

  • vma为需要映射的进程的vma(进程调用mmap的时候内核会找到一个合适的vma), addr为vma中的一个起始映射地址(这是用户空间的一个虚拟地址),pfn为页帧号(在驱动的mmap接口中会将内核空间的地址转化为物理地址的页帧号),size为需要映射的大小,prot为映射的权限(一般取mmap时传递的权限如rw)

  • remap_pfn_range实现主要如下代码段:

  • 解释下:remap_pfn_range函数会查找进程的页表,然后填写页表,会将映射的物理页帧号和访问权限填写到进程的对应页表中,这会遍历进程的各级页表找到最终的页表项然后进行填写,具体过程自行查看代码。

  • 我们需要注意的是:

  1. 一般情况下,用户程序调用mmap只是申请虚拟内存(即是获得一块没有使用用户空间内存,使用vma描述),实际的物理页表都是通过进程访问的时候缺页异常的方式来申请的,但是本场景中是物理页面已经申请好了,进程访问时不会再发生缺页异常,不会申请物理页面。

  2. 同样,物理页面到用户空间虚拟页面的映射也在调用mmap的时候,驱动调用mmap接口的remap_pfn_range映射好了,也不需要在访问的时候发生缺页异常来建立映射。所以,只要用户进程通过mmap映射之后就可以正常访问,访问过程中不会发生缺页异常,映射虚拟页对应的物理页面已经在驱动中申请好映射好。

  • 下面给出mmap映射原理的图示:

4、应用场景

  • 最后,我们来看下使用framebuffer的lcd对0拷贝的使用情况:


lcd驱动代码中会设置好最终注册framebuffer:

  • 可以看到当系统支持framebuffer设备时,在fbmem_init中会创建framebuffer设备类关联字符设备操作集fb_fops,lcd的驱动代码中会调用register_framebuffer创建framebuffer设备(就会创建出了/dev/fdx 设备节点),应用程序就可以通过mmap来映射framebuffer设备到用户空间,然后进行屏幕绘制操作,不需要任何数据拷贝。

5.总结

  • 可以看的出,通过mmap实现0拷贝非常简单,只需要在驱动的mmap接口中调用remap_pfn_range来将内核空间映射的那块物理页再次映射到用户空间即可,这就实现了用户空间和内核空间的数据共享,这和用户进程之间的共享内存机制非常相似,都需要操作进程的页表将这段物理内存映射到进程虚拟地址空间。


一篇带你深入理解mmap 0拷贝技术(从这五点入手~)的评论 (共 条)

分享到微博请遵守国家法律