前言
为什么要了解Binder的原理呢,会用不就可以了吗?深入了解Binder不仅仅是为了了解Binder的通信方式,而是从Binder设计中去学习一种优秀的架构思想和问题的解决思路。当我们遇到复杂的通信场景时,或者在一个新的平台上,需要一种新的通信方式提高安全或者效率性能等问题时,我们可以从Binder的设计中借鉴灵感。架构师的成长之路,就是不断的从优秀的设计中学习经验和解决方案。所以,作为一个有追求的程序员,开始对Binder的深入了解吧。网上介绍Binder的文章非常多,我为什么还要再写呢?主要是因为Binder很大部分都是在Linux内核中,如果想真正深入的掌握Binder,就需要掌握很多Linux的知识,网上大部分介绍Binder的文章都对Linux的讲解很少,所以这篇会介绍更多的Linux系统相关的知识,掌握了这些Linux系统相关的知识,才能真正的深入理解Binder的原理。由于篇幅的限制,我会分上下两篇来深入讲解Binder的通信原理。
为什么是Binder
在《深入理解Android进程间通信》这篇文章中我介绍了Android中所有的IPC通信的方式,包括Linux早期的FIFO,管道,信号,到后来的信号量,消息队列,共享内存,再到eventfd,mmap内存映射,Socket等等。Linux系统的IPC通信机制有很多,并且Linux系统上的程序应用也一直在使用这些方式进程IPC通信,那么为什么Android要设计Binder呢?直接用Linux的IPC通信机制不行吗?
我觉得,Android之所以设计一套新的IPC通信机制,一是因为Android系统相比于Linux,对安全性有更高的要求,Android中的每个应用都是一个沙盒模型,不能访问其他应用的数据,每个进程之间也不能直接访问其他进程的数据,相比于Android,Linux的限制就松了很多,所以PC的系统的病毒也比手机系统的病毒多很多,因为PC系统中一个进程的访问权限会大很多。二是因为Android的IPC场景也会比Linux多很多,一个程序可能需要频繁的获取系统中其他进程的信息,比如电量,通讯录等等,并且前面提到的安全性的限制,也会使Android的IPC场景比Linux中的IPC场景更加频繁。在这种更加频繁的IPC和安全的场景要求下,Linux原有的IPC机制在使用的便利性或者性能等方面不能很好的胜任。
既然原有的IPC机制不能胜任,需要重新设计IPC的方式,那么需要怎么设计呢?我们需要考虑下面几点,这几点不仅是Binder的设计需要考虑的,也是我们在程序架构上需要考虑的几点。
- 性能
- 安全
- 可扩展性和低耦合性
下面讲一下Binder是如何保证上面三点的。
性能
在Linux系统中,进程间传输数据性能最好的方式就是共享内存,或是以共享内存为原理衍生出来的技术,如mmap内存映射。共享内存或者mmap都只需要进行一次数据拷贝,即把想要传输的数据拷贝到共享或者映射的内存区域中,另一个共享或者映射了这段内存的进程就可以直接使用内存中的数据了,其他的IPC传输都需要两次拷贝,即将传输的数据拷贝的指定的内存区域(一般是内核空间),然后又将指定的内存区域的数据拷贝到需要通信的进程的内存中去。所以为了性能考虑,Binder在设计时,采用了mmap内存映射这种方式来进行数据的传输。关于mmap的知识在后面会详讲。
安全性
Binder设计中为了安全性的考虑, 天然支持携带进程ID,这样在进程间通信时,可以通过进程ID进程相应的权限控制。并且Binder是CS架构,Servcer更容易对Client的访问权限进行控制。
可扩展和低耦合
Binder的可扩展和低耦合体现在两个方面的架构上,一是它的C/S架构s合计,在CS架构上,Clinet和Server都容易扩展,想要扩展通信,只需要增加Client或者Server就可以了,而不用去管中间的通信流程。二是基于驱动的架构设计,在Android8.0之前,Binder只在Frameworkd之间使用,Binder挂载在dev/binder目录下,8.0开始硬件供应商部分,如相机,手电筒等硬件进程通信也开始使用binder,硬件部分的Binder挂载在/dev/vnbinder或/dev/hwbinder,可以看到,基于驱动的设计下,只需要新增一个虚拟设备,就可以很容易的实现扩展Binder通信的范围。
Linux基础知识
了解了Android为什么要设计Binder,接着会开始深入了解Binder,但在这之前,需要先熟悉一些Linux的基础知识,这是我们掌握Binder原理的基石。
用户空间与内核空间
Linux系统在32位机上为每个Linux进程分配了4G(2^32)的虚拟内存,其中有3GB的虚拟地址供该进程使用,称为用户空间,还有1GB留给其页表和其他内核数据,称为内核空间,内核空间是所有进程共享的。在用户态下运行时,内核的1GB是不可见的,但是当进程陷入到内核时是可以访问的。
mmap内存映射
mmap可以将一个文件或者其它对象映射进进程的用户空间,这种情况下,可以像使用自己进程的内存一样使用这段内存。Linux系统的mmap函数原型是这样的
void *mmap(void *addr,size_t length,int prot,int flags,int fd, off_t offset);
- 参数addr指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。
- 参数length表示将文件中多大的部分映射到内存
- 参数prot指定映射区域的读写权限
- 参数flags指定映射时的特性,如是否允许其他进程映射这段内存
- 参数fd指定映射内存的文件描述符
- 参数offset指定映射位置的偏移量,一般为0
非mmap或者内存共享的Linux IPC机制常用的通信方式如下,数据发送进程的用户空间数据通过copy_from_user,复制到内核空间,由于内核空间是所有进程共享,所以内核通过调用copy_to_user将数据写入到数据接收进程,通过两次拷贝的方式,完成了IPC的通信。
通过mmap或者内存共享的Linux IPC机制,直接将同一段内存映射到数据发送进程和数据接收进程的用户空间,这样数据发送进程只需要将数据拷贝到共享的内存区域,数据接收进程就可以直接使用数据了。
Linux设备驱动
这里为什么要介绍Linux的设备驱动相关的知识呢?因为Binder的重要组成部分就是Binder驱动设备,为了更好的理解Binder,我们需要知道什么是Linux的设备驱动。
Linux的设备,主要包括字符设备(如键盘,鼠标,触摸屏等),块设备(硬盘,内存,U盘等)和网络接口(网卡,蓝牙等)等,都需要驱动程序才能和系统进行通信。这些驱动程序,都挂载在dev目录下,如硬盘的驱动挂载在dev/sda上,内存的驱动挂载在/dev/ram上。
块设备和字符设备的驱动程序一般都要实现open、close、read和write等系统调用函数,这些函数都属于系统I/O函数,直接我们就可以直接通过open或者read等I/O函数,读写设备的数据。而且dev目录下不仅仅是挂载真实的物理设备驱动,还可以挂载虚拟设备的驱动。虚拟设备的设计主要用来实现系统的功能,虽然虚拟设备没有具体的物理设备,但是我们依然需要在驱动程序中实现I/O函数,只不过虚拟设备驱动的I/O函数不是对物理设备的操作,而是功能逻辑操作。网桥就是Linux的一个虚拟设备,Binder也是一个挂载在dev/binder下的虚拟设备。
Binder的架构设计
前面已经知道了Android为什么要设计Binder,我们接着来看看Binder的架构设计。
Binder主要由这几部分组成
- Binder设备驱动
- Client端,数据发送端
- Server端,数据接收端
- ServiceManager
下面介绍以下这几个组成部分
Binder设备驱动
Binder驱动设备是真正分配内存空间用来存放通信数据的部分,在Binder的架构中,Clinet端发送的数据拷贝到Binder驱动设备分配的内存空间中,Server会通过mmap将Binder驱动设备中分配的内存映射到自己进程的用户空间中,映射完成后,Server在用户空间就可以直接读取Binder驱动中存放数据的这段内存了。
Clinet端
Client端是数据发送方,它会通过I/O函数,ioctl陷入内核,通知binder驱动将client端的数据通过copy_from_user函数拷贝过来,并存放在binder驱动的内存中。
Server端
Server是数据接收方,它接收数据方式的方式是映射Binder驱动中存放Clinet端数据的内存到自己的用户空间,这样就可以直接使用这段内存了。
ServiceManager
ServiceManager是专门用来管理Server端的,Client端想要和Server通信,必须知道Server的映射的内存地址,这样才能往这这段内存中拷贝数据,但是我们不可能知道所有Server端的地址,所以这个时候,我们只需要知道ServiceManager的地址,在ServiceManager中寻找其他Server的地址就可以了。所以ServiceManager有点类似DNS服务器。
Binder实现原理
了解了Binder的架构,我们开始分模块(即Binder驱动设备,ServiceManage,Client,Server这四个模块)深入解析Binder的具体实现。
Binder设备驱动
在前面已经大致介绍过了Linux的设备驱动,Binders设备驱动是Binder很重要的组成部分,之所以将Binder设计成驱动,我觉得是可以充分利用驱动热插拔,以及驱动程序天然支持I/O操作这两个特性。我们接着来深入了解一下Binder的设备驱动设计。
和所有其他的设备驱动一样,Binder驱动也是随着Linux的内核启动而一起启动的。在内核启动的过程中,只要位于deriver目录下的驱动程序在代码中按照规定的方式添加了初始化函数,这个驱动程序就会被内核自动加载,那么这个规定的方式是怎么样的呢?它的方式定义在/include/linux/init.h文件中。
……
#define pure_initcall(fn) __define_initcall(fn, 0)
#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn) __define_initcall(fn, 7)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)
……
可以看到,里面有很多xxx_initcall的宏定义函数,如core_initcall,device_initcall等,这些宏定义都按照了优先级的顺序定义的,想要内核在启动的时候,能够启动驱动程序,只需要在驱动程序的代码里面加上xxx_initcall的宏定义方法,就能按照优先级被内核动态加载。
我们看看Binder驱动的源码,它位于/drivers/staging/目录下,我们知道Linux的drivers目录就是专门用来存放系统驱动程序的目录,它的源码里就可以看到**device_initcall **这行代码,device_initcall是最常用的一个initcall函数,于是内核在启动的过程中,就会自动的去加载binder.c驱动程序中的binder_init初始化函数。
/drivers/staging/android/binder.c
……
device_initcall(binder_init);
……
我们再看一下binder的驱动程序的binder_init函数的实现
/drivers/staging/android/binder.c
static int __init binder_init(void)
{
int ret;
binder_deferred_workqueue = create_singlethread_workqueue("binder");
if (!binder_deferred_workqueue)
return -ENOMEM;
binder_debugfs_dir_entry_root = debugfs_create_dir("binder", NULL);
if (binder_debugfs_dir_entry_root)
binder_debugfs_dir_entry_proc = debugfs_create_dir("proc",
binder_debugfs_dir_entry_root);
//注册binder为杂项设备驱动
ret = misc_register(&binder_miscdev);
……
return ret;
}
binder_init中主要只做了一件事,就是调用misc_register函数将当前驱动程序,也就是Binder驱动程序注册成杂项设备驱动,在前面讲Linux设备时,提到过Linux设备主要有字符设备,块设备等,杂项设备也属于Linux的一种设备类型,它是嵌入设系统用的比较多的一种设备。注册的设备的信息定义在binder_miscdev结构体里。
看一下binder_miscdev里有什么内容
/drivers/staging/android/binder.c
static struct miscdevice binder_miscdev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "binder",
.fops = &binder_fops
};
static const struct file_operations binder_fops = {
.owner = THIS_MODULE,
.poll = binder_poll,
.unlocked_ioctl = binder_ioctl,
.compat_ioctl = binder_ioctl,
.mmap = binder_mmap,
.open = binder_open,
.flush = binder_flush,
.release = binder_release,
};
可以看到,binder_miscdev结构体中定义了当前的驱动名为binder,并指定了open,mmap,compat_ioctl,compat_ioctl等I/O函数的实现函数。这些函数在后面说到的时候再详细讲。
ServerManager
从ServerManager开始分析,是因为他比较特殊,即是Client和Server通信的中间人,Client要先去ServerManager中寻找Server的Binder地址,同时也是一个特殊的Server端,作为一个特殊的Server,他的功能很单一,就是返回指定Server的Binder地址。
Android系统在启动过程中,首先会在用户空间启动init进程,然后启动zygote进程,zygote接着fork出ServerManager进程,ServerManager进程启动完成后,会接着启动system_server进程,system_server进程中会启动AMS,WMS等framework的服务,并将这些服务的binder句柄添加到到ServerManager中维护,最后启动Launcher这个应用进程,关于Android详细启动流程可以看看我的文章《Android底层启动解析》,了解了这一背景,我们看一下ServerManager进程启动时做了哪些事情。
/frameworks/native/cmds/servicemanager/service_manager.c
int main(int argc, char** argv)
{
struct binder_state *bs;
union selinux_callback cb;
char *driver;
if (argc > 1) {
driver = argv[1];
} else {
driver = "/dev/binder";
}
//打开Binder驱动
bs = binder_open(driver, 128*1024);
if (!bs) {
return -1;
}
//将ServerManager设置成BinderManager
if (binder_become_context_manager(bs)) {
ALOGE("cannot become context manager (%s)\n", strerror(errno));
return -1;
}
//调用循环,并不断的读取数据
binder_loop(bs, svcmgr_handler);
return 0;
}
ServerManager的main函数主要做的事情有这三件
- 调用binder_open函数,打开binder驱动,并在binder驱动中为ServiceManager分配128kb的空间
- 调用binder_become_context_manager,将ServiceManager的binder设置成BinderManager。
- 调用binder_loop,让ServiceManager不断循环,并在循环的过程中,去读缓冲区中是否有其他进程发送过来的数据
下面详细讲解一下这三件事情。
打开Binder驱动
先看第一件事情,binder_open函数,它的实现如下
/frameworks/native/cmds/servicemanager/binder.c
struct binder_state *binder_open(const char* driver, size_t mapsize)
{
struct binder_state *bs;
struct binder_version vers;
bs = malloc(sizeof(*bs));
if (!bs) {
errno = ENOMEM;
return NULL;
}
//打开binder驱动
bs->fd = open(driver, O_RDWR | O_CLOEXEC);
if (bs->fd < 0) {
goto fail_open;
}
//获取binder驱动版本号
if ((ioctl(bs->fd, BINDER_VERSION, &vers) == -1) ||
goto fail_open;
}
bs->mapsize = mapsize;
//将binder驱动的内存映射到Servermanager的用户空间
bs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs->fd, 0);
if (bs->mapped == MAP_FAILED) {
goto fail_map;
}
return bs;
fail_map:
close(bs->fd);
fail_open:
free(bs);
return NULL;
}
可以看到这个binder_open函数做了三件事情
- 调用I/O函数open,打开binder驱动
- 调用I/O函数ioctl,获取binder驱动版本
- 调用I/O函数mmap,映射128kb大小的binder驱动内核空间的内存到ServerManager用户空间
我们先看open函数时如何打开binder驱动程序的
open到binder_open
在前面说过open方法最终会调用到binder驱动程序的open_binder函数。那么从open到open_binder,里面经历了哪些流程呢?里面的流程涉及到了Linux系统调用的相关机制,秉着对技术追求,这里我们一起看看Linux的系统调用是怎么实现的。
open函数的实现在Bionic库中,Bionic库是Android平台为了使用C/C++进行原生应用程序开发所有提供的POSIX标准C库。
int open(const char* pathname, int flags, ...) {
mode_t mode = 0;
if ((flags & O_CREAT) != 0) {
va_list args;
va_start(args, flags);
mode = static_cast<mode_t>(va_arg(args, int));
va_end(args);
}
return __openat(AT_FDCWD, pathname, force_O_LARGEFILE(flags), mode);
}
open函数调用了**__openat**函数。__openat函数的实现是一段字节码指令。
/bionic/libc/arch-arm/syscalls/__openat.S
ENTRY(__openat)
mov ip, r7
.cfi_register r7, ip
ldr r7, =__NR_openat //系统调用函数
swi #0 //软件中断
mov r7, ip
.cfi_restore r7
cmn r0, #(MAX_ERRNO + 1)
bxls lr
neg r0, r0
b __set_errno_internal
END(__openat)
这段字节码指令实际上是通过swi指令,这个是arm平台中的软中断指令让当前的进程陷入内核调用,__NR_mmap2是调用的系统函数。我们去Linux的系统函数调用表,这个表记录了用户进程能调用的所有内核函数,查找NR_mmap2这个函数,他的定义如下。
/arch/arm/include/uapi/asm/unistd.h
#define __NR_openat (__NR_SYSCALL_BASE+322)
在表中可以查到**__NR_openat **函数的索引为(__NR_SYSCALL_BASE+322),知道索引后,内核就会去calls.S文件中根据索引寻找对应的内核函数
/* 320 */ CALL(sys_get_mempolicy)
CALL(sys_set_mempolicy)
CALL(sys_openat)
在calls.S中,序号是322的系统函数是sys_openat函数。以sys_开头的函数说明进入了最终的系统调用函数,系统调用函数都会在syscalls文件中被重新定义
……
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
……
asmlinkage long sys_fork(void);
asmlinkage long sys_vfork(void);
asmlinkage long sys_clone(unsigned long, unsigned long, int, int __user *,int __user *, int);
……
asmlinkage long sys_openat(int dfd, const char __user *filename, int flags,
umode_t mode);
这个文件中申明了所有的系统调用方法,比如我们熟悉frok函数的系统调用函sys_fork,clone的系统调用函数sys_clone等,并将所有的系统调用函数定义成宏函数,定义的规则是根据函数入参的数量定义成对应的SYSCALL_DEFINEx,这个x就表示入参的数量,比如sys_openat有四个入参,就被定义成了宏函数SYSCALL_DEFINE4。知道了sys_openat被定义成SYSCALL_DEFINE4,我们接着去这个函数最终实现的地方。
SYSCALL_DEFINE4(openat, int, dfd, const char __user *, filename, int, flags,
umode_t, mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(dfd, filename, flags, mode);
}
从SYSCALL_DEFINE4函数开始,我们在用户进程调用的open函数就已经真正的开始内核函数调用的流程中了,在内核函数的调用中,会根据我们open的文件名"dev/binder",最终调用到位于"dev/binder"驱动程序的open函数,也就是binder_open方法。我们接着往下看这个流程是怎么走的。
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
struct open_flags op;
struct filename *tmp;
……
tmp = getname(filename);
if (IS_ERR(tmp))
return PTR_ERR(tmp);
//寻找一个空闲的文件描述符fd
fd = get_unused_fd_flags(flags);
if (fd >= 0) {
struct file *f = do_filp_open(dfd, tmp, &op);
……
}
putname(tmp);
return fd;
}
do_sys_open主要是分配文件描述符,然后执行do_filp_open函数。
struct file *do_filp_open(int dfd, struct filename *pathname,
const struct open_flags *op)
{
struct nameidata nd;
int flags = op->lookup_flags;
struct file *filp;
filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU);
if (unlikely(filp == ERR_PTR(-ECHILD)))
filp = path_openat(dfd, pathname, &nd, op, flags);
if (unlikely(filp == ERR_PTR(-ESTALE)))
filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_REVAL);
return filp;
}
do_filp_open函数中执行了path_openat函数
static struct file *path_openat(int dfd, struct filename *pathname,
struct nameidata *nd, const struct open_flags *op, int flags)
{
struct file *base = NULL;
struct file *file;
struct path path;
int opened = 0;
int error;
file = get_empty_filp();
if (IS_ERR(file))
return file;
//初始化文件路径数据结构
error = path_init(dfd, pathname->name, flags | LOOKUP_PARENT, nd, &base);
if (unlikely(error))
goto out;
//解析路径
error = link_path_walk(pathname->name, nd);
if (unlikely(error)