Android Binder传递文件描述符原理分析

前言

Binder是Android中最常用,最重要的进程间通信机制。我们知道,Binder不仅可以传递普通的数据,还可以传递文件描述符。本文尝试分析Binder传递文件描述符的原理,切入点是工作中遇到一个和文件藐视符相关的问题。本文讲解分析和解决该问题的思路,在分析该问题的过程中,Binder传递文件描述符的原理也会呈现出来。我们都知道,Binder机制是由内核支持的,所以在这个过程中,也会深入到内核中去分析。


问题描述

一个native进程需要读取sdcard上的一个大文件,但是没有读sdcard权限。所以在app中打开该文件,并通过binder机制将该文件的fd传递到native进程中。但是在native进程中接收到fd后,无法通过fd读取文件。

问题原因

native进程在读取到fd后,会在C++层的Parcel对象析构时,关闭fd,相当于app端传递过来的fd,只在Parcel对象析构之前有效。下面看代码:

Parcel.cpp 析构函数

Parcel::~Parcel()
{
    freeDataNoInit();
    LOG_ALLOC("Parcel %p: destroyed", this);
}

Parcel的析构函数会释放Parcel中存放的资源,他调用了freeDataNoInit

Parcel.cpp freeDataNoInit方法:

void Parcel::freeDataNoInit()
{
    if (mOwner) {
        LOG_ALLOC("Parcel %p: freeing other owner data", this);
        //ALOGI("Freeing data ref of %p (pid=%d)", this, getpid());
        mOwner(this, mData, mDataSize, mObjects, mObjectsSize, mOwnerCookie);
    } else {
        LOG_ALLOC("Parcel %p: freeing allocated data", this);
        releaseObjects();
        if (mData) {
            LOG_ALLOC("Parcel %p: freeing with %zu capacity", this, mDataCapacity);
            pthread_mutex_lock(&gParcelGlobalAllocSizeLock);
            gParcelGlobalAllocSize -= mDataCapacity;
            gParcelGlobalAllocCount--;
            pthread_mutex_unlock(&gParcelGlobalAllocSizeLock);
            free(mData);
        }
        if (mObjects) free(mObjects);
    }
}

Parcel releaseObjects方法:

void Parcel::releaseObjects()
{
    const sp<ProcessState> proc(ProcessState::self());
    size_t i = mObjectsSize;
    uint8_t* const data = mData;
    binder_size_t* const objects = mObjects;
    while (i > 0) {
        i--;
        const flat_binder_object* flat
            = reinterpret_cast<flat_binder_object*>(data+objects[i]);
        release_object(proc, *flat, this, &mOpenAshmemSize);
    }
}

Parcel release_object方法

static void release_object(const sp<ProcessState>& proc,
    const flat_binder_object& obj, const void* who, size_t* outAshmemSize)
{
    switch (obj.type) {
        ......
        ......
        case BINDER_TYPE_FD: {
            if (outAshmemSize != NULL) {
                if (obj.cookie != 0) {
                    int size = ashmem_get_size_region(obj.handle);
                    if (size > 0) {
                        *outAshmemSize -= size;
                    }
                    close(obj.handle);
                }
            }
            return;
        }
    }

    ALOGE("Invalid object type 0x%08x", obj.type);
}

最后我们跟到release_object方法,case BINDER_TYPE_FD就是处理文件描述符的,可以看到调用了close,关闭了文件描述符。

所以,当我们从Parcel中通过调用readFileDescriptor读到文件描述符(fd)后,如果这时Parcel对象被析构了,那么我们读取的文件描述符就是无效的。因为底层的文件已经关闭了,我们读取的这个fd就只是一个普通的整数,已经和底层的文件没有关联了。


解决方法

在Parcel析构之前,读取到fd,并且调用dup方法复制一个新的fd。首先看一下Parcel.h中readFileDescriptor方法的注释:

Parcel.h

    // Retrieve a file descriptor from the parcel.  This returns the raw fd
    // in the parcel, which you do not own -- use dup() to get your own copy.
    int                 readFileDescriptor() const;

这个注释的意思是,如果你通过readFileDescriptor读取一个文件描述符,那么你读取的是一个原生(raw)的文件描述符,这个描述符并不属于你,也就是说这个文件描述符还是归Parcel所有,所以Parcel可以在析构时关闭文件。如果我们要使用这个文件描述符,那么就需要调用dup,复制这个文件描述符,然后我们使用复制的这个文件描述符,就不怕Parcel关闭文件了。

分析到这里,我们已经知道如何解决这个问题了。但是我们并不满足。还有一些问题是我们没搞明白的。比如:

1 文件描述符到底是怎么通过Binder传递的?
2 dup的原理是什么?
3 即使我们使用dup复制了文件描述符,但是Parcel里的close还是调用了,为什么文件没有被关闭?

我们接下来深入分析这些问题。


Android Binder传递文件描述符原理分析

我们先看客户端是怎么发起Binder请求的。在C++层中,客户端发起Binder调用,是在BpBindertransact函数中:

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) {
        status_t status = IPCThreadState::self()->transact(
            mHandle, code, data, reply, flags);
        if (status == DEAD_OBJECT) mAlive = 0;
        return status;
    }

    return DEAD_OBJECT;
}

BpBindertransact函数会调用IPCThreadStatetransact函数:

status_t IPCThreadState::transact(int32_t handle,
                                  uint32_t code, const Parcel& data,
                                  Parcel* reply, uint32_t flags)
{
    status_t err = data.errorCheck();
    ......
    ......
    if (err == NO_ERROR) {
        LOG_ONEWAY(">>>> SEND from pid %d uid %d %s", getpid(), getuid(),
            (flags & TF_ONE_WAY) == 0 ? "READ REPLY" : "ONE WAY");
        err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, NULL);
    }
    ......
    ......
    err = waitForResponse(NULL, NULL);
    
    return err;
}

该函数中首先调用writeTransactionData封装数据,然后调用waitForResponse
关键代码在waitForResponse

status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)
{
    uint32_t cmd;
    int32_t err;

    while (1) {
        if ((err=talkWithDriver()) < NO_ERROR) break;
        err = mIn.errorCheck();
        if (err < NO_ERROR) break;
        if (mIn.dataAvail() == 0) continue;
        
        cmd = (uint32_t)mIn.readInt32();
        ......
        ......

关键是talkWithDriver的调用:

status_t IPCThreadState::talkWithDriver(bool doReceive)
{
......
......
        if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)
            err = NO_ERROR;
        else
            err = -errno;
......
......

可以看出通过ioctl和binder驱动交互
mProcess->mDriver就是/dev/binder,BINDER_WRITE_READ是命令号,bwr是要传输的数据。

ioctl是一个系统调用,通过ioctl就陷入到了内核中,下面我们继续跟踪内核中的ioctl是如何处理Binder请求的。

Binder驱动实现在 kernel/drivers/android/binder.c中,用户空间调用ioctl后,会执行到kernel/drivers/android/binder.c中的binder_ioctl

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    int ret;
    struct binder_proc *proc = filp->private_data;
    struct binder_thread *thread;
    unsigned int size = _IOC_SIZE(cmd);
......
......

    switch (cmd) {
    case BINDER_WRITE_READ:
        ret = binder_ioctl_write_read(filp, cmd, arg, thread);
        if (ret)
            goto err;
        break;
......
......

filp是一个struct file指针,在kernel中struct file代表当前进程打开的一个文件,这里是/dev/binder
filp->private_data保存的是一个struct binder_proc,是在当前进程打开binder驱动时保存进去的,代表的是一个当前正在调用binder的一个客户端进程,arg是用户空间中数据的地址。

用户空间中调用ioctl时,传的命令号是BINDER_WRITE_READ,所以继续调用的是binder_ioctl_write_read:

static int binder_ioctl_write_read(struct file *filp,
                unsigned int cmd, unsigned long arg,
                struct binder_thread *thread)
{
    int ret = 0;
    struct binder_proc *proc = filp->private_data;
    unsigned int size = _IOC_SIZE(cmd);
    void __user *ubuf = (void __user *)arg;
    struct binder_write_read bwr;

    if (size != sizeof(struct binder_write_read)) {
        ret = -EINVAL;
        goto out;
    }
    if (copy_from_user(&bwr, ubuf, sizeof(bwr))) {
        ret = -EFAULT;
        goto out;
    }
    ......
    ......
    if (bwr.write_size > 0) {
        ret = binder_thread_write(proc, thread,
                      bwr.write_buffer,
                      bwr.write_size,
                      &bwr.write_consumed);
......
......
    }
......
......

首先调用copy_from_user从用户空间拷贝数据,可以先简单的认为这个数据就是要传递的Parcel对象,这里将数据拷贝到struct binder_write_read bwr中,然后调用binder_thread_write

int binder_thread_write(struct binder_proc *proc,
            struct binder_thread *thread,
            binder_uintptr_t binder_buffer, size_t size,
            binder_size_t *consumed)
{
......
......
        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;
        }
......
......

继续调用binder_transaction

static void binder_transaction(struct binder_proc *proc,
                   struct binder_thread *thread,
                   struct binder_transaction_data *tr, int reply)
{
......
......
        case BINDER_TYPE_FD: {
            int target_fd;
            struct file *file;
......
......
            file = fget(fp->handle);
            if (file == NULL) {
                binder_user_error("%d:%d got transaction with invalid fd, %d\n",
                    proc->pid, thread->pid, fp->handle);
                return_error = BR_FAILED_REPLY;
                goto err_fget_failed;
            }
            target_fd = task_get_unused_fd_flags(target_proc, O_CLOEXEC);
            if (target_fd < 0) {
                fput(file);
                return_error = BR_FAILED_REPLY;
                goto err_get_unused_fd_failed;
            }
            task_fd_install(target_proc, target_fd, file);
            trace_binder_transaction_fd(t, fp->handle, target_fd);
            binder_debug(BINDER_DEBUG_TRANSACTION,
                     "        fd %d -> %d\n", fp->handle, target_fd);
            /* TODO: fput? */
            fp->binder = 0;
            fp->handle = target_fd;
        } break;
......
......

上面的代码是Binder驱动处理传递的fd的过程。

首先调用fget,传递的参数fp->handle是客户端进程要传递的文件fd,fget根据fd获取到相关的struct file,一个struct file代表被进程打开的一个文件,当一个进程打开一个文件的时候,内核会创建一个struct file,然后将一个fd关联到该struct file,然后将fd返回到用户空间。

然后调用task_get_unused_fd_flags,参数target_proc代表Binder通信的目标进程,即Binder服务端进程。调用这个函数在目标进程中找到一个可用的fd。

在目标进程中找到一个可用的fd后,然后调用task_fd_install将上一步中找到的struct file关联到目标进程中的fd。

程序执行到这里,相当于Binder驱动在内核空间中帮目标进程打开了这个文件。然后将fd返回目标进程的用户空间,用户空间的程序就可以通过这个fd访问到相关文件。

dup原理分析

首先我们看一下dup的文档:

NAME
       dup, dup2, dup3 - duplicate a file descriptor

SYNOPSIS
       #include <unistd.h>

       int dup(int oldfd);
       int dup2(int oldfd, int newfd);

       #define _GNU_SOURCE             /* See feature_test_macros(7) */
       #include <fcntl.h>              /* Obtain O_* constant definitions */
       #include <unistd.h>

       int dup3(int oldfd, int newfd, int flags);

DESCRIPTION
       The dup() system call creates a copy of the file descriptor oldfd, using the lowest-
       numbered unused descriptor for the new descriptor.

       After a successful return, the old and new file descriptors may be used interchangeably.  
       They refer to the same open file description (see open(2)) and thus share file off‐set and 
       file status flags; for example, if the file offset is modified by using lseek(2) on one of 
       the descriptors, the offset is also changed for the other.

       The two descriptors do not share file descriptor flags (the close-on-exec flag).  The 
       close-on-exec flag (FD_CLOEXEC; see fcntl(2)) for the duplicate descriptor is off.

dup的文档上说dup调用复制一个文件描述符,新的描述符和旧的描述符,它们指向同一个打开的文件(在内核中就是同一个struct file)。

struct file定义在kernel/include/linux/fs.h中:

struct file {
    union {
        struct llist_node   fu_llist;
        struct rcu_head     fu_rcuhead;
    } f_u;
    struct path     f_path;
#define f_dentry    f_path.dentry
    struct inode        *f_inode;   /* cached value */
    const struct file_operations    *f_op;
    
    /*
     * Protects f_ep_links, f_flags, f_pos vs i_size in lseek SEEK_CUR.
     * Must not be taken from IRQ context.
     */
    spinlock_t      f_lock;
    atomic_long_t       f_count;
    unsigned int        f_flags;
    fmode_t         f_mode;
    loff_t          f_pos;
    struct fown_struct  f_owner;
    const struct cred   *f_cred;
    struct file_ra_state    f_ra;

    u64         f_version;
#ifdef CONFIG_SECURITY
    void            *f_security;
#endif
    /* needed for tty driver, and maybe others */
    void            *private_data;

#ifdef CONFIG_EPOLL
    /* Used by fs/eventpoll.c to link all the hooks to this file */
    struct list_head    f_ep_links;
    struct list_head    f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
    struct address_space    *f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNT
    unsigned long f_mnt_write_state;
#endif
};

文件状态f_flags和文件偏移f_pos都定义在struct file中,所以和struct file关联的fd会共享文件状态和文件偏移量。并且struct file中有一个使用计数f_count。当一个fd被关闭后,f_count减1,只有减到0后,struct file才会被释放,才算是真正关闭文件。

所以关闭旧的fd后,dup出的新的fd还是有效的。这也是为什么,Parcel析构时关闭了文件描述符后,我们自己dup出的文件描述符还是可用的。因为底层的文件(struct file)还有fd引用它,所以不会关闭,通过新的fd还是可以访问的。

下面是测试代码:

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

using namespace std;

void test_dup();
void test_dup2();

int main() {
    test_dup();
    test_dup2();
}

void test_dup() {
    cout << "========== test dup() =========" << endl;

    int fd = open("test.file", O_RDWR);
    if (fd == -1) {
        cout << "open test.file fail" << endl;
        return;
    }
    cout << "original fd is " << fd << endl;

    int fd1 = dup(fd);
    if (fd1 == -1) {
        cout << "dup fd fail" << endl;
        close(fd);
        return;
    }
    cout << "duplicate fd is " << fd1 << endl;
    
    cout << "close original fd" << endl;
    close(fd);

    cout << "start read from duplicated fd" << endl;
    char buf[20];
    if (read(fd1, buf, sizeof(buf)) == -1) {
        cout << "can not read from duplicated fd after orignal fd closed" << endl;
    } else {
        cout << "can not read from duplicated fd after orignal fd closed" << endl;
        cout << "read result is : " << buf << endl;
    }

    close(fd1);
}

void test_dup2() {
    cout << "========== test dup2() =========" << endl;
    
    int fd = open("test1.file", O_RDWR | O_CREAT, 0666);
    if (fd == -1) {
        cout << "open test1.file fail: " << strerror(errno) << endl;
        return;
    }
    
    // write to stdout fd 1
    char msg[50] = "Test write this msg to stdout\n";
    write(1, msg, sizeof(msg)); 

    // dup test1.file fd to stdout fd
    int fd1 = dup2(fd, 1);
    
    if (fd1 == -1) {
        cout << "dup2(fd, 1) fail : " << strerror(errno) << endl;
        return;
    }

    //Test write "hello" to fd 1, now fd 1 pointer to test1.file, so "hello" will write to test1.file
    char msg[6] = "hello\n";
    write(1, msg, sizeof(msg)); 

    close(fd);
    close(fd1);
}

总结

1 通过Binder传递文件描述符,相当Binder驱动为服务端进程打开同一个文件,这时客户端和服务端进程在内核中共享同一个struct file

2 当服务端进程将fd传递到用户空间的Parcel中时,Parcel在析构时会关闭这个fd,这是因为考虑到如果服务端进程不使用这个fd,这个fd就永远无法关闭,会造成fd泄露。

3 如果我们要使用这个fd,需要dup一个新的fd来使用,即使Parcel析构时关闭了原来的fd,新的fd不受影响。


原理图

最后画一张原理图:

在这里插入图片描述


Java层不需要显示调用dup

关于Binder的使用,相对于C++接口,我们更常用的是Java接口。我们使用Java接口读取其他进程传递过来的文件描述符时,并没有显示的调用dup,为什么我们读到的文件描述符是可用的呢?

其实不是不需要dup,而是我们在Java层中调用Parcel.java的readFileDescriptor的时候,jni层会自动帮我们dup出一个文件描述符,然后返回这个dup的文件描述符,而不是直接返回原生的文件描述符。

下面通过代码分析:

Parcel.java的readFileDescriptor方法:

    public final ParcelFileDescriptor readFileDescriptor() {
        FileDescriptor fd = nativeReadFileDescriptor(mNativePtr);
        return fd != null ? new ParcelFileDescriptor(fd) : null;
    }

readFileDescriptor直接调用的nativeReadFileDescriptor:

    private static native FileDescriptor nativeReadFileDescriptor(long nativePtr);

和Java层的Parcel相对用的jni层,实现在frameworks/base/core/jni/android_os_Parcel.cpp中。jni层初始化的时候,会注册方法,就是把Java层的方法和C++层的方法关联起来:

int register_android_os_Parcel(JNIEnv* env)
{
    jclass clazz = FindClassOrDie(env, kParcelPathName);

    gParcelOffsets.clazz = MakeGlobalRefOrDie(env, clazz);
    gParcelOffsets.mNativePtr = GetFieldIDOrDie(env, clazz, "mNativePtr", "J");
    gParcelOffsets.obtain = GetStaticMethodIDOrDie(env, clazz, "obtain", "()Landroid/os/Parcel;");
    gParcelOffsets.recycle = GetMethodIDOrDie(env, clazz, "recycle", "()V");

    return RegisterMethodsOrDie(env, kParcelPathName, gParcelMethods, NELEM(gParcelMethods));
}

gParcelMethods是一个JNINativeMethod数组,在该数组中我们可以看到如下的一个JNINativeMethod项:

 {"nativeReadFileDescriptor",  "(J)Ljava/io/FileDescriptor;", (void*)android_os_Parcel_readFileDescriptor},

可以看出,和Parcel.java中的nativeReadFileDescriptor对应的,是frameworks/base/core/jni/android_os_Parcel.cpp中的android_os_Parcel_readFileDescriptor函数。下面看一下android_os_Parcel_readFileDescriptor:

static jobject android_os_Parcel_readFileDescriptor(JNIEnv* env, jclass clazz, jlong nativePtr)
{
    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
    if (parcel != NULL) {
        int fd = parcel->readFileDescriptor();
        if (fd < 0) return NULL;
        fd = dup(fd);
        if (fd < 0) return NULL;
        return jniCreateFileDescriptor(env, fd);
    }
    return NULL;
}

可以看到,该方法首先从一个C++的Parcel对象中读取到fd,然后通过dup复制一个新的fd。

所以,调用Java层的Parcel.java的readFileDescriptor,获取的就是通过dup复制的文件描述符。

猜你喜欢

转载自blog.csdn.net/brave2211/article/details/83502195