声明
- 其实对于Android系统的Ashmem匿名共享内存系统早就有分析的想法,记得2019年6、7月份Mr.Deng离职期间约定一起对其进行研究的,但因为我个人问题没能实施这个计划,留下些许遗憾…
- 文中参考了很多书籍及博客内容,可能涉及的比较多先不具体列出来了;
- 本文使用的代码是LineageOS的cm-14.1,对应Android 7.1.2,可以参考我的另一篇博客:cm-14.1 Android系统启动过程分析(1)-如何下载Nexus5的LineageOS14.1(cm-14.1)系统源码并编译、刷机
1 Ashmem的架构
Android 系统实现的 Ashmem 匿名共享内存子系统,用来在应用程序之间共享数据。Ashmem 与传统的Linux系统实现的共享内存一样,都是基于内核提供的临时文件系统tmpfs实现的,但是 Ashmem 对内存块进行了更为精细化的管理。应用程序可以动态地将一块匿名共享内存划分为若干个小块,当这些小块内存不再需要使用时,它们就可以被内存管理系统回收。通过这种动态的、分而治之的内存管理方式,Android系统就能够有效地使用系统内存,适应内存较小的移动设备环境。
匿名共享内存系统是以Ashmem驱动程序为基础的,系统中所有的匿名共享内存都由Ashmem驱动程序负责分配和管理。Android系统在 Native 层提供了 C/C++ 调用接口和 Framework 层提供了 Java 调用接口。
- 在Framework 层中,提供了两个C++类 MemoryBase 和 MemoryHeapBase,以及一个 Java 类 MemoryFile 来使用匿名共享内存。
- 在运行时库 cutils 中,主要提供了三个C函数 ashmem_create_region、ashmem_pin_region 和 ashmem_unpin_region 来访问 Ashmem 驱动程序。
Ashmem驱动程序在启动时,会创建一个 /dev/ashmem 设备文件,这样,运行时库 cutils 中的匿名共享内存接口就可以通过文件操作函数 open 和 ioctl 等来访问 Ashmem 驱动程序。
传统的 Linux 系统使用一个整数来标志一块共享内存,而 Android 系统则使用一个文件描述符来标志一块匿名共享内存。使用文件描述符来描述一块匿名共享内存有两个好处:
- 可以方便地将它映射到进程的地址空间,从而可以直接访问它的内容;
- 可以使用 Binder 进程间通信机制来传输这个文件描述符,从而实现在不同的应用程序之间共享一块匿名内存。
Binder 进程间通信机制使用一个类型为 BINDER_TYPE_FD 的 Binder 对象来描述一个文件描述符,当 Binder 驱动程序发现进程间通信数据中包含有这种 Binder 对象时,就会将对应的文件描述符复制到目标进程中,从而实现在两个进程中共享同一个文件。
2 运行时库cutils的Ashmem访问接口
运行时库 cutils 的匿名共享内存访问接口实现在源码 system/core/libcutils/ashmem-dev.c 文件中。这个文件提供了五个 C 接口来访问 Ashmem 驱动程序,它们分别是:
- ashmem_create_region
- ashmempin_region
- ashmem_unpin_region
- ashmem_set_prot_region
- ashmem_get_size_region
2.1 ashmem_create_region
/*
* ashmem_create_region - creates a new ashmem region and returns the file
* descriptor, or <0 on error
*
* `name' is an optional label to give the region (visible in /proc/pid/maps)
* `size' is the size of the region, in page-aligned bytes
*/
int ashmem_create_region(const char *name, size_t size)
{
int ret, save_errno;
int fd = __ashmem_open();
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));
if (ret < 0) {
goto error;
}
}
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;
}
函数 ashmem_create_region 用来请求 Ashmem 驱动程序为应用程序创建一块匿名共享内存,其中参数 name 和 size 分别表示请求创建的匿名共享内存的名称和大小。请求Ashmem驱动程序创建一块匿名共享内存分三步来完成。
-
第一步是调用函数 open 打开设备文件 ASHMEM_DEVICE,即设备文件 /dev/ashmem,它的定义如下所示:
#define ASHMEM_DEVICE "/dev/ashmem"
调用函数 open 打开设备文件 /dev/ashmem 时,Ashmem 驱动程序的函数 ashmem_open 就会被调用主要是为应用程序创建一个 ashmem_area 结构体,用来描述一块匿名共享内存。打开了设备文件 /dev/ashmem 之后,就会得到一个文件描述符,接下来就可以通过这个文件描述符来访问前面请求 Ashmem 驱动程序创建的匿名共享内存。
-
第二步是使用 IO 控制命令 ASHMEM_SET_NAME 来请求 Ashmem 驱动程序将前面所创建的匿名共享内存的名称修改为name。
-
第三步是使用 IO 控制命令 ASHMEM_SET_SIZE 来请求 Ashmem 驱动程序将前面所创建的匿名共享内存的大小修改为 size。
2.2 ashmem_pin_region
/*
fd: 前面打开设备文件 /dev/ashmem 所得到的一个文件描述符;
offset: 用来指定要锁定的内存块在其宿主匿名共享内存中的偏移地址
len: 用来指定要锁定的内存块在其宿主匿名共享内存中的长度
*/
int ashmem_pin_region(int fd, size_t offset, size_t len)
{
struct ashmem_pin pin = { offset, len };
int ret = __ashmem_is_ashmem(fd);
if (ret < 0) {
return ret;
}
return TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_PIN, &pin));
}
函数 ashmem_pin_region 使用 IO 控制命令 ASHMEM_PIN 来请求 Ashmem 驱动程序锁定一小块匿名共享内存。
2.3 ashmem_unpin_region
/*
fd: 前面打开设备文件 /dev/ashmem 所得到的一个文件描述符;
offset: 用来指定要解锁的内存块在其宿主匿名共享内存中的偏移地址
len: 用来指定要解锁的内存块在其宿主匿名共享内存中的长度
*/
int ashmem_unpin_region(int fd, size_t offset, size_t len)
{
struct ashmem_pin pin = { offset, len };
int ret = __ashmem_is_ashmem(fd);
if (ret < 0) {
return ret;
}
return TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_UNPIN, &pin));
}
函数 ashmem_unpin_region 使用 IO 控制命令 ASHMEM_UNPIN 来请求 Ashmem 驱动程序解锁一小块匿名共享内存。
2.4 ashmem_set_prot_region
/*
fd: 前面打开设备文件 /dev/ashmem 所得到的一个文件描述符;
prot: 指定要修改的访问保护位,它的取值为PROT_EXEC、PROTREAD、PROT_WRITE或其组合值;
*/
int ashmem_set_prot_region(int fd, int prot)
{
int ret = __ashmem_is_ashmem(fd);
if (ret < 0) {
return ret;
}
return TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_PROT_MASK, prot));
}
函数 ashmem_set_prot_region 使用 IO 控制命令 ASHMEM_SET_PROT_MASK 来请求 Ashmem 驱动程序修改一块匿名共享内存的访问保护位。Ashmem 驱动程序中的函数 set_prot_mask 负责处理 IO 控制命令 ASHMEM_SET_PROT_MASK,它的实现如下所示:
static int set_prot_mask(struct ashmem_area *asma, unsigned long prot)
{
int ret = 0;
mutex_lock(&ashmem_mutex);
/* the user can only remove, not add, protection bits */
if (unlikely((asma->prot_mask & prot) != prot)) {
ret = -EINVAL;
goto out;
}
/* does the application expect PROT_READ to imply PROT_EXEC? */
if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
prot |= PROT_EXEC;
asma->prot_mask = prot;
out:
mutex_unlock(&ashmem_mutex);
return ret;
}
Ashmem 驱动程序在创建一块匿名共享内存时,将它的访问保护位设置为 PROT_MASK,表示这块匿名共享内存具有可执行、读和写权限。此后,应用程序只能删除它的访问保护位,而不能增加它的访问保护位。因此,第8行的i语句首先检查要修改的访问保护位 prot 是否超出了目标匿名共享内存 asma 所允许的范围。
有一种特殊情况,即当当前进程 current 的 personality 属性的 READIMPLIES_EXEC 位被设置为1时,第14行就会检查参数 prot 的 PROT_READ 位是否被设置为1。如果是,那么第15行就将它的 PROT_EXEC 位也设置为1,因为当一个进程的 personality 属性的 READ_IMPLIES_EXEC 位被设置为1时,就表示当它有权限读一块内存时,也隐含着它对该内存有执行权限。最后,第17行将目标匿名共享内存 asma 的访问保护位 prot_mask 设置为参数 prot 的值。
2.5 ashmem_get_size_region
/*
fd: 前面打开设备文件 /dev/ashmem 所得到的一个文件描述符;
*/
int ashmem_get_size_region(int fd)
{
int ret = __ashmem_is_ashmem(fd);
if (ret < 0) {
return ret;
}
return TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_GET_SIZE, NULL));
}
函数 ashmem_get_size_region 使用 IO 控制命令 ASHMEM_GET_SIZE 来请求 Ashmem 驱动程序返回块匿名共享内存的大小。Ashmem 驱动程序中的函数 ashmem_ioctl 负责处理 IO 控制命 ASHMEM_GET_SIZE,它的实现如下所示:
static long ashmem_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct ashmem_area *asma = file->private_data; //先找到要获得其大小的匿名共享内存 asma
long ret = -ENOTTY;
switch (cmd) {
case ASHMEM_SET_NAME:
ret = set_name(asma, (void __user *) arg);
break;
case ASHMEM_GET_NAME:
ret = get_name(asma, (void __user *) arg);
break;
case ASHMEM_SET_SIZE:
ret = -EINVAL;
if (!asma->file) {
ret = 0;
asma->size = (size_t) arg;
}
break;
case ASHMEM_GET_SIZE:
ret = asma->size; //再将它的大小size返回给调用者
break;
case ASHMEM_SET_PROT_MASK:
ret = set_prot_mask(asma, arg);
break;
case ASHMEM_GET_PROT_MASK:
ret = asma->prot_mask;
break;
case ASHMEM_PIN:
case ASHMEM_UNPIN:
case ASHMEM_GET_PIN_STATUS:
ret = ashmem_pin_unpin(asma, cmd, (void __user *) arg);
break;
case ASHMEM_PURGE_ALL_CACHES:
ret = -EPERM;
if (capable(CAP_SYS_ADMIN)) {
struct shrink_control sc = {
.gfp_mask = GFP_KERNEL,
.nr_to_scan = 0,
};
ret = ashmem_shrink(&ashmem_shrinker, &sc);
sc.nr_to_scan = ret;
ashmem_shrink(&ashmem_shrinker, &sc);
}
break;
}
return ret;
}
至此,分析完成运行时库 cutils 中的匿名共享内存的C访问接口了。