内容简介:
继上一节的servicemanager之后,本节开始介绍binder的更多相关知识。
Binder在Android系统中用于进程间通讯。为了实现用户空间和内核空间的数据交换,Android Binder机制采用了内存映射的方式,具体映射到/dev/binder这个设备驱动文件节点上。
Binder源码分析系列文章:
Android系统源码分析-进程间通信机制binder(一):守护进程servicemanager
Android系统源码分析-进程间通信机制binder(二):binder内存映射
Android系统源码分析-进程间通信机制binder(三):从framework层到Native层
Android系统源码分析-进程间通信机制binder(四):从Native层到Driver层
内核空间与用户空间:
在Linux的进程管理体系中,进程的用户空间是互相独立的,进程A无法直接访问进程B的资源。
如下图:
为了实现进程间的通信,Linux提供了一系列进程间通信机制,例如共享内存,信号量,socket,管道...。在Android系统中,又提供了Binder机制。
Binder机制的特点:
1. 性能高。从性能来说,Binder只需要一次内存拷贝,这主要是因为Binder采用了mmap映射到binder驱动的缘故。传统的管道队列模式采用内存缓冲区的方式,数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程;而socket都知道传输效率低,开销大,用于跨网络进程交互比较多。
2. 安全可靠。从可靠安全角度来考虑,Android作为一个开放式,拥有众多开发者的的平台,应用程序的来源广泛,确保终端安全是非常重要的,传统的IPC通信方式没有任何措施,基本依靠上层协议,其一无法确认对方可靠的身份,Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志,传统的IPC要发送类似的UID也只能放在数据包里,但也容易被拦截,恶意进攻,socket则需要暴露自己的ip和端口,知道这些恶意程序则可以进行任意接入。
3. 易用。从易用性来说,Android binder机制提供了C/S架构的RPC远程过程调用方式。
从代码编写或者调用的角度来看,binder还提供了RPC(remote process call)功能,即远程过程调用。这样,一个进程调用另外一个进程中的方法就非常简单了,就像是调用本进程的方法一样。
Binder机制在Android系统中的体现:
1. 从数据结构的角度来看:binder提供了内存映射;
2. 从Androic系统的native层来看:servicemanger提供了service的管理(其实就是通过binder的数据结构来实现的);
3.从Android Framewrok层来看:binder采用C/S(client/service)架构的方式来为上层应用来服务。
解释如下:
Binder机制中,用于存储进程间通信的数据是binder driver映射的内存空间,128*1024bytes,这块空间在servicemanger的初始化时就申请了。进程间通信所需要的数据就是通过binder driver来进行的。事实上,我们可以简单的把binder driver和这块内存空间看作是同一个资源。
进程间通信的数据保存在Binder IPC Data中,对应的数据结构都定义在binder.h中,主要有:
struct binder_object:保存binder对象
struct binder_object
{
uint32_t type;
uint32_t flags;
void *pointer;
void *cookie;
};
struct binder_txn:ServiceManager中与binder_transaction_data对应的结构体。
struct binder_txn
{
void *target;
void *cookie;
uint32_t code;
uint32_t flags;
uint32_t sender_pid;
uint32_t sender_euid;
uint32_t data_size;
uint32_t offs_size;
void *data;
void *offs;
};
struct binder_io:binder读写接口
struct binder_io
{
char *data; /* pointer to read/write from */
uint32_t *offs; /* array of offsets */
uint32_t data_avail; /* bytes available in data buffer */
uint32_t offs_avail; /* entries available in offsets array */
char *data0; /* start of data buffer */
uint32_t *offs0; /* start of offsets buffer */
uint32_t flags;
uint32_t unused;
};
Binder C/S架构:
用ActivityManager来举例,简要框图如下:
说明:为了凸显RPC功能,这里省略了中间很多的组件(这些将在后续的文章中进行分析) 。关键组件分析:
ActivityManager:
是client端,作为被调用者,被App调用;
作为调用者,调用AcitivityManagerService的对应方法。而这两者不在同一个进程中,这就是典型的RPC调用。
AcitivityManagerService:
作为服务端:真正提供具体的服务。
client和server端通过binder机制来进行进程间通行,他们都通过系统调用去访问内核空间。
一个典型的ActivityManager使用场景:获取正在运行的应用进程。
代码实现:在你的App的Acitivity中,可以如下调用:
ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
am.getRunningAppProcesses();
Binder IPC 的内存映射mmap分析:
mmap简介:
内存映射,主要作用就是将内核空间的一段内存区域映射到用户空间。映射成功后,用户对这段内存区域的修改可以直接反映到内核空间,相反,内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间与用户空间两者之间需要大量数据传输等操作的话效率是非常高的。当然,也可以将内核空间的一段内存区域同时映射到多个进程,这样还可以实现进程间的共享内存通信。
系统调用mmap()就是用来实现上面说的内存映射。最长见的操作就是文件(在Linux下设备也被看做文件)的操作,可以将某文件映射至内存(进程空间),如此可以把对文件的操作转为对内存的操作,以此避免更多的lseek()与read()、write()操作,这点对于大文件或者频繁访问的文件而言尤其受益。
mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。munmap执行相反的操作,删除特定地址区域的对象映射。
当使用mmap映射文件到进程后,就可以直接操作这段虚拟地址进行文件的读写等操作,不必再调用read,write等系统调用。但需注意,直接对该段内存写时不会写入超过当前文件大小的内容。
采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
通常使用mmap()的三种情况: 提高I/O效率、匿名内存映射、共享内存进程通信 。
用户空间mmap()函数声明:
void *mmap(void *start, size_t length, int prot, int flags,int fd, off_t offset)
,下面就其参数解释如下:
start:用户进程中要映射的用户空间的起始地址,通常为NULL(由内核来指定)
length:要映射的内存区域的大小
prot:期望的内存保护标志
flags:指定映射对象的类型
fd:文件描述符(由open函数返回)
offset:设置在内核空间中已经分配好的的内存区域中的偏移,例如文件的偏移量,大小为PAGE_SIZE的整数倍
返回值:mmap()返回被映射区的指针,该指针就是需要映射的内核空间在用户空间的虚拟地址
现在,再来看一下在哪里使用了mmap。在打开binder驱动(binder_open函数)时,会调用mmap函数,如下:
//binder.c
struct binder_state *binder_open(unsigned mapsize)
{
struct binder_state *bs;
bs = malloc(sizeof(*bs));
if (!bs) {
errno = ENOMEM;
return 0;
}
bs->fd = open("/dev/binder", O_RDWR);
......
bs->mapsize = mapsize;
bs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs->fd, 0);
if (bs->mapped == MAP_FAILED) {
fprintf(stderr,"binder: cannot map device (%s)\n",
strerror(errno));
goto fail_map;
}
/* TODO: check version */
return bs;
......
}
这里的mapsize,就是128*1024bytes;
bs->fd就是/dev/binder这个设备驱动文件。
通过mmap,既实现了进程间通信,又提高了代码执行效率,因此,Android Binder IPC使用mmap来进行binder driver和内存空间的映射,就很好理解设计者的初衷了。
内存,内存,还是内存。