閱讀以前,不妨先思考一個問題,在Android系統中,APP端View視圖的數據是如何傳遞SurfaceFlinger服務的呢?View繪製的數據最終是按照一幀一幀顯示到屏幕的,而每一幀都會佔用必定的存儲空間,在APP端執行draw的時候,數據很明顯是要繪製到APP的進程空間,可是視圖窗口要通過SurfaceFlinger圖層混排纔會生成最終的幀,而SurfaceFlinger又運行在另外一個獨立的服務進程,那麼View視圖的數據是如何在兩個進程間傳遞的呢,普通的Binder通訊確定不行,由於Binder不太適合這種數據量較大的通訊,那麼View數據的通訊採用的是什麼IPC手段呢?答案就是共享內存,更精確的說是匿名共享內存。共享內存是Linux自帶的一種IPC機制,Android直接使用了該模型,不過作出了本身的改進,進而造成了Android的匿名共享內存(Anonymous Shared Memory-Ashmem)。經過Ashmem,APP進程同SurfaceFlinger共用一塊內存,如此,就不須要進行數據拷貝,APP端繪製完畢,通知SurfaceFlinger端合成,再輸出到硬件進行顯示便可,固然,箇中細節會更復雜,本文主要分析下匿名共享內存的原理及在Android中的特性,下面就來看下箇中細節,不過首先看一下Linux的共享內存的用法,簡單瞭解下:java
首先看一下兩個關鍵函數,node
參考網上的一個demo,簡單的看下,其中key_t是共享內存的惟一標識,能夠說,Linux的共享內存實際上是有名共享內存,而名字就是key,具體用法以下linux
讀取進程android
int main() { void *shm = NULL;//分配的共享內存的原始首地址 struct shared_use_st *shared;//指向shm int shmid;//共享內存標識符 //建立共享內存 shmid = shmget((key_t)12345, sizeof(struct shared_use_st), 0666|IPC_CREAT); //將共享內存映射到當前進程的地址空間 shm = shmat(shmid, 0, 0); //設置共享內存 shared = (struct shared_use_st*)shm; shared->written = 0; //訪問共享內存 while(1){ if(shared->written != 0) { printf("You wrote: %s", shared->text); if(strncmp(shared->text, "end", 3) == 0) break; }} //把共享內存從當前進程中分離 if(shmdt(shm) == -1) { } //刪除共享內存 if(shmctl(shmid, IPC_RMID, 0) == -1) { } exit(EXIT_SUCCESS); } 複製代碼
寫進程緩存
int main() { void *shm = NULL; struct shared_use_st *shared = NULL; char buffer[BUFSIZ + 1];//用於保存輸入的文本 int shmid; //建立共享內存 shmid = shmget((key_t) 12345, sizeof(struct shared_use_st), 0666|IPC_CREAT); //將共享內存鏈接到當前進程的地址空間 shm = shmat(shmid, (void*)0, 0); printf("Memory attached at %X\n", (int)shm); //設置共享內存 shared = (struct shared_use_st*)shm; while(1)//向共享內存中寫數據 { //數據尚未被讀取,則等待數據被讀取,不能向共享內存中寫入文本 while(shared->written == 1) { sleep(1); } //向共享內存中寫入數據 fgets(buffer, BUFSIZ, stdin); strncpy(shared->text, buffer, TEXT_SZ); shared->written = 1; if(strncmp(buffer, "end", 3) == 0) running = 0; } //把共享內存從當前進程中分離 if(shmdt(shm) == -1) { } sleep(2); exit(EXIT_SUCCESS); } 複製代碼
能夠看到,Linux共享內存通訊效率很是高,進程間不須要傳遞數據,即可以直接訪問,缺點也很明顯,Linux共享內存沒有提供同步的機制,在使用時,要藉助其餘的手段來處理進程間同步。Anroid自己在覈心態是支持System V的功能,可是bionic庫刪除了glibc的shmget等函數,使得android沒法採用shmget的方式實現有名共享內存,固然,它也沒想着用那個,Android在此基礎上,建立了本身的匿名共享內存方式。markdown
Android可使用Linux的一切IPC通訊方式,包括共享內存,不過Android主要使用的方式是匿名共享內存Ashmem(Anonymous Shared Memory),跟原生的不太同樣,好比它在本身的驅動中添加了互斥鎖,另外經過fd的傳遞來實現共享內存的傳遞。MemoryFile是Android爲匿名共享內存而封裝的一個對象,這裏經過使用MemoryFile來分析,Android中如何利用共享內存來實現大數據傳遞,同時MemoryFile也是進程間大數據傳遞的一個手段,開發的時候可使用:數據結構
IMemoryAidlInterface.aidlapp
package com.snail.labaffinity;
import android.os.ParcelFileDescriptor;
interface IMemoryAidlInterface {
ParcelFileDescriptor getParcelFileDescriptor();
}複製代碼
MemoryFetchServiceionic
public class MemoryFetchService extends Service { @Nullable @Override public IBinder onBind(Intent intent) { return new MemoryFetchStub(); } static class MemoryFetchStub extends IMemoryAidlInterface.Stub { @Override public ParcelFileDescriptor getParcelFileDescriptor() throws RemoteException { MemoryFile memoryFile = null; try { memoryFile = new MemoryFile("test_memory", 1024); memoryFile.getOutputStream().write(new byte[]{1, 2, 3, 4, 5}); Method method = MemoryFile.class.getDeclaredMethod("getFileDescriptor"); FileDescriptor des = (FileDescriptor) method.invoke(memoryFile); return ParcelFileDescriptor.dup(des); } catch (Exception e) {} return null; }}}複製代碼
TestActivity.java ide
Intent intent = new Intent(MainActivity.this, MemoryFetchService.class); bindService(intent, new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { byte[] content = new byte[10]; IMemoryAidlInterface iMemoryAidlInterface = IMemoryAidlInterface.Stub.asInterface(service); try { ParcelFileDescriptor parcelFileDescriptor = iMemoryAidlInterface.getParcelFileDescriptor(); FileDescriptor descriptor = parcelFileDescriptor.getFileDescriptor(); FileInputStream fileInputStream = new FileInputStream(descriptor); fileInputStream.read(content); } catch (Exception e) { }} @Override public void onServiceDisconnected(ComponentName name) { } }, Service.BIND_AUTO_CREATE); 複製代碼
以上是應用層使用匿名共享內存的方法,關鍵點就是文件描述符(FileDescriptor)的傳遞,文件描述符是Linux系統中訪問與更新文件的主要方式。從MemoryFile字面上看出,共享內存被抽象成了文件,不過本質也是如此,就是在tmpfs臨時文件系統中建立一個臨時文件,(只是建立了節點,而沒有看到實際的文件) 該文件與Ashmem驅動程序建立的匿名共享內存對應,能夠直接去proc/pid下查看:
下面就基於MemoryFile主要分析兩點,共享內存的分配與傳遞,先看下MemoryFile的構造函數
public MemoryFile(String name, int length) throws IOException { mLength = length; mFD = native_open(name, length); if (length > 0) { mAddress = native_mmap(mFD, length, PROT_READ | PROT_WRITE); } else { mAddress = 0; } }複製代碼
能夠看到 Java層只是簡單的封裝,具體實如今native層 ,首先是經過native_open調用ashmem_create_region建立共享內存,
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); }複製代碼
接着經過native_mmap調用mmap將共享內存映射到當前進程空間,以後Java層就能利用FileDescriptor,像訪問文件同樣訪問共享內存。
static jint android_os_MemoryFile_mmap(JNIEnv* env, jobject clazz, jobject fileDescriptor, jint length, jint prot) { int fd = jniGetFDFromFileDescriptor(env, fileDescriptor); <!--系統調用mmap,分配內存--> jint result = (jint)mmap(NULL, length, prot, MAP_SHARED, fd, 0); if (!result) jniThrowException(env, "java/io/IOException", "mmap failed"); return result; } 複製代碼
ashmem_create_region這個函數是如何向Linux申請一塊共享內存的呢?
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; }複製代碼
ASHMEM_DEVICE其實就是抽象的共享內存設備,它是一個雜項設備(字符設備的一種),在驅動加載以後,就會在/dev下穿件ashem文件,以後用戶就可以訪問該設備文件,同通常的設備文件不一樣,它僅僅是經過內存抽象的,同普通的磁盤設備文件、串行端口字段設備文件不同:
#define ASHMEM_DEVICE "/dev/ashmem" static struct miscdevice ashmem_misc = { .minor = MISC_DYNAMIC_MINOR, .name = "ashmem", .fops = &ashmem_fops, };複製代碼
接着進入驅動看一下,如何申請共享內存,open函數很普通,主要是建立一個ashmem_area對象
static int ashmem_open(struct inode *inode, struct file *file) { struct ashmem_area *asma; int ret; ret = nonseekable_open(inode, file); if (unlikely(ret)) return ret; asma = kmem_cache_zalloc(ashmem_area_cachep, GFP_KERNEL); if (unlikely(!asma)) return -ENOMEM; INIT_LIST_HEAD(&asma->unpinned_list); memcpy(asma->name, ASHMEM_NAME_PREFIX, ASHMEM_NAME_PREFIX_LEN); asma->prot_mask = PROT_MASK; file->private_data = asma; return 0; }複製代碼
接着利用ashmem_ioctl設置共享內存的大小,
static long ashmem_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct ashmem_area *asma = file->private_data; long ret = -ENOTTY; switch (cmd) { ... case ASHMEM_SET_SIZE: ret = -EINVAL; if (!asma->file) { ret = 0; asma->size = (size_t) arg; } break; ... } return ret; } 複製代碼
能夠看到,其實並未真正的分配內存,這也符合Linux的風格,只有等到真正的使用的時候,纔會經過缺頁中斷分配內存,接着mmap函數,它會分配內存嗎?
static int ashmem_mmap(struct file *file, struct vm_area_struct *vma) { struct ashmem_area *asma = file->private_data; int ret = 0; mutex_lock(&ashmem_mutex); ... if (!asma->file) { char *name = ASHMEM_NAME_DEF; struct file *vmfile; if (asma->name[ASHMEM_NAME_PREFIX_LEN] != '\0') name = asma->name; // 這裏建立的臨時文件實際上是備份用的臨時文件,之類的臨時文件有文章說只對內核態可見,用戶態不可見,咱們也沒有辦法經過命令查詢到 ,能夠看作是個隱藏文件,用戶空間看不到!! <!--校準真正操做的文件--> vmfile = shmem_file_setup(name, asma->size, vma->vm_flags); asma->file = vmfile; } get_file(asma->file); if (vma->vm_flags & VM_SHARED) shmem_set_file(vma, asma->file); else { if (vma->vm_file) fput(vma->vm_file); vma->vm_file = asma->file; } vma->vm_flags |= VM_CAN_NONLINEAR; out: mutex_unlock(&ashmem_mutex); return ret; }複製代碼
其實這裏就複用了Linux的共享內存機制,雖說是匿名共享內存,但底層其實仍是給共享內存設置了名稱(前綴ASHMEM_NAME_PREFIX+名字),若是名字未設置,那就默認使用ASHMEM_NAME_PREFIX做爲名稱。不過,在這裏沒直接看到內存分配的函數。可是,有兩個函數shmem_file_setup與shmem_set_file很重要,也是共享內存比較很差理解的地方,shmem_file_setup是原生linux的共享內存機制,不過Android也修改Linux共享內存的驅動代碼,匿名共享內存其實就是在Linux共享內存的基礎上作了改進,
struct file *shmem_file_setup(char *name, loff_t size, unsigned long flags)
{
int error;
struct file *file;
struct inode *inode;
struct dentry *dentry, *root;
struct qstr this;
error = -ENOMEM;
this.name = name;
this.len = strlen(name);
this.hash = 0; /* will go */
root = shm_mnt->mnt_root;
dentry = d_alloc(root, &this);//分配dentry cat/proc/pid/maps能夠查到
error = -ENFILE;
file = get_empty_filp(); //分配file
error = -ENOSPC;
inode = shmem_get_inode(root->d_sb, S_IFREG | S_IRWXUGO, 0, flags);//分配inode,分配成功就比如創建了文件,也許並未存在真實文件映射
d_instantiate(dentry, inode);//綁定
inode->i_size = size;
inode->i_nlink = 0; /* It is unlinked */
// 文件操做符,這裏彷佛真的是不在內存裏面建立什麼東西???
init_file(file, shm_mnt, dentry, FMODE_WRITE | FMODE_READ,
&shmem_file_operations);//綁定,並指定該文件操做指針爲shmem_file_operations
...
}複製代碼
經過shmem_file_setup在tmpfs臨時文件系統中建立一個臨時文件(也許只是內核中的一個inode節點),該文件與Ashmem驅動程序建立的匿名共享內存對應,不過用戶態並不能看到該臨時文件,以後就可以使用該臨時文件了,注意共享內存機制真正使用map的對象實際上是這個臨時文件,而不是ashmem設備文件,這裏之因此是一次mmap,主要是經過vma->vm_file = asma->file完成map對象的替換,當映射的內存引發缺頁中斷的時候,就會調用shmem_file_setup建立的對象的函數,而不是ashmem的,看下臨時文件的對應的hook函數,
void shmem_set_file(struct vm_area_struct *vma, struct file *file) { if (vma->vm_file) fput(vma->vm_file); vma->vm_file = file; vma->vm_ops = &shmem_vm_ops; }複製代碼
到這裏回到以前的MemoryFile,看一下寫操做:
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); }複製代碼
進入native代碼
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); return -1; } env->GetByteArrayRegion(buffer, srcOffset, count, (jbyte *)address + destOffset); if (unpinned) { ashmem_unpin_region(fd, 0, 0); } return count; }複製代碼
在內核中,一塊內存對應的數據結構是ashmem_area:
struct ashmem_area { char name[ASHMEM_FULL_NAME_LEN];/* optional name for /proc/pid/maps */ struct list_head unpinned_list; /* list of all ashmem areas */ struct file *file; /* the shmem-based backing file */ size_t size; /* size of the mapping, in bytes */ unsigned long prot_mask; /* allowed prot bits, as vm_flags */ };複製代碼
當使用Ashmem分配了一塊內存,部分不被使用時,就能夠將這塊內存unpin掉,內核能夠將unpin對應的物理頁面回收,回收後的內存還能夠再次被得到(經過缺頁handler),由於unpin操做並不會改變已經mmap的地址空間,不過,MemoryFile只會操做整個共享內存,而不會分塊訪問,因此pin與unpin對於它沒多大意義,能夠看作整個區域都是pin或者unpin的,首次經過env->GetByteArrayRegion訪問會引起缺頁中斷,進而調用tmpfs 文件的相應操做,分配物理頁,在Android如今的內核中,缺頁中斷對應的vm_operations_struct中的函數是fault,在共享內存實現中,對應的是shmem_fault以下,
static struct vm_operations_struct shmem_vm_ops = { .fault = shmem_fault, #ifdef CONFIG_NUMA .set_policy = shmem_set_policy, .get_policy = shmem_get_policy, #endif };複製代碼
當mmap的tmpfs文件引起缺頁中斷時, 就會調用shmem_fault函數,
static int shmem_fault(struct vm_area_struct *vma, struct vm_fault *vmf) { struct inode *inode = vma->vm_file->f_path.dentry->d_inode; int error; int ret; if (((loff_t)vmf->pgoff << PAGE_CACHE_SHIFT) >= i_size_read(inode)) return VM_FAULT_SIGBUS; error = shmem_getpage(inode, vmf->pgoff, &vmf->page, SGP_CACHE, &ret); if (error) return ((error == -ENOMEM) ? VM_FAULT_OOM : VM_FAULT_SIGBUS); return ret | VM_FAULT_LOCKED; }複製代碼
到這裏,就能夠看到會調用shmem_getpage函數分配真實的物理頁,具體的分配策略比較複雜,不在分析。
pin自己的意思是壓住,定住,ashmem_pin_region和ashmem_unpin_region這兩個函數從字面上來講,就是用來對匿名共享內存鎖定和解鎖,標識哪些內存正在使用須要鎖定,哪些內存是不使用的,這樣,ashmem驅動程序能夠必定程度上輔助內存管理,提供必定的內存優化能力。匿名共享內存建立之初時,全部的內存都是pinned狀態,只有用戶主動申請,纔會unpin一塊內存,只有對於unpinned狀態的內存塊,用戶才能夠從新pin。如今仔細梳理一下驅動,看下pin與unpin的實現
static int __init ashmem_init(void) { int ret; <!--建立 ahemem_area 高速緩存--> ashmem_area_cachep = kmem_cache_create("ashmem_area_cache", sizeof(struct ashmem_area), 0, 0, NULL); ... <!--建立 ahemem_range高速緩存--> ashmem_range_cachep = kmem_cache_create("ashmem_range_cache", sizeof(struct ashmem_range), 0, 0, NULL); ... <!--註冊雜項設備去送--> ret = misc_register(&ashmem_misc); ... register_shrinker(&ashmem_shrinker); return 0; }複製代碼
打開ashem的時候 ,會利用ashmem_area_cachep告訴緩存新建ashmem_area對象,並初始化unpinned_list,開始確定爲null
static int ashmem_open(struct inode *inode, struct file *file) { struct ashmem_area *asma; int ret; ret = nonseekable_open(inode, file); asma = kmem_cache_zalloc(ashmem_area_cachep, GFP_KERNEL); <!--關鍵是初始化unpinned_list列表--> INIT_LIST_HEAD(&asma->unpinned_list); memcpy(asma->name, ASHMEM_NAME_PREFIX, ASHMEM_NAME_PREFIX_LEN); asma->prot_mask = PROT_MASK; file->private_data = asma; return 0; }複製代碼
一開始都是pin的,看一下pin與unpin的調用範例:
int ashmem_pin_region(int fd, size_t offset, size_t len) { struct ashmem_pin pin = { offset, len }; return ioctl(fd, ASHMEM_PIN, &pin); } int ashmem_unpin_region(int fd, size_t offset, size_t len) { struct ashmem_pin pin = { offset, len }; return ioctl(fd, ASHMEM_UNPIN, &pin); }複製代碼
接着看ashmem_unpin
static int ashmem_unpin(struct ashmem_area *asma, size_t pgstart, size_t pgend) { struct ashmem_range *range, *next; unsigned int purged = ASHMEM_NOT_PURGED; restart: list_for_each_entry_safe(range, next, &asma->unpinned_list, unpinned) { if (range_before_page(range, pgstart)) break; if (page_range_subsumed_by_range(range, pgstart, pgend)) return 0; if (page_range_in_range(range, pgstart, pgend)) { pgstart = min_t(size_t, range->pgstart, pgstart), pgend = max_t(size_t, range->pgend, pgend); purged |= range->purged; range_del(range); goto restart; } } return range_alloc(asma, range, purged, pgstart, pgend); }複製代碼
這個函數主要做用是建立一個ashmem_range ,並插入ashmem_area的unpinned_list,在插入的時候可能會有合併爲,這個時候要首先刪除原來的unpin ashmem_range,以後新建一個合併後的ashmem_range插入unpinned_list。
下面來看一下pin函數的實現,先理解了unpin,pin就很好理解了,其實就是將一塊共享內存投入使用,若是它位於unpinedlist,就將它摘下來:
static int ashmem_pin(struct ashmem_area *asma, size_t pgstart, size_t pgend) { struct ashmem_range *range, *next; int ret = ASHMEM_NOT_PURGED; list_for_each_entry_safe(range, next, &asma->unpinned_list, unpinned) { /* moved past last applicable page; we can short circuit */ if (range_before_page(range, pgstart)) break; if (page_range_in_range(range, pgstart, pgend)) { ret |= range->purged; if (page_range_subsumes_range(range, pgstart, pgend)) { range_del(range); continue; } if (range->pgstart >= pgstart) { range_shrink(range, pgend + 1, range->pgend); continue; } if (range->pgend <= pgend) { range_shrink(range, range->pgstart, pgstart-1); continue; } range_alloc(asma, range, range->purged, pgend + 1, range->pgend); range_shrink(range, range->pgstart, pgstart - 1); break; } } return ret; }複製代碼
原生Linux共享內存是經過傳遞已知的key來處理的,可是Android中不存在這種機制,Android是怎麼處理的呢?那就是經過Binder傳遞文件描述符來處理,Android的Binder對於fd的傳遞也作了適配,原理其實就是在內核層爲要傳遞的目標進程轉換fd,由於在linux中fd只是對本進程是有效、且惟一,進程A打開一個文件獲得一個fd,不能直接爲進程B使用,由於B中那個fd可能壓根無效、或者對應其餘文件,不過,雖然同一個文件能夠有多個文件描述符,可是文件只有一個,在內核層也只會對應一個inode節點與file對象,這也是內核層能夠傳遞fd的基礎,Binder驅動經過當前進程的fd找到對應的文件,而後爲目標進程新建fd,並傳遞給目標進程,核心就是把進程A中的fd轉化成進程B中的fd,看一下Android中binder的實現:
void binder_transaction(){ ... case BINDER_TYPE_FD: { int target_fd; struct file *file; <!--關鍵點1 能夠根據fd在當前進程獲取到file ,多個進程打開同一文件,在內核中對應的file是同樣--> file = fget(fp->handle); <!--關鍵點2,爲目標進程獲取空閒fd--> target_fd = task_get_unused_fd_flags(target_proc, O_CLOEXEC); <!--關鍵點3將目標進程的空閒fd與file綁定--> task_fd_install(target_proc, target_fd, file); fp->handle = target_fd; } break; ... } <!--從當前進程打開的files中找到file在內核中的實例--> struct file *fget(unsigned int fd) { struct file *file; struct files_struct *files = current->files; rcu_read_lock(); file = fcheck_files(files, fd); rcu_read_unlock(); return file; } static void task_fd_install( struct binder_proc *proc, unsigned int fd, struct file *file) { struct files_struct *files = proc->files; struct fdtable *fdt; if (files == NULL) return; spin_lock(&files->file_lock); fdt = files_fdtable(files); rcu_assign_pointer(fdt->fd[fd], file); spin_unlock(&files->file_lock); }複製代碼
爲何Android用戶看不到共享內存對應的文件,Google到的說法是:在內核沒有定義defined(CONFIG_TMPFS) 狀況下,tmpfs對用戶不可見:
If CONFIG_TMPFS is not set, the user visible part of tmpfs is not build. But the internal mechanisms are always present.
而在Android的shmem.c驅動中確實沒有defined(CONFIG_TMPFS) ,這裏只是猜想,也許還有其餘解釋,若有了解,望能指導。
Android匿名共享內存是基於Linux共享內存的,都是在tmpfs文件系統上新建文件,並將其映射到不一樣的進程空間,從而達到共享內存的目的,只是,Android在Linux的基礎上進行了改造,並藉助Binder+fd文件描述符實現了共享內存的傳遞。
做者:看書的小蝸牛
原文連接:Android匿名共享內存(Ashmem)原理
僅供參考,歡迎指正