当前位置: 首页 > 科技观察

这样理解mmap很有意思!

时间:2023-03-19 21:41:56 科技观察

或许雍正皇帝永远也想不到,在西历2022年的少男少女眼中,他会有截然不同的两种形象。1根据我对身边同学朋友的观察,大部分男生都喜欢看《雍正王朝》。在他们眼里,雍正很可能是实行“消火还公”、“分钱入亩”等政策遏制贪污减税的人。著名的改革家是一个惊心动魄、腹黑的政治家,经历了九子夺位。他是一个工作狂,登基后依然努力工作,熬夜加班996。大部分女生都喜欢看《甄嬛传》。在他们眼里,雍正就是一个“大胖橘子”、“大猪蹄”。最后是被牛玉禄和甄嬛气死的负心汉。甄嬛传我没看完,不过有幸和家人一起在吃饭的时候看了几集。正所谓后宫佳丽三千,铁杵磨成绣花针……又用了一些奇招,其中出现的次数比较多。根据我的总结,这个技巧的核心思想是:使用者既然无法得到正确的拥有者,只能拿自己身边的布片等物品模仿小人,将主人的经脉画在身上它,写上名字,施展某种魔法,然后用针刺入手边小人的某个穴位,远处的主人相应部位也会受到同样的折磨。虽然有点神奇,但却让人羡慕,高不可攀。不过在linux内核的开发中,可以利用mmap机制来实现类似的效果。2mmap的核心思想是:由于用户在用户态不能直接操作寄存器的物理地址,所以采用mmap的方式进行内存映射,在用户态将物理地址映射到虚拟地址,然后用户就可以读写手边的虚拟地址,实现对物理地址的读写。两者的共同点是,由于不能直接操作目标,所以通过一定的方法在可以操作的东西和目标之间建立映射关系,从而实现打哪里打哪里,哪个痛.只要能够建立到目标的映射,我们自然会有很大的想象空间。所以mmap有很多用途,有人用它来实现进程间通信。有些人用它来移动数据。对于我们嵌入式工程师来说,可以用它来点灯。哎,想不出来了!让我慢慢告诉你。3作为一名嵌入式工程师,花哨的灯光是必不可少的技能。无论是写裸机代码来操作GPIO口,还是通过物联网云端远程控制LED,从硬件角度来说,核心原理都是找到连接LED的GPIO口,让它输出电信号。从软件的角度来看,最终的目的是找到GPIO口对应的寄存器地址,让CPU根据实际电路要求写入一个1或者0。在裸机上开发时,我们可以直接找到物理地址进行操作。但在Linux系统中略有不同。因为操作系统中有内核空间,我们写的程序都是运行在用户态,硬件需要内核驱动,不能直接操作物理地址。当然你也可以选择为这个LED写一个驱动,从而在用户空间通过读写来操作它的状态。但是,有的同学听说要写司机,就想唱一首蜀道难的诗来表达自己的犹豫。所以一直不懂驱动的同学也可以选择使用更直接的方式:mmap。就好像可以选择给妃子下药来控制她,只是这种方法需要熟练掌握药理和时机,成本高,难度大。只要能达到目的,有时候绑个小人或许更省钱。在上一家公司的时候,用ARMCortex-A9芯片做一个项目。开发过程大概是先和硬件同事约定一个协议,然后我通过GPIO口的输入输出模拟这个协议,通过它读取寄存器。编写配置,驱动硬件ADC进行采样,然后将采集到的数据通过DMA传输,最后到应用层进行分析处理。驱动GPIO口的部分使用了mmap。项目很大,搞了半年多。完全解释它不太现实,但我们可以从驱动GPIO口入手,体验一下软件驱动硬件的神秘过程。如果你很聪明,你可以举一反三。4作为一个软件工程师,当你拿到板子的时候,硬件工程师通常会给你一个文档,像这样:这个文档会说明如果你要操作这个GPIO端口,需要使用GPIO外设的基地址加上偏移地址移位找到对应的寄存器地址,然后使用位操作将命令写入指定位。但是,由于我对FPGA了解很多,所以我在我们公司构建了FPGA的框图。搭建好FPGA硬件工程后,做一个综合,从AddressMap中可以看到我要使用的GPIO端口地址。如图:不管怎样,你现在有了所谓的硬件寄存器地址,然后我们就可以和小人绑在一起了。上图中我拿到的0x43C00000就是一个例子。这是寄存器的地址。能不能在应用程序中直接把0x43C00000赋给一个指针,然后读写呢?玩裸机时确实如此。但是上面说了,linux系统是有虚拟内存的,所以你不能这么做。因为理论上我可以在系统中开启100个进程,而这100个进程的地址都是0x43C00000,那么这100个地址中哪个才是真正的寄存器地址呢?可能两者都不是。因为进程中的0x43C00000是虚拟的,没有人知道它实际对应的物理地址在哪里。要将虚拟地址映射到物理地址,必须使用mmap进行内存映射。5mmap的函数接口定义如下:voidmmap(voidaddr,size_tlength,intprot,intflags,intfd,off_toffset);有很多参数。其中addr一般指定为NULL,prot用于设置映射区域的权限,如是否可读可写;flags用于指定是共享映射还是私有映射;而fd、offset、length三个参数表示fd对应的文件,从offset位置开始,将length长度的内容映射到进程的地址空间。需要注意的是,mmap的运行单位是页,即最后一次映射的offset参数必须是内存页大小的整数倍,Linux系统内存页大小一般为4096字节。我程序中的示例调用:#defineAXI_GPIO_BASEADDR0x43C00000intmemfd=open("/dev/mem",O_RDWR|O_SYNC);if(-1==memfd){printf("无法打开/dev/mem\n");返回-1;}unsignedint*led_gpio=(unsignedint*)(mmap(NULL,MMAP_SIZE,PROT_READ|PROT_WRITE,MAP_SHARED,memfd,AXI_GPIO_BASEADDR));调用mmap后,我们得到一个指针,通过这个指针对指向的地址做任何操作,对应的寄存器物理地址也会有同样的效果。于是我们循环给它赋值0101,对应寄存器控制的GPIO口输出电信号,于是板上的灯就成功闪了,类似于奥特曼筋疲力尽时的能量灯。6多说几句,mmap除了操作GPIO/字符设备,还有操作块设备的常用场景。它与传统使用read和write的区别在于,最重要的是保存一份。比如要读取磁盘上某个文件的数据,如果使用readwrite,因为涉及到系统调用,进程不能直接访问内核,所以在read系统调用返回之前,内核需要将数据从内核复制到缓冲区中指定的进程。但是如果你使用mmap,那么这块数据会先被复制到内存中作为页面缓存(即pagecache)。使用mmap将这段内存映射到用户空间,那么进程可以直接通过指针读写pagecache,不需要冗余的系统调用和内存拷贝。但是,虽然少了一份,mmap还是会触发页面错误(pagefault),比内存复制的代价更大。所以在性能上,mmap在大多数情况下并不比read/write好。说到页面缓存,我在之前的公司做项目的时候,也被脏页延迟坑过。限于篇幅,pagecache涉及缺页中断、脏页、延迟回写、sync强制回写等,下次再详细说。7好了,我们就学会了用mmap点灯了。想象下一个场景:你对公司研发部最漂亮的女同事说,“嗨,领导给我们安排了一个新任务,你能写一个驱动来控制LED吗?”“嗯?司机太难了,我做不到。”“哦,没关系,那你就用mmap吧。”“喂,你骂什么!”