In-depth analysis of Android anonymous memory

Android anonymous memory parsing

With the binder mechanism, why do we need anonymous memory to implement IPC? I think the big reason is that binder transmission has size restrictions, not to mention application layer restrictions. The binder transfer size is limited to 4M in the driver, and sharing a picture may exceed this limit. The main solution to anonymous memory is to transfer file descriptors through binder so that both processes can access the same address for sharing.

MemoryFile usage

In normal development, Android provides MemoryFile to implement anonymous memory. Let’s look at the simplest implementation.

Service side


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
    }
}

 The function of the Server is very simple. When receiving the GET_ASH_MEMORY request, it creates a MemoryFile, writes a byte array of string into it, and then writes the fd and character length into the reply and returns it to the client.

 

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)
        }
    }
}

The client is also very simple. It starts the service, sends a request to obtain MemoryFile, then gets the fd and length through reply, uses FileInputStream to read the contents of fd, and finally uses toast to verify that the message has been obtained.

AshMemory creation principle

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

MemoryFile is a layer of encapsulation of SharedMemory, and the specific functions are implemented by SharedMemory. Look at the implementation of 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;

Obtaining fd through a JNI, it can be inferred from here that the java layer is just a package, and what you get is the created 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;
}

Creation through ashmem_create_region function in cutils

//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;
}

Standard driver interaction

1.open open the driver

2. Interact with the driver through ioctl

Let’s take a look at the process of 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;
}

Go back to the constructor of MemoryFile, get the driver's fd and call 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);
    }

At this point, there is a question. Linux has shared memory. Why does Android have to build its own one? We can only look at the implementation of the Ashmemory driver.

The first step in driving is to look at init and 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 cache\n");
        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 cache\n");
        goto out_free1;
    }
    ret = misc_register(&ashmem_misc);//注册为了一个misc设备
    ........
    return ret;
}

Two memory allocators ashmem_area_cachep and ashmem_range_cachep are created for allocating ashmem_area and 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 calls 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;
}

The ioctl to set the name and length is called 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;
    }
  ........
  }

The implementation is also very simple, just change the value in asma. Next is the focus of mmap, specifically how to allocate memory.

​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] != '\0')
            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;
}

The function is very long, but the idea is still very clear. Create temporary files and set up file operations. All the system functions called are Linux system functions. See the actual shmem_zero_setup function.

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
};

The initialization of shared memory ends here.

AshMemory read and write

​//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);
        }
    }

Among them, beginAccess and endAccess are corresponding. What is called is native_pin, which is a native function. One parameter is true and the other is false. The function of the pin is to lock this memory from being recycled by the system and unlock it when not in use.

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;
}

Call ashmem_pin_region and ashmem_unpin_region to achieve unlocking and unlocking. The implementation is still in 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)));
}

The driver passed is also the ioclt notification. The details of locking will not be expanded upon. The specific writing is sharing using the shared memory mechanism of Linux.

Introduction to Linux sharing mechanism

The simple way to achieve sharing is to mmap the same file. However, the reading and writing speed of real files is too slow, so we use tmpfs, a virtual file system, to create a virtual file for reading and writing. At the same time, this virtual memory is also written on it and vm_ops is rewritten. When a process operates this virtual memory, a page fault will be triggered, and then the Page cache will be searched. Since it is the first time, there is no cache, the physical memory will be read, and the Page cache will be added at the same time. When the second process comes in, it will also The Page cache can be found when a page fault is triggered, so they operate the same physical memory.

Summarize

After reading it, I found that AshMemory is implemented based on Linux shared memory. Made some modifications

  • First, a whole block of memory is turned into regions, so that they can be unlocked and recycled by the system when not in use.
  • The integer of Linux shared memory is marked as shared memory, and AshMemory uses fd, so that it can utilize the fd transmission of the binder mechanism.
  • The read and write settings have been locked, which reduces the difficulty for users.

Guess you like

Origin blog.csdn.net/jdsjlzx/article/details/134587730