首頁 > 軟體

Android 匿名記憶體深入分析

2023-11-03 18:00:36

Android 匿名記憶體解析

有了binder機制為什麼還需要匿名記憶體來實現IPC呢?我覺得很大的原因就是binder傳輸是有大小限制的,不說應用層的限制。在驅動中binder的傳輸大小被限制在了4M,分享一張圖片可能就超過了這個限制。匿名記憶體的主要解決思路就是通過binder傳輸檔案描述符,使得兩個程序都能存取同一個地址來實現共用。

MemoryFile使用

在平常開發中android提供了MemoryFile來實現匿名記憶體。看下最簡單的實現。

Service端

​
const val GET_ASH_MEMORY = 1000
class MyService : Service() {
    val ashData = "AshDemo".toByteArray()
    override fun onBind(intent: Intent): IBinder {
        return object : Binder() {
            override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
                when(code){
                    GET_ASH_MEMORY->{//收到使用者端請求的時候會煩
                        val descriptor = createMemoryFile()
                        reply?.writeParcelable(descriptor, 0)
                        reply?.writeInt(ashData.size)
                        return true
                    }
                    else->{
                        return super.onTransact(code, data, reply, flags)
                    }
                }
            }
        }
    }
    private fun createMemoryFile(): ParcelFileDescriptor? {
        val file = MemoryFile("AshFile", 1024)//建立MemoryFile
        val descriptorMethod = file.javaClass.getDeclaredMethod("getFileDescriptor")
        val fd=descriptorMethod.invoke(file)//反射拿到fd
        file.writeBytes(ashData, 0, 0,ashData.size)//寫入字串
        return ParcelFileDescriptor.dup(fd as FileDescriptor?)//返回一個封裝的fd
    }
}

Server的功能很簡單收到GET_ASH_MEMORY請求的時候建立一個MemoryFile,往裡寫入一個字串的byte陣列,然後將fd和字元長度寫入reply中返回給使用者端。

Client端

​
class MainActivity : AppCompatActivity() {
    val connect = object :ServiceConnection{
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            val reply = Parcel.obtain()
            val sendData = Parcel.obtain()
            service?.transact(GET_ASH_MEMORY, sendData, reply, 0)//傳輸訊號GET_ASH_MEMORY
            val pfd = reply.readParcelable<ParcelFileDescriptor>(javaClass.classLoader)
            val descriptor = pfd?.fileDescriptor//拿到fd
            val size = reply.readInt()//拿到長度
            val input = FileInputStream(descriptor)
            val bytes = input.readBytes()
            val message = String(bytes, 0, size, Charsets.UTF_8)//生成string
            Toast.makeText(this@MainActivity,message,Toast.LENGTH_SHORT).show()
        }
​
        override fun onServiceDisconnected(name: ComponentName?) {
        }
​
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<TextView>(R.id.intent).setOnClickListener {
          //啟動服務
            bindService(Intent(this,MyService::class.java),connect, Context.BIND_AUTO_CREATE)
        }
    }
}

使用者端也很簡單,啟動服務,傳送一個獲取MemoryFile的請求,然後通過reply拿到fd和長度,用FileInputStream讀取fd中的內容,最後通過toast可以驗證這個message已經拿到了。

AshMemory 建立原理

    public MemoryFile(String name, int length) throws IOException {
        try {
            mSharedMemory = SharedMemory.create(name, length);
            mMapping = mSharedMemory.mapReadWrite();
        } catch (ErrnoException ex) {
            ex.rethrowAsIOException();
        }
    }

MemoryFile就是對SharedMemory的一層封裝,具體的工能都是SharedMemory實現的。看SharedMemory的實現。

    public static @NonNull SharedMemory create(@Nullable String name, int size)
            throws ErrnoException {
        if (size <= 0) {
            throw new IllegalArgumentException("Size must be greater than zero");
        }
        return new SharedMemory(nCreate(name, size));
    }
  private static native FileDescriptor nCreate(String name, int size) throws ErrnoException;

通過一個JNI獲得fd,從這裡可以推斷出java層也只是一個封裝,拿到的已經是建立好的fd。

//frameworks/base/core/jni/android_os_SharedMemory.cpp
jobject SharedMemory_nCreate(JNIEnv* env, jobject, jstring jname, jint size) {
    const char* name = jname ? env->GetStringUTFChars(jname, nullptr) : nullptr;
    int fd = ashmem_create_region(name, size);//建立匿名記憶體塊
    int err = fd < 0 ? errno : 0;
    if (name) {
        env->ReleaseStringUTFChars(jname, name);
    }
    if (fd < 0) {
        jniThrowErrnoException(env, "SharedMemory_create", err);
        return nullptr;
    }
    jobject jifd = jniCreateFileDescriptor(env, fd);//建立java fd返回
    if (jifd == nullptr) {
        close(fd);
    }
    return jifd;
}

通過cutils中的ashmem_create_region函數實現的建立

//system/core/libcutils/ashmem-dev.cpp
int ashmem_create_region(const char *name, size_t size)
{
    int ret, save_errno;
​
    if (has_memfd_support()) {//老版本相容用
        return memfd_create_region(name ? name : "none", size);
    }
​
    int fd = __ashmem_open();//開啟Ashmem驅動
    if (fd < 0) {
        return fd;
    }
    if (name) {
        char buf[ASHMEM_NAME_LEN] = {0};
        strlcpy(buf, name, sizeof(buf));
        ret = TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_NAME, buf));//通過ioctl設定名字
        if (ret < 0) {
            goto error;
        }
    }
    ret = TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_SIZE, size));//通過ioctl設定大小
    if (ret < 0) {
        goto error;
    }
    return fd;
error:
    save_errno = errno;
    close(fd);
    errno = save_errno;
    return ret;
}
​

標準的驅動互動操作

1.open開啟驅動

2.通過ioctl與驅動進行互動

下面看下open的流程

static int __ashmem_open()
{
    int fd;
​
    pthread_mutex_lock(&__ashmem_lock);
    fd = __ashmem_open_locked();
    pthread_mutex_unlock(&__ashmem_lock);
​
    return fd;
}
​
/* logistics of getting file descriptor for ashmem */
static int __ashmem_open_locked()
{
    static const std::string ashmem_device_path = get_ashmem_device_path();//拿到Ashmem驅動路徑
    if (ashmem_device_path.empty()) {
        return -1;
    }
    int fd = TEMP_FAILURE_RETRY(open(ashmem_device_path.c_str(), O_RDWR | O_CLOEXEC));
    return fd;
}

回到MemoryFile的建構函式中,拿到了驅動的fd之後呼叫了mapReadWrite

    public @NonNull ByteBuffer mapReadWrite() throws ErrnoException {
        return map(OsConstants.PROT_READ | OsConstants.PROT_WRITE, 0, mSize);
    }
 public @NonNull ByteBuffer map(int prot, int offset, int length) throws ErrnoException {
        checkOpen();
        validateProt(prot);
        if (offset < 0) {
            throw new IllegalArgumentException("Offset must be >= 0");
        }
        if (length <= 0) {
            throw new IllegalArgumentException("Length must be > 0");
        }
        if (offset + length > mSize) {
            throw new IllegalArgumentException("offset + length must not exceed getSize()");
        }
        long address = Os.mmap(0, length, prot, OsConstants.MAP_SHARED, mFileDescriptor, offset);//呼叫了系統的mmap
        boolean readOnly = (prot & OsConstants.PROT_WRITE) == 0;
        Runnable unmapper = new Unmapper(address, length, mMemoryRegistration.acquire());
        return new DirectByteBuffer(length, address, mFileDescriptor, unmapper, readOnly);
    }
​

到這裡就有一個疑問,Linux就有共用記憶體,android為什麼要自己搞一套,只能看下Ashmemory驅動的實現了。

驅動第一步看init和file_operations

static int __init ashmem_init(void)
{
    int ret = -ENOMEM;
​
    ashmem_area_cachep = kmem_cache_create("ashmem_area_cache",
                           sizeof(struct ashmem_area),
                           0, 0, NULL);//建立
    if (!ashmem_area_cachep) {
        pr_err("failed to create slab cachen");
        goto out;
    }
​
    ashmem_range_cachep = kmem_cache_create("ashmem_range_cache",
                        sizeof(struct ashmem_range),
                        0, SLAB_RECLAIM_ACCOUNT, NULL);//建立
    if (!ashmem_range_cachep) {
        pr_err("failed to create slab cachen");
        goto out_free1;
    }
​
    ret = misc_register(&ashmem_misc);//註冊為了一個misc裝置
    ........
    return ret;
}

建立了兩個記憶體分配器ashmem_area_cachep和ashmem_range_cachep用於分配ashmem_area和ashmem_range

//common/drivers/staging/android/ashmem.c
static const struct file_operations ashmem_fops = {
    .owner = THIS_MODULE,
    .open = ashmem_open,
    .release = ashmem_release,
    .read_iter = ashmem_read_iter,
    .llseek = ashmem_llseek,
    .mmap = ashmem_mmap,
    .unlocked_ioctl = ashmem_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl = compat_ashmem_ioctl,
#endif
#ifdef CONFIG_PROC_FS
    .show_fdinfo = ashmem_show_fdinfo,
#endif
};
​

open呼叫的就是ashmem_open

static int ashmem_open(struct inode *inode, struct file *file)
{
    struct ashmem_area *asma;
    int ret;
​
    ret = generic_file_open(inode, file);
    if (ret)
        return ret;
​
    asma = kmem_cache_zalloc(ashmem_area_cachep, GFP_KERNEL);//分配一個ashmem_area
    if (!asma)
        return -ENOMEM;
​
    INIT_LIST_HEAD(&asma->unpinned_list);//初始化unpinned_list
    memcpy(asma->name, ASHMEM_NAME_PREFIX, ASHMEM_NAME_PREFIX_LEN);//初始化一個名字
    asma->prot_mask = PROT_MASK;
    file->private_data = asma;
    return 0;
}

ioctl設定名字和長度呼叫的就是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_NAME:
        ret = set_name(asma, (void __user *)arg);
        break;
    case ASHMEM_SET_SIZE:
        ret = -EINVAL;
        mutex_lock(&ashmem_mutex);
        if (!asma->file) {
            ret = 0;
            asma->size = (size_t)arg;
        }
        mutex_unlock(&ashmem_mutex);
        break;
    }
  ........
  }

實現也都很簡單就是改變了一下asma裡的值。接下來就是重點mmap了,具體是怎麼分配記憶體的。

​static int ashmem_mmap(struct file *file, struct vm_area_struct *vma)
{
    static struct file_operations vmfile_fops;
    struct ashmem_area *asma = file->private_data;
    int ret = 0;
​
    mutex_lock(&ashmem_mutex);
​
    /* user needs to SET_SIZE before mapping */
    if (!asma->size) {//判斷設定了size
        ret = -EINVAL;
        goto out;
    }
​
    /* requested mapping size larger than object size */
    if (vma->vm_end - vma->vm_start > PAGE_ALIGN(asma->size)) {//判斷大小是否超過了虛擬記憶體
        ret = -EINVAL;
        goto out;
    }
​
    /* requested protection bits must match our allowed protection mask */
    if ((vma->vm_flags & ~calc_vm_prot_bits(asma->prot_mask, 0)) &
        calc_vm_prot_bits(PROT_MASK, 0)) {//許可權判斷
        ret = -EPERM;
        goto out;
    }
    vma->vm_flags &= ~calc_vm_may_flags(~asma->prot_mask);
​
    if (!asma->file) {//是否建立過臨時檔案,沒建立過進入
        char *name = ASHMEM_NAME_DEF;
        struct file *vmfile;
        struct inode *inode;
​
        if (asma->name[ASHMEM_NAME_PREFIX_LEN] != '')
            name = asma->name;
​
        /* ... and allocate the backing shmem file */
        vmfile = shmem_file_setup(name, asma->size, vma->vm_flags);//呼叫linux函數在tmpfs中建立臨時檔案
        if (IS_ERR(vmfile)) {
            ret = PTR_ERR(vmfile);
            goto out;
        }
        vmfile->f_mode |= FMODE_LSEEK;
        inode = file_inode(vmfile);
        lockdep_set_class(&inode->i_rwsem, &backing_shmem_inode_class);
        asma->file = vmfile;
        /*
         * override mmap operation of the vmfile so that it can't be
         * remapped which would lead to creation of a new vma with no
         * asma permission checks. Have to override get_unmapped_area
         * as well to prevent VM_BUG_ON check for f_ops modification.
         */
        if (!vmfile_fops.mmap) {//設定了臨時檔案的檔案操作,防止有其他程式mmap這個臨時檔案
            vmfile_fops = *vmfile->f_op;
            vmfile_fops.mmap = ashmem_vmfile_mmap;
            vmfile_fops.get_unmapped_area =
                    ashmem_vmfile_get_unmapped_area;
        }
        vmfile->f_op = &vmfile_fops;
    }
    get_file(asma->file);
​
    /*
     * XXX - Reworked to use shmem_zero_setup() instead of
     * shmem_set_file while we're in staging. -jstultz
     */
    if (vma->vm_flags & VM_SHARED) {//這塊記憶體是不是需要跨程序
        ret = shmem_zero_setup(vma);//設定檔案
        if (ret) {
            fput(asma->file);
            goto out;
        }
    } else {
    /**
    實現就是把vm_ops設定為NULL
    static inline void vma_set_anonymous(struct vm_area_struct *vma)
        {
            vma->vm_ops = NULL;
        }
    */
        vma_set_anonymous(vma);
    }
​
    vma_set_file(vma, asma->file);
    /* XXX: merge this with the get_file() above if possible */
    fput(asma->file);
​
out:
    mutex_unlock(&ashmem_mutex);
    return ret;
}

函數很長,但是思路還是很清晰的。建立臨時檔案,設定檔案操作。其中呼叫的都是linux的系統函數了,看真正設定的shmem_zero_setup函數

int shmem_zero_setup(struct vm_area_struct *vma)
{
    struct file *file;
    loff_t size = vma->vm_end - vma->vm_start;
​
    /*
     * Cloning a new file under mmap_lock leads to a lock ordering conflict
     * between XFS directory reading and selinux: since this file is only
     * accessible to the user through its mapping, use S_PRIVATE flag to
     * bypass file security, in the same way as shmem_kernel_file_setup().
     */
    file = shmem_kernel_file_setup("dev/zero", size, vma->vm_flags);
    if (IS_ERR(file))
        return PTR_ERR(file);
​
    if (vma->vm_file)
        fput(vma->vm_file);
    vma->vm_file = file;
    vma->vm_ops = &shmem_vm_ops;//很重要的操作將這塊虛擬記憶體的vm_ops設定為shmem_vm_ops
​
    if (IS_ENABLED(CONFIG_TRANSPARENT_HUGEPAGE) &&
            ((vma->vm_start + ~HPAGE_PMD_MASK) & HPAGE_PMD_MASK) <
            (vma->vm_end & HPAGE_PMD_MASK)) {
        khugepaged_enter(vma, vma->vm_flags);
    }
​
    return 0;
}
static const struct vm_operations_struct shmem_vm_ops = {
    .fault      = shmem_fault,//Linux的共用記憶體實現的基礎
    .map_pages  = filemap_map_pages,
#ifdef CONFIG_NUMA
    .set_policy     = shmem_set_policy,
    .get_policy     = shmem_get_policy,
#endif
};

到這裡共用記憶體的初始化就結束了。

AshMemory 讀寫

​//frameworks/base/core/java/android/os/MemoryFile.java
public void writeBytes(byte[] buffer, int srcOffset, int destOffset, int count)
            throws IOException {
        beginAccess();
        try {
            mMapping.position(destOffset);
            mMapping.put(buffer, srcOffset, count);
        } finally {
            endAccess();
        }
    }
    private void beginAccess() throws IOException {
        checkActive();
        if (mAllowPurging) {
            if (native_pin(mSharedMemory.getFileDescriptor(), true)) {
                throw new IOException("MemoryFile has been purged");
            }
        }
    }
​
    private void endAccess() throws IOException {
        if (mAllowPurging) {
            native_pin(mSharedMemory.getFileDescriptor(), false);
        }
    }

其中beginAccess和endAccess是對應的。呼叫的都是native_pin是一個native函數,一個引數是true一個是false。pin的作用就是鎖住這塊記憶體不被系統回收,當不使用的時候就解鎖。

static jboolean android_os_MemoryFile_pin(JNIEnv* env, jobject clazz, jobject fileDescriptor,
        jboolean pin) {
    int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);
    int result = (pin ? ashmem_pin_region(fd, 0, 0) : ashmem_unpin_region(fd, 0, 0));
    if (result < 0) {
        jniThrowException(env, "java/io/IOException", NULL);
    }
    return result == ASHMEM_WAS_PURGED;
}

呼叫的ashmem_pin_region和ashmem_unpin_region來實現解鎖和解鎖。實現還是在ashmem-dev.cpp

//system/core/libcutils/ashmem-dev.cpp
int ashmem_pin_region(int fd, size_t offset, size_t len)
{
    .......
    ashmem_pin pin = { static_cast<uint32_t>(offset), static_cast<uint32_t>(len) };
    return __ashmem_check_failure(fd, TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_PIN, &pin)));
}

通過的也是ioclt通知的驅動。加鎖的細節就不展開了。具體的寫入就是利用linux的共用記憶體機制實現的共用。

Linux共用機制簡介

共用簡單的實現方式就是通過mmap同一個檔案來實現。但是真實檔案的讀寫速度實在是太慢了,所以利用tmpfs這個虛擬檔案系統,建立了一個虛擬檔案來讀寫。同時這塊虛擬記憶體在上面也寫到重寫了vm_ops。當有程序操作這個虛擬記憶體的時候會觸發缺頁錯誤,接著會去查詢Page快取,由於是第一次所以沒有快取,讀取實體記憶體,同時加入Page快取,當第二個程序進來的時也觸發缺頁錯誤時就能找到Page快取了,那麼他們操作的就是同一塊實體記憶體了。

總結

看完之後發現AshMemory是基於Linux的共用記憶體實現的。做了幾點改造

  • 首先把一整塊記憶體變成了一個個region,這樣在不用的時候可以解鎖來讓系統回收。
  • 將Linux共用記憶體的整數標記共用記憶體,而AshMemory是用的fd,讓它可以利用binder機制的fd傳輸。
  • 讀寫設定都做了加鎖的處理,減少了使用者使用的難度。

以上就是Android 匿名記憶體深入分析的詳細內容,更多關於Android 匿名記憶體的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com