1. 程式人生 > >Android Binder 分析——匿名共享記憶體(好文)

Android Binder 分析——匿名共享記憶體(好文)

前面分析了 binder 中用來打包、傳遞資料的 Parcel,一般用來傳遞 IPC 中的小型引數和返回值。binder 目前每個程序 mmap 接收資料的記憶體是 1M,所以就算你不考慮效率問題用 Parcel 來傳,也無法傳過去。只要超過 1M 就會報錯(binder 無法分配接收空間)。所以 android 裡面有一個專門用來在 IPC 中傳遞大型資料的東西—— Ashmem(Anonymous Shared Memroy)。照例把相關程式碼的位置說一下(4.4):

123456789101112131415161718192021222324252627282930313233
# MemroyFile 是 ashmem java 層介面frameworks/base/core/java/os/Parcel.javaframeworks/base/core/java/os/Parcelable.javaframeworks/base/core/java/os/ParcelFileDescriptor.javaframeworks/base/core/java/os/MemoryFile.java# jni 相關frameworks/base/core/jni/android_os_Parcel.hframeworks/base/core/jni/android_os_MemoryFile.cppframeworks/base/core/jni/android_os_Parcel.cpplibnativehelper/JNIHelp.cpp# 封裝了 ashmem 驅動的 c 介面
system/core/include/cutils/ashmem.hsystem/core/libcutils/ashmem-dev.c# MemoryXx 是 ashmem 的 native 介面frameworks/native/include/binder/Parcel.hframeworks/native/include/binder/IMemory.hframeworks/native/include/binder/MemoryHeapBase.hframeworks/native/include/binder/MemoryBase.hframeworks/native/libs/binder/Parcel.cppframeworks/native/libs/binder/Memory.cppframeworks/native/libs/binder/MemoryHeapBase.cppframeworks/native/libs/binder/MemoryBase.cpp# kernel binder 驅動
kernel/drivers/staging/android/binder.hkernel/drivers/staging/android/binder.c# kernel ashmem 驅動kernel/include/linux/ashmem.hkernel/mm/ashmem.c

(這和 Parcel 篇的基本一樣麼 -_-||)

原理概述

ashmem 並像 binder 是 android 重新自己搞的一套東西,而是利用了 linux 的 tmpfs 檔案系統。關於 tmpfs 我目前還不算很瞭解,可以先看下這裡的2篇,有個基本的瞭解:

那麼大致能夠知道,tmpfs 是一種可以基於 ram 或是 swap 的高速檔案系統,然後可以拿它來實現不同程序間的記憶體共享。

然後大致思路和流程是:

  • Proc A 通過 tmpfs 建立一塊共享區域,得到這塊區域的 fd(檔案描述符)
  • Proc A 在 fd 上 mmap 一片記憶體區域到本程序用於共享資料
  • Proc A 通過某種方法把 fd 倒騰給 Proc B
  • Proc B 在接到的 fd 上同樣 mmap 相同的區域到本程序
  • 然後 A、B 在 mmap 到本程序中的記憶體中讀、寫,對方都能看到了

其實核心點就是建立一塊共享區域,然後2個程序同時把這片區域 mmap 到本程序,然後讀寫就像本程序的記憶體一樣。這裡要解釋下第3步,為什麼要倒騰 fd,因為在 linux 中 fd 只是對本程序是唯一的,在 Proc A 中開啟一個檔案得到一個 fd,但是把這個開啟的 fd 直接放到 Proc B 中,Proc B 是無法直接使用的。但是檔案是唯一的,就是說一個檔案(file)可以被開啟多次,每開啟一次就有一個 fd(檔案描述符),所以對於同一個檔案來說,需要某種轉化,把 Proc A 中的 fd 轉化成 Proc B 中的 fd。這樣 Proc B 才能通過 fd mmap 同樣的共享記憶體檔案(額,其實這裡相關知識我也還沒了解,瞎扯一下)。

java 層介面

java 層的介面要拿 2.3 的來說,因為從 4.1(具體哪個版本我不好說,反正我手上只有 4.1 之後的)之後 java 層的 MemroyFile 應該就無法正常使用了,搜尋程式碼發現,除了 test 有 MemroyFile 其它地方就去掉了。具體原因後面分析程式碼就知道了。

咋先來點感性的認識(MemroyFile.java):

1234567891011121314151617181920
private FileDescriptor mFD;        // ashmem file descriptorprivate int mAddress;   // address of ashmem memoryprivate int mLength;    // total length of our ashmem regionprivate boolean mAllowPurging = false;  // true if our ashmem region is unpinnedprivate final boolean mOwnsRegion;  // false if this is a ref to an existing ashmem region/* * Allocates a new ashmem region. The region is initially not purgable. * * @param name optional name for the file (can be null). * @param length of the memory file in bytes. * @throws IOException if the memory file could not be created. */public MemoryFile(String name, int length) throws IOException {    mLength = length;    mFD = native_open(name, length);    mAddress = native_mmap(mFD, length, PROT_READ | PROT_WRITE);    mOwnsRegion = true;}

MemroyFile 還是比較簡單的,成員變數也比較少,上面基本上就是所有的變量了。FileDescriptor 這個是 java 本身的物件,應該是 natvie fd 的封裝吧。後面的地址、長度不說了。後面2個 boolean, mAllowPurging 表示這塊 ashmem 是否允許被回收。 ashmem 在驅動那向 kernel 註冊了一個記憶體回收演算法,當 kernel 進行記憶體掃描的時候會呼叫這個回收演算法,當標記了可以回收的時候,會把標記的記憶體給回收掉。這個設計的目的估計是想更高效的使用記憶體(能夠標記一段共享記憶體不用了),但是後面你會發現這個東西目前還是個擺設。mOwnsRegion 表示只有建立者才能標記這塊共享記憶體被回收。

MemroyFile 的使用方法,就只有建構函式一個。而且預設 mAllowPurging 是 false。這個建構函式是建立共享記憶體的,所以 mOwnsRegion 是 true。回想下前面原理,說的 Proc A 首先要建立一塊共享記憶體,然後再 mmap 到本程序。這裡正好2個 jni:

1234567891011121314151617
static jobject android_os_MemoryFile_open(JNIEnv* env, jobject clazz, jstring name, jint length){    const char* namestr = (name ? env->GetStringUTFChars(name, NULL) : NULL);    int result = ashmem_create_region(namestr, length);    if (name)        env->ReleaseStringUTFChars(name, namestr);    if (result < 0) {        jniThrowException(env, "java/io/IOException", "ashmem_create_region failed");        return NULL;    }    return jniCreateFileDescriptor(env, result);}

這個 jni 很簡單了,前面的 MemroyFile 傳了要建立的共享記憶體的名字以及大小。這裡主要是呼叫 libcutils 裡面的 ashmem-dev.c 的介面去建立共享記憶體:

12345678910111213141516171819202122232425262728293031323334353637
#define ASHMEM_DEVICE   "/dev/ashmem"/* * ashmem_create_region - creates a new ashmem region and returns the file * descriptor, or <0 on error * * `name' is an optional label to give the region (visible in /proc/pid/maps) * `size' is the size of the region, in page-aligned bytes */int ashmem_create_region(const char *name, size_t size){    int fd, ret;    fd = open(ASHMEM_DEVICE, O_RDWR);    if (fd < 0)        return fd;    if (name) {        char buf[ASHMEM_NAME_LEN];        strlcpy(buf, name, sizeof(buf));        ret = ioctl(fd, ASHMEM_SET_NAME, buf);        if (ret < 0)            goto error;    }    ret = ioctl(fd, ASHMEM_SET_SIZE, size);    if (ret < 0)        goto error;    return fd;error:    close(fd);    return ret;}

熟悉 linux 環境程式設計的也沒啥要說的, open 開啟裝置。/dev/ashmem 在前面有篇文章說到,在 init.rc 裡面和 /dev/binder 是系統 init 程序建立好的裝置節點(虛擬機器裝置)。然後 ioctl 去設定名字和大小。這裡就要走到 kernel 的驅動裡面去了,這些後面再說。然後返回 fd。然後回到 jni 裡面,通過 fd 構造出 java 的 FileDescriptor(JNIHelp.cpp):

1234567891011121314151617181920
jobject jniCreateFileDescriptor(C_JNIEnv* env, int fd) {    JNIEnv* e = reinterpret_cast<JNIEnv*>(env);    static jmethodID ctor = e->GetMethodID(JniConstants::fileDescriptorClass, "<init>", "()V");    jobject fileDescriptor = (*env)->NewObject(e, JniConstants::fileDescriptorClass, ctor);    jniSetFileDescriptorOfFD(env, fileDescriptor, fd);    return fileDescriptor;}int jniGetFDFromFileDescriptor(C_JNIEnv* env, jobject fileDescriptor) {    JNIEnv* e = reinterpret_cast<JNIEnv*>(env);    static jfieldID fid = e->GetFieldID(JniConstants::fileDescriptorClass, "descriptor", "I");    return (*env)->GetIntField(e, fileDescriptor, fid);}void jniSetFileDescriptorOfFD(C_JNIEnv* env, jobject fileDescriptor, int value) {    JNIEnv* e = reinterpret_cast<JNIEnv*>(env);    static jfieldID fid = e->GetFieldID(JniConstants::fileDescriptorClass, "descriptor", "I");    (*env)->SetIntField(e, fileDescriptor, fid, value);}

這裡就能看得出,FileDescriptor 就是把 fd 封裝了一下,核心還是這個 int 值啊(通過反射,用 fd 設定了一下 FileDescriptor 的 fileDescriptor 這個變數)。然後看下 mmap:

12345678910
static jint android_os_MemoryFile_mmap(JNIEnv* env, jobject clazz, jobject fileDescriptor,        jint length, jint prot){    int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);    jint result = (jint)mmap(NULL, length, prot, MAP_SHARED, fd, 0);    if (!result)        jniThrowException(env, "java/io/IOException", "mmap failed");    return result;}

這個更簡單,通過 FileDescriptor 得到 fd,直接系統 mmap 。這裡 mmap 也是要進到 kernel 的驅動裡面的。稍微注意下, port 是 PORT_READ | PORT_WRITE 讀寫, flag 是 MAP_SHARED,就說明這是專為共享設定的。

Proc A 算是把共享記憶體建立好了也 mmap 到本程序,現在就要把 fd 倒騰給 Proc B。現在我們假設 Proc A 是 Bn 端,Proc B 是 Bp 端。然後來看看 MemroyFile 的一個介面:

123456789101112131415161718
/* * Gets a ParcelFileDescriptor for the memory file. See {@link #getFileDescriptor()} * for caveats. This must be here to allow classes outside <code>android.os</code< to * make ParcelFileDescriptors from MemoryFiles, as * {@link ParcelFileDescriptor#ParcelFileDescriptor(FileDescriptor)} is package private. *  *  * @return The file descriptor owned by this memory file object. *         The file descriptor is not duplicated. * @throws IOException If the memory file has been closed. *  * @hide */public ParcelFileDescriptor getParcelFileDescriptor() throws IOException {    FileDescriptor fd = getFileDescriptor();    return fd != null ? new ParcelFileDescriptor(fd) : null;}

ParcelFileDescriptor,看名字你是不是明白了什麼咧,能夠 Parcelable 的 fd,這個就是讓你拿來用 binder 傳給 Proc B 的啊。

1234567891011121314151617181920212223242526272829303132333435
/*package */ParcelFileDescriptor(FileDescriptor descriptor) {    super();    if (descriptor == null) {        throw new NullPointerException("descriptor must not be null");    }    mFileDescriptor = descriptor;    mParcelDescriptor = null;}/* * {@inheritDoc} * If {@link Parcelable#PARCELABLE_WRITE_RETURN_VALUE} is set in flags, * the file descriptor will be closed after a copy is written to the Parcel. */public void writeToParcel(Parcel out, int flags) {    out.writeFileDescriptor(mFileDescriptor);    if ((flags&PARCELABLE_WRITE_RETURN_VALUE) != 0 && !mClosed) {        try {            close();        } catch (IOException e) {            // Empty        }    }}public static final Parcelable.Creator<ParcelFileDescriptor> CREATOR        = new Parcelable.Creator<ParcelFileDescriptor>() {    public ParcelFileDescriptor createFromParcel(Parcel in) {        return in.readFileDescriptor();    }    public ParcelFileDescriptor[] newArray(int size) {        return new ParcelFileDescriptor[size];    }};

ParcelFileDescriptor 其實挺簡單,主要是看它的 Paracelable 介面。又是呼叫 Parcel 的對應介面(java jni native 放一起了,麻煩,而且下面是 2.3 的程式碼,4.4 的不一樣了,基本上好像太能配合 MemroyFile 使用了)。

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
// java ================================    /**     * Write a FileDescriptor into the parcel at the current dataPosition(),     * growing dataCapacity() if needed.     *      * <p class="caution">The file descriptor will not be closed, which may     * result in file descriptor leaks when objects are returned from Binder     * calls.  Use {@link ParcelFileDescriptor#writeToParcel} instead, which     * accepts contextual flags and will close the original file descriptor     * if {@link Parcelable#PARCELABLE_WRITE_RETURN_VALUE} is set.</p>     */    public final native void writeFileDescriptor(FileDescriptor val);    /**     * Read a FileDescriptor from the parcel at the current dataPosition().     */    public final ParcelFileDescriptor readFileDescriptor() {        FileDescriptor fd = internalReadFileDescriptor();        return fd != null ? new ParcelFileDescriptor(fd) : null;    }// jni ================================static void android_os_Parcel_writeFileDescriptor(JNIEnv* env, jobject clazz, jobject object){    Parcel* parcel = parcelForJavaObject(env, clazz);    if (parcel != NULL) {        const status_t err = parcel->writeDupFileDescriptor(                env->GetIntField(object, gFileDescriptorOffsets.mDescriptor));        if (err != NO_ERROR) {            jniThrowException(env, "java/lang/OutOfMemoryError", NULL);        }    }}// 這個在 jni 註冊那裡是叫 internalReadFileDescriptor -_-||static jobject android_os_Parcel_readFileDescriptor(JNIEnv* env, jobject clazz){    Parcel* parcel = parcelForJavaObject(env, clazz);    if (parcel != NULL) {        int fd = parcel->readFileDescriptor();        if (fd < 0) return NULL;        fd = dup(fd);        if (fd < 0) return NULL;        jobject object = env->NewObject(                gFileDescriptorOffsets.mClass, gFileDescriptorOffsets.mConstructor);        if (object != NULL) {            //LOGI("Created new FileDescriptor %p with fd %d\n", object, fd);            env->SetIntField(object, gFileDescriptorOffsets.mDescriptor, fd);        }        return object;    }    return NULL;}// native ================================status_t Parcel::writeDupFileDescriptor(int fd){    flat_binder_object obj;    obj.type = BINDER_TYPE_FD;    obj.flags = 0x7f | FLAT_BINDER_FLAG_ACCEPTS_FDS;    obj.handle = dup(fd);    obj.cookie = (void*)1;    return writeObject(obj, true);}int Parcel::readFileDescriptor() const{    const flat_binder_object* flat = readObject(true);    if (flat) {        switch (flat->type) {            case BINDER_TYPE_FD:                           //LOGI("Returning file descriptor %ld from parcel %p\n", flat->handle, this);                return flat->handle;        }            }    return BAD_TYPE;}

最後,是到 Parcel ,通過 flat_binder_object 來傳遞的。回想下前面幾篇的內容,Parcel 傳遞 flat_binder_object到 binder 驅動的時候,有好幾種類型,當時是不是有一種 BINDER_TYPE_FD 型別被選擇性的無視了,現在知道這個 FD 是專門拿來倒騰 fd 用的了吧。這裡 writeDupFileDescriptor 用 dup 複製了一個 fd 封裝在 flat_binder_object 裡面,然後 kernel 那裡倒騰後面再說。反正 binder 傳到 Proc B 那邊,通過 Parcelable 的 CREATEOR 呼叫到 readFileDescriptor 會把 flat_binder_object 讀出來,然後這裡的 fd 就是經過倒騰的,是 Proc B 程序能夠用的了。

Proc B 拿到 fd 後就可以 mmap Proc A 建立的共享記憶體了(還是建立 MemroyFile):

123456789101112131415161718192021222324252627282930
/* * Creates a reference to an existing memory file. Changes to the original file * will be available through this reference. * Calls to {@link #allowPurging(boolean)} on the returned MemoryFile will fail. *  * @param fd File descriptor for an existing memory file, as returned by *        {@link #getFileDescriptor()}. This file descriptor will be closed *        by {@link #close()}. * @param length Length of the memory file in bytes. * @param mode File mode. Currently only "r" for read-only access is supported. * @throws NullPointerException if <code>fd</code> is null. * @throws IOException If <code>fd</code> does not refer to an existing memory file, *         or if the file mode of the existing memory file is more restrictive *         than <code>mode</code>. *  * @hide */public MemoryFile(FileDescriptor fd, int length, String mode) throws IOException {    if (fd == null) {        throw new NullPointerException("File descriptor is null.");    }    if (!isMemoryFile(fd)) {               throw new IllegalArgumentException("Not a memory file.");    }    mLength = length;    mFD = fd;    mAddress = native_mmap(mFD, length, modeToProt(mode));    mOwnsRegion = false;}

這個就是 Proc A 那裡省去了 open 的操作(當然,因為有現成的 fd 了)。Proc B 也把共享記憶體檔案 mmap 到本程序後,A、B 就可以通過 MemroyFile 的 read、write 介面讀寫了:

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
// java ================================    /**     * Reads bytes from the memory file.     * Will throw an IOException if the file has been purged.     *     * @param buffer byte array to read bytes into.     * @param srcOffset offset into the memory file to read from.     * @param destOffset offset into the byte array buffer to read into.     * @param count number of bytes to read.     * @return number of bytes read.     * @throws IOException if the memory file has been purged or deactivated.     */    public int readBytes(byte[] buffer, int srcOffset, int destOffset, int count)             throws IOException {                   if (isDeactivated()) {            throw new IOException("Can't read from deactivated memory file.");        }        if (destOffset < 0 || destOffset > buffer.length || count < 0                || count > buffer.length - destOffset                || srcOffset < 0 || srcOffset > mLength                || count > mLength - srcOffset) {             throw new IndexOutOfBoundsException();        }        return native_read(mFD, mAddress, buffer, srcOffset, destOffset, count, mAllowPurging);    }    /**     * Write bytes to the memory file.     * Will throw an IOException if the file has been purged.     *      * @param buffer byte array to write bytes from.     * @param srcOffset offset into the byte array buffer to write from.     * @param destOffset offset  into the memory file to write to.     * @param count number of bytes to write.     * @throws IOException if the memory file has been purged or deactivated.     */    public void writeBytes(byte[] buffer, int srcOffset, int destOffset, int count)            throws IOException {                   if (isDeactivated()) {            throw new IOException("Can't write to deactivated memory file.");        }        if (srcOffset < 0 || srcOffset > buffer.length || count < 0                || count > buffer.length - srcOffset                || destOffset < 0 || destOffset > mLength                || count > mLength - destOffset) {            throw new IndexOutOfBoundsException();        }        native_write(mFD, mAddress, buffer, srcOffset, destOffset, count, mAllowPurging);    }// jni ================================static jint android_os_MemoryFile_read(JNIEnv* env, jobject clazz,        jobject fileDescriptor, jint address, jbyteArray buffer, jint srcOffset, jint destOffset,        jint count, jboolean unpinned){    int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);    if (unpinned && ashmem_pin_region(fd, 0, 0) == ASHMEM_WAS_PURGED) {        ashmem_unpin_region(fd, 0, 0);        jniThrowException(env, "java/io/IOException", "ashmem region was purged");        return -1;    }    env->SetByteArrayRegion(buffer, destOffset, count, (const jbyte *)address + srcOffset);    if (unpinned) {        ashmem_unpin_region(fd, 0, 0);    }    return count;}static jint android_os_MemoryFile_write(JNIEnv* env, jobject clazz,        jobject fileDescriptor, jint address, jbyteArray buffer, jint srcOffset, jint destOffset,        jint count, jboolean unpinned){    int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);    if (unpinned && ashmem_pin_region(fd, 0, 0) == ASHMEM_WAS_PURGED) {        ashmem_unpin_region(fd, 0, 0);        jniThrowException(env, "java/io/IOException", "ashmem region was purged");        return -1;    }    env->GetByteArrayRegion(buffer, srcOffset, count, (jbyte *)address + destOffset);    if (unpinned) {        ashmem_unpin_region(fd, 0, 0);    }    return count;}

jni 裡面,除去那個 unpinned 不看(mAllowPurging 預設是 false),read 和 write 很簡單,就是單純的從 mAddress(mmap 到本程序的地址)讀或寫資料(資料都是二進位制的,至於怎麼用,那是上層業務的事情了)。就算手動設定了 mAllowPurging(2.3 的原始碼系統裡面也沒主動設定的地方),ashmem_pin_region 的範圍都是 0,在 kernel 驅動中, 0 代表整塊區域,所以就算設定了,也暫時沒起到分塊使用的作用。所以這些就忽略這些東西(主要是我也不太懂 -_-||)。

是用完之後就可以呼叫 close 介面,先 munmap 記憶體對映,然後再關掉共享記憶體檔案:

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
// java ================================    /**     * Closes the memory file. If there are no other open references to the memory     * file, it will be deleted.     */    public void close() {        deactivate();        if (!isClosed()) {            native_close(mFD);        }    }    /**     * Unmaps the memory file from the process's memory space, but does not close it.     * After this method has been called, read and write operations through this object     * will fail, but {@link #getFileDescriptor()} will still return a valid file descriptor.     *     * @hide     */    public voi