Android Shared Memory

注意
本文最后更新于 2024-05-21,文中内容可能已过时。

原文链接:https://ovea-y.cn/android_sharedmemory/

1. 简介

在Android系统中,进程间的通信(IPC)是非常常见的需求,但也是一个挑战,因为每个进程都运行在自己的虚拟内存空间中。因此,单纯通过传统的内存指针是无法在不同进程间直接共享数据的。

为了有效地在不同进程间共享和传递大量数据,Android引入了ashmem,即Anonymous Shared Memory的简称。这是一个在Linux环境下实现的轻量级共享内存机制,它可以让多个不同的进程访问同一块内存区域,而不需要将数据拷贝到每个进程的私有内存空间中。ashmem的主要优势包括:

  1. 效率:通过ashmem,数据可以在进程间共享而无需复制,这减少了内存的使用和CPU的负担。

  2. 匿名:ashmem允许不相关的进程共享内存而无需它们之间有明确的关系(例如,一个父进程与子进程间共享内存)。共享的内存区域没有名称,因此是匿名的。

  3. 可回收的内存:另一个重要特性是,当系统内存不足时,这些共享内存区域可以被Android系统自动回收,从而减轻内存压力。每个ashmem区域都可以标记其重要性,以帮助系统决定在内存紧张时哪些区域可以被回收。

由于这些优势,ashmem成为Android系统中一个重要的特性,广泛用于需要高效进程间通信和数据共享的场景,如跨进程通信(IPC)、高效传输大型数据对象等。

结合Binder机制,ashmem提供了一种有效且安全的方式来进行高效的跨进程通信。这对于保持Android应用的性能和响应性至关重要。

2. 内核驱动实现

代码位置:common/drivers/staging/android

bash

CONFIG_ASHMEM=y

text

/dev/ashmem

Android Ashmem子系统的初始化包含以下步骤:

  1. 创建内存缓存:通过调用kmem_cache_create函数,为ashmem_areaashmem_range结构创建两个内存缓存。这些缓存用于快速分配和释放ashmem的数据结构实例,提高内存分配效率。

    • ashmem_area_cachep:为ashmem_area结构体创建内存缓存,该结构体代表一个ashmem区域。
    • ashmem_range_cachep:为ashmem_range结构体创建内存缓存,该结构体代表ashmem区域内的一个范围。
  2. 注册杂项设备:通过调用misc_register函数,把ashmem作为一个杂项设备注册到系统中。这一步操作关联了设备名称(“ashmem”)和之前定义的文件操作(ashmem_fops),这样用户空间的应用就可以通过/dev/ashmem这个设备文件访问ashmem功能。

  3. 注册内存回收器:调用ashmem_init_shrinker函数注册内存回收器(shrinker),它允许内核在内存变得紧张时回收由ashmem使用的内存页。

c

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);
	if (ret) {
		pr_err("failed to register misc device!\n");
		goto out_free2;
	}

	ret = ashmem_init_shrinker();
	if (ret) {
		pr_err("failed to register shrinker!\n");
		goto out_demisc;
	}

	pr_info("initialized\n");

	return 0;

out_demisc:
	misc_deregister(&ashmem_misc);
out_free2:
	kmem_cache_destroy(ashmem_range_cachep);
out_free1:
	kmem_cache_destroy(ashmem_area_cachep);
out:
	return ret;
}

// device_initcall(ashmem_init); 这行代码是一个宏调用,它确保`ashmem_init`函数在设备初始化阶段被调用。这意味着在系统启动过程中,内核会自动调用`ashmem_init`函数来初始化ashmem子系统。
device_initcall(ashmem_init);

c

static const struct file_operations ashmem_fops = {
	// 指定模块的所有者,通常设置为`THIS_MODULE`,以确保在使用文件操作时模块不会被卸载。
	.owner = THIS_MODULE,
	// 当打开ashmem虚拟文件时调用的函数(此处为`ashmem_open`),它负责初始化或分配资源。
	.open = ashmem_open,
	// 当关闭ashmem虚拟文件时调用的函数(此处为`ashmem_release`),它负责释放由`.open`分配的资源。
	.release = ashmem_release,
	// 实现了对该设备文件的读操作(此处为`ashmem_read_iter`)。
	.read_iter = ashmem_read_iter,
	// 重新定位文件(此处为`ashmem_llseek`),使得文件的读写位置可以改变,以支持随机访问。
	.llseek = ashmem_llseek,
	// 将设备或文件的某段内存映射到用户空间(此处为`ashmem_mmap`),以便提供更高效的数据访问方式。
	.mmap = ashmem_mmap,
	// 提供一个设备特定的I/O操作接口(此处为`ashmem_ioctl`)。
	.unlocked_ioctl = ashmem_ioctl,
#ifdef CONFIG_COMPAT
	// 如果条件编译配置`CONFIG_COMPAT`开启):为32位应用程序提供支持,允许它们在64位系统上执行ioctl调用。
	.compat_ioctl = compat_ashmem_ioctl,
#endif
#ifdef CONFIG_PROC_FS
	// 如果条件编译配置`CONFIG_PROC_FS`开启):该函数(此处为`ashmem_show_fdinfo`)允许显示关于文件描述符的信息,增强proc文件系统中的`/proc`
	// /proc/<pid>/fdinfo/<fd>
	.show_fdinfo = ashmem_show_fdinfo,
#endif
};

// `miscdevice` 结构体 `ashmem_misc` 用于注册 ashmem 作为一个杂项(miscellaneous)设备。在 Linux 内核中,杂项设备是一种简化的设备注册方式,相比完整的字符设备驱动来说,其注册过程更简单,被广泛用于不需要自己专属设备号的小型设备。
static struct miscdevice ashmem_misc = {
	// 指定设备的次设备号。设置为 `MISC_DYNAMIC_MINOR` 表示请求动态分配一个次设备号,而不需要手动指定。
	.minor = MISC_DYNAMIC_MINOR,
	// 设备的名称,这将出现在 `/dev/` 目录下。对于ashmem,设备文件通常会被创建为 `/dev/ashmem`。
	.name = "ashmem",
	// 指向前面定义的 `file_operations` 结构体的指针。这个结构体包含了设备支持的文件操作函数,如打开、释放、读取、写入、映射等。
	.fops = &ashmem_fops,
};

这段代码定义了一个函数 ashmem_init_shrinker,它是一个初始化函数,专门用来设置和注册一个内存回收器(shrinker),这在内存紧张时允许内核回收分配给ashmem的内存页。在Android系统中,这有助于管理匿名共享内存(ashmem)使用的内存,特别是在内存不足的情况下。

这段注册代码的主要工作:

  1. 分配回收器:通过调用shrinker_alloc函数,分配一个新的收缩器对象ashmem_shrinker。如果分配失败,函数返回 -ENOMEM,表示没有足够的内存完成操作。
  2. 设置回收器回调函数
    • count_objects:指向ashmem_shrink_count函数的指针,该函数被调用来估算收缩器可以释放的对象数量。
    • scan_objects:指向ashmem_shrink_scan函数的指针,该函数被调用来实际扫描并尝试释放对象。
  3. 设置收缩器参数seeks字段调整了收缩器的回收“优先级”,这个字段估计了收缩器每释放一个对象所花的IO成本。这个值用于帮助内核决定这个收缩器是否是回收内存的一个好的选择。将其设置为 DEFAULT_SEEKS * 4 意味着让这个收缩器在评估中变得更加重要,从而更频繁地被调用。
  4. 注册收缩器:通过调用shrinker_register函数,把ashmem_shrinker注册到内核中,这样就可以在系统运行中动态地管理ashmem所使用的内存了。

收缩器是内核内存管理的一个组件,它提供了一种机制,允许内核在内存压力下通过回调函数释放缓存的对象,从而减少内存使用,并避免系统OOM(Out of Memory)的情况。通过实现这个,ashmem系统能够在内存紧张时释放不再需要的内存区域,从而为系统节省内存资源。

c

static struct shrinker *ashmem_shrinker;

static int __init ashmem_init_shrinker(void)
{
	ashmem_shrinker = shrinker_alloc(0, "android-ashmem");
	if (!ashmem_shrinker)
		return -ENOMEM;

	ashmem_shrinker->count_objects = ashmem_shrink_count;
	ashmem_shrinker->scan_objects = ashmem_shrink_scan;
	/*
	 * XXX (dchinner): I wish people would comment on why they need on
	 * significant changes to the default value here
	 */
	ashmem_shrinker->seeks = DEFAULT_SEEKS * 4;

	shrinker_register(ashmem_shrinker);

	return 0;
}

内存回收器(Shrinker)在Linux内核中扮演着回收缓存内存的角色,特别是在内存紧张(内存压力)的情况下。Ashmem的内存收缩器通过以下机制发挥作用:

  1. 估算可释放的对象数

    • 当内核决定需要释放内存时,它会调用注册的收缩器的count_objects函数。对于ashmem,这一函数是ashmem_shrink_count。这个函数负责估计当前可以尝试释放的对象(在ashmem的上下文中,指的是可以从匿名共享内存中回收的内存页)的数量。这个估计值会被内核用来决定是否继续进行内存回收,以及哪些收缩器会被调用来释放内存。
  2. 扫描并尝试释放对象

    • 如果内核决定由该收缩器释放一些内存,它会调用收缩器的scan_objects函数。对ashmem来说,这一函数是ashmem_shrink_scanscan_objects函数负责实际扫描并尝试释放之前估算出的数量的对象。在ashmem的上下文中,这意味着标记为可回收的ashmem区域(未被“固定”的区域)会被收缩器回收。这一操作通常涉及到对这些内存页执行特定操作(如清零),使得它们可以被系统回收并用于其他用途。
  3. 释放内存的顺序

    • Ashmem通过维护一个LRU(最近最少使用)列表来追踪哪些内存页可以被回收。当收缩器被触发时,它会首先释放这个列表中的内存页。这确保了只有在没有活跃使用长时间未访问的内存页时,它们才会被回收。
  4. 内存压力反应

    • Ashmem收缩器的效率受到其在ashmem_init_shrinker中设置的seeks值的影响。这个值表示内核为了释放一定量的内存,愿意寻找(seek)多少次。一个较高的seeks值意味着该收缩器更有可能在内存紧张时被调用。通过调整这个值,ashmem收缩器可以根据其在系统中的优先级和效率更智能地参与内存回收。

ashmem收缩器通过估算可释放的对象数、扫描并释放内存对象,在内存变得紧张时帮助释放ashmem分配的内存,从而维护系统的健康运行状态。这是一个自动化的机制,无需应用程序的直接干预。

该函数的主要作用是为内核提供当前可从ashmem中回收的内存页数(而不是对象数),帮助内核决策在内存压力情况下是否需要回收ashmem分配的内存页,以及可从ashmem中回收多少页内存。

c

// *shrink : 指向当前收缩器实例的指针
// *sc: 该参数包含了关于当前收缩请求的上下文信息,包括触发收缩操作的原因(比如内存压力)
static unsigned long
ashmem_shrink_count(struct shrinker *shrink, struct shrink_control *sc)
{
	/*
	 * note that lru_count is count of pages on the lru, not a count of
	 * objects on the list. This means the scan function needs to return the
	 * number of pages freed, not the number of objects scanned.
	 */
	 // 一个全局变量,它代表了当前在ashmem的最近最少使用(LRU)列表中未被固定并且可被收缩(回收/清除)的内存页数。此函数简单地返回该计数,这样内核就知道有多少页可能因为ashmem的操作而被释放。
	return lru_count;
}

在系统内存紧张时,这个函数负责释放或清理ashmem分配的内存区域中那些标记为未固定(unpinned)状态的页面。这有助于在系统需要时回收内存资源,提升整体内存使用效率。

这段代码的功能是遍历最近最少使用(LRU)列表,查找可以被释放的内存范围(ashmem_range)。对于列表中的每一项,该函数计算开始和结束位置,并利用fallocate系统调用(配合FALLOC_FL_PUNCH_HOLE标志)来释放这些页面。

此处有通过fallocate的FALLOC_FL_PUNCH_HOLE标记进行内存释放的操作,针对嵌入式设备的优化,更早出发内存页面释放。

c

/*
 * ashmem_shrink - our cache shrinker, called from mm/vmscan.c
 *
 * 'nr_to_scan' is the number of objects to scan for freeing.
 *
 * 'gfp_mask' is the mask of the allocation that got us into this mess.
 *
 * Return value is the number of objects freed or -1 if we cannot
 * proceed without risk of deadlock (due to gfp_mask).
 *
 * We approximate LRU via least-recently-unpinned, jettisoning unpinned partial
 * chunks of ashmem regions LRU-wise one-at-a-time until we hit 'nr_to_scan'
 * pages freed.
 */
static unsigned long
ashmem_shrink_scan(struct shrinker *shrink, struct shrink_control *sc)
{
	unsigned long freed = 0;

	/* We might recurse into filesystem code, so bail out if necessary */
	if (!(sc->gfp_mask & __GFP_FS))
		return SHRINK_STOP;

	if (!mutex_trylock(&ashmem_mutex))
		return -1;

	while (!list_empty(&ashmem_lru_list)) {
		struct ashmem_range *range =
			list_first_entry(&ashmem_lru_list, typeof(*range), lru);
		loff_t start = range->pgstart * PAGE_SIZE;
		loff_t end = (range->pgend + 1) * PAGE_SIZE;
		struct file *f = range->asma->file;

		get_file(f);
		atomic_inc(&ashmem_shrink_inflight);
		range->purged = ASHMEM_WAS_PURGED;
		lru_del(range);

		freed += range_size(range);
		mutex_unlock(&ashmem_mutex);
		f->f_op->fallocate(f,
				   FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE,
				   start, end - start);
		fput(f);
		if (atomic_dec_and_test(&ashmem_shrink_inflight))
			wake_up_all(&ashmem_shrink_wait);
		if (!mutex_trylock(&ashmem_mutex))
			goto out;
		if (--sc->nr_to_scan <= 0)
			break;
	}
	mutex_unlock(&ashmem_mutex);
out:
	return freed;
}

原文链接:https://ovea-y.cn/android_sharedmemory/