mmap那些事之android property实现之二
1 基于tmpfs的mmap系统调用过程
前面一篇blog:mmap那些事之android property实现,讲述了android的属性系统是基于tmpfs的mmap来实现内存的共享,只是论述了应用层的使用,并未涉及到内核空间是怎么处理的。 包括如下几个问题: mmap系统调用过程 tmpfs文件针对mmap做了哪些处理?这里包括tmpfs是怎样分配实际的物理内存到共享内存的,然后其他应用进程映射到这个tmpfs的文件时,又是怎么取得这个共享物理内存的,并且又是怎么建立到自己所属进程的地址空间映射页表的)2 mmap的系统调用过程
首先简单说明下mmap的系统调用过程:do_mmap是应用空间mmap调用在内核空间入口,该函数前面只是做了些参数的合法性检查,在这里addr一般为0,如果不为0,则说明应用空间希望内核使用该地址作为虚拟地址的开始地址,但实际返回的地址是由当前进程的地址空间使用情况决定的,所以返回值并不一定是用户希望的addr的值,应用空间应用使用mmap系统嗲用返回的addr值。len参数指定映射的虚拟内存的长度。我们直接转到do_mmap_pgoff函数: ............
............
上面的line998 addr参数就是linux mm系统自动为我们分配的这段映射内存的开始虚拟地址 mmap_region函数主要做如下几件事情:
首先将[addr,addr+len]的这段虚拟地址空间的之前的映射拆除掉。
其次将[addr,addr+len]这段地址范围跟相邻的wma进行合并
如果不能合并,则分配新的wma结构体来管理[addr,addr+len]这段地址范围
最重要的地方出现了,如下图高亮部分,执行该映射文件句柄对应的mmap操作函数,该函数是需要支持mmap系统调用的驱动来实现的。file_operations中的mmap操作函数的实现方法有两种典型实现,ldd3的参考书籍上有详细的描述和实例,在这里,我们发现op->mmap的函数原型跟系统调用的mmap函数原型已经简化多了,因为linux的mm子系统已经为我们做了大部分事情,譬如已经为我们找到了一块合适的虚拟内存空间(vma数据结构体来表示)来为映射具体的物理内存空间做准备,并且在调用了驱动中的mmap函数后,将这个vma结构体连接到当前进程的mm结构体中。
最后,将vma数据结构连接到所属进程的mm内存管理数据结构中,
3 tmpfs对mmap调用的支持
现在回到我们的主菜,即android上层在调用open创建一个文件:/dev/__properties__,接着针对该文件执行mmap系统调用,这个时候内存做了些什么事情?他们为什么选择在/dev目录下,而不是其他目录,譬如/data目录下创建并映射这个文件可以吗? 带着这些问题,开始我们的内核之旅:
在linux控制台执行mount命令:
如上图的黑色高亮部分显示的,/dev/目录下的文件对应的都是tmpfs文件。所以我们的讨论得从tmpfs开始。linux内核部分,tmpfs文件系统的实现时在trunk/mm/shmem.c文件中。在这里限于篇幅,我就不展开说,一个linux内核是如何实现并注册一个文件系统的。
3.1 tmpfs的open调用流程
android上层通过调用:fd = open("/dev/__properties__", O_RDWR | O_CREAT, 0600);语句来创建一个tmpfs的文件:/dev/__properties__,该文件由于是首次打开,所以打开的时候就会创建它,见内核的open调用中的如下过程:line2259 判断该文件不存在,则在line2281处调用vfs_create来创建目录dir下的对应于dentry的文件。
由于对应于tmpfs文件系统,所以line2074对应的i_op结构体就是:
所以就调用static int shmem_create(struct inode *dir, struct dentry *dentry, umode_t mode,struct nameidata *nd)函数来创建对应于/dev/__properties__的文件,在执行这个函数时,参数dir对应的目录名称应该是dev,参数dentry目录项对应的文件名字应该就是__properties__。以上函数最终调用如下函数:
继续展开shmem_get_inode函数
至此,在应用层调用open函数,tmpfs主要是通过shmem_create函数来在/dev/目录下,创建一个__properties__文件,主要是生成该文件对应的inode节点,并且初始化该inode节点,并将该节点跟dentry关联起来,最终会将这个两个重要成员填充到 struct file结构体成员中,并返回对应的文件句柄。
3.2 tmpfs的mmap调用过程
至此android应用在获取到open返回的文件句柄后,调用如下函数来将共享内存映射到自己的进程地址空间: data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
该应用层的mmap系统调用在经历上述描述的mmap内核通用层的调用历程后,会掉到驱动中的mmap的实现,在这里的就是上面line1151种的shmem_file_operations结构体中的mmap成员函数:
针对struct file_operations操作中的mmap的实现,ldd3中有详细的描述,概括的将分为两种实现方式: 一种是通过调用remap_pfn_range事先就将虚拟到物理地址的映射表建立好,此种方法在mmap调用完后,虚拟到物理映射的页表已经建立好了。 一种是通过nopage的方式来实现。这方式,其实在调用完mmap后,虚地到物理地址的映射还没建立,而是在应用具体到访问到这个虚地址时,会产生page fault错误,在缺页处理中,会调用nopage获得虚拟地址对应的物理地址,并将对应的虚拟到物理的映射表建立好。 而tmpfs的mmap的实现是使用的第二种方法:
line1058 最终会调用shmem_getpage_gfp函数,展开如下:
继续上面的函数,省略部分不相关的代码
继续上面的函数,省略部分不相关的代码
结合上面代码中的注释,应该不难理解这个共享物理内存页的分配及管理。
4 应用层如何使用mmap建立共享内存
这里假设有两个进程:进程A和进程B,他们要共享一块物理内存:region_c(通过/dev/目录的tmpfs文件: ashmem_test),再具体点,譬如进程A就是camera进程,进程B就是H264的编码线程所在的进程。则一个camera的录像过程就是:进程A获取未压缩的yuv原始视频数据,并将该视频数据数据放置在共享内存region_c区域,然后通过内存共享,进程B可以直接从共享内存region_c区域取出未压缩的原始视频数据作为该编码进程的输入,进行编码。这样就避免了大量的数据在跨进程间的拷贝,从而结余了大量的cpu时间和物理内存空间。
下面是一个如何使用共享内存的一个简单列子,需要特别注意的有以下几点:
- 关闭文件句柄,并不会清除映射
- 只有在调用munmap或是进程被终止时,才会拆除进程对应的mmap映射
- mmap系统调用后,并不会马上建立映射,而是会在随后对映射后的地址空间进行访问(读或写时)才会真正去分配物理内存,并建立相应的映射
- 对零长度的文件,可以进行mmap,但访问不成功。所以必须设置一个非零的文件长度,一般就设置为共享内存的大小。
- 映射和拆除映射都是以物理页为单位的
#include <sys/mman.h>#include <stdio.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#define SIZE_MAP 1024*4#define FILE_NAME "/dev/__properties5__"int printf_buf(unsigned char * buf, int len){int i;for(i =0; i<len; i++){printf("buf[%d]:0x%x ",i,buf[i]);if(!(i%4))printf("\n");}}int main(int argc, char * argv[]){int ret = 0;int fd = 0;unsigned int i =0 ;unsigned char *buf = NULL;if(!strcmp(argv[1],"a")) //进程A{fd = open(FILE_NAME,O_RDWR | O_CREAT,0777);if(fd > 0)printf("open success fd:%d\n",fd);elseprintf("open failed\n");lseek(fd,10,SEEK_SET); //注意必须要设置下文件的大小,如果文件大小是0,则不能映射 write(fd,"",1); buf = (unsigned char *) mmap(NULL,SIZE_MAP,PROT_READ|PROT_WRITE, MAP_SHARED,fd,0);if (buf == MAP_FAILED) {printf("mmap failed for fd:%d\n",fd); close(fd); return -1; }printf("mmap success buf:%x \n",buf);printf_buf(buf, 50);//打印映射后初始化前的buf的内容memset(buf,0x5a,SIZE_MAP);//设置映射后的buf的内容close(fd);//关闭文件句柄,并不清楚映射,映射只有在munmap或进程被终止时才会被拆除for(i=0; i<20; i++) //设置映射后的buf的内容{buf[i] = 'a'+i;}munmap(buf,SIZE_MAP); //拆除进程A的映射printf("set share memory buf to 'a' for process A \n");// do not close fd}else if(!strcmp(argv[1],"b")) //进程B{fd = open(FILE_NAME,O_RDWR);if(fd > 0)printf("open success\n");elseprintf("open failed\n");buf = (unsigned char *) mmap(NULL,SIZE_MAP,PROT_READ|PROT_WRITE, MAP_SHARED,fd,0); //将内存映射到进程Bif (buf == MAP_FAILED) {printf("mmap failed for fd:%d\n",fd); close(fd); return -1; }close(fd); //关闭文件句柄,并不清除映射,映射只有在munmap或进程被终止时才会被拆除printf("mmap for process B success,buf:%x\n",buf);printf("printf share memory for process B\n");printf_buf(buf, 50);//打印共享内存的内容,在进程B中munmap(buf,SIZE_MAP);//拆除进程B的映射printf("completed\n");}else{printf("please inpu the correct parameter:\n");printf("shmem a \n");printf("or \n");printf("shmem b \n");}return 0;}
以上测试程序在X86 linux pc机上编译、测试结果如下:
rubbitxiao@szmce15:~/test/share_memory$ rm -rf shmem
rubbitxiao@szmce15:~/test/share_memory$
rubbitxiao@szmce15:~/test/share_memory$
rubbitxiao@szmce15:~/test/share_memory$ gcc -o shmem shmem.c
rubbitxiao@szmce15:~/test/share_memory$
rubbitxiao@szmce15:~/test/share_memory$
rubbitxiao@szmce15:~/test/share_memory$ ls -l
total 24
-rwxrwxr-x 1 rubbitxiao rubbitxiao 13070 Dec 8 20:51 shmem
-rwxr--r-- 1 rubbitxiao rubbitxiao 2447 Dec 8 20:50 shmem.c
-rw-rw-r-- 1 rubbitxiao rubbitxiao 3000 Dec 8 18:13 shmem.o
rubbitxiao@szmce15:~/test/share_memory$ sudo ./shmem a
open success fd:3
mmap success buf锛歝b52d000
buf[0]:0x0
buf[1]:0x0 buf[2]:0x0 buf[3]:0x0 buf[4]:0x0
buf[5]:0x0 buf[6]:0x0 buf[7]:0x0 buf[8]:0x0
buf[9]:0x0 buf[10]:0x0 buf[11]:0x0 buf[12]:0x0
buf[13]:0x0 buf[14]:0x0 buf[15]:0x0 buf[16]:0x0
buf[17]:0x0 buf[18]:0x0 buf[19]:0x0 buf[20]:0x0
buf[21]:0x0 buf[22]:0x0 buf[23]:0x0 buf[24]:0x0
buf[25]:0x0 buf[26]:0x0 buf[27]:0x0 buf[28]:0x0
buf[29]:0x0 buf[30]:0x0 buf[31]:0x0 buf[32]:0x0
buf[33]:0x0 buf[34]:0x0 buf[35]:0x0 buf[36]:0x0
buf[37]:0x0 buf[38]:0x0 buf[39]:0x0 buf[40]:0x0
buf[41]:0x0 buf[42]:0x0 buf[43]:0x0 buf[44]:0x0
buf[45]:0x0 buf[46]:0x0 buf[47]:0x0 buf[48]:0x0
buf[49]:0x0 set share memory buf to 'a' for process A
rubbitxiao@szmce15:~/test/share_memory$
rubbitxiao@szmce15:~/test/share_memory$
rubbitxiao@szmce15:~/test/share_memory$ sudo ./shmem b
open success
mmap for process B success,buf:25647000
printf share memory for process B
buf[0]:0x61
buf[1]:0x62 buf[2]:0x63 buf[3]:0x64 buf[4]:0x65
buf[5]:0x66 buf[6]:0x67 buf[7]:0x68 buf[8]:0x69
buf[9]:0x6a buf[10]:0x6b buf[11]:0x6c buf[12]:0x6d
buf[13]:0x6e buf[14]:0x6f buf[15]:0x70 buf[16]:0x71
buf[17]:0x72 buf[18]:0x73 buf[19]:0x74 buf[20]:0x5a
buf[21]:0x5a buf[22]:0x5a buf[23]:0x5a buf[24]:0x5a
buf[25]:0x5a buf[26]:0x5a buf[27]:0x5a buf[28]:0x5a
buf[29]:0x5a buf[30]:0x5a buf[31]:0x5a buf[32]:0x5a
buf[33]:0x5a buf[34]:0x5a buf[35]:0x5a buf[36]:0x5a
buf[37]:0x5a buf[38]:0x5a buf[39]:0x5a buf[40]:0x5a
buf[41]:0x5a buf[42]:0x5a buf[43]:0x5a buf[44]:0x5a
buf[45]:0x5a buf[46]:0x5a buf[47]:0x5a buf[48]:0x5a
buf[49]:0x5a completed
rubbitxiao@szmce15:~/test/share_memory$ ^C
rubbitxiao@szmce15:~/test/share_memory$
5 总结
最后回到我们开始提出的几个问题: 使用mmap实现内存共享的话,如果不想自己专门实现驱动层的mmap函数,则应该使用tmpfs提供的共享内存机制,所以必须要创建在基于tmpfs的文件系统中,至于文件叫什么名字都不重要 像之前举例的data分区,由于不是tmpfs文件系统,而是yaffs2文件系统,所以是不能用来实现内存共享的。 android属性系统对应的共享内存所对应的物理内存页都是由init进程分配的,并且挂在/dev/__properties__文件对应的file->f_path.dentry.d_inode->i_mapping中的平衡二叉树中。 所有其他以只读方式mmap这个/dev/__properties__文件的,则会去将init进程分配的物理内存页映射到自己的进程的地址空间,从而实现物理内存在多个进程间的共享。更多相关文章
- 一款常用的 Squid 日志分析工具
- GitHub 标星 8K+!一款开源替代 ls 的工具你值得拥有!
- RHEL 6 下 DHCP+TFTP+FTP+PXE+Kickstart 实现无人值守安装
- Linux 环境下实战 Rsync 备份工具及配置 rsync+inotify 实时同步
- 一款霸榜 GitHub 的开源 Linux 资源监视器!
- Android逆向之旅—Hook神器Cydia Substrate使用详解
- 《老罗的Android之旅》导读PPT
- 如何制作Ext4文件系统镜像
- Android(安卓)使用adb向虚拟机里安装APK