进程间通信——Binder
本文主要内容
- Binder驱动接口
- ServiceManager的启动
- Binder设计理念
- 总结
Binder驱动接口
Binder驱动一共有3个重要的接口:
- binder_open,打开binder驱动
- binder_mmap,内存映射
- binder_ioctl,与binder驱动通信
binder_mmap是整套机制的基础,通过内存映射,不同进程间有一块共享内存,得以实现IPC。
binder_ioctl,类似于文件读写方法,它承担了Binder驱动的大部分业务,与Binder驱动通信。
看上图,只需要把进程A的内存空间复制到内核空间中(copy from user),内核空间中有了进程A的东西,然后通过内存映射给进程B读取,所以说Binder通信只需要一次内存复制
关于以上3个方法,后续再补充详细说明。
ServiceManager的启动
ServiceManager是非常重要的系统服务,它类似于网络世界中的DNS(请参见进程能通信——智能指针)。 它是在init.rc中启动的,代码位于/framework/native/cmds/servicemanager 目录中,sm启动代码如下:
//service_manager.c
int main(int argc, char** argv)
{
struct binder_state *bs;
char *driver;
if (argc > 1) {
driver = argv[1];
} else {
driver = "/dev/binder";
}
//打开驱动
bs = binder_open(driver, 128*1024);
//将自己设置成context manager角色,ServiceManager系统中肯定只有一个
if (binder_become_context_manager(bs)) {
ALOGE("cannot become context manager (%s)\n", strerror(errno));
return -1;
}
//进入循环
binder_loop(bs, svcmgr_handler);
}
代码较类似于socket通信server端代码,新建ServerSocket,启动死循环,等待连接。查看binder_open代码:
struct binder_state *binder_open(const char* driver, size_t mapsize)
{
struct binder_state *bs;
bs = malloc(sizeof(*bs));
//真正地打开binder驱动
bs->fd = open(driver, O_RDWR | O_CLOEXEC);
//内存映射
bs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs->fd, 0);
return bs;
}
上一章中提到的binder驱动三大方法中,现在已经出现俩了,binder_open方法中打开binder驱动,并且完成内存映射。
接下来看看binder_loop方法:
void binder_loop(struct binder_state *bs, binder_handler func)
{
for (;;) {
//调用ioctl,与binder驱动通信,从驱动中读取数据,并存放在bwr指针中
res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);
//处理读取的指令
res = binder_parse(bs, 0, (uintptr_t) readbuf, bwr.read_consumed, func);
}
}
binder_loop中存在一个死循环,循环中不断从驱动中读取指令,并处理指令。接着看binder_parse方法:
int binder_parse(struct binder_state *bs, struct binder_io *bio,
uintptr_t ptr, size_t size, binder_handler func)
{
while (ptr < end) {
switch(cmd) {
case BR_TRANSACTION:
struct binder_transaction_data *txn = (struct binder_transaction_data *) ptr;
if (func) {
//func是一个函数指针,在此调用函数并返回,此函数类似于onTransact
res = func(bs, txn, &msg, &reply);
if (txn->flags & TF_ONE_WAY) {
binder_free_buffer(bs, txn->data.ptr.buffer);
} else {
binder_send_reply(bs, &reply, txn->data.ptr.buffer, res);
}
}
ptr += sizeof(*txn);
break;
}
}
}
binder_parse方法中,根据cmd的值执行对应的方法,当客户端调用transact方法时,ServiceManager会回调func函数。func是一个函数指针,它实质上是:
int svcmgr_handler(struct binder_state *bs,
struct binder_transaction_data *txn,
struct binder_io *msg,
struct binder_io *reply)
{
switch(txn->code) {
case SVC_MGR_GET_SERVICE:
case SVC_MGR_CHECK_SERVICE:
//调用do_find_service,查找对应的service binder
handle = do_find_service(s, len, txn->sender_euid, txn->sender_pid);
case SVC_MGR_ADD_SERVICE:
//添加service
do_add_service(bs, s, len, handle, txn->sender_euid,allow_isolated, txn->sender_pid)
case SVC_MGR_LIST_SERVICES: {
//链表数据结构,返回节点
si = svclist;
while ((n-- > 0) && si)
si = si->next;
}
return 0;
}
所以,当service manager从binder驱动中读取指令,并根据不同的指令执行不同的方法,最后将结果返回给binder驱动。
service manager与binder交互的逻辑,非常类似于socket通信,先打开binder驱动,再调用mmap方法映射内存,最后启动死循环,调用ioctl方法从binder驱动中获取指令,根据指令调用对应方法,返回结果给binder驱动。
Binder设计理念
如果是我们自己来设计binder,那么我们该怎么做?
很明显,参照service manager,在server端,用户需要打开binder驱动,执行内存映射,建议死循环,与binder驱动通信等等。client端应用还是需要执行以上步骤。过程相当繁锁,必须要进行相应封装,以使用用户开发。
而且以上代码均是c或c++代码,android上层用户更多地使用java,需要给java用户提供更友好的使用方式。
binder主要在两个方面进行封装:
- 封装与binder驱动交互的部分
- 使用代理,proxy
1、ProcessState和IPCThreadState
ProcessState,专门管理每个应用进程中的Binder操作,包括与打开驱动、内存映射等等,每个进程中只需要打开一次驱动,执行一次内存映射即可。
应用进程中肯定会有多个线程,每个线程都可能需要进行进程间通信,负责这个工作的就是IPCThreadState。实际上ProcessState只是负责打开驱动、内存映射等,而与binder驱动实质进行通信的是IPCThreadState。
2、proxy
binder中proxy无处不在,用户不便直接与service manager打交道,尤其是在java端,所以设计出proxy,从而更方便客户调用。
总结
关于binder机制,其实以前也曾写过一篇文章,Android binder机制(native服务篇),关于客户端部分如何与server端交互,请参照上文,今天再次阅读《深入理解android内核设计思想》,有了新的感悟,果然是常看常新。
有时往往代码特别多,不要害怕,宏大的代码,无数的文件往往只是作者在掩盖一些具体细节,在封装,方便用户调用而已,而繁琐的背后只是那一点点特别简单的道理,比如binder机制,核心原理和socket通信几乎没有差别。只要我们抓住主线,不要怕麻烦,一切都能尽在掌握当中
请允许我装逼一下,一切有为法,皆梦幻泡影,如雾亦如电,应作如是观。
转载自:https://juejin.cn/post/7225111825065476151