mmap 是 Linux 中最为大家熟悉的共享内存方式。通过打开同一个文件,并且使用 MAP_SHARED 标志来调用 mmap() 函数,两个进程就能共享一片内存空间了。但是这种方式存在一个问题,如果分配的空间有一部分不需要了,不能单独释放这些不再使用的“物理内存”,为什么是物理内存呢,因为mmap分配的是地址空间,只有当进程存取某个页面时,才会去分配实际物理内存。这些物理内存只能通过 munmap() 一次性的释放掉。如果某个页面的物理内存不需要了,想把他单独释放,传统的 mmap 时无法做到的。所以就有了 ashmem
Android 提供了一组使用 ashmem 的函数。头文件 ashmem.h 在/system/core/include/libcutil/下,实现代码 ashmem-dev.c 位于 /system/core/libcutil/ 中。使用 ashmem 步骤如下:
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 设置内存大小。
// addr:共享内存的地址,如果为NULL,则会自动分配一块内存
// length:共享内存的长度
// prot:内存保护的一些flags(比如说:匿名,读,写权限等)
// flags:是否对其他进程可见,更新是否会传递到底层文件
// fd:文件描述符(用于对内存初始化)
// offset:偏移量(用于初始化,offset从fd哪个位置开始读取,length可以表示读取长度
void* base = mmap(0,length,prot,flags,fd,offset)
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));
}
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 代表已经被释放,就不会被重复释放了。