0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

通过mmap实现零拷贝技术

Linux阅码场 来源:Linux内核远航者 作者:Linux内核远航者 2022-06-28 17:33 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

1.开场白

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

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

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

d0997780-f677-11ec-ba43-dac502259ad0.png

2. 体验一下

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

驱动代码:

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

0copy_demo.c

#include
#include
#include
#include
#include


#defineMISC_DEV_MINOR5

staticchar*kbuff;


staticssize_tmisc_dev_read(structfile*filep,char__user*buf,size_tcount,loff_t*offset)
{

intret;

size_tlen=(count>PAGE_SIZE?PAGE_SIZE:count);

pr_info("######%s:%dkbuff:%s######
",__func__,__LINE__,kbuff);

ret=copy_to_user(buf,kbuff,len);//这里使用copy_to_user来进程内核空间到用户空间拷贝

returnlen-ret;
}

staticssize_tmisc_dev_write(structfile*filep,constchar__user*buf,size_tcount,loff_t*offset)
{
pr_info("######%s:%d######
",__func__,__LINE__);
return0;
}

staticintmisc_dev_mmap(structfile*filep,structvm_area_struct*vma)
{
intret;
unsignedlongstart;

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页

returnret;
}

staticlongmisc_dev_ioctl(structfile*filep,unsignedintcmd,unsignedlongargs)
{
pr_info("######%s:%d######
",__func__,__LINE__);
return0;
}



staticintmisc_dev_open(structinode*inodep,structfile*filep)
{
pr_info("######%s:%d######
",__func__,__LINE__);
return0;
}

staticintmisc_dev_release(structinode*inodep,structfile*filep)
{
pr_info("######%s:%d######
",__func__,__LINE__);
return0;
}


staticstructfile_operationsmisc_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,
};

staticstructmiscdevicemisc_dev={
MISC_DEV_MINOR,
"misc_dev",
&misc_dev_fops,
};

staticint__initmisc_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######
",__func__,__LINE__);
return0;
}

staticvoid__exitmisc_demo_exit(void)
{
free_page((unsignedlong)kbuff);

misc_deregister(&misc_dev);
pr_info("######%s:%d######
",__func__,__LINE__);
}

module_init(misc_demo_init);
module_exit(misc_demo_exit);
MODULE_LICENSE("GPL");

应用代码:test.c

#include
#include
#include
#include
#include
#include
#include



intmain(intargc,char**argv)
{

intfd;
char*ptr;
charbuff[32];

fd=open("/dev/misc_dev",O_RDWR);//打开字符设备
if(fd< 0) {
  perror("failtoopen");
return-1;
}

ptr=mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);//映射字符设备到进程的地址空间权限为可读可写映射为共享大小为一个页面
if(ptr==MAP_FAILED){
perror("failtommap");
return-1;
}


memcpy(ptr,"helloworld!!!", 15);//写mmap映射的内存直接操作,不需要进行特权级别的陷入!


if(read(fd,buff,15)==-1){//读接口来读取映射的内存,这里会进行内核空间到用户空间的数据拷贝(需要调用系统调用在内核空间进行拷贝,然后才能访问)
perror("failtoread");
return-1;
}
puts(buff);

pause();
return0;
}

Makefile文件:

exportARCH=arm64
exportCROSS_COMPILE=aarch64-linux-gnu-

KERNEL_DIR?=~/kernel/linux-5.11
obj-m:=0copy_demo.o

modules:
$(MAKE)-C$(KERNEL_DIR)M=$(PWD)modules

app:
aarch64-linux-gnu-gcctest.c-otest
cptest$(KERNEL_DIR)/kmodules

clean:
$(MAKE)-C$(KERNEL_DIR)M=$(PWD)clean

install:
cp*.ko$(KERNEL_DIR)/kmodules

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

编译驱动模块代码:
$makemodules

编译并拷贝应用:
$makeapp

拷贝驱动模块到qemu:
$makeinstall

加载驱动代码:
#insmod0copy_demo.ko
[23328.532194]######misc_demo_init:91######

查看生成的设备节点:
#ls-l/dev/misc_dev
crw-rw----10010,5Apr719:26/dev/misc_dev

后台运行应用程序:
#./test&
#[23415.280501]######misc_dev_open:56######
[23415.281052]######misc_dev_read:20kbuff:helloworld!!!######
helloworld!!!

查看test的pid:
#pidoftest
1768


查看内存映射:
#cat/proc/1768/maps
aaaabc5a0000-aaaabc5a1000r-xp0000000000:198666193/mnt/test
aaaabc5b0000-aaaabc5b1000r--p0000000000:198666193/mnt/test
aaaabc5b1000-aaaabc5b2000rw-p0000100000:198666193/mnt/test
aaaacf033000-aaaacf054000rw-p0000000000:000[heap]
ffff8a911000-ffff8aa52000r-xp00000000fe:00152/lib/libc-2.27.so
ffff8aa52000-ffff8aa61000---p00141000fe:00152/lib/libc-2.27.so
ffff8aa61000-ffff8aa65000r--p00140000fe:00152/lib/libc-2.27.so
ffff8aa65000-ffff8aa67000rw-p00144000fe:00152/lib/libc-2.27.so
ffff8aa67000-ffff8aa6b000rw-p0000000000:000
ffff8aa6b000-ffff8aa88000r-xp00000000fe:00129/lib/ld-2.27.so
ffff8aa91000-ffff8aa92000rw-s0000000000:05152/dev/misc_dev//映射设备文件到用户空间
ffff8aa92000-ffff8aa94000rw-p0000000000:000
ffff8aa94000-ffff8aa96000r--p0000000000:000[vvar]
ffff8aa96000-ffff8aa97000r-xp0000000000:000[vdso]
ffff8aa97000-ffff8aa98000r--p0001c000fe:00129/lib/ld-2.27.so
ffff8aa98000-ffff8aa9a000rw-p0001d000fe:00129/lib/ld-2.27.so
ffffecb5a000-ffffecb7b000rw-p0000000000:000[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函数参数如下:

intremap_pfn_range(structvm_area_struct*vma,unsignedlongaddr,
¦unsignedlongpfn,unsignedlongsize,pgprot_tprot)

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映射原理的图示:

d0adc1d6-f677-11ec-ba43-dac502259ad0.png


4.应用场景

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

fbmem_init//drivers/video/fbdev/core/fbmem.c
->register_chrdev(FB_MAJOR,"fb",&fb_fops)//注册framebuffer字符设备

->structfile_operationsfb_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来将内核空间映射的那块物理页再次映射到用户空间即可,这就实现了用户空间和内核空间的数据共享,这和用户进程之间的共享内存机制非常相似,都需要操作进程的页表将这段物理内存映射到进程虚拟地址空间。

原文标题:5.总结

文章出处:【微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。

审核编辑:汤梓红
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 处理器
    +关注

    关注

    68

    文章

    20148

    浏览量

    246942
  • 内核
    +关注

    关注

    4

    文章

    1436

    浏览量

    42481
  • Linux
    +关注

    关注

    88

    文章

    11627

    浏览量

    217893
  • 内存映射
    +关注

    关注

    0

    文章

    15

    浏览量

    7581

原文标题:5.总结

文章出处:【微信号:LinuxDev,微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    实测2778MB/s,AMP核间通信“快如闪电”,瑞芯微RK3576

    的AMP核间通信——共享内存方案,具有“ 拷贝 ”、“ 高带宽 ”的显著优势,直接解决用户痛点,下面用实测数据说话!   ▍共享内存方案优势 拷贝 “共享内存方案”能够让不同核心直
    的头像 发表于 12-04 14:14 22次阅读
    实测2778MB/s,AMP核间通信“快如闪电”,瑞芯微RK3576

    磁通电流探头的技术原理与应用分析

    磁通电流探头通过磁平衡技术解决传统开环探头的测量瓶颈,具有高精度、宽频带、低插入阻抗和良好的温度稳定性,广泛应用于电力电子和精密测量领域。
    的头像 发表于 10-20 09:31 190次阅读

    通过PWM全桥转换器实现电压开关

    设计小贴士中,将演示对脉宽调制 (PWM) 控制的全桥的简单修改,该全桥可以通过实现电压开关 (ZVS) 来提高效率,并消除变压器绕组上的谐振振铃。
    的头像 发表于 10-07 10:08 1094次阅读
    <b class='flag-5'>通过</b>PWM全桥转换器<b class='flag-5'>实现</b><b class='flag-5'>零</b>电压开关

    碳园区如何实现智慧管理?有哪些方面?

    碳园区是指在园区内,通过综合运用能源转型、产业升级、技术创新、管理优化等手段,实现园区运营全生命周期内碳排放总量持续下降并趋近于的综合性
    的头像 发表于 08-07 13:35 470次阅读
    <b class='flag-5'>零</b>碳园区如何<b class='flag-5'>实现</b>智慧管理?有哪些方面?

    黑芝麻智能一芯多域拷贝共享内存技术:破解车载大数据传输效能困局

    通过 拷贝共享内存技术 ,黑芝麻智能解决车载多域间大数据传输的延迟与资源消耗问题。核心技术包括 全局内存管理单元 和 dmabuf机制优化
    发表于 06-23 17:53 1655次阅读
    黑芝麻智能一芯多域<b class='flag-5'>零</b><b class='flag-5'>拷贝</b>共享内存<b class='flag-5'>技术</b>:破解车载大数据传输效能困局

    安科瑞微电网能源管理系统助力园区实现碳工厂 实现低碳转型

    竞争力、规避碳关税壁垒的战略选择。然而,实现工厂运营的“净排放”,需突破能源结构单一、能效管理粗放、碳排数据模糊等瓶颈。 1、 什么是碳工厂? 碳工厂是指以
    的头像 发表于 03-28 14:05 603次阅读
    安科瑞微电网能源管理系统助力园区<b class='flag-5'>实现</b><b class='flag-5'>零</b>碳工厂 <b class='flag-5'>实现</b>低碳转型

    飞凌嵌入式ElfBoard ELF 1板卡-内核空间与用户空间的数据拷贝之数据拷贝介绍

    在Linux系统中,内核空间和用户空间是两个独立的地址空间,它们有不同的访问权限和内存保护机制。在内核空间和用户空间之间进行数据传输时,需要进行数据拷贝操作。Linux内核提供了几种方法来实现内核
    发表于 03-19 08:55

    USB3.0移动硬盘拷贝机 如何选 ?

    捷美USB3.2移动硬盘拷贝机专为大规模硬盘复制设计,一次可同时复制40颗硬盘,仅需3.5小时完成,比传统计算机拷贝快500%。采用一键式操作,无需人工监控,降低人力成本。内建bit to bit比对功能,确保数据完整性和安全性。独立运行,开机即用,无需计算机或额外软件,
    的头像 发表于 03-18 17:16 1203次阅读
    USB3.0移动硬盘<b class='flag-5'>拷贝</b>机 如何选 ?

    如何通过高效工程评审EQ流程,实现PCB缺陷制造?

    如何通过高效工程评审实现PCB缺陷制造?关键步骤解析!​ 在PCB制造中,​Gerber文件是设计到生产的核心桥梁,但超过30%的原始文件存在需澄清的风险。如何通过高效的工程评审(
    的头像 发表于 03-07 14:51 1849次阅读

    四频拷贝遥控器走俏海外

    常有同事反馈和客户沟通时,需要了解客户的遥控器频率才能推荐合适的拷贝遥控器,这就需要客户拆开遥控器查看才能确定,而有些扣位结合的原装遥控器,强行拆开会导致遥控器损坏,对于客户来讲就是得不偿失的事了
    的头像 发表于 02-08 15:34 877次阅读
    四频<b class='flag-5'>拷贝</b>遥控器走俏海外

    FB08 1对7 U盘拷贝格式化机——高效数据复制工具

    FB08 1对7 U盘拷贝格式化机,采用台湾捷美原厂工艺,具备25MB/s的高速拷贝速度和8口同步复制功能,支持加密U盘免解密拷贝及USB 2.0/3.0设备。支持FAT16/FAT32格式化,兼容
    的头像 发表于 02-08 13:51 767次阅读
    FB08 1对7 U盘<b class='flag-5'>拷贝</b>格式化机——高效数据复制工具

    FB16 1对15 U盘拷贝格式化机——高效数据复制工具

    FB16 1对15 U盘拷贝格式化机,采用台湾捷美原厂工艺,具备25MB/s高速拷贝速度和16口同步复制功能,支持加密U盘免解密拷贝及USB 2.0/3.0设备。支持FAT16/FAT32格式化
    的头像 发表于 02-07 17:36 747次阅读
    FB16 1对15 U盘<b class='flag-5'>拷贝</b>格式化机——高效数据复制工具

    碳节能工厂是什么?有什么功能?如何实现?  

    碳节能工厂是一种在生产制造过程中实施技术性节能减排措施,实现工厂综合碳排放表现为的目标的工厂。这种工厂不仅所使用的能源全部来自于可再生能源,如太阳能、风能、水能等清洁能源,而且在生
    的头像 发表于 12-17 17:07 803次阅读

    SATA HDD/SSD工业用硬盘拷贝机优势分析:对比市面对拷机

    探索U-Reach SATA硬盘拷贝机如何以其高速拷贝、坏轨处理能力和数据擦除功能,超越传统硬盘对拷机。这款工业级设备支持多种硬盘格式和接口,提供直观操作界面和便携设计,是数据管理和恢复的理想选择。
    的头像 发表于 12-17 16:18 1057次阅读
    SATA HDD/SSD工业用硬盘<b class='flag-5'>拷贝</b>机优势分析:对比市面对拷机

    M.2硬盘拷贝,该怎么选择适合的工具?FPGA拷贝机到底有没有用?

    探索SP-B1011 M.2 FPGA硬盘拷贝机,一款支持NVMe和SATA接口的高效数据管理工具。无需计算机,即插即用,提供快速拷贝、全盘拷贝和安全抹除模式。适用于个人和专业IT人士,满足各种数据备份和恢复需求。
    的头像 发表于 12-12 11:02 1076次阅读
    M.2硬盘<b class='flag-5'>拷贝</b>,该怎么选择适合的工具?FPGA<b class='flag-5'>拷贝</b>机到底有没有用?