美文网首页
Binder对象跨进程传输的理解

Binder对象跨进程传输的理解

作者: huafresh | 来源:发表于2020-04-03 10:57 被阅读0次

问题来源

做Android的都知道,我们绑定Service的代码一般是这么写的:

bindService(service, object : ServiceConnection {
    override fun onServiceDisconnected(name: ComponentName?) {
    }
    override fun onServiceConnected(name: ComponentName?, service: IBinder) {
    }
}, Context.BIND_AUTO_CREATE)

在onServiceConnected回调中拿到IBinder对象,再调用asInterface即可转成接口实例。但是大家想过没有,这个IBinder对象到底是个啥?和我们在Service中的onBind方法返回的Binder对象是同一个吗?稍微对进程有点概念的人应该都知道肯定不会是同一个对象。那么到底是什么呢?为什么拿到了这个IBinder对象就能调用Service中的方法了呢?可能很多人会觉得这都不算问题吧,反正多半和binder驱动有关呗。嗯,好吧,笔者以前也是这么觉得的,直到后面看了一些framework层的一些源码后,发现binder对象跨进程传输很频繁。举个例子:我们知道AMS能控制应用程序的Activity的启动和相关的生命周期,AMS是使用token来标识一个应用程序的Activity实例的,这个token就是IBinder对象。为什么IBinder对象可以作为token呢?如果IBinder对象可以作为唯一标识,那不就说明Binder对象跨进程来回传输的时候在相同的进程会恢复出相同的实例?有了一些疑问,笔记决定好好捋一捋binder对象到底是怎么传到另一个进程的。

Binder对象传输过程

应用进程部分

我们以服务的注册过程来详细阐述binder对象的传输过程。
从Java层的ServiceManager的addService方法开始分析,该方法用于向SMgr注册服务,代码如下:

// ServiceManager.java
public static void addService(String name, IBinder service) {
    getIServiceManager().addService(name, service, false);
}
private static IServiceManager getIServiceManager() {
    if (sServiceManager != null) {
        return sServiceManager;
    }
    // Find the service manager
    sServiceManager = ServiceManagerNative.asInterface(Binder.allowBlocking(BinderInternal.getContextObject()));
    return sServiceManager;
}

BinderInternal.getContextObject()这个方法是native方法,它的实现是android_util_binder.cpp的android_os_BinderInternal_getContextObject方法:

// android_util_Binder.cpp
static jobject android_os_BinderInternal_getContextObject(JNIEnv* env, jobject clazz)
{
    sp<IBinder> b = ProcessState::self()->getContextObject(NULL);
    return javaObjectForIBinder(env, b);
}

这里两个冒号在C++中的意思是指定命名空间调用函数。self函数一般就是返回单例对象。这里的ProcessState是进程内的单例对象,它在实例化时会打开binder驱动。我们继续看其getContextObject方法:

// ProcessState.cpp
sp<IBinder> ProcessState::getContextObject(const sp<IBinder>& /*caller*/)
{
    return getStrongProxyForHandle(0);
}

直接调用了另一个方法,注意传的handle句柄是0,后面会看到,驱动通过handle句柄就能找到目标进程的binder实体对象,而0号句柄特指SMgr进程的binder实体对象。我们现在跟的代码当前就是要获取SMgr进程提供的服务,而服务说白了不就是Binder实体对象嘛,继续跟进:

// ProcessState.cpp
sp<IBinder> ProcessState::getStrongProxyForHandle(int32_t handle)
{
    sp<IBinder> result;
    // 这里的handle_entry是缓存,如果不存在则创建新项,新项的binder域为空
    handle_entry* e = lookupHandleLocked(handle);
    if (e != NULL) {
        IBinder* b = e->binder;
        if (b == NULL || !e->refs->attemptIncWeak(this)) {
            ...
            // 这里很关键,BpBinder是Binder Proxy的缩写,也就是说BpBinder是C++层的binder代理对象,这里构造传入了handle句柄。
            b = new BpBinder(handle);
            e->binder = b;
            ...
            result = b;
        } ...
    }

    return result;
}

看代码,注释写的很清楚了。
获取到BpBinder对象后,我们继续接上上面的流程,看javaObjectForIBinder方法,这个方法的主要作用就是获取java层的BinderProxy对象:

jobject javaObjectForIBinder(JNIEnv* env, const sp<IBinder>& val)
{
    ...
    // 这里先根据给定的IBinder对象尝试获取原来绑定的BinderProxy对象,如果原来绑定的对象还可以用,那么直接返回,否则就会new一个新的;
    // 如果已经不可用了,则会执行解绑。
    jobject object = (jobject)val->findObject(&gBinderProxyOffsets);
    if (object != NULL) {
        jobject res = jniGetReferent(env, object);
        if (res != NULL) {
            return res;
        }
        // 不可用,解绑
        val->detachObject(&gBinderProxyOffsets);
        env->DeleteGlobalRef(object);
    }
    // 这里反射实例化BinderProxy,这里反射用到的类和构造方法等信息是在进程启动时初始化的,具体初始化过程可以参考这篇博客:
    // https://blog.csdn.net/fchyang/article/details/82260138
    object = env->NewObject(gBinderProxyOffsets.mClass, gBinderProxyOffsets.mConstructor);
    if (object != NULL) {
        // 这里对新的BinderProxy对象的mObject赋值为IBinder的指针,很关键。
        // 这样后续和远程进程通信时,就能通过mObject找到BpBinder对象。
        env->SetLongField(object, gBinderProxyOffsets.mObject, (jlong)val.get());
        ...
        // 进行绑定。
        val->attachObject(&gBinderProxyOffsets, refObject,
                jnienv_to_javavm(env), proxy_cleanup);
        ...
    }
    return object;
}

这个返回了,我们就得到了一个java层的BinderProxy对象了,并且它通过mObject域与C++层的BpBinder对象进行关联。
接下来我们回到java层,Binder.allowBlocking只是把返回的BinderProxy对象的mWarnOnBlocking变量赋值为false,暂时不知是何含义。
紧接着看ServiceManagerNative.asInterface方法,该方法仅仅是new了一个ServiceManagerProxy对象,而其mRemote指向上面分析的从C++层返回的BinderProxy对象。
这个ServiceManagerProxy实现了IServiceManager接口,这个类的作用就是完成了参数写入parcel的脏活累活。至此,我们有了一个java层的接口实例,并且通过mRemote域指向BinderProxy,而BinderProxy又和C++层的BpBinder绑定,BpBinder内部包含handler句柄,这层层的持有关系都是为了拿到handler句柄。当然了,这一系列的封装最大的目的就是为了使远程方法调用像调用一般方法一样简单。
继续分析之前,我们先来了解下Parcel。
对于这个Parcel,java层的Parcel只是对C++层Parcel的一个封装,通过mNativePtr指针指向C++层的Parcel对象。C++层的Parcel其数据的组织方式就是一个字节数组,写入数据的时候就直接追加到数组中,因此读取数据时必须按照写入的顺序来读取,不然类型就对不上了。既然是扁平的数组,那string和binder对象是怎么存储和恢复的呢?答:存储还是和基本类型数据一样,只不过多存储了String或者对象的大小,这样当我们readStrongBinder时,Parcel首先会读取一个整型值,然后把其当成len再读取后面的数据,最后再强转成相应的结构体。
ok,了解了Parcel,我们继续分析addService方法,此时我们知道addService的具体实现是ServiceManagerProxy类,ok,跟进去:

// ServiceManagerProxy.java
public void addService(String name, IBinder service, boolean allowIsolated)
        throws RemoteException {
    Parcel data = Parcel.obtain();
    Parcel reply = Parcel.obtain();
    // 这里记一下我们写入了什么数据,以便理解后面经过重重调用后,在遥远的smgr服务中是怎么解析数据的。
    data.writeInterfaceToken(IServiceManager.descriptor); // descriptor是字符串,值为:android.os.IServiceManager
    data.writeString(name); // 服务的名称
    data.writeStrongBinder(service); // 要注册的binder实体对象
    data.writeInt(allowIsolated ? 1 : 0);
    // 如前所述,mRemote是BinderProxy对象
    mRemote.transact(ADD_SERVICE_TRANSACTION, data, reply, 0);
    reply.recycle();
    data.recycle();
}

writeStrongBinder方法用于向parcel中写入binder实体对象,binder实体对象和一般的实体对象不同,首先是Binder并没有实现Parcelable接口,因此写入Parcel的仅仅是指针,另外因为binder驱动需要为传输的binder实体对象建立数据结构体,因此必须要标记出parcel中哪里存在binder对象的传输。
binder实体对象在parcel中以flat_binder_object的结构形式存储,它的定义如下:

// binder.h
struct flat_binder_object {
   /* 8 bytes for large_flat_header. */
   unsigned long     type;
   unsigned long     flags;

   /* 8 bytes of data. */
   union {
      void      *binder;   /* local object */
      signed long    handle;       /* remote object */
   };

   /* extra data associated with local object */
   void         *cookie;
};

type域表明了存储的是binder实体对象还是binder引用,如果是实体对象,则后面的binder指针指向该实体对象,如果是引用,则handle域存储binder句柄。
flags域只对第一次传递Binder实体时有效,因为此刻驱动需要在内核中创建相应的实体节点,有些参数需要从该域取出:第0-7位:代码中用FLAT_BINDER_FLAG_PRIORITY_MASK取得,表示处理本实体请求数据包的线程的最低优先级。当一个应用程序提供多个实体时,可以通过该参数调整分配给各个实体的处理能力。第8位:代码中用FLAT_BINDER_FLAG_ACCEPTS_FDS取得,置1表示该实体可以接收其它进程发过来的文件形式的Binder。由于接收文件形式的Binder会在本进程中自动打开文件,Server可以用该标志禁止该功能,以防打开过多文件。
最后的cookie域则是额外携带的数据,驱动不关心该值。
往parcel中写入binder对象的时候,parcel内部会记录flat_binder_object的位置,以及数量,后续会用来赋值给binder_transaction_data结构体:

// parcel.h
size_t*             mObjects; // 指针,指向一个数组,数组的每一个元素对应一个Parcel中保存的binder对象的偏移。
size_t              mObjectsSize;

参数等数据写入Parcel后,就调用mRemote的transact方法,回忆下上面的流程,这里的mRemote就是从C++层返回的BinderProxy对象,而BinderProxy对象是Binder.java的一个内部类,它的transact方法的实现是一个native方法,具体实现是android_util_binder.cpp的android_os_BinderProxy_transact方法,我们跟进去喽:

// android_util_binder.cpp
static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
        jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException
{
    ...
    // 使用java层Parcel的mNativePtr获取C++层的Parcel对象。下同
    Parcel* data = parcelForJavaObject(env, dataObj);
    ...
    Parcel* reply = parcelForJavaObject(env, replyObj);
    ...
    // 获取前面分析的JNI层的BpBinder对象
    IBinder* target = (IBinder*)
        env->GetLongField(obj, gBinderProxyOffsets.mObject);
    ...
    // 使用BpBinder执行数据传输
    status_t err = target->transact(code, *data, reply, flags);
    //...
    return err;
}

整体来看逻辑很简单,就是根据mObject域获取BpBinder对象,然后调用其transact方法。
BpBinder的transact的实现又进一步委托给了IPCThreadState,这个类就是专门和驱动打交道的,包括server端那边的BBinder也是委托给这个类来实现和驱动交互的。
PS:驱动传输数据会有自己的一套规则,IPCThreadState就是封装屏蔽了规则的细节,让使用者只需关心业务处理即可:

status_t BpBinder::transact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
    // Once a binder has died, it will never come back to life.
    if (mAlive) {
        // 特别注意这里传了mHandle进去,他就是目标Binder实体的handle句柄,在我们这个环境handle是0,也就是会找到SMgr的Binder实体。
        status_t status = IPCThreadState::self()->transact(
            mHandle, code, data, reply, flags);
        if (status == DEAD_OBJECT) mAlive = 0;
        return status;
    }
    return DEAD_OBJECT;
}

关于这个handle已经强调了很多次了,之所以一直强调也是这次研究binder底层得到的最大收获,也就是binder对象跨进程传输的本质就是handle句柄,仅仅是一个整型值。
来看IPCThreadState的transact方法:

status_t IPCThreadState::transact(int32_t handle,
                                  uint32_t code, const Parcel& data,
                                  Parcel* reply, uint32_t flags)
{
    ... 
    if (err == NO_ERROR) {
        err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, NULL);
    }
    // 这里后面的代码是处理回复的,我们暂时先把请求走通,后续再补充响应(其实响应也可以理解为是目标进程的一次写入)
    return err;
}

这里writeTransactionData的第一个参数BC_TRANSACTION是一个驱动命令字,具体可以查看binder驱动协议部分的笔记。我们跟进这个方法,就会看到binder协议中提到的binder_transaction_data结构体了:

status_t IPCThreadState::writeTransactionData(int32_t cmd, uint32_t binderFlags,
    int32_t handle, uint32_t code, const Parcel& data, status_t* statusBuffer)
{
    binder_transaction_data tr; // 此结构体定义参见Binder驱动部分笔记
    tr.target.ptr = 0; // 此时ptr是0,后面解析handle后就会赋值为目标binder实体对象的地址。
    tr.target.handle = handle; // handle句柄给target的handle域,很关键。
    tr.code = code;
    tr.flags = binderFlags;
    ...
    const status_t err = data.errorCheck();
    if (err == NO_ERROR) {
        tr.data_size = data.ipcDataSize();
        tr.data.ptr.buffer = data.ipcData(); // 这个方法其实就是返回parcel的起始地址
        // 下面两行代码的赋值,就是大佬文章中提到的在buffer中标记出传输的binder对象,这里data是Parcel,
        // 在往Parcel中写入Strong binder时,Parcel内部已经对binder对象进行计数了,并且记录了其偏移量,
        // 这一点去看writeStrongBinder的代码就知道了。
        tr.offsets_size = data.ipcObjectsCount() * sizeof(binder_size_t);
        tr.data.ptr.offsets = data.ipcObjects(); // 返回数组的起始地址
    } // ...
    // 先写命令字,再写结构体数据
    mOut.writeInt32(cmd);
    mOut.write(&tr, sizeof(tr));
    return NO_ERROR;
}

从上述代码可以看到数据写入了mOut中,这个mOut是Parcel类型的,用于暂存即将写入驱动的数据,还有一个mInt,用于暂存从驱动中接收到的数据。
那么,什么时候会将mOut真正写入驱动呢?
答案是调用writeTransactionData方法之后的waitResponse方法,该方法内部会调用talkWithDriver,进一步的该方法内部会通过ioctl方法将mOut中的数据传入驱动。

status_t IPCThreadState::talkWithDriver(bool doReceive)
{
    ...
    // 这里binder_write_read的结构可以看驱动协议部分,以下代码就是对bwr相关的域进行赋值,然后调用ioctl传入驱动。
    binder_write_read bwr;
    ...
    // 先重点关注buffer部分,这里赋值为了parcel的起始地址,因此整个mOut的内容会写入驱动
    bwr.write_buffer = (long unsigned int)mOut.data();
    ...
    status_t err;
    do {
        if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)
            err = NO_ERROR;
        else
            err = -errno;
        ...
    } while (err == -EINTR);
    ...
    return err;
}

OK,到此就要开始进入驱动了,在这里总结一下目前为止mOut中都存了哪些内容:
[cmd, binder_transaction_data] ....
其中cmd是BC_TRANSACTION,tr中:target.handle = 0,data域的buffer指向了我们最上层要传输的内容。

驱动部分

对于字符设备驱动,以前也学过一点,驱动在实现的时候有个地方会注册当上层调用open、ioctl等方法时,驱动对应的实现是哪个方法,对于binder驱动,ioctl方法对应的就是binder_ioctl方法,该方法实现在binder.c,我们进去看:

// binder.c
static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
   int ret;
   // binder_proc结构体中存储了和用户进程有关的信息,binder_open的时候会实例化。
   struct binder_proc *proc = filp->private_data;
   struct binder_thread *thread;
   ...
   // 这个方法用于获取给定进程一个空闲的binder线程,请注意,一个线程是不是binder线程本质上就是
   // 在驱动中是不是有相应的binder_thread结构体和它对应(通过pid绑定)。
   // 如果这个方法没有找到binder_thread和当前的线程绑定,则会实例化一个然后建立绑定关系。
   // PS:线程池的内容还有待进一步研究,这里先不做过多讨论,本次分析的重点在于handle如何解析,以及parcel中的实体对象如何传递
   thread = binder_get_thread(proc);
   ...
   switch (cmd) {
       case BINDER_WRITE_READ: {
          struct binder_write_read bwr;
          ...
          if (bwr.write_size > 0) {
             ret = binder_thread_write(proc, thread, (void __user *)bwr.write_buffer, bwr.write_size, &bwr.write_consumed);
             ...
          }
          if (bwr.read_size > 0) {
             // 这里是读,通常同步的ipc请求会等待响应,因此读的时候可能会阻塞。
          }
          ...
          break;
   }
   ...
   return ret;
}

接着继续看binder_thread_write方法,重点关注对buffer的处理:

int binder_thread_write(struct binder_proc *proc, struct binder_thread *thread,
         void __user *buffer, int size, signed long *consumed)
{
   uint32_t cmd;
   void __user *ptr = buffer + *consumed;
   void __user *end = buffer + size;
   // 根据binder驱动协议的内容,buffer中的结构就是cmd+特定的数据结构,因此这里是一个循环,取出每一个命令字处理
   // 我们要关注的是BC_TRANSACTION这个命令字
   while (ptr < end && thread->return_error == BR_OK) {
      // 读命令字
      if (get_user(cmd, (uint32_t __user *)ptr))
         return -EFAULT;
      ...
      switch (cmd) {
      ...
          case BC_TRANSACTION:
          case BC_REPLY: {
             struct binder_transaction_data tr;
             if (copy_from_user(&tr, ptr, sizeof(tr)))
                return -EFAULT;
             ptr += sizeof(tr);
             binder_transaction(proc, thread, &tr, cmd == BC_REPLY);
             break;
          }
      }
      // ...
   }
   return 0;
}

可以看到,会拷贝BC_TRANSACTION命令后面的数据到内核,并用binder_transaction_data结构体存储,然后调用了binder_transaction进一步处理,以下代码重点关注对tr结构体的处理:

static void binder_transaction(struct binder_proc *proc,
                struct binder_thread *thread,
                struct binder_transaction_data *tr, int reply)
{
   ... // 省略大量变量声明
   if (reply) {
      ... // 先忽略响应的场景
   } else {
       // 高能,这里开始用handle解析出驱动中的引用
      if (tr->target.handle) {
         struct binder_ref *ref;
         // 这里binder_get_ref会从红黑树中找binder_ref对象,对比的字段就是handle和ref的desc域是否一致,
         // 而ref的desc域我们在后面会看到,其实就是红黑树的索引,从1开始的编号,可见贯穿Binder机制始终的句柄原来就是一个编号而已。。。
         ref = binder_get_ref(proc, tr->target.handle);
         if (ref == NULL) {
            ... // 错误处理,ref是一定要被找到的,不然就是无效的handle句柄
         }
         // 找到ref,轻松就能找到目标节点,即binder_node对象,node里就有实体对象的ptr地址
         target_node = ref->node;
      } else {
          // 这里binder_context_mgr_node特指smgr服务的binder_node节点,由此可见,如果handle不传,驱动会默认访问的是smgr进程。
         target_node = binder_context_mgr_node;
         ...
      }
      ...
   }
   ...
   // t的类型是binder_transaction,驱动协议中没有讨论这个结构体,因为它是驱动内部使用的,对它的赋值也很关键,重点关注以下几个域的赋值:
   ...
   t->code = tr->code;
   ...
   t->buffer->target_node = target_node;
   ...
   // t->buffer->data会用于存储tr中的data.ptr.buffer + data.ptr.offsets的数据,即把parcel中的数据和offsets数组扁平化了。
   // 这里要注意理解,别把我们传入驱动的数据跟丢了。这里offp会指向offsets数组的起始位置。
   offp = (size_t *)(t->buffer->data + ALIGN(tr->data_size, sizeof(void *)));
    // 拷贝用户数据,先拷贝parcel数据部分
    if (copy_from_user(t->buffer->data, tr->data.ptr.buffer, tr->data_size)) {
       ...
    }
    // 拷贝offsets数组
    if (copy_from_user(offp, tr->data.ptr.offsets, tr->offsets_size)) {
       ...
    }
    ...
   off_end = (void *)offp + tr->offsets_size;
   // 开始遍历处理parcel中传输的binder对象
   for (; offp < off_end; offp++) {
      struct flat_binder_object *fp;
      ...
      fp = (struct flat_binder_object *)(t->buffer->data + *offp);
      switch (fp->type) {
          // 我们这里是注册服务,所以type是BINDER_TYPE_BINDER,后面的case处理的是引用的情况,
          // 从smgr中获取服务时,数据缓存区中存放的就是binder引用。
      case BINDER_TYPE_BINDER:
      case BINDER_TYPE_WEAK_BINDER: {
         struct binder_ref *ref;
         struct binder_node *node = binder_get_node(proc, fp->binder);
         if (node == NULL) {
             // 这里非常关键,前面尝试用binder域,即binder实体的指针找出驱动和它绑定的node对象,
             // 如果找不到,说明该binder实体是首次在驱动中传输,因此这里new了新的node节点和它绑定。
            node = binder_new_node(proc, fp->binder, fp->cookie);
            ...
         }
         ...
         // 这里尝试获取一个和node相对应的ref对象,如果不存在,就会生成一个,因为等下马上要去smgr进程了,
         // node是不可能传过去的,只能ref过去,也就相当于binder实体对象尝试跨驱动传输,但是却止步于当前进程,传过去的只是ref而已。
         ref = binder_get_ref_for_node(target_proc, node);
         // 这里把新的ref的desc域的赋值逻辑贴一下,以便理解handle句柄的本质:
         // 以下片段是binder_get_ref_for_node方法里的,贴在这里方便分析:
         
         // 片段start
          new_ref->desc = (node == binder_context_mgr_node) ? 0 : 1; // 赋起始值,对于smgr的node,起始赋为0。
          // 传进来的proc是target的proc,refs_by_desc就是target进程持有的所有的ref引用,用红黑树存储,for循环就是遍历这棵树。
          for (n = rb_first(&proc->refs_by_desc); n != NULL; n = rb_next(n)) {
               // 这句代码涉及到linux中红黑树的写法,暂时不用管,只需要知道执行完后,ref指向当前遍历到的引用对象。
               ref = rb_entry(n, struct binder_ref, rb_node_desc);
               if (ref->desc > new_ref->desc)
                  break;
               new_ref->desc = ref->desc + 1; // 看到了吧,就是这么简单,挨个加1就是handle句柄
           }
         // 片段end
         
         ...
         if (fp->type == BINDER_TYPE_BINDER)
            fp->type = BINDER_TYPE_HANDLE; // 关键点:修改type为handle类型,后面即将对handle域赋值。
         else
            fp->type = BINDER_TYPE_WEAK_HANDLE;
         fp->handle = ref->desc; // handle就是desc
         // 以上代码直接操作fp指向的内存,修改了type和handle域,这样的话数据区域只需整体拷贝到目标进程即可,
         // 散落在parcel中的flat_binder_object的位置等是不变的。
         ...
      } break;
      ... // 省略其他case
   }
   // 以上,for循环执行完毕,则散落在parcel各个地方的binder实体对象就相应的生成了node节点了,并且后续的传输已经变成了handle的传输了。
   if (reply) {
      ...
   } else if (!(t->flags & TF_ONE_WAY)) {
       ... // 一般rpc调用都是同步的,所以看这个if
      thread->transaction_stack = t;
   } else {
      ...
   }
   t->work.type = BINDER_WORK_TRANSACTION;
   // 把t中的binder_work工作项添加到target_list中,这个target_list就是目标进程的todo队列,
   // 每个参与进程间通信的进程在驱动中都有这样的队列,binder线程们会不断从中取出work工作项处理。
   list_add_tail(&t->work.entry, target_list); 
   ...
   return;

OK,目前为止,传输的数据被封装成了工作项,放到了目标进程的todo队列中了。现在要把镜头转向smgr进程了,也就是我们的目标进程。

SMgr进程部分

SMgr进程启动的时候,会open驱动 -> 注册成为smgr服务 -> 主线程注册成为binder线程 -> 主线程循环等待请求数据。所谓的循环等待其实就是尝试从驱动中读取数据,每次的尝试读,驱动会检查有没有你这个线程的工作项,以及检查有没有你这个线程所属进程的工作项,如果有,就会把工作项取出来,然后返回,如果没有,则会等待在binder_proc的wait域上。具体我们看代码。
smgr启动过程可以看ServiceManager部分的笔记,这里直接从ioctl开始分析:

res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);

BINDER_WRITE_READ这个命令我们在上面已经见过了,这里是读,上面我们分析的是写,读部分的代码如下:

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
   // ...
   switch (cmd) {
   case BINDER_WRITE_READ: {
      struct binder_write_read bwr;
      // ...
      if (bwr.write_size > 0) {
         // ... 写已经分析过了
      }
      if (bwr.read_size > 0) {
         ret = binder_thread_read(proc, thread, (void __user *)bwr.read_buffer, bwr.read_size, 
                                 &bwr.read_consumed, filp->f_flags & O_NONBLOCK);
         if (!list_empty(&proc->todo))
            wake_up_interruptible(&proc->wait);
         if (ret < 0) {
            if (copy_to_user(ubuf, &bwr, sizeof(bwr)))
               ret = -EFAULT;
            goto err;
         }
      }
      // ...
      break;
   }
   // ...
   return ret;
}

OK,跟进binder_thread_read方法:

static int binder_thread_read(struct binder_proc *proc,
               struct binder_thread *thread,
               void  __user *buffer, int size,
               signed long *consumed, int non_block)
{
   // ...
   int wait_for_proc_work;
   // ... 
   // 这里判断thread自己是否有工作项要处理
   wait_for_proc_work = thread->transaction_stack == NULL &&
            list_empty(&thread->todo);
   // ...
   if (wait_for_proc_work) {
      // ...
      // non_block是外面传过来的,表示线程没工作时是否阻塞
      if (non_block) {
         if (!binder_has_proc_work(proc, thread))
            ret = -EAGAIN;
      } else {
          // 等待在proc的wait上,第二个参数是等待的condition,即条件
          ret = wait_event_interruptible_exclusive(proc->wait, binder_has_proc_work(proc, thread));
      } 
   } else {
      if (non_block) {
         if (!binder_has_thread_work(thread))
            ret = -EAGAIN;
      } else
         ret = wait_event_interruptible(thread->wait, binder_has_thread_work(thread));
   }
   // ...
   // 代码能走到这里,说明线程唤醒了,此时有工作项要处理了,因此进入循环处理每一个work项。
   while (1) {
      uint32_t cmd;
      struct binder_transaction_data tr;
      struct binder_work *w;
      struct binder_transaction *t = NULL;
      // 优先看线程自己有没有todo,没有就去处理进程的todo  
      if (!list_empty(&thread->todo))
         w = list_first_entry(&thread->todo, struct binder_work, entry);
      else if (!list_empty(&proc->todo) && wait_for_proc_work)
         w = list_first_entry(&proc->todo, struct binder_work, entry);
      else {
         // ...
      }
      // ...
      // 判断工作项的类型,回忆前面的流程,我们这个场景的工作项类型是BINDER_WORK_TRANSACTION
      switch (w->type) {
          case BINDER_WORK_TRANSACTION: {
              // 可见,只是把t取出来了,t的类型就是前面提到的binder_transaction
             t = container_of(w, struct binder_transaction, work);
          } break;
          // ... 省略其他case
      }
      // ...
      if (t->buffer->target_node) {
         struct binder_node *target_node = t->buffer->target_node;
         // tr是刚刚定义的binder_transaction_data类型的变量,后面的代码就是对这个结构体进行赋值,最后会传给目标进程处理
         tr.target.ptr = target_node->ptr;
         tr.cookie =  target_node->cookie;
         t->saved_priority = task_nice(current);
         // ...
         cmd = BR_TRANSACTION;
      } else {
         // ...
      }
      tr.code = t->code;
      tr.flags = t->flags;
      // ...
      tr.data_size = t->buffer->data_size;
      tr.offsets_size = t->buffer->offsets_size;
      // 前面说过,我们传入驱动的数据被扁平化到了t->buffer->data里,这里则分别取出来赋值到tr中,
      // 但是注意,数据依然是连续的,只不过此时用了两个指针去存储而已。
      tr.data.ptr.buffer = (void *)t->buffer->data +
               proc->user_buffer_offset;
      tr.data.ptr.offsets = tr.data.ptr.buffer +
               ALIGN(t->buffer->data_size,
                   sizeof(void *));
      // ptr就是smgr进程读的时候告诉内核要写入的地址,这里先写命令字进去,我们的场景该值为BR_TRANSACTION             
      if (put_user(cmd, (uint32_t __user *)ptr))
         return -EFAULT;
      ptr += sizeof(uint32_t);
      // 随后再写入tr,即binder_transaction_data结构体
      if (copy_to_user(ptr, &tr, sizeof(tr)))
         return -EFAULT;
      ptr += sizeof(tr);
      // ...
      break;
   }
   // ...
   return 0;
}

ok,现在smgr进程提供的缓冲区中已经被驱动写入binder_transaction_data结构的数据了,现在回到smgr进程,看看如何处理的:
smgr的for循环中,一旦ioctl返回,就调用binder_parse解析数据:

void binder_loop(struct binder_state *bs, binder_handler func)
{
    // ...
    for (;;) {
        // ...
        res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);
        // ...
        res = binder_parse(bs, 0, readbuf, bwr.read_consumed, func);
        // ...
    }
}

其中readbuf就是驱动写入数据的地址,跟进parse方法:

int binder_parse(struct binder_state *bs, struct binder_io *bio,
                 uint32_t *ptr, uint32_t size, binder_handler func)
{
    int r = 1;
    uint32_t *end = ptr + (size / 4);
    // 因为驱动可能传了很多个[cmd+数据]形式的数据过来,因此这里循环逐个处理,我们关注的是BR_TRANSACTION这个命令。
    while (ptr < end) {
        // 第一个是命令字,直接读出来
        uint32_t cmd = *ptr++;
        // ...
        switch(cmd) {
        // ...
            case BR_TRANSACTION: {
                // 注意这里,非常骚气,一度看的我很懵逼,ptr既然是cmd+数据格式的数据,那么BR_TRANSACTION后面跟的
                // 应该是binder_transaction_data结构的数据啊,怎么这里强转成了binder_txn呢?
                // 后面仔细对比,原来这个binder_txn结构体和binder_transaction_data结构体惊人的相似,仅仅是把union去掉了,
                // 在C++中,这种强转是没毛病的。
                struct binder_txn *txn = (void *) ptr;
                // 这里把binder_txn这个结构体贴出来,然后总结下各个域目前代表的含义,毕竟数据经过了一波驱动,记忆会有些模糊。
                struct binder_txn
                {
                    // 驱动已经悄悄修改成了目标binder实体的指针,代码片段:tr.target.ptr = target_node->ptr;
                    void *target; 
                    void *cookie;
                    // 方法编号,赋值片段:tr.code = t->code;
                    uint32_t code;
                    uint32_t flags;
                    
                    uint32_t sender_pid;
                    uint32_t sender_euid;
                    // 数据区的大小,赋值片段:tr.data_size = t->buffer->data_size;
                    uint32_t data_size;
                    // 偏移数组的大小,片段:tr.offsets_size = t->buffer->offsets_size;
                    uint32_t offs_size;
                    // 原来tr这里的data是一个union,里面有个ptr结构体存储了两个指针,一个是数据区,一个offsets区,
                    // 强转后,第一个指针就给了这里的data域,而第二个指针就给了这里的offs域。
                    void *data;
                    void *offs;
                };
                // ...
                if (func) { // func函数是传进来的,名称是svcmgr_handler
                    // ...
                    struct binder_io msg;
                    // ...
                    // 这里把txn中的相关域赋值给了binder_io,后面会从这个binder_io中读取数据,它里面维护了当前读到了哪里。
                    bio_init_from_txn(&msg, txn);
                    res = func(bs, txn, &msg, &reply);
                    // 。。。
                }
                ptr += sizeof(*txn) / sizeof(uint32_t); // 偏移指针,处理下一个命令字。
                break;
            }
        // ...
        }
    }

    return r;
}

拿到binder_io后,调用了svcmgr_handler函数,我们继续跟进:

int svcmgr_handler(struct binder_state *bs,
                   struct binder_txn *txn,
                   struct binder_io *msg,
                   struct binder_io *reply)
{
    struct svcinfo *si;
    uint16_t *s;
    unsigned len;
    void *ptr;
    uint32_t strict_policy;
    int allow_isolated;
    // ...
    // 这里bio_get_unit32会从msg中的data数据区中读一个指针出来,但是这个strict_policy后面没有用到,
    // 而且一路跟源码过来,好像没有哪里会往数据区中写入指针啊???
    // 其实在Server端的addService方法中,有这么一行代码:
    // data.writeInterfaceToken(IServiceManager.descriptor);
    // descriptor的值就是:android.os.IServiceManager
    // writeInterfaceToken这个方法在往parcel中写入desc的之前,写入了一个整型值:
    // writeInt32(IPCThreadState::self()->getStrictModePolicy() | STRICT_MODE_PENALTY_GATHER);
    // ok,现在知道指针的来历了,不过现在不考究strict_policy的含义,之所以这里展开说明是为了要证明msg的data数据区
    // 跟最开始我们写入parcel中的数据是完全一致的。
    strict_policy = bio_get_uint32(msg);
    // 这里就是对应获取descriptor的值,读字符串就是先读len,然后读len个字节的数据。
    s = bio_get_string16(msg, &len);
    if ((len != (sizeof(svcmgr_id) / 2)) ||
        // svcmgr_id是一个字节数组,该值正好是android.os.IServiceManager
        memcmp(svcmgr_id, s, sizeof(svcmgr_id))) {
        fprintf(stderr,"invalid id %s\n", str8(s));
        return -1;
    }
    // 根据code调用相应的方法,我们关注ADD_SERVICE_TRANSACTION
    switch(txn->code) {
        // ...
        case SVC_MGR_ADD_SERVICE:
            // 读出service的名称
            s = bio_get_string16(msg, &len);
            // 这个方法下面要展开分析,因为涉及到handle句柄的读取问题,这里先只需知道返回的ptr指针会指向一个整型值,而这个整型值
            // 就是handle句柄。
            ptr = bio_get_ref(msg);
            allow_isolated = bio_get_uint32(msg) ? 1 : 0;
            if (do_add_service(bs, s, len, ptr, txn->sender_euid, allow_isolated))
                return -1;
            break;
        // ..
    }

    bio_put_uint32(reply, 0);
    return 0;
}

bio_get_ref这个方法我们展开分析:

void *bio_get_ref(struct binder_io *bio)
{
    // 这个binder_object和flat_binder_object也是惊人的一致,C++中这种强转的方式前面的binder_txn已经见过了
    struct binder_object *obj;
    
    obj = _bio_get_obj(bio);
    if (!obj)
        return 0;

    if (obj->type == BINDER_TYPE_HANDLE)
        return obj->pointer;

    return 0;
}

跟进_bio_get_obj方法:

static struct binder_object *_bio_get_obj(struct binder_io *bio)
{
    unsigned n;
    // 这里bio.data和bio.data0是同时被初始化为数据区的起始地址的,但是bio后面被读的时候data在偏移,但是data0是不动的,
    // 因此这里data-data0不就是当前读的字节的偏移吗?parcel中的数据不就是写完了name,再写入strongBinder的嘛,
    // 因此这里断定off开始处,一定是那个要注册进来的binder对象。
    unsigned off = bio->data - bio->data0;
    /* TODO: be smarter about this? */  // 因为有写死的嫌疑,所以源码这里有个todo
    for (n = 0; n < bio->offs_avail; n++) {
        // 对比offs数组中是不是存在这样一个off,存在即说明数据传输正常,于是把binder对象恢复出来。
        if (bio->offs[n] == off)
            return bio_get(bio, sizeof(struct binder_object));
    }

    bio->data_avail = 0;
    bio->flags |= BIO_F_OVERFLOW;
    return 0;
}

OK,这个方法返回,则正确恢复出了要注册的binder对象,并且强转成了和flat_binder_object结构体类似的结构体binder_object。
回忆一下,parcel中的binder对象在驱动传输过程中,做了以下赋值:

if (fp->type == BINDER_TYPE_BINDER)
   fp->type = BINDER_TYPE_HANDLE;
else
   fp->type = BINDER_TYPE_WEAK_HANDLE;
fp->handle = ref->desc; 

可见,union中handle域被赋值为了desc域,即handle句柄,那么强转成binder_object后,binder_object的pointer这个指针域就指向handle句柄这个整型值了。
OK,说清楚了pointer域的含义后,咱们回到svcmgr_handler方法,继续接着看do_add_service方法:

int do_add_service(struct binder_state *bs,
                   uint16_t *s, unsigned len,
                   void *ptr, unsigned uid, int allow_isolated)
{
    // 传进来的s就是服务的名称,ptr上面说了,指向handle句柄这个整型值。
    struct svcinfo *si;
    // ...
    si = find_svc(s, len);
    if (si) {
        // 原来存在,则进行覆盖,被覆盖的被标记为死亡。
        if (si->ptr) {
            svcinfo_death(bs, si);
        }
        si->ptr = ptr;
    } else {
        si = malloc(sizeof(*si) + (len + 1) * sizeof(uint16_t));
        // ...
        si->ptr = ptr;
        si->len = len;
        memcpy(si->name, s, (len + 1) * sizeof(uint16_t));
        si->name[len] = '\0';
        // ...
        si->next = svclist; // 可见,smgr管理service是一个链表
        svclist = si;
    }
    // ...
    return 0;
}

到此,就把整个addService方法的调用流程都分析完了。
总结一下:
1、client进程拿到的IBinder对象只是一个本地的代理对象,真正起作用的是代理对象C++层的handle句柄,而handle句柄其实就是client进程在驱动中持有的所有的binder_ref的一个序号,驱动根据序号找到ref对象,进而找到binder_node对象。node对象中记录了target进程以及目标binder实体对象的地址,因此可以确切的找到调用的目的地。
2、parcel中试图首次传递binder实体对象时,驱动会为其建立binder_node对象,随后生成一个ref引用,最后把parcel中的传递的binder对象类型改为引用类型,这样目标进程接收到的IBinder就是本地的代理对象,一样的,通过handle来和远程的Binder实体对象关联。
3、binder线程就是一个普通的用户进程的线程,只不过在驱动中建立对应的binder_thread结构体而已,驱动只会把work工作项给目标进程的binder线程处理。对于上面分析的流程,smgr进程是主动把主线程注册成为binder线程的,但是对于一般的应用进程是怎么处理的呢?这个目前没研究相关的代码,不过可以来看下大佬是怎么说的:

应用程序通过INDER_SET_MAX_THREADS告诉驱动最多可以创建几个线程,每当驱动接收完数据包返回读Binder的线程时,都要检查一下是不是已经没有闲置线程了,
如果是,而且线程总数不会超出线程池最大线程数,就会在当前读出的数据包后面再追加一条BR_SPAWN_LOOPER消息,告诉用户线程即将不够用了,请再启动一些,
新线程一启动又会通过BC_xxx_LOOP告知驱动更新状态。这样只要线程没有耗尽,总是有空闲线程在等待队列中随时待命,及时处理请求。

根据目前的理解,以上能成立,需要用户进程至少有一个线程是一直在循环读binder的。

相关文章

网友评论

      本文标题:Binder对象跨进程传输的理解

      本文链接:https://www.haomeiwen.com/subject/aovcphtx.html