匿名共享内存 ashmem

概述

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 设置内存大小。

  • 得到文件描述符后,通过 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 代表已经被释放,就不会被重复释放了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值