Android跨进程通信--共享内存(ashmem)实例

mmap

mmap 是 Linux 中最为大家熟悉的共享内存方式。通过打开同一个文件,并且使用 MAP_SHARED 标志来调用 mmap() 函数,两个进程就能共享一片内存空间了。但是这种方式存在一个问题,如果分配的空间有一部分不需要了,不能单独释放这些不再使用的“物理内存”,为什么是物理内存呢,因为mmap分配的是地址空间,只有当进程存取某个页面时,才会去分配实际物理内存。这些物理内存只能通过 munmap() 一次性的释放掉。如果某个页面的物理内存不需要了,想把他单独释放,传统的 mmap 时无法做到的,所以就有了 ashmem。

ashmem 的作用和用法

  • 为了弥补 mmap 的不足,Android开发了 ashmem 匿名共享内存机制,这种新的机制是建立在 mmap 基础上,但是 ashmem 提供了 pin 和 unpin 两个 io 操作,能够部分释放内存空间的物理内存。
  • 使用 ashmem 需要打开 “dev/asheme”,并把描述符传给 mmap() 作为参数。但是要注意的是,每次 open() 打开这个设备都会得到一个不同的文件描述符,代表了一块不同的内存区域。因此如果需要多块共享内存,只要多次打开设备 “dev/asheme” 就可以了。但是 ashmem 用作共享内存是会遇到一个问题,Linux 实现共享内存是,要求两个进程同时打开一个文件,并把文件描述符传递给内核,因为文件在 Linux 中是唯一的,这样系统就能知道两个进程需要操作同一块内存。但是 asheme 只能打开 “dev/asheme” 文件,这样无法用一个文件对象表示多块共享内存,而文件描述符是一个数字,只在本进程中有效。因此也不能通过某种方式把文件描述符传递给另外一个进程中使用。
  • Android 的 Binder 提供了两个进程中传递文件描述符的手段,这样 Binder 的 Service 和 client 之间就可以实现内存共享了。因此 ashmem 并不能用于任意两个进程之间的共享内存,必须是在通过 Binder 建立了联系的两个进程之间。

Android 提供了一组使用 ashmem 的函数。头文件 ashmem.h 在/system/core/include/libcutil/下,实现代码 ashmem-dev.c 位于 /system/core/libcutil/ 中。使用 ashmem 步骤如下:

1. 首先创建一个共享区域,通过调用 int ashmem_create_region(const char *name, size_t size) 来完成

int ashmem_create_region(const char *name, size_t size)
{
    int ret, save_errno;
    // 打开设备文件 /dev/ashmem/ 返回一个文件描述符 id
    int fd = __ashmem_open();
    if (fd < 0) {
        return fd;
    }
    // 判断 name 是否为 NULL 如果不为 NULL 通过 ASHMEM_NAME_LEN 来设置属性
    if (name) {
        char buf[ASHMEM_NAME_LEN] = {0};

        strlcpy(buf, name, sizeof(buf));
        // 由 ioctl 操作 ASHMEM_SET_NAME 来设置名称
        // ioctl(input/output control)是一个专用于设备输入输出操作的系统调用
        ret = TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_NAME, buf));
        if (ret < 0) {
            goto error;
        }
    }
    // 通过 ioctl 设置内存大小
    ret = TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_SIZE, size));
    if (ret < 0) {
        goto error;
    }

    return fd;

error:
    save_errno = errno;
    close(fd);
    errno = save_errno;
    return ret;
}

(1)ashmem_create_region() 的主要工作是,打开设备文件 dev/ashmem 得到一个文件描述符 fd。
(2)如果 name 不为 Null 则通过 ioctl 操作 TEMP_FAILURE_RETRY 来设置名称。
(3)通过 ioctl 调用 TEMP_FAILURE_RETRY 设置内存大小。

2. 得到文件描述符后,通过 mmap 分配内存。

// addr:共享内存的地址,如果为NULL,则会自动分配一块内存
// length:共享内存的长度
// prot:内存保护的一些flags(比如说:匿名,读,写权限等)
// flags:是否对其他进程可见,更新是否会传递到底层文件
// fd:文件描述符(用于对内存初始化)
// offset:偏移量(用于初始化,offset从fd哪个位置开始读取,length可以表示读取长度
void* base = mmap(0,length,prot,flags,fd,offset)

如果需要修改内存属性通过下面函数,prot 参数和mmap的属性一致

int ashmem_set_prot_region(int fd, int prot)
{
    int ret = __ashmem_is_ashmem(fd, 1);
    if (ret < 0) {
        return ret;
    }
    return TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_PROT_MASK, prot));
}


解锁部分内存调用如下函数

int ashmem_unpin_region(int fd, size_t offset, size_t len)
{
    // TODO: should LP64 reject too-large offset/len?
    ashmem_pin pin = { static_cast<uint32_t>(offset), static_cast<uint32_t>(len) };

    int ret = __ashmem_is_ashmem(fd, 1);
    if (ret < 0) {
        return ret;
    }

    return TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_UNPIN, &pin));
}


解锁后,这部分内存会在内存不足时会被回收。

如果需要可以把解锁的内存再次锁住

int ashmem_pin_region(int fd, size_t offset, size_t len)
{
    // TODO: should LP64 reject too-large offset/len?
    ashmem_pin pin = { static_cast<uint32_t>(offset), static_cast<uint32_t>(len) };

    int ret = __ashmem_is_ashmem(fd, 1);
    if (ret < 0) {
        return ret;
    }

    return TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_PIN, &pin));
}

如果需要获取内存块的大小,则调用下面的函数

int ashmem_get_size_region(int fd)
{
    int ret = __ashmem_is_ashmem(fd, 1);
    if (ret < 0) {
        return ret;
    }

    return TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_GET_SIZE, NULL));
}


ashmem 实现原理

andorid 5.0 上

ashamed 是建立在 Linux 已有的内存分配和共享的基础上,本身做的事情并不复杂。ashmem 的驱动的主要工作是维护一个链表,这个链表的作用就是用户 IO 操作 unpin 后的一个个节点,里面保存了里面需要解锁的内存的开始和结束地址。当系统内存不足时就会通过这个链表释放一部分内存。而比较复杂的工作,如分配地址空间,还是通过内核去完成的,所以 ashmem 的主要工作就是维护 unpinned 链表。

struct ashmem_area {
    char name[ASHMEM_FULL_NAME_LEN]; /* 名字出现在 /proc/pid/maps */
    struct list_head unpinned_list;     /* 列表头 用户调用 pin 和 unpin 生成的*/
    struct file *file;         /* 用来分配虚拟空间的文件 这是通过 mmap 分配内存生成的对象和打开设备的文件不同 */
    size_t size;             /* 内存块的大小 */
    unsigned long vm_start;         /* 映射这个ashmem的vm_area的起始地址 */
    unsigned long prot_mask;     /* 内存块的尺寸 */
};

当用户进程调用 open() 打开设备文件时,就会创建一个 ashmem_area 对象,保存一些 ashmem 内存块的信息,这个 ashmem_area 对象的指针会保存到设备文件对象 file 的 private_data 字段中,当用户进程使用文件描述符来调用 IO 操作时,驱动就能从文件对象中得到这个 ashmem_area 对象了。

另外一个很重要的结构式 ashmem_range 记录了被解锁内存块的基本信息。ashmem_range 就是unpinned 链表的节点,记录了被解锁内存块的基本信息。当用户进程执行unpin 操作时,驱动会生成一个 ashmem_range 的节点,这个节点会挂到一个全局链表 LRU 中,当系统内存不足时,会调用驱动注册的 ashmem_shrinker() 函数来释放内存,而 ashmem_shrinker() 函数将通过 LRU 链表来找到解锁的内存并释放模块,内存被释放后 ashmem_range 将会被移除,但是还会留在进程的 unpinned_list 中,同时其内部属性 purged 会设置成 true 代表已经被释放,就不会被重复释放了。

共享内存(ashmem)实例 

ashmem 通信原理是每个进程打开同一个文件,所以文件描述符需要通过其他方式传递给其他进程,所以这里采用了 Service + aidl 的方式把共享内存的文件描述符传递过去。然后客户端获取到的文件描述符读取该内存下的内容具体步骤如下:其中 MemoryFile 是通过 ashmem 实现的

Server 端

1. 定义 aidl 接口,如何使用 aidl 略

// IFileDescriptorInterface.aidl
package com.process.server;

interface IFileDescriptorInterface {
	// 定义接口 返回 ParcelFileDescriptor 文件描述符
      ParcelFileDescriptor getPfd();
}


2. ParcelFileDescriptor 实现了Parcelable 所以可以通过binder传输。

接下来定义 service ,为了方便测试只是简简单单的创建了个 MemoryFile 并返回操作符,MemoryFile 内部采用了ashmem 。记住在 manifest 中注册 service 并且设置 action 因为不是本地 service。

class MyService : Service() {

    /**
     * aidl 定义获取文件描述符的方法
     */
    var stub: IFileDescriptorInterface.Stub = object : IFileDescriptorInterface.Stub() {
        @Throws(RemoteException::class)
        override fun getPfd(): ParcelFileDescriptor {
            return createMemoryFile()
        }
    }
    
    override fun onBind(intent: Intent): IBinder {
        return stub
    }

    /**
     * 创建
     * @return ParcelFileDescriptor
     */
    fun createMemoryFile(): ParcelFileDescriptor {
        // 定义空间大小 这里只是随便定义了哥 1024
        val memoryFile = MemoryFile("test_ashmem", 1024)
        val writeString = "hi client,am server msg... "
        memoryFile.outputStream.write(writeString.toByteArray())
        // 可以接下来写各种想写的东西...
        val method: Method = memoryFile.javaClass.getDeclaredMethod("getFileDescriptor")
        val fd = method.invoke(memoryFile) as FileDescriptor
        return ParcelFileDescriptor.dup(fd);
    }
}

客户端

客户端要定义和服务端一样的 aidl 规则:

package com.process.server;

// Declare any non-default types here with import statements

interface IFileDescriptorInterface {
    ParcelFileDescriptor getPfd();
}


接下来连接 service
 

class MainActivity : AppCompatActivity() {

    private val conn = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            val asInterface = IFileDescriptorInterface.Stub.asInterface(service)
            val pfd = asInterface.pfd
            val fileDescriptor = pfd.fileDescriptor
            val fis = FileInputStream(fileDescriptor)
            val string = fis.bufferedReader().use { it.readText() }
            Log.e("tag", "read result = $string")
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            Log.e("tag", "onServiceDisconnected")
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    fun bindService(view: View) {
        val intent = Intent()
        intent.component = ComponentName("com.process.server", "com.process.server.MyService")
        val bindService = bindService(intent, conn, Context.BIND_AUTO_CREATE)
        Log.e("tag", "bindService = $bindService")
    }
}

Ashmem 简述

1. 概述

Ashmem(Anonymous Shared Memory,Android 匿名共享内存),它基于 mmap 系统调用,可以让不同进程将同一段物理内存映射到各自的虚拟地址中,从而实现内存共享。

它以驱动程序的形式在内核空间中实现,并通过文件描述符来传递共享内存的句柄。

相对于 Linux 的共享内存, Ashmem 驱动中添加了互斥锁以此实现了同步的机制,并能够辅助内存管理系统来有效地管理不再使用的内存块。

2. 原理

两个进程就像两个平行的世界,A 进程没法直接访问 B 进程的数据,其用户空间的数据存在进程隔离,而内核空间的数据则可以进程间共享。

应用程序不能直接操作设备地址,操作系统通过内存映射(mmap),可以把设备地址映射到进程内核空间的虚拟内存区域。

 


Android 中,/dev/ashmem是一个虚拟设备,不存在实际文件,只在内核驱动中对应一个inode节点。通过 ashmem 和 binder 相关方法,存在于两个进程中的不同文件描述符,会对应到同一个基于/dev/ashmem创建的临时文件,并将该文件指向的物理内存分别映射到各个进程自己的虚拟内存中,最终实现进程间内存共享。


3. 源码

3.1 源码路径

Framework:
  frameworks/base/core/java/android/os/SharedMemory.java
  frameworks/base/core/java/android/os/MemoryFile.java
  frameworks/base/core/jni/android_os_SharedMemory.cpp
  frameworks/base/core/jni/android_os_MemoryFile.cpp

Native:
  frameworks/native/libs/binder/IMemory.cpp
  frameworks/native/libs/binder/MemoryBase.cpp
  frameworks/native/libs/binder/MemoryHeapBase.cpp
  frameworks/native/libs/binder/MemoryDealer.cpp
  frameworks/native/libs/binder/include/binder/IMemory.h
  frameworks/native/libs/binder/include/binder/MemoryBase.h
  frameworks/native/libs/binder/include/binder/MemoryHeapBase.h
  frameworks/native/libs/binder/include/binder/MemoryDealer.h

System:
  system/core/libcutils/ashmem-host.c   提供给模拟器
  system/core/libcutils/ashmem-dev.c    提供给实际设备
  system/core/include/cutils/ashmem.h

Driver:  
  kernel/msm-5.4/drivers/staging/android/ashmem.c
  kernel/msm-5.4/drivers/staging/android/ashmem.h
  kernel/msm-5.4/drivers/staging/android/uapi/ashmem.h


3.2 驱动层

依托于/dev/ashmem设备,Linux 内核分配并管理 ashmem 的全局内存。

kernel/msm-5.4/drivers/staging/android/ashmem.c
kernel/msm-5.4/drivers/staging/android/ashmem.h

ashmem_area 结构体

/**
 * The lifecycle of this structure is from our parent file's open() until
 * its release(). It is also protected by 'ashmem_mutex'
 * Warning: Mappings do NOT pin this structure; It dies on close()
 */
struct ashmem_area {
  char name[ASHMEM_FULL_NAME_LEN]; // optional name in /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_init()

  • ashmem 的 init 入口,通过 kmem_cache_create(),创建ashmem_area_cachep和ashmem_range_cachep两个 kmem_cache 对象,此时并没有分配内存;
  • 通过调用misc_register()将其注册为ashmem名称的 misc 设备;
  • 通过调用register_shrinker()向内存管理系统注册内存回收函数。
ashmem_open()
  • 从 ashmem_area_cachep 中分配一块 ashmem_area(asma)共享内存给进程;
  • 对 asma 进行初始化,然后将其记录在 file->private_data 中,进程可以通过返回的 file 指针访问该共享内存。
ashmem_mmap()

Memory Map(内存映射),把 /dev/ashmem 设备文件映射到进程虚拟内存中。

  1. 从file->private_data中取出 ashmem_open() 时创建的 asma;
  2. 若asma->file为空,则当前为第一次访问该共享空间的进程,需要调用 Linux 提供的 shmem_file_setup() ,在 tmpfs 中创建临时文件用于进程间的内存共享。

ashmem_ioctl()
  • 设置/获取名称;
  • 设置/获取 size 大小;
  • 设置/获取保护位;
  • 锁定/解锁 asma 下的内存块 。
ashmem_pin_unpin()

锁定(pin)或解锁(unpin)内存块。

Ashmem 机制中,正在使用的内存块需要被锁定,不被使用的内存块需要被解除锁定。unpin操作仅改变相关状态标记,并不会改变已经 mmap 的地址空间。因此,用户可以在解锁后重新锁定某块内存块。

Ashmem 机制建立在 Linux 内核的共享内存实现上。同时又向 Linux 内存管理系统的内存回收算法注册接口。系统内存不足时,会依据 LRU 算法回收unpin内存块对应的物理页面。如果不希望内存对象被回收,可以通过修改其状态为pin来保护它。


3.3 系统层

system/core/libcutils/ashmem-dev.c
system/core/include/cutils/ashmem.h

Ashmem 匿名共享内存,需要依赖 System 层和 Driver 层进行交互:

// ashmem.h
// 根据名称和大小,创建 ashmem 区域,返回文件描述符
int ashmem_create_region(const char *name, size_t size);
// 设置 ashmem 访问保护位
int ashmem_set_prot_region(int fd, int prot);
// ashmem 锁定
int ashmem_pin_region(int fd, size_t offset, size_t len);
// ashmem 解锁
int ashmem_unpin_region(int fd, size_t offset, size_t len);
// ashmem 区域的大小
int ashmem_get_size_region(int fd);
3.4 Native层

frameworks/native/libs/binder/MemoryDealer.cpp

MemoryDealer类可以看做是对 ashmem 的封装,源码位于frameworks/native/libs/binder/目录下。其内部拥有两个重要的成员变量:

  • mHeap,为MemoryHeapBase类对象,用于描述一块内存;
  • mAllocator,为SimpleBestFitAllocator类对象,用于分配内存。

MemoryHeapBase继承自抽象类BnMemoryHeap,并使用libcutils库中的 ashmem_create_region和mmap方法进行 ashmem 创建和内存映射。而MemoryBase继承自BnMemory,用于包装MemoryHeapBase对象。

IMemory.h 中,定义了IMemory、IMemoryHeap、BnMemory 和 BnMemoryHeap等匿名共享内存相关的访问接口。几者间的关系如下:


3.5 Framework 层

frameworks/base/core/java/android/os/SharedMemory.java
frameworks/base/core/java/android/os/MemoryFile.java

Java层借助MemoryFile或者SharedMemory创建匿名共享内存。

SharedMemory通过 JNI 调用ashmem_create_region进行匿名共享内存区创建。

MemoryFile对SharedMemory进行了包装,能够标记不再使用的内存区域,方便系统回收。

4. Ashmem 的应用

ashmem 通过共享内存进行数据传递,共享内存的操作需要通过文件句柄 fd。所以,当不同进程间进行内存共享时,需要跨进程传递 fd 信息。因此,在使用 Ashmem 之前,需要在两个进程之前建立 Binder 连接。

通过 Ashmem 传递内容需要进行以下步骤:

  • 创建 Ashmem 区域,并写入待传输数据;
  • 获得该共享内存的 fd 句柄;
  • 打包 fd 到 bundle;
  • 通过 Binder 发送 fd 信息到服务端。

下面来讲一下这个方案怎么实现。考虑到进程间通信的场景,通常是Activity通过AIDL与Service通信,我们建立一个Activiy和Service,让他们分别跑在不同的进程。在Activiy端,具体的实现代码如下:

byte[] contentBytes = new byte[100];  
MemoryFile mf = new MemoryFile(“memfile”, contentBytes.length);  
mf.writeBytes(contentBytes, 0, 0, contentBytes.length);  
Method method = MemoryFile.class.getDeclaredMethod("getFileDescriptor");  
FileDescriptor fd = (FileDescriptor) method.invoke(mf);  
pfd = ParcelFileDescriptor.dup(fd);  

其中MemoryFile需要两个参数,第一个是文件名,这个可以为null,第二个参数是文件长度,然后调用writeBytes将数据写入文件。由于MemoryFile的getFileDescriptor方法是@hide的,需要用反射得到文件描述符。最后调用ParcelFileDescriptor.dup()的目的是将文件描述符序列化,这是因为通过Binder传递的数据必须都是可序列化的。这样文件描述符就通过Binder传到了Service进程中。
在Service端,通过AIDL拿到文件描述符后,通过正常的文件读取方式,就可以读取到数据。

fis = new FileInputStream(fd.getFileDescriptor());  
fis.read(new byte[100]);  

通过这样的方式,可以避免Binder对传递的数据过大的限制,又可以解决跨进程传递数据的效率问题。

使用

  • Java层借助MemoryFile或者SharedMemory
  • Native层借助MemoryHeapBase或者MemoryBase
  • Native层直接调用libc的ashmem_create_regionmmap系统调用。
MemoryFile基于 SharedMemory。  MemoryBase基于 MemoryHeapBase。  SharedMemoryMemoryHeapBase都是基于 ashmem_create_region/mmap

MemoryFile

MemoryFile是对SharedMemory的包装,官方推荐直接使用SharedMemory。

Applications should generally prefer to use {@link SharedMemory} which offers more flexible access & control over the shared memory region than MemoryFile does.

SharedMemory

SharedMemory只能通过调用SharedMemory.create静态方法或者通过Parcel反序列化的方式进行创建。 SharedMemory的创建进程通过SharedMemory.create创建,使用进程通过Parcel反序列化创建。

因为SharedMemory类实现了Parcelable,所以可以通过binder跨进程传输。

MemoryBase和MemoryHeapBase

MemoryBase是对MemoryHeapBase的包装。MemoryHeapBase对应一块共享内存,使用ashmem_create_region/mmap创建,MemoryHeapBase内部保存了共享内存的地址和大小。通过MemoryBase可以获取其包装的MemoryHeapBase

MemoryBaseMemoryHeapBase都是Binder本地对象(BBinder),可以直接传到其他进程。其他进程分别使用IMemoryIMemoryHeap进行跨进程调用。

MemoryHeapBase跨进程传输本质上传输的是共享内存的fd,fd在经过binder驱动时会被转换成目标进程的fd,MemoryHeapBase的客户端代理对象BpMemoryHeap在创建时候会将fd映射到自己的内存空间,这样客户端进程在使用IMemoryHeap接口获取到的内存地址就是自己进程空间的地址。

ashmem_create_region 和 mmap

ashmem_create_region/mmap是SharedMemory和MemoryHeapBase的实现基础。

int ashmem_create_region(const char *name, size_t size)

用于创建共享内存,函数内部首先通过open函数打开/dev/ashmem设备,得到文件描述符后,通过调用ioctl设置fd的名称和大小。

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

通过binder将fd传递到其他进程后,其他进程可以通过mmap系统调用,将共享内存映射到当前进程的地址空间,之后就可以通过返回的内存首地址进行内存读写。

这样,两个进程之间就实现了直接的内存共享,获得了极高的进程间通信效率。

Ashmem的Pin和Unpin

ashmem驱动提供了两个用于内存管理的ioctl操作命令:pin/unpin,直接通过ashmem_create_region创建的共享内存默认是pined的状态,也就是说,应用程序不主动关闭共享内存fd的情况下,这篇内存会始终保留,直到进程死亡。

如果调用unpin将共享内存中的某段内存解除锁定,之后如果系统内存不足,会自动释放这部分内存,再次使用同一段内存前应该先执行pin操作,如果pin操作返回ASHMEM_WAS_PURGED,也就是说内存已经被回收,已经回收的内存再次访问会触发缺页中断重新进行物理内存的分配,因此这段内存里的数据已经不是起初的那个数据了,如果仍旧当做原始数据进行访问必然引发错误。

通过pin/unpin命令,配合ashmem驱动,可以进行简单的内存管理。

原理

Ashmem的核心原理主要是两部分:驱动和fd传递。

驱动

Ashmem是Linux内核中的一个misc设备,对应的设备文件是/dev/ashmem,此设备是一个虚拟设备,不存在实际文件,只在内核驱动中对应一个inode节点。Ashmem在驱动层是基于linux系统的共享内存功能实现的,Ashmem可以理解为只是对原生的共享内存进行了一层包装,使其更方便在Android系统上使用。

ashmem设备文件支持如下操作:

// /drivers/staging/android/ashmem.c
809static const struct file_operations ashmem_fops = {
810 .owner = THIS_MODULE,
811 .open = ashmem_open,
812 .release = ashmem_release,
813 .read = ashmem_read,
814 .llseek = ashmem_llseek,
815 .mmap = ashmem_mmap,
816 .unlocked_ioctl = ashmem_ioctl,
817#ifdef CONFIG_COMPAT
818 .compat_ioctl = compat_ashmem_ioctl,
819#endif
820};

ashmem创建:(从Java层到驱动层的调用链)

[java] android.os.SharedMemory#create
[jni] /frameworks/base/core/jni/android_os_SharedMemory.cpp#SharedMemory_create
[libc] /system/core/libcutils/ashmem-dev.c#ashmem_create_region
[driver] /drivers/staging/android/ashmem.c#ashmem_open

ashmem_open

ashmem_open中只是创建了一个标识ashmem的结构体,然后返回fd,并没有进行实际的内存分配(无论是虚拟内存还是物理内存)。 得到文件描述符后,就可以使用ashmem_mmap将内核中的共享内存区域映射到进程的虚拟地址空间。

ashmem_mmap

ashmem_mmap通过调用内核中shmem相关函数在tempfs创建了一个大小等于创建ashmem时传入大小的临时文件(由于是内存文件,所以磁盘上不存在实际的文件),然后将文件对应的内存映射到调用mmap的进程。(注意map的是临时文件而不是ashmem文件)

其中涉及到的shmem函数包括shmem_file_setupshmem_set_file,他们为该临时文件创建inode节点,将文件关联到为该文件配的虚拟内存,同时为该文件设置自己的文件操作函数(Linux共享内存shmem的文件操作),并为虚拟内存设置缺页处理函数。这样后续对共享内存的操作就变为了对tempfs文件节点的操作。当首次访问共享内存时触发缺页中断处理函数并为该虚拟内存分配实际的物理内存。

tempfs是Unix-like系统中一种基于内存的文件系统,具有极高的访问效率。
shmem是Linux自带的进程间通信机制:共享内存 Shared Memory
共享内存的虚拟文件记录在 /proc/<pid>/maps文件中,pid表示打开这个共享内存文件的进程ID。

ashmem_pin/ashmem_unpin

pinunpinashmemioctl支持的两个操作,用于共享内存的分块使用和分块回收,用于节省实际的物理内存。 新创建的共享内存默认都是pined的,当调用unpin时,驱动将unpined的内存区域所在的页挂在一个unpinned_list链表上,后续内存回收就是基于unpinned_list链表进行。

ashmem驱动初始化函数ashmem_init里调用了内核函数register_shrinker,注册了一个内存回收回调函数ashmem_shrink,当系统内存紧张时,就会回调ashmem_shrink,由驱动自身进行适当的内存回收。驱动就是在ashmem_shrink中遍历unpinned_list进行内存回收,以释放物理内存。

ashmem fd的传递:

fd通过Binder传递。

Binder机制不仅支持binder对象的传递,还支持文件描述符的传递。fd经过binder驱动时,binder驱动会将源进程的fd转换成目标进程的fd,转换过程为:取出发送方binder数据里的fd,通过fd找到文件对象,然后为目标进程创建fd,将目标进程fd和文件对象进行关联,将发送方binder数据里的fd改为目标进程的fd,然后将数据发送给目标进程。这个过程相当于文件在目标进程又打开了一次,目标进程使用的是自己的fd,但和源进程都指向的是同一个文件。这样源进程和目标进程就都可以map到同一片内存了。

使用场景

  • 进程间共享体积较大的数据,比如bitmap。
  • 提升进程间传输数据的效率,比如ContentProvider基于共享内存进行数据传送。
  • 借助Bitmap解码的inPurgeable属性,在android4.x及以下系统版本中实现内存在ashmem中分配,以节省Java堆内存。比如fresco图片加载库针对Android4.x及以下的机型对inPurgeable属性的使用。

5. ashmem 设备查看

在使用时,进程在 /dev/ashmem/ 目录中创建了一个文件条目,然后删除,但因为它至少有一个打开的文件描述符,所以相应的inode和对应内存区域仍然存在。创建多个具有相同名称的 ashmem 区域,它们都显示为/dev/ashmem/<name> (deleted),但它们中的每一个都对应于不同的inode,因此对应着不同的内存区域。当最后一个文件描述符关闭时,ashmem 临时文件的inode和对应内存会自动回收。


6. 总结


关于Fresco在图片管理上的一些知识点

Purgeable Bitmap

Ashmem一般在应用层中是无法直接访问的,除了几个特例之外。其中之一就是 decode bitmap ,我们可以通过设置 BitmapFactory.Optinons.inPurgeable = true 来创建一个 Purgeable Bitmap ,这样decode出来的bitmap是在Ashmem内存中,GC无法自动回收它。当该Bitmap在被使用时会被 pin 住,使用完之后就 unpin ,这样系统就可以在将来某一时间释放这部分内存。

如果一个 unpinned 的bitmap在之后又要被使用,系统会运行时又将它重新decode,但是这个decode操作是发生在UI线程中的有可能会造成掉帧现象,因此改做法已经被Google废弃掉,转为鼓励使用 inBitmap 来告知bitmap解码器去尝试使用已经存在的内存区域,新解码的bitmap会尝试去使用之前那张bitmap在heap中所占据的pixel data内存区域,而不是去问内存重新申请一块区域来存放bitmap。利用这种特性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数量的内存大小。


但是使用 inBitmap 需要注意几个限制条件:


在SDK 11 -> 18之间,重用的bitmap大小必须是一致的,例如给inBitmap赋值的图片大小为100-100,那么新申请的bitmap必须也为100-100才能够被重用。从SDK 19开始,新申请的bitmap大小必须小于或者等于已经赋值过的bitmap大小。 新申请的bitmap与旧的bitmap必须有相同的解码格式,例如大家都是8888的,如果前面的bitmap是8888,那么就不能支持4444与565格式的bitmap了。 我们可以创建一个包含多种典型可重用bitmap的对象池,这样后续的bitmap创建都能够找到合适的“模板”去进行重用。

Bitmap在Ashmem中的使用

Ashmem内存区域是不能被Java应用直接使用的,但这其中有一些例外,而Bitmap是其中一个.

BitmapFactory.Options = new BitmapFactory.Options();
options.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);

Purgeable被设置成true以后,这个Bigmap就是保存在Ashmem内存区域中的,Java的垃圾回收是不能回收这篇区域的内存的.
当Android系统需要渲染这个Bitmap的时候,会调用pin,渲染完成后会调用unpin.而unpin后的内存空间表示能被其他人所使用.
如果被unpin的Bitmap需要重新渲染,系统会再次Decode这个Bitmap.而这个Decode的过程是在UI线程上完成的.所以Google后来废弃了这个pureable的参数.

后来Google提供了另外一个Flag,叫inBitmap.很遗憾的是,知道Android4.4后,这个新的Flag才得到完善.而Fresco致力于实现一个包括Android2.3以及以上的Android系统都能完美工作的图片加载管理开源库,因此Fresco放弃了使用inBitmap的解决方案.

Fresco是如何利用Ashmem去给Bitmap分配和管理内存?

MemoryFile memoryFile = new MemoryFile(null, inputStream.available());
memoryFile.allowPurging(false);
OutputStream outputStream = memoryFile.getOutputStream();
outputStream.write(1024);


上面可以看到allowPurging这个调用,这个就是之前说的”pin”和”unpin”,在设置了allowPurging为false之后,这个MemoryFile对应的Ashmem就会被标记成”pin”,那么即使在android系统内存不足的时候,也不会对这段内存进行回收。另外,由于Ashmem默认都是”unpin”的,因此申请的内存在某个时间点内都可能会被回收掉,这个时候是不可以再读写了.
上面说到的pin和unpin两个操作,对应的NDK调用是AndroidBitmap_lockPixels和unlockPixels.按照我们一惯认知,为了避免内存泄漏,这两者必须成对出现.而Fresco为了避免Bitmap再次渲染而导致的在UI线程Decode的过程,偏偏不在渲染完成后调用unlockPixels.
这做后,Fresco需要自己去管理这块内存区域,保证当这个Bitmap不再使用时,Ashmem的内存空间能被unpin.


而Fresco选择在Bitmap离开屏幕可视范围时候(onDetachWindow等时候),去做unpin.

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值