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 介面 |
(這和 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 |