1.开场白 环境: 处理器架构:arm64 内核源码:linux-5.11 ubuntu版本:20.04.1 代码阅读工具:vim+ctags+cscope 我们知道,linux系统中用户空间和内核空间是隔离的,用户空间程序不能随意的访问内核空间数据,只能通过中断或者异常的方式进入内核态,一般情况下,我们使用copy_to_user和copy_from_user等内核api来实现用户空间和内核空间的数据拷贝,但是像显存这样的设备如果也采用这样的方式就显的效率非常底下,因为用户经常需要在屏幕上进行绘制,要消除这种复制的操作就需要应用程序直接能够访问显存,但是显存被映射到内核空间,应用程序是没有访问权限的,如果显存也能同时映射到用户空间那就不需要拷贝操作了,于是字符设备中提供了mmap接口,可以将内核空间映射的那块物理内存再次映射到用户空间,这样用户空间就可以直接访问不需要任何拷贝操作,这就是我们今天要说的0拷贝技术。
下面是正常情况下用户空间和内核空间数据访问图示:
2. 体验一下 首先我们通过一个例子来感受一下:
驱动代码:
注:驱动代码中使用misc框架来实现字符设备,misc框架会处理如创建字符设备,创建设备等通用的字符设备处理,我们只需要关心我们的实际的逻辑即可(内核中大量使用misc设备框架来使用字符设备操作集如ioctl接口,像实现系统虚拟化kvm模块,实现安卓进程间通信的binder模块等)。
0copy_demo.c
#include #include #include #include #include #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 ######, __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 ######, __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 ######, __func__, __line__); return 0;}static int misc_dev_open(struct inode *inodep, struct file *filep){ pr_info(###### %s:%d ######, __func__, __line__); return 0;}static int misc_dev_release(struct inode *inodep, struct file *filep){ pr_info(###### %s:%d ######, __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 ######, __func__, __line__); return 0;}static void __exit misc_demo_exit(void){ free_page((unsigned long)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 int main(int argc, char **argv){ int fd; char *ptr; char buff[32]; fd = open(/dev/misc_dev, o_rdwr); //打开字符设备 if (fd > 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映射原理的图示:
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来将内核空间映射的那块物理页再次映射到用户空间即可,这就实现了用户空间和内核空间的数据共享,这和用户进程之间的共享内存机制非常相似,都需要操作进程的页表将这段物理内存映射到进程虚拟地址空间。
原文标题:5.总结
文章出处:【微信公众号:linux阅码场】欢迎添加关注!文章转载请注明出处。
Microchip MOST150助力起亚旗舰轿车K900
2024年,中国区块链市场规模将达到22.8亿美元
激光盘煤仪整个盘点过程一气呵成
人工智能在航天方面的应用和展望是怎样的
关于GPS网关在智慧物流解决方案中的应用
通过mmap实现零拷贝技术
爱立信携手沃达丰在德国正式推出了5G商用网络
数据分析公司Splunk发布第一季度财报
集创北方宣布并购iML,1+1>2强化未来发展新动力
关于勒索软件攻击的场景案例分享
百度何俊杰:“人均一个数字人”时代已经到来
无人机面临“大跃进”危机,资本加入巨头难现
OC门和OD门的主要区别
农产品合格证一体机的应用原理是怎样的
CDN工作原理和访问过程
凌阳新款电机驱动微控制器增强定时计数和PWM输出
利用智能仪表AI808和MCGS组态软件设计串联双容水箱液位控制系统
一位从外企辞职加入阿里的IT工程师的故事
Google更新了其最新的Android版本市场份额表
集成自举二极管的超高集成度电机驱动MCU芯片介绍