Android匿名共享記憶體(Ashmem)原理

看書的小蝸牛發表於2019-03-04

閱讀之前,不妨先思考一個問題,在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的共享記憶體的用法,簡單瞭解下:

View繪製與共享記憶體.jpg
View繪製與共享記憶體.jpg

Linux共享記憶體

首先看一下兩個關鍵函式,

  • int shmget(key_t key, size_t size, int shmflg); 該函式用來建立共享記憶體
  • void shmat(int shm_id, const void shm_addr, int shmflg); 要想訪問共享記憶體,必須將其對映到當前程式的地址空間

參考網上的一個demo,簡單的看下,其中key_t是共享記憶體的唯一標識,可以說,Linux的共享記憶體其實是有名共享記憶體,而名字就是key,具體用法如下

讀取程式

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在此基礎上,建立了自己的匿名共享記憶體方式。

Android的匿名共享記憶體

Android可以使用Linux的一切IPC通訊方式,包括共享記憶體,不過Android主要使用的方式是匿名共享記憶體Ashmem(Anonymous Shared Memory),跟原生的不太一樣,比如它在自己的驅動中新增了互斥鎖,另外通過fd的傳遞來實現共享記憶體的傳遞。MemoryFile是Android為匿名共享記憶體而封裝的一個物件,這裡通過使用MemoryFile來分析,Android中如何利用共享記憶體來實現大資料傳遞,同時MemoryFile也是程式間大資料傳遞的一個手段,開發的時候可以使用:

IMemoryAidlInterface.aidl

package com.snail.labaffinity;
import android.os.ParcelFileDescriptor;

interface IMemoryAidlInterface {
    ParcelFileDescriptor getParcelFileDescriptor();
}複製程式碼

MemoryFetchService

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

 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下檢視:

申請的共享記憶體在proc中的展示.jpg
申請的共享記憶體在proc中的展示.jpg

下面就基於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函式分配真實的物理頁,具體的分配策略比較複雜,不在分析。

Android匿名共享記憶體的pin與unpin

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。

共享記憶體.jpg
共享記憶體.jpg

下面來看一下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;
}複製程式碼

pin共享記憶體.jpg
pin共享記憶體.jpg

Android程式共享記憶體的傳遞-fd檔案描述符的傳遞

原生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);
}複製程式碼

fd傳遞.jpg
fd傳遞.jpg

為什麼看不到匿名共享記憶體對應的檔案呢

為什麼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)原理
僅供參考,歡迎指正

相關文章