您的位置:首页 > 其它

Parcel数据传输过程,简要分析Binder流程

2017-02-13 21:32 417 查看
Android Binder框架跨进程传输的,基本都是通过Parcel来封装的数据[service_manager除外],这个是十分底层的实现了。对于Framework开发来说,除了了解RPC的抽象意义外,对底层的具体通信细节的实现,也是要比较了解的,这里简单记录一下RPC通信数据传递的过程。

1. parcel是啥

Parcel主要是方便实现上层抽象对象或数据打包做跨进程传输而封装的一个类,类名的字面意思也差不多说明了。其作用,说白了,就是将要写入的数据,规整到一个连续的buffer内存中,同时记录一些数据信息属性。远端进程可接收后根据这些属性和读取顺序来克隆还原。连续buffer内存方便驱动实现,同时效率也高。目前Android中,除了service_manager外[太简单了,而且纯C写的..],都是走parcel。

2.一个简单的binder通信例子 

这里,我们简单的用一个dump服务的接口实现,来看一下在binder通信过程中,数据在进程中的传递过程。

#include <utils/Atomic.h>
#include <utils/Errors.h>
#include <utils/Log.h>
#include <binder/IBinder.h>
#include <binder/IPCThreadState.h>
#include <binder/IServiceManager.h>
#include <binder/ProcessState.h>
#include <stdio.h>
using namespace android;
int main() {
    status_t err = NO_ERROR;
    const sp<IServiceManager> sm = defaultServiceManager();
    sp<IBinder> ibinder = sm->getService(String16("activity"));
    // 这里开始Binder传输,请求服务端执行dump,只有发送数据,不需要接收数据
    if (ibinder != NULL ) {
        // 构造两个空的Parcel,一个是用于发送,一个用于接受
        Parcel send;
        Parcel reply;
        send.writeFileDescriptor(STDOUT_FILENO); // 写入文件FD.
        send.writeInt32(0); // dump命令的参数个数,这里是0个,不带参数了。
        status_t err = ibinder->transact(IBinder::DUMP_TRANSACTION, send, &reply);
    }
    // 这是binder传输,获取服务的descriptor, 不需要发送数据,只需要接收数据
    {
        Parcel send, reply;
        status_t err = ibinder->transact(IBinder::INTERFACE_TRANSACTION, send, &reply);
        if (err == NO_ERROR) {
            printf("Read service interface: %s\n", String8(reply.readString16()).string());
        }
    }
    return 0;
}


通信过程比较简单,将要传递的数据,通过Parcel的writeXXX系列函数写入到Parcel中,然后调用IBinder的transact,发起远程通信请求,通信的数据结果,存在Parcel对象reply中,从这个对象中读出结果即可。

Binder通信的详细流程不分析了,扒一下Binder驱动和IPCThreadState,整个Binder的一次传输过程,可以用如下图简单描述一下:



通信过程的数据传递,后面接着看一下。

3. 发送时待传输的Parcel对象

Binder通信基本上都是通过Parcel来传递数据的,调用Parcel.writexxx将要传递数据存到对象中. 看下流程。
本地端,启动服务传输:
Parce构造函数,实际没做什么事情,没有分配存储对象的内存:

Parcel::Parcel()
{
LOG_ALLOC("Parcel %p: constructing", this);
initState();
}
..
void Parcel::initState()
{
LOG_ALLOC("Parcel %p: initState", this);
mError = NO_ERROR;
mData = 0;
mDataSize = 0;
mDataCapacity = 0;
mDataPos = 0;
ALOGV("initState Setting data size of %p to %zu", this, mDataSize);
ALOGV("initState Setting data pos of %p to %zu", this, mDataPos);
mObjects = NULL;
mObjectsSize = 0;
mObjectsCapacity = 0;
mNextObjectHint = 0;
mHasFds = false;
mFdsKnown = true;
mAllowFds = true;
mOwner = NULL;
}


Parcel存储数据的内存缓冲[mData],是在写入对象时(这里基本数据类型,也可以看做对象,实现稍有差异,不过原理都一样),根据写入数据,自动调整的。看一下上面例子中,写入parcel的方法:

1. send.writeFileDescriptor(STDOUT_FILENO);

时,

status_t Parcel::writeFileDescriptor(int fd, bool takeOwnership)
{
flat_binder_object obj;
obj.type = BINDER_TYPE_FD;
obj.flags = 0x7f | FLAT_BINDER_FLAG_ACCEPTS_FDS;
obj.binder = 0; /* Don't pass uninitialized stack data to a remote process */
obj.handle = fd;
obj.cookie = takeOwnership ? 1 : 0;
return writeObject(obj, true);
}


这里传输fd会比较特殊。fd虽然是一个整形,但它代表的是一个文件句柄,跨进程传递到对方进程中。代码也比较简单,把fd封装到flat_binder_object中,再通过writeObject写入Parcel中。

status_t Parcel::writeObject(const flat_binder_object& val, bool nullMetaData)
{
const bool enoughData = (mDataPos+sizeof(val)) <= mDataCapacity;
const bool enoughObjects = mObjectsSize < mObjectsCapacity;
//首次写,是没有申请内存的, mDataCapacity是0,此条件不成立
if (enoughData && enoughObjects) {
restart_write:
*reinterpret_cast<flat_binder_object*>(mData+mDataPos) = val;
....
return finishWrite(sizeof(flat_binder_object));
}
// 内存不够, 增加容量,申请到可以容纳写入进程的空间
if (!enoughData) {
const status_t err = growData(sizeof(val));
if (err != NO_ERROR) return err;
}
...
goto restart_write;
}


growData是真正申请内存的地方:

status_t Parcel::growData(size_t len)
{
size_t newSize = ((mDataSize+len)*3)/2;
return (newSize <= mDataSize)
? (status_t) NO_MEMORY
: continueWrite(newSize);
}


这里新申请的内存,是需要的1.5倍,目的应该也就是避免频繁grow的情况吧,下面可以看到,如果要grow的话,代价很大的!

status_t Parcel::continueWrite(size_t desired)
{
...
// 这里的mOwner变量,是很重要的一个东西,在从Binder读取数据的时候用到。
if (mOwner) {
...
} else if (mData) {  // 第一次,mData也是NULL,如果不为NULL,要有memcpy动作了,这里就是比较浪费
...
} else {
// This is the first data.  Easy!
// 上面是原生注释,第一次简单,直接分配就行,第二次或以后,麻烦再要拷贝移动了。
uint8_t* data = (uint8_t*)malloc(desired);
if (!data) {
mError = NO_MEMORY;
return NO_MEMORY;
}
...
mData = data;
mDataSize = mDataPos = 0;
ALOGV("continueWrite Setting data size of %p to %zu", this, mDataSize);
ALOGV("continueWrite Setting data pos of %p to %zu", this, mDataPos);
mDataCapacity = desired;
}
return NO_ERROR;
}


分配好内存后,下面就是写了, 写入继续见writeObject函数,分配好后goto restart_write,直接写入数据:

       *reinterpret_cast<flat_binder_object*>(mData+mDataPos)=
val;


其实还是蛮简单的。
写入完成后,finishWrite:

status_t Parcel::finishWrite(size_t len)
{
//printf("Finish write of %d\n", len);
mDataPos += len;
ALOGV("finishWrite Setting data pos of %p to %zu", this, mDataPos);
if (mDataPos > mDataSize) {
mDataSize = mDataPos;
ALOGV("finishWrite Setting data size of %p to %zu", this, mDataSize);
}
//printf("New pos=%d, size=%d\n", mDataPos, mDataSize);
return NO_ERROR;
}


只是更新一下数据指针。

2. send.writeInt32(0);
这是写入一个int数,比较简单。 上一步分配的内存容量,实际是还够写入一个int的,直接写入即可。注意数据是按照数据类型对齐的。

status_t Parcel::writeInt32(int32_t val)
{
return writeAligned(val);
}
..
template<class T>
status_t Parcel::writeAligned(T val) {
COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE(sizeof(T)) == sizeof(T));
if ((mDataPos+sizeof(val)) <= mDataCapacity) {
restart_write:
*reinterpret_cast<T*>(mData+mDataPos) = val;
return finishWrite(sizeof(val));
}
status_t err = growData(sizeof(val));
if (err == NO_ERROR) goto restart_write;
return err;
}


以上是写入Parcel数据的过程,蛮简单的,内存容量的增长和数据大小等概念,和c++中的vector实现有些类似。
不过实际使用,可能还有一些注意的:

fd的写入,需要封装成一个flat_binder_object, object还用另外一个mObjects管理,mObjects存放的是objects在mData中的偏移位置,这个也要发给驱动,这里没展开说。

正常主动通过Parcel写入的数据,都是放在malloc分配的堆上的,这里也就没有特别的尺寸限制了。但并不是说对尺寸没要求,因为数据的接收端有要求!

这里Parcel只是对数据的打包类,在面向对象的抽象上,可以将可序列化的对象,flatten到一个parcel中通过binder传递。不过上层的对象和这里的flat_binder_object是两个不同概念,flat_binder_object专指binder和fd[这两个是不能序列化的,所以要特殊处理了,binder驱动会针对做特殊处理..]。

啰嗦这么多,其实就一句话:IPC传输时的Parcel数据,就是写在malloc的堆中,其他那些复杂的code,都是为了效率和方便性。
 

4. binder的传输数据过程

具体负责binder通信的类,是IPCThreadState。IBinder的transact,实际调用的就是IPCThreadState::transact
Parcel是专门供binder transact过程中,打包传递数据使用的。瞅一下传递过程:
调用IBinder的trasact,实际方法是在IPCThreadState类中,这个类是实际负责和Binder驱动操作。

IPCThreadState类不再展开说了,基本上,就是将通信的数据,再封在一个Parcel类中,开始传输:

status_t IPCThreadState::transact(int32_t handle,
uint32_t code, const Parcel& data,
Parcel* reply, uint32_t flags)
...
if (err == NO_ERROR) {
...
// 将parcel组织起来,放到待写入结构中
err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, NULL);
}

if (err != NO_ERROR) {
if (reply) reply->setError(err);
return (mLastError = err);
}

if ((flags & TF_ONE_WAY) == 0) {
...
// 如果不是异步binder,则要等待一下,这个transaction请求发送成功,并等待远程端处理完的回应
if (reply) {
err = waitForResponse(reply);
} else {
Parcel fakeReply;
// 如果不需要远端回应返回数据,同步调用也要等待binder的远端执行完成结束,这里通过fakeReply实现这一逻辑
err = waitForResponse(&fakeReply);
}
...
} else {
// 异步binder,只等待transaction命令写入成功即可
err = waitForResponse(NULL, NULL);
}

return err;
}


这里主要是将待写入的数据,封装成一个binder_transaction_data的结构体,然后写入binder驱动。binder通信过程是在waitForResponse中实现的。

status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)
{
while (1) {
if ((err=talkWithDriver()) < NO_ERROR) break;;
...
cmd = mIn.readInt32();
...
switch (cmd) {
case BR_TRANSACTION_COMPLETE:
if (!reply && !acquireResult) goto finish;
break;
...
case BR_REPLY:
...
goto finish;
default:
err = executeCommand(cmd);
if (err != NO_ERROR) goto finish;
break;
}
}
finish:
if (err != NO_ERROR) {
if (acquireResult) *acquireResult = err;
if (reply) reply->setError(err);
mLastError = err;
}

return err;
}


对于发送端,通过这个函数,就将数据写入到binder驱动中,驱动接收后会反馈BR_TRANSACTION_COMPLETE消息,如果是异步调用,直接就通信结束了,如果是同步的,还要再talkWithDriver,等服务端发送的调用结果。

5. binder接收方得到Parcel

服务端在talkWithDriver时,收到驱动的消息,在transact调用的BR_TRANSACT时,会执行服务端的transact处理,响应代码如下:

status_t IPCThreadState::executeCommand(int32_t cmd)
{
BBinder* obj;
RefBase::weakref_type* refs;
status_t result = NO_ERROR;

...

case BR_TRANSACTION:
{
binder_transaction_data tr;
result = mIn.read(&tr, sizeof(tr));
Parcel buffer;
buffer.ipcSetDataReference(
reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),    //驱动填入的buffer地址,binder驱动分配的,是binder内存
tr.data_size,
reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets),
tr.offsets_size/sizeof(binder_size_t), freeBuffer, this); // 释放binder内存的释放函数。
...
Parcel reply;
status_t error;
if (tr.target.ptr) {
sp<BBinder> b((BBinder*)tr.cookie);
error = b->transact(tr.code, buffer, &reply, tr.flags);
} else {
error = the_context_object->transact(tr.code, buffer, &reply, tr.flags);
}
...
}
break;

...

return result;
}


这里主要由两点:

1.  buffer.ipcSetDataReference
这个就是客户端调用时,写过来的parcel的数据内容。parcel通过ipcSetDataReference重构出一个和客户端一样的内容。这个函数注意的就是传入的buffer地址,这个是驱动分配的binder
buffer地址。另外还提供了free这个buffer的方法(freeBuffer)。
2.  调用binder服务端的transcat。
    这里注意的是,当target.ptr为0时,实际就是和service_manager通信了。不过service_manager自己走自己实现那套,这个是死代码,不可能走进来的。正常都是走b->transact,即:

status_t BBinder::transact(
uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
data.setDataPosition(0);
status_t err = NO_ERROR;
switch (code) {
case PING_TRANSACTION:
reply->writeInt32(pingBinder());
break;
default:
err = onTransact(code, data, reply, flags);
break;
}
if (reply != NULL) {
reply->setDataPosition(0);
}
return err;
}


default分支,就是服务端的onTransact执行,具体的远程代码执行了。

简单总结: 接收端也比较简单,从驱动中拿到通信对方的数据,执行对应的请求即可。数据来自驱动,处理完了后,Parcel的析构函数,自动会free这片内存。(这里有个坑!!!,后面再说)

6. 驱动干的事情

一些注意事项
强调Binder共享内存大小
强调注意释放接收内存,否则Binder挂了
关于Binder的内存,是驱动mmap出来的,每个参与binder通信的进程都会分配,除了service_manager外,其他进程的大小都是固定的,不到1M,定义在ProcessState.cpp中:

#define BINDER_VM_SIZE ((1*1024*1024) - (4096 *2))

特别注意的是,这里虽然是mmap的,看样子应该是共享内存,可以供跨进程使用,实际不然。binder内存不能跨进程使用,操作是:

每个binder进程都需要分配,只能分配的Binder进程读取

驱动负责分配和回收(回收需要用户进程主动向驱动发命令去free),当然,驱动是内核态的,可以看到所有进程的内存

以上,通信过程必然要有一次copy,copy动作是在驱动中做的。

binder buffer仅供接收数据用。

Binder驱动中数据的传递流程:
另外注意: BC_TRANSACT和BC_REPLY 实际在驱动层看,是一样的。这里,上层抽象的RPC调用,如果从通信的角度看,也就是互相往发数据而已。(不过binder实现是CS架构,只能客户端发通信请求)
Binder传输请求方发送数据流:
talkWithDriver@IPCThreadState.cpp
     binder ioctl [BC_TRASACTION]
        binder_ioctl@kernel/driver/staging/android/binder.c
            binder_thread_write@kernel/driver/staging/android/binder.c
                binder_transaction@kernel/driver/staging/android/binder.c

int binder_thread_write(struct binder_proc *proc, struct binder_thread *thread,
binder_uintptr_t binder_buffer, size_t size,
binder_size_t *consumed)
{
uint32_t cmd;
void __user *buffer = (void __user *)(uintptr_t)binder_buffer;
void __user *ptr = buffer + *consumed;
void __user *end = buffer + size;
// 先获取传输的command,确定要binder做何事。
while (ptr < end && thread->return_error == BR_OK) {
if (get_user(cmd, (uint32_t __user *)ptr))
return -EFAULT;
ptr += sizeof(uint32_t);
...
switch (cmd) {
...
case BC_TRANSACTION:
case BC_REPLY: {
struct binder_transaction_data tr;
// 从用户态获取transact_data
if (copy_from_user(&tr, ptr, sizeof(tr)))
return -EFAULT;
ptr += sizeof(tr);
binder_transaction(proc, thread, &tr, cmd == BC_REPLY);
break;
}
...
}
*consumed = ptr - buffer;
}
return 0;
}


处理Transaction 请求:
static void binder_transaction(struct binder_proc *proc,
struct binder_thread *thread,
struct binder_transaction_data *tr, int reply)
{
...
// 不是reply,走else
if (reply) {
...
} else {
// 查找获得通信的target端。
if (tr->target.handle) {
struct binder_ref *ref;
ref = binder_get_ref(proc, tr->target.handle);...
target_node = ref->node;
} else {
target_node = binder_context_mgr_node;...
}
e->to_node = target_node->debug_id;
target_proc = target_node->proc;...
}
...
}
if (target_thread) {
e->to_thread = target_thread->pid;
target_list = &target_thread->todo;
target_wait = &target_thread->wait;
} else {
target_list = &target_proc->todo;
target_wait = &target_proc->wait;
}
e->to_proc = target_proc->pid;

// 构建transaction结构
/* TODO: reuse incoming transaction for reply */
t = kzalloc(sizeof(*t), GFP_KERNEL);...

tcomplete = kzalloc(sizeof(*tcomplete), GFP_KERNEL);...
if (!reply && !(tr->flags & TF_ONE_WAY))
t->from = thread;
else
t->from = NULL;
t->sender_euid = proc->tsk->cred->euid;
t->to_proc = target_proc;
t->to_thread = target_thread;
t->code = tr->code;
t->flags = tr->flags;
t->priority = task_nice(current);
// 分配binder buffer,消耗的是目标进程的buffer
t->buffer = binder_alloc_buf(target_proc, tr->data_size,
tr->offsets_size, !reply && (t->flags & TF_ONE_WAY));...
t->buffer->allow_user_free = 0;
t->buffer->debug_id = t->debug_id;
t->buffer->transaction = t;
t->buffer->target_node = target_node;

if (target_node)
binder_inc_node(target_node, 1, 0, NULL);
offp = (binder_size_t *)(t->buffer->data +
ALIGN(tr->data_size, sizeof(void *)));
// 这里,将用户态写入的要通信的Parcel中的数据[存在用户malloc堆中],拷贝到内核binder buffer中
if (copy_from_user(t->buffer->data, (const void __user *)(uintptr_t)
tr->data.ptr.buffer, tr->data_size)) {...}
if (copy_from_user(offp, (const void __user *)(uintptr_t)
tr->data.ptr.offsets, tr->offsets_size)) {...}
...
// 调用请求给到目标进程,唤醒目标进程,看是工作
t->work.type = BINDER_WORK_TRANSACTION;
list_add_tail(&t->work.entry, target_list);
tcomplete->type = BINDER_WORK_TRANSACTION_COMPLETE;
list_add_tail(&tcomplete->entry, &thread->todo);
if (target_wait)
wake_up_interruptible(target_wait);
return;
...
}


上面就是copy的动作。Binder传输数据是有一次拷贝的。当Transaction开始后,唤醒目标进程,tranasction请求和数据到目标进程的路径如下:
binder_thread_read@kernel/driver/staging/android/binder.c 

    executeCommand@IPCThreadState.cpp
       
 注意:

目标进程启动binder线程,在执行joinThreadPool后,或者binder线程通信完成,空闲后,都是在驱动的binder_thread_read这个函数中wait,等待请求。

目标进程收到数据后,talkWithDriver收到数据,调用executeCommand处理数据的各种请求

RPC的结果返回(Reply)流程差不多,就不说了,就是将驱动从发送方进程copy来的Parcel中的数据,通过ipcSetDataReference设置到本进程parcel中,获取数据和命令,响应请求即可。

整个通信过程的数据流结构图和流向大约如下:


 

7. 其他,用Parcel传递对象。

以上只是binder通信,用parcel传递数据的过程。上述描述的主要是native层的,基本上角度是通信的角度,如果从上层角度看,Parcel主要是方便实现上层抽象对象的跨进程传输,可以将本地对象,拆包后,扁平化数据流的形式,通过Binder,传输到远端进程,远端通过接收到的数据,重新构基本一样的对象,可以做到用户看起来好像所有对象或执行代码都在本地一样。基本上,Parcel的打包解包,和序列化过程差不多:



这里基本上就是业务逻辑上层,封了一层。实现也比较简单,就是类实现Parcelable接口,支持将对象数据写入Parcel,通过binder传递到远程进程中,再根据这些数据,在远端重新构建出对象即可,这个理解起来比较简单。

其他一些注意事项:

Binder支持fd的传递,fd传递的话,fd 在本进程还要用的话,一定要dup一份,否则,在parcel在通信完成后析构时,是要close fd的。

Parcel中数据的释放,会在类析构的时候,释放内存。parcel通信返回的reply,是一个指针。注意如果是用java代码实现的通信逻辑的话,是直接保存这个reply指针的,而且java是没有析构的概念,java的Parcel类被GC,也没有相应的释放IPC数据库的动作,必须在不用的时候,调用recycle来显示的回收这个数据。否则很快这个binder内存(只有1M-8K)就耗干了。基本上原生代码,看起来就是MediaPlayer的getMetaData没有做这个事,会泄漏(好像最新版本android也没修)。

从上可知,binder内存很小的,特别要注意不要传递大对象和数据,否则很容易失败。如果数据量比较大,可以用数据库,或者参考Bitmap的传递实现,用Blob。

简单总结:

Binder通信,通信发起端(BC_XXX)往binder驱动中写入的数据,是创建在自己进程的堆中的

Binder通信的发起端进程的通信数据,在binder驱动中,会拷贝到响应端的binder内存中,binder内存,除service_manager外,每个进程只有1M-8K,注意通信传输数据量要小

响应端(BR_XXX)得到的数据,是存储在Binder内存中的,使用完后,注意可能有free问题,免得泄露


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Binder Parcel IPC