7.6.3 跨进程传递文件描述符的探讨

在回答上一节的问题之前,不知读者是否思考过下面的问题:实现文件描述符跨进程传递的目的是什么?

以上节读取音乐专辑的缩略图为例,问题的答案就是,让客户端能够读取专辑的缩略图文件。为什么客户端不先获得对应专辑缩略图的文件存储路径,然后直接打开这个文件,却要如此大费周章呢?原因有二:

出于安全的考虑,MediaProvider不希望客户端绕过它去直接读取存储设备上的文件。另外,客户端须额外声明相关的存储设备读写权限,然后才能直接读取其上面的文件。

虽然本例针对的是一个实际文件,但是从可扩展性角度看,我们希望客户端使用一个更通用的接口,通过这个接口可读取实际文件的数据,也可读取来自的网络的数据,而作为该接口的使用者无须关心数据到底从何而来。

提示 实际上还有更多的原因,读者不妨尝试在以上两点原因的基础上拓展思考。

1.序列化ParcelFileDescriptor

继续讨论本例的情况。现在服务端已经打开了某个缩略图文件,并且获得了一个文件描述符对象FileDescriptor。这个文件是服务端打开的。如何让客户端也打开这个文件呢?根据前文分析,客户端不会也不应该通过文件路径自己去打开这个文件。那该如何处理?

没关系,Binder驱动支持跨进程传递文件描述符。先来看ParcelFileDescriptor的序列化函数writeToParcel,代码如下:

[—>ParcelFileDescriptor. java:writeToParcel]


public void writeToParcel(Parcel out, int flags){

//往Parcel包中直接写入mFileDescriptor指向的FileDescriptor对象

out.writeFileDescriptor(mFileDescriptor);

if((flags&PARCELABLE_WRITE_RETURN_VALUE)!=0&&!mClosed){

try{

close();

}……

}

}


Parcel的writeFileDescriptor是一个native函数,代码如下:

[—>android_util_Binder. cpp:android_os_Parcel_writeFileDescriptor]


static void android_os_Parcel_writeFileDescriptor(JNIEnv*env,

jobject clazz, jobject object)

{

Parcel*parcel=parcelForJavaObject(env, clazz);

if(parcel!=NULL){

//先调用jniGetFDFromFileDescriptor从Java层FileDescriptor对象中

//取出对应的文件描述符。在Native层,文件描述符是一个int整型

//然后调用Native parcel对象的writeDupFileDescriptor函数

const status_t err=

parcel->writeDupFileDescriptor(

jniGetFDFromFileDescriptor(env, object));

if(err!=NO_ERROR){

signalExceptionForError(env, clazz, err);

}

}

}


Native Parcel类的writeDupFileDescriptor函数的代码如下:

[—>Parcel. cpp:writeDupFileDescriptor]


status_t Parcel:writeDupFileDescriptor(int fd)

{

return writeFileDescriptor(dup(fd),true);

}

//直接来看writeFileDescriptor函数

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.handle=fd;//将MediaProvider打开的文件描述符传递给Binder协议

obj.cookie=(void*)(takeOwnership?1:0);

return writeObject(obj, true);

}


由上边代码可知,ParcelFileDescriptor的序列化过程就是将其内部对应文件的文件描述符取出,并存储到一个由Binder驱动的flat_binder_object对象中。该对象最终会发送给Binder驱动。

2.反序列化ParcelFileDescriptor

假设客户端进程收到了来自服务端的回复,客户端要做的就是根据服务端的回复包构造一个新的ParcelFileDescriptor。我们重点关注文件描述符的反序列化,其中调用的函数是Parcel的readFileDescriptor,其代码如下:

[—>ParcelFileDescriptor. java:readFileDescriptor]


public final ParcelFileDescriptor readFileDescriptor(){

//从internalReadFileDescriptor中返回一个FileDescriptor对象

FileDescriptor fd=internalReadFileDescriptor();

//构造一个ParcelFileDescriptor对象,该对象对应的文件就是服务端打开的那个缩略图文件

return fd!=null?new ParcelFileDescriptor(fd):null;


internalReadFileDescriptor是一个native函数,其实现代码如下:

[—>android_util_Binder. cpp:android_os_Parcel_readFileDescriptor]


static jobject android_os_Parcel_readFileDescriptor(JNIEnv*env, jobject clazz)

{

Parcel*parcel=parcelForJavaObject(env, clazz);

if(parcel!=NULL){

//调用Parcel的readFileDescriptor得到一个文件描述符

int fd=parcel->readFileDescriptor();

if(fd<0)return NULL;

fd=dup(fd);//调用dup复制该文件描述符

if(fd<0)return NULL;

//调用jniCreateFileDescriptor以返回一个Java层的FileDescriptor对象

return jniCreateFileDescriptor(env, fd);

}

return NULL;

}


来看Parcel的readFileDescriptor函数,代码如下:

[—>Parcel. cpp:readFileDescriptor]


int Parcel:readFileDescriptor()const

{

const flat_binder_object*flat=readObject(true);

if(flat){

switch(flat->type){

case BINDER_TYPE_FD:

//当服务端发送回复包的时候,handle变量指向fd。当客户端接收回复包的时候,

//又从handle中得到fd。此fd是彼fd吗?

return flat->handle;

}}

return BAD_TYPE;

}


笔者在以上代码中提到了一个较深刻的问题:此fd是彼fd吗?这个问题的真实含义是:

服务端打开了一个文件,得到了一个fd。注意,fd是一个整型。在服务端上,这个fd确实对应了一个已经打开的文件。

客户端得到的也是一个整型值,它对应的是一个文件吗?

如果说客户端得到一个整型值,就认为它得到了一个文件,这种说法未免有些草率。在以上代码中,我们发现客户端确实根据收到的那个整型值创建了一个FileDescriptor对象。那么,怎样才可知道这个整型值在客户端中一定代表一个文件呢?

这个问题的终极解答在Binder驱动的代码中。来看它的binder_transaction函数。

3.文件描述符传递之Binder驱动的处理

这部分的代码如下:

[—>binder. c:binder_transaction]


static void binder_transaction(struct binder_proc*proc,

struct binder_thread*thread,

struct binder_transaction_data*tr, int reply)

……

switch(fp->type){

case BINDER_TYPE_FD:{

int target_fd;

struct file*file;

if(reply){

……

//Binder驱动根据服务端返回的fd找到内核中文件的代表file,其数据类型是

//struct file

file=fget(fp->handle);

……

//target_proc为客户端进程,task_get_unused_fd_flags函数用于从客户端

//进程中找一个空闲的整型值,用作客户端进程的文件描述符

target_fd=task_get_unused_fd_flags(target_proc,

O_CLOEXEC);

……

//将客户端进程的文件描述符和代表文件的file对象绑定

task_fd_install(target_proc, target_fd, file);

fp->handle=target_fd;

}break;

……//其他处理

}


一切真相大白!原来,Binder驱动代替客户端打开了对应的文件,所以现在可以肯定,客户端收到的整型值确确实实代表一个文件。

4.深入讨论

在研究这段代码时,笔者曾经向所在团队同仁问过这样一个问题:在Linux平台上,有什么办法能让两个进程共享同一文件的数据呢?曾得到下面这些回答:

两个进程打开同一文件。这种方式前面讨论过了,安全性和可扩展性都比较差,不是我们想要的方式。

通过父子进程的亲缘关系,使用文件重定向技术。由于这两个进程关系太亲近,这种实现方式拓展性较差,也不是我们想要的。

跳出两个进程打开同一个文件的限制。在两个进程间创建管道,然后由服务端读取文件数据并写入管道,再由客户端进程从管道中获取数据。这种方式和前面介绍的openAssetFileDescriptor有殊途同归之处。

在缺乏类似Binder驱动支持的情况下,要在Linux平台上做到文件描述符的跨进程传递是件比较困难的事。从上面3种回答来看,最具扩展性的是第三种方式,即进程间采用管道作为通信手段。但是对Android平台来说,这种方式的效率显然不如现有的openAssetFileDescriptor的实现。原因在于管道本身的特性。

服务端必须单独启动一个线程来不断地往管道中写入数据,即整个数据的流动是由写端驱动的(虽然当管道无空间的时候,如果读端不读取数据,写端也没法再写入数据,但是如果写端不写数据,则读端一定读不到数据。基于这种认识,笔者认为管道中数据流动的驱动力应该在写端)。

Android 3. 0以后为CP提供了管道支持,我们来看相关的函数。

[—>ContentProvider. java:openPipeHelper]


public<T>ParcelFileDescriptor openPipeHelper(final Uri uri,

final String mimeType, final Bundle opts,

final T args, final PipeDataWriter<T>func)

throws FileNotFoundException{

try{

//创建管道

final ParcelFileDescriptor[]fds=ParcelFileDescriptor.

createPipe();

//构造一个AsyncTask对象

AsyncTask<Object, Object, Object>task=new

AsyncTask<Object, Object, Object>(){

@Override

protected Object doInBackground(Object……params){

//往管道写端写数据,如果没有这个后台线程的写操作,客户端无论如何

//也读不到数据的

func.writeDataToPipe(fds[1],uri, mimeType, opts, args);

try{

fds[1].close();

}……

return null;

}

};

//AsyncTask.THREAD_POOL_EXECUTOR是一个线程池,task的doInBackground

//函数将在线程池中的一个线程中运行

task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,

(Object[])null);

return fds[0];//返回读端给客户端

}……

}


由以上代码可知,采用管道这种方式的开销确实比客户端直接获取文件描述符的开销大。