所谓的0拷贝不就是为了让CPU休息吗?深入理解mmap

1.开场白

  • 环境:

    处理器架构:arm64

    内核源码:linux-5.11

    ubuntu版本:20.04.1

    代码阅读工具:vim+ctags+cscope

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

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

453207f5504ffcc347d7cdea1a22347a.png

2. 体验一下

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

驱动代码:

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

0copy_demo.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/mm.h>
#include <linux/miscdevice.h>#define MISC_DEV_MINOR 5static char *kbuff;static ssize_t misc_dev_read(struct file *filep, char __user *buf, size_t count, loff_t *offset)
{int ret;size_t len = (count > PAGE_SIZE ? PAGE_SIZE : count);pr_info("###### %s:%d kbuff:%s ######\n", __func__, __LINE__, kbuff);ret = copy_to_user(buf, kbuff, len);  //这里使用copy_to_user  来进程内核空间到用户空间拷贝return len - ret;
}static ssize_t misc_dev_write(struct file *filep, const char __user *buf, size_t count, loff_t *offset)
{pr_info("###### %s:%d ######\n", __func__, __LINE__);return 0;
}static int misc_dev_mmap(struct file *filep, struct vm_area_struct *vma)
{int ret;unsigned long start;start = vma->vm_start;ret =  remap_pfn_range(vma, start, virt_to_phys(kbuff) >> PAGE_SHIFT,PAGE_SIZE, vma->vm_page_prot); //使用remap_pfn_range来映射物理页面到进程的虚拟内存中  virt_to_phys(kbuff) >> PAGE_SHIFT作用是将内核的虚拟地址转化为实际的物理地址页帧号  创建页表的权限为通过mmap传递的 vma->vm_page_prot   映射大小为1页return ret;
}static long misc_dev_ioctl(struct file *filep, unsigned int cmd, unsigned long args)
{pr_info("###### %s:%d ######\n", __func__, __LINE__);return 0;
}static int misc_dev_open(struct inode *inodep, struct file *filep)
{pr_info("###### %s:%d ######\n", __func__, __LINE__);return 0;
}static int misc_dev_release(struct inode *inodep, struct file *filep)
{pr_info("###### %s:%d ######\n", __func__, __LINE__);return 0;
}static struct file_operations misc_dev_fops = {.open = misc_dev_open,.release = misc_dev_release,.read = misc_dev_read,.write = misc_dev_write,.unlocked_ioctl = misc_dev_ioctl,.mmap = misc_dev_mmap,
};static struct miscdevice misc_dev = {MISC_DEV_MINOR,"misc_dev",&misc_dev_fops,
};static int __init misc_demo_init(void)
{misc_register(&misc_dev);  //注册misc设备 (让misc来帮我们处理创建字符设备的通用代码,这样我们就不需要在去做这些和我们的实际逻辑无关的代码处理了)kbuff = (char *)__get_free_page(GFP_KERNEL);  //申请一个物理页面(返回对应的内核虚拟地址,内核初始化的时候会做线性映射,将整个ddr内存映射到线性映射区,所以我们不需要做页表映射)if (NULL == kbuff)return -ENOMEM;pr_info("###### %s:%d ######\n", __func__, __LINE__);return 0;
}static void __exit misc_demo_exit(void)
{free_page((unsigned long)kbuff);misc_deregister(&misc_dev);pr_info("###### %s:%d ######\n", __func__, __LINE__);
}module_init(misc_demo_init);
module_exit(misc_demo_exit);
MODULE_LICENSE("GPL");

应用代码:test.c

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>int main(int argc, char **argv)
{int fd;char *ptr;char buff[32];fd = open("/dev/misc_dev", O_RDWR);  //打开字符设备if (fd < 0) {perror("fail to open");return -1;}ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); //映射字符设备到进程的地址空间  权限为可读可写 映射为共享  大小为一个页面if (ptr == MAP_FAILED) {perror("fail to mmap");return -1;}memcpy(ptr, "hello world!!!", 15);   //写mmap映射的内存  直接操作,不需要进行特权级别的陷入!if(read(fd, buff, 15) == -1) {  //读接口  来读取映射的内存,这里会进行内核空间到用户空间的数据拷贝 (需要调用系统调用 在内核空间进行拷贝,然后才能访问)perror("fail to read");return -1;}puts(buff);  pause();return 0;
}

Makefile文件:

export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-KERNEL_DIR ?= ~/kernel/linux-5.11
obj-m := 0copy_demo.omodules:$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modulesapp:aarch64-linux-gnu-gcc test.c -o testcp test $(KERNEL_DIR)/kmodulesclean:$(MAKE) -C $(KERNEL_DIR) M=$(PWD) cleaninstall:cp *.ko $(KERNEL_DIR)/kmodules

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

编译驱动模块代码:
$ make modules编译并拷贝应用:
$ make app拷贝驱动模块到qemu:
$ make install 加载驱动代码:
# insmod 0copy_demo.ko
[23328.532194] ###### misc_demo_init:91 ######查看生成的设备节点:
# ls -l /dev/misc_dev 
crw-rw----    1 0        0          10,   5 Apr  7 19:26 /dev/misc_dev后台运行应用程序:
# ./test&
# [23415.280501] ###### misc_dev_open:56 ######
[23415.281052] ###### misc_dev_read:20 kbuff:hello world!!! ######
hello world!!!查看test的pid:
# pidof test
1768查看内存映射:
# cat /proc/1768/maps 
aaaabc5a0000-aaaabc5a1000 r-xp 00000000 00:19 8666193                    /mnt/test
aaaabc5b0000-aaaabc5b1000 r--p 00000000 00:19 8666193                    /mnt/test
aaaabc5b1000-aaaabc5b2000 rw-p 00001000 00:19 8666193                    /mnt/test
aaaacf033000-aaaacf054000 rw-p 00000000 00:00 0                          [heap]
ffff8a911000-ffff8aa52000 r-xp 00000000 fe:00 152                        /lib/libc-2.27.so
ffff8aa52000-ffff8aa61000 ---p 00141000 fe:00 152                        /lib/libc-2.27.so
ffff8aa61000-ffff8aa65000 r--p 00140000 fe:00 152                        /lib/libc-2.27.so
ffff8aa65000-ffff8aa67000 rw-p 00144000 fe:00 152                        /lib/libc-2.27.so
ffff8aa67000-ffff8aa6b000 rw-p 00000000 00:00 0 
ffff8aa6b000-ffff8aa88000 r-xp 00000000 fe:00 129                        /lib/ld-2.27.so
ffff8aa91000-ffff8aa92000 rw-s 00000000 00:05 152                        /dev/misc_dev      //映射设备文件到用户空间
ffff8aa92000-ffff8aa94000 rw-p 00000000 00:00 0 
ffff8aa94000-ffff8aa96000 r--p 00000000 00:00 0                          [vvar]
ffff8aa96000-ffff8aa97000 r-xp 00000000 00:00 0                          [vdso]
ffff8aa97000-ffff8aa98000 r--p 0001c000 fe:00 129                        /lib/ld-2.27.so
ffff8aa98000-ffff8aa9a000 rw-p 0001d000 fe:00 129                        /lib/ld-2.27.so
ffffecb5a000-ffffecb7b000 rw-p 00000000 00:00 0                          [stack]

执行了以上步骤可以发现最终内核中出现了我在应用程序中写入的“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函数参数如下:

int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,         ¦   unsigned long pfn, unsigned long size, pgprot_t prot)

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

remap_pfn_range实现主要如下代码段:

remap_pfn_range...pgd = pgd_offset(mm, addr);                                    flush_cache_range(vma, addr, end);                             do {                                                           next = pgd_addr_end(addr, end);                        err = remap_p4d_range(mm, pgd, addr, next,             pfn + (addr >> PAGE_SHIFT), prot);     if (err)                                               break;                                         } while (pgd++, addr = next, addr != end);

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

我们需要注意的是:

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

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

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

66f7932700773685d9436c59f1e3aa44.png



4.应用场景

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

fbmem_init    //drivers/video/fbdev/core/fbmem.c
->register_chrdev(FB_MAJOR, "fb", &fb_fops)  //注册framebuffer字符设备-> struct file_operations fb_fops = {->.mmap =         fb_mmap    -> fb_mmap    //framebuffer的实现->vm_iomap_memory->io_remap_pfn_range->remap_pfn_range->  fb_class = class_create(THIS_MODULE, "graphics")  //创建设备类

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

xxxfb_probe
->register_framebuffer->do_register_framebuffer-> fb_info->dev = device_create(fb_class, fb_info->device,¦    MKDEV(FB_MAJOR, i), NULL, "fb%d", i);  //创建设备  会出现/dev/fdx 设备节点

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

5.总结

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

6c1d38ee9c9eb3a23a9786fec8d345ef.gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/463992.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

boost::function的用法(一)

boost::function的用法 本片文章主要介绍boost::function的用法。 boost::function 就是一个函数的包装器(function wrapper)&#xff0c;用来定义函数对象。 1. 介绍 Boost.Function 库包含了一个类族的函数对象的包装。它的概念很像广义上的回调函数。其有着和函数指针相同的…

nhibernate学习之集合组合依赖

1.学习目标还是学习compenent的用法&#xff0c;上节实现了简单字段的组合&#xff0c;这节中将讨论两个问题&#xff1a;1.依赖对象有一个指向容器对象的引用。2。集合依赖2.开发环境和必要准备开发环境为:windows 2003,Visual studio .Net 2005,Sql server 2005 developer ed…

追更这个做嵌入式的大佬

在知乎上看到一个做嵌入式91年小年轻&#xff0c;分享给大家在他看来&#xff0c;嵌入式也是一个很吃香的技术&#xff0c;在周末写这篇文章的时候&#xff0c;也刚收到一个朋友的微信消息&#xff0c;他说自己拿到了70多万的年包offer。大家想追更作者的原文&#xff0c;可以点…

CentOS6.5安装ElasticSearch6.2.3

CentOS6.5安装ElasticSearch6.2.3 1、Elastic 需要 Java 8 环境。&#xff08;安装步骤&#xff1a;http://www.cnblogs.com/hunttown/p/5450463.html&#xff09; 2、安装包下载&#xff1a; #官网地址 https://www.elastic.co/downloads/elasticsearch 3、新建用户 Elastic高…

这道字符串反转的题目,你能想到更好的方法吗?

周末有一个朋友问了一个笔试题目&#xff0c;当时还直播写了答案&#xff0c;但是总觉得写得不够好&#xff0c;现在把题目放出来。大家看看有没有什么更好的解法题目有一个字符串&#xff0c;如下&#xff0c;要求对字符串做反转后输出//input the sky is blue//output blue …

流媒体服务器搭建实例——可实现录音,录像功能

由于我也是刚开始接触这个东东&#xff0c;原理什么的不是很清楚&#xff0c;这里我就不说了&#xff0c;免得误人子弟&#xff0c;嘿嘿&#xff01;第一步&#xff0c;下载FlashMediaServer3.5&#xff0c;网上有很多资源&#xff0c;这里就不提供了&#xff0c;大家google一下…

一个女孩子居然做了十年硬件。​。。

本文转自面包板社区。--正文--2011年&#xff0c;一个三本大学的电子信息专业的大三女学生跟2个通信专业的大二男生组成了一组代表学校参加2011年“瑞萨杯”全国大学生电子设计大赛&#xff0c;很意外的获得了湖北赛区省三等奖&#xff0c;虽然很意外&#xff0c;但还是挺高兴的…

之前字符串反转的题目

之前发的字符串反转的题目这道字符串反转的题目&#xff0c;你能想到更好的方法吗&#xff1f;有很多人评论了&#xff0c;有的人还写了自己的解题思路&#xff0c;还有人写了自己的代码还有其中呼声很高的压栈解法我相信很多人在笔试的时候一定会遇到这类题目&#xff0c;给你…

hdu 3488

可以作为KM 二分图最大权匹配模板 View Code #include <stdio.h>#include <iostream>#include <string.h>using namespace std;const int N210;const int inf0x2fffffff;const int Max20000;int match[N],n,m,lack,w[N][N],lx[N],ly[N];bool vx[N],vy[N];bo…

心情不好,我就这样写代码

在 GitHub 上有一个项目&#xff0c;它描述了「最佳垃圾代码」的十九条关键准则。从变量命名到注释编写&#xff0c;这些准则将指导你写出最亮眼的烂代码。为了保持与原 GitHub 项目一致的风格&#xff0c;下文没有进行转换。读者们可以以相反的角度来理解所有观点&#xff0c;…

递归是会更秀strtok

前几天发的字符串反转题目&#xff0c;后面有一个新同学用了递归的方法来实现&#xff0c;看了下&#xff0c;真的是很秀。之前字符串反转的题目代码如下#include "stdio.h" #include "string.h" char input[] {"the sky is blue cris 1212321 apple…

ios开发网络篇—HTTP协议 - 转

一.URL 1.基本介绍 URL的全称是Uniform Resource Locator(统一资源定位符) &#xff0c;通过1个URL&#xff0c;能找到互联网唯一的1个资源 &#xff0c;URL就是资源的地址&#xff0c;位置&#xff0c;互联网上的每个资源都有一个唯一的URL 2.URL中常见的协议 (1)HTTP&#…

总结的一些内存问题

前言之前在实习时&#xff0c;听了 OOM 的分享之后&#xff0c;就对 Linux 内核内存管理充满兴趣&#xff0c;但是这块知识非常庞大&#xff0c;没有一定积累&#xff0c;不敢写下&#xff0c;担心误人子弟&#xff0c;所以经过一个一段时间的积累&#xff0c;对内核内存有一定…

云计算-从基础到应用架构系列-云计算的演进

为什么80%的码农都做不了架构师&#xff1f;>>> 开篇 本篇是主要讲述云计算的发展历程&#xff0c;由于云计算本身提出来也不是太久&#xff0c;并且其实云计算也是经过前人的一些经验总结提出&#xff0c;所以我们对之前的一 些计算机的发展史有个一定的了解&…

这样理解mmap,挺有意思!

大概雍正皇帝怎么也不会想到&#xff0c;自己在西历2022年的男生和女生眼里&#xff0c;会是截然不同的两种形象。1以我对身边同学朋友的观察&#xff0c;男生们大多爱看《雍正王朝》&#xff0c;他们眼中的雍正&#xff0c;大约是个推行了“火耗归公”、“摊丁入亩”等遏制贪腐…

软件开发中的11个系统思维定律

为什么80%的码农都做不了架构师&#xff1f;>>> http://sd.csdn.net/a/20101217/284119.html?1292550154 彼得圣吉在其著作《第五项修炼》中提到的系统思维定律同样适用于软件开发。 1. 今日的问题源于昨日的解决方案&#xff08;Today’s problems come from yes…

为什么我对流程情有独钟?

写这个标题的原因是我有一个同事兼朋友&#xff0c;他的名字刚好和流程谐音&#xff0c;最近他刚离职回苏州工作&#xff0c;在球场下&#xff0c;他是我的良师益友&#xff0c;在球场上&#xff0c;他是我们可以信任的队友&#xff0c;我们不仅一次把比我们高大、速度比我们快…

基于boost asio实现的支持ssl的通用socket框架

情景分析现已存在一个可用稳定的异步客户端类http_client_base&#xff0c;该类基于boost asio实现了连接服务器&#xff0c;发送请求&#xff0c;获取响应和解析http数据等操作&#xff0c;该类的大致实现框架如下1classhttp_client_base 2{ 3public: 4 http_client_ba…

C#创建简单的验证码

首先&#xff0c;创建一个CLASS类&#xff0c;然后需要add Reference的方式添加 System.Drawing&#xff08;画画的类&#xff09; 方法代码如下&#xff1a; 1/**//**//**//// <summary> 2 /// 定义显示的随机字符 3 /// </summary> 4 /// &…

昨天的事情想说一下

发那篇文章的目的昨天发文章之后&#xff0c;我的一个好朋友微信找我&#xff0c;跟我说了很多关于文章的事情&#xff0c;所以&#xff0c;我自己也思考了许多。关于泄愤这个事情&#xff0c;我还是挺想说的。可能很多人看到了一个不好的东西&#xff0c;然后网上发发这个&…