binder传输大数据分析
Posted lin-0410
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了binder传输大数据分析相关的知识,希望对你有一定的参考价值。
**
内存映射Binder mmap
**
通过zygote去fork一个子进程(包括应用进程和systemserver)时,其步骤中会执行onZygoteInit()。
androidRuntime.h
AndroidRunTime的子类AppRuntime覆写了这个方法:
App_main.cpp
一个进程要具有提供Binder服务的能力,需要做的事情包括,打开Binder设备,做好内存映射,初始化好跟Binder驱动做命令交互的线程池,向ServiceManager注册服务等。
这些进程内共性的操作,由一个封装来完成会更方便,这个封装就是ProcessState,IPCThreadState,两个都是进程中的单实例。
ProcessState负责打开Binder设备和做内存映射,IPCThreadState负责跟Binder驱动做命令交互,这些操作,只在进程创建时执行一次。
ProcessState.cpp
在ProcessState构造函数中,执行了打开Binder设备,做内存映射的操作
解释参数初始化列表中的参数:其中
DEFAULT_MAX_BINDER_THREADS,表示默认初始化线程池的大小,默认值是15
也可以通过ioctl BINDER_SET_MAX_THREADS设置线程池大小。
默认的映射的内存大小是BINDER_VM_SIZE,1M-8kb,为什么要去减掉2页大小呢?
从google的代码修改注释看,内核中的后台存储需要一页保护页,所以分配1M大小空间时,实际占用的是1M+4kb,这会有比较糟糕的内存碎片,所以减去了2页大小来符合2的次方的大小,提高内核使用虚拟内存的效率。
CPU从内存读数据到缓存,是不可能一个字节一个字节去读的,而是一块一块的,这块的最小单位就是CacheLine,目前主流的cpu,cache line的大小是64Bytes,也即是16个32位的整数,cpu去内存读数据的单位一定是2的n次方。
所以上面,1M – 8kb + 4kb,就可以按照1M高效的读取了。
映射1M大小的空间是android fork进程时的默认行为,也是对普通androidservice的限制,如果要映射更大内存,需要自己实现打开binder设备,执行mmap的操作。
打开的binder设备是misc device,
Binder驱动支持的文件操作: binder_device->miscdev.fops = &binder_fops;有结构体binder_fops中函数指针完成。
另外,第三部分要说的匿名共享内存,也是一个misc设备。
最大能够映射内存的大小,可以通过mmap的代码了解:
Binder驱动的代码在kernel/msm-4.14/drivers/android/Binder.c
申请超过4M,不会报错,只分配4M,这是kernel对Binder传输数据大小的限制。
其中vma是应用程序使用的虚拟内存,vm_start, vm_end分别是这块连续的虚拟内存的起止点。
Binder传输数据大小的限制
在回头看内存映射的实现:
Binder_alloc.c
解释几个变量:
Binder_proc 这是Binder驱动为应用进程分配的一个数据结构,用于存储跟这个进程相关的所有信息,包括内存分配,线程管理等。当通过binder_open打开binder设备时,binder驱动会在proc系统目录下生成各种管理信息,binder_proc就是这些管理信息的记录体,每个进程都有独立的记录。
Binder_alloc 管理binder分配的缓冲区的地址空间的结构体,这个地址空间不仅用于表示用户可见的buffers,也保存在binder_buffer结构体中用于跟踪buffers的使用。
Binder_buffer 用于记录binder传输中buffers的使用。
alloc->buffer_size 通过mmap指定的总的地址空间
alloc->free_async_space 用于异步调用的总的地址空间,是alloc->buffer_size的一半,异步调用就是标记为ONE_WAY的接口或者方法,是单项的请求,就是client发起请求后,就返回了,不会被驱动wait,也不需要等待server端返回结果。
异步请求的优先级低于同步请求。
TransactionTooLargeException.java
一次binder请求或者返回响应的数据,如果大于transaction buffer就会抛出这个异常。
Binder transaction buffer有一个限制的固定大小,就是上面的alloc->buffer_size,alloc->free_async_space,这个空间是一个进程中所有在进行中的transactions共享的,所以异常可能在这样一个情况下抛出:多数的单独的transactions数据是中等大小,但是有很多这样的transaction在进行中,也会抛出这个异常。
**
匿名共享内存实现跨进程大数据传输
**
前一部分已经明确了,Binder是不能直接跨进程传输大数据的,借助匿名内存共享可以实现跨进程大数据的传递。
下面以Bitmap为例,说明如何跨进程传输bitmap数据。
在看bitmap的具体代码之前,先介绍一个类Parcel,翻译过来就是“打包”,这个是进程间数据传输的一个载体,可以类比寄快递时的包装盒。Parcel提供了丰富的接口,方便读写基本数据类型,封装类型。
封装类型想要具备这种打包重组的能力,就要实现一个接口Parcelable,如android中最常用的传递Intent数据的Bundle就实现了这个接口。
Parcel除了可以打包实现了Parcelable接口的对象,还可以打包文件描述符FileDescriptor,这个就跟匿名共享内存相关了。
都有那些类型可以通过Parcel打包跨进程传递,可以查看:
Parcel.java
Bitmap实现了Parcelable接口,下面看Bitmap如何打包的。
Bitmap.java
直接看Native层的实现:
Bitmap.cpp
注释写的很清晰,如果是not mutable,且parcel允许传递fds(可以设置不允许fds),就会把fd写入了Parcel。
不满足上面的条件,就把bitmap数据拷贝到blob中
看一下blob缓冲区:
Parcel.cpp
如果bitmap小于Blob可传递的数据,就直接写入blob中:
static const size_t BLOB_INPLACE_LIMIT = 16 * 1024;
如果bitmap超过16kb,还是写入存储bitmap的fd。
所以,在android中跨进程传递一个超过16kb的bitmap时,实际传递的是匿名共享内存的fd。
为了方便使用匿名共享内存,android framework层提供了MemoryFile.java文件,它实际是对ShareMemory的封装。
MemoryFile的使用比较简单,传入构造匿名共享内存的文件名字和大小,创建MemoryFile,然后通过Binder把memoryFile的文件描述符传给另一个进程,接收进程拿到文件描述符就可以读文件了。
MemoryFile.java
Native层使用匿名共享内存的方式:
可以直接使用ashmem_create_region()方法,也可以使用Sharedmem.cpp这个类
ashmem_create_region方法的实现在Ashmem-dev.cpp:
system/core/libcutils/Ashmem-dev.cpp
也可以使用另外一个类MemoryDealer.cpp,可以看作是对Ashmem的封装。
/framework/native/libs/binder/MemoryDealer.cpp
MemoryHeapBase.cpp
通过ashmem_create_region打开ashmem设备,返回文件描述符,通过mapfd把设备空间映射到当前进程。
借助Binder把fd共享到其他进程。
Android源码中使用MemoryDealer的地方也很多,如音频回放,AudioTrack和AudioFlinger就是通过MemoryDealer实现的跨进程音频数据的传递。
**
匿名共享内存跨进程共享的原理
**
在说明匿名共享内存的原理前,先来说明一个问题:binder是怎么实现把一个进程的文件描述符传递到另外一个进程后,还能够访问这块缓冲区的。
在Linux系统中,文件描述符就是一个整数,每个进程在内核空间都有一个打开文件的数组,这个文件描述符的整数值就是用来索引这个数组的,所以这个文件描述符只在本进程内有效,所以不同的进程中,相同的文件描述符的整数值,可能代表了不同的打开文件。
因此,在进程间传递文件描述符时,不是简单的把这个整数值从一个进程传到另一个进程,中间要做一次转换,使得这个文件描述符在目标进程中是有效的,并且还要和源进程的文件描述符对应的打开文件是一致的,这样才能保证内存共享。
负责这个转换工作的数据结构就是:flat_binder_object
Kernel/msm-4.14/include/uapi/linux/android/Binder.h
其中 binder_object_header的type就是跨进程传递的binder_object类型,
其中的:BINDER_TYPE_BINDER,BINDER_TYPE_WEAK_BINDER代表的是一个Binder实体,这两种类型只能针对client和server在同一个进程的情况,就是说client和server在同一个进程中也是可以通过Binder完成通信的;
BINDER_TYPE_HANDLE,BINDER_TYPE_WEAK_HANDLE这两个是Binder句柄,只要是获得Binder引用的进程都能发送这种类型的Binder。
解释下以上四种类型,无论是Binder实体,还是对实体的引用,都是属于某个进程,直接把这个结构传给其他进程是没有意义的,需要经过Binder驱动转换,当Server把Binder实体传给Client时,flat_binder_object中的type是BINDER_TYPE_BINDER,指向server进程的地址空间,驱动会对这个Binder转换,将type改成BINDER_TYPE_HANDLE,为这个Binder在client进程中创建位于内核中的引用并将引用号填入flat_binder_object结构体的handle属性中,client进程在向server发送请求时,会把这个handle填入发送数据包binder_tranaction_data结构体的target.handle属性中,驱动就会根据这个target.handle在转换成对应的Binder实体,也就换成了server中真正的Binder对象了。所以发起Binder请求的引用号不是随便指定的,一定是Binder驱动授权后交给你的,否侧Binder请求就会被拒绝。
BINDER_TYPE_FD 这是文件类型的Binder,可以认为是把文件看成实体,打开的文件号看成Binder引用,站在Binder角度,linux在内核创建的打开文件描述结构体是struct file,也就是Binder实体,打开文件号是该进程对实体的引用,在向接收进程传递时也会做转换,驱动会在接收进程中创建一个新的打开文件号,将它跟发送进程已有的文件描述结构struct file绑定,然后将新的打开文件号交给了接收进程。
Binder驱动做这些转换的操作是在binder_transaction()方法中,这个可以理解为跨进程访问远程实体的核心。
Binder.c
task_fd_install()把新的文件描述符跟打开文件结构体做了关联。
最后,看下匿名共享内存。
Anonymous shared Memory(Ashmem)是android特有的内存共享机制,可以将指定的物理内存映射到各个进程自己的虚拟内存空间,实现进程间的内存共享。这套机制是建立在Linux内核实现的共享内存基础上的。
Linux内核中的共享内存机制其实是一种进程间通信(IPC)机制,它的实现比较复杂,Android系统的匿名共享内存机制正是由于直接使用了Linux内核共享内存机制,它才会很小巧,它站在巨人的肩膀上。
Android系统的匿名共享内存是在虚拟地址空间连续的,但是在物理地址空间就不一定是连续的了。
Ashmme的实现依赖/dev/ashmem设备,这个设备的注册是android系统启动时完成的,具体是init进程解析init.rc时,启动了ueventd进程,uevented会去解析ueventd.rc来加载指定的设备:
Uenented.rc
Ashmem.c
Ashmem是一个misc设备,通过misc_register完成注册,对应的设备结构体,提供的文件操作接口有:
当使用ashmem执行映射操作时调用的时ashmem_mmap。
Ashmem.c
函数shmem_file_setup是Linux内核中用来创建共享内存文件的方法,他会在tmpfs中创建一个临时文件,用于进程间的内存共享。
函数shmem_set_file 也是linux提供的函数,执行内存映射。
使用ashmem共享内存,两个进程实际拥有的是一个共同的tmpfs中的临时文件:
当一个进程执行mmap时,如果asma->file为空,ashmem会为这个进程创建临时文件,并记录在asma->file中,之后,另一个进程在执行mmap时,如果asma->file不为空,不再创建tmpfs临时文件,直接用shmem_set_file做好内存映射。所以,两个进程拥有的是同一个tmpfs临时文件的fd描述符。
以上是关于binder传输大数据分析的主要内容,如果未能解决你的问题,请参考以下文章