深入理解 Android 内核设计思想(二)内存管理

157 阅读11分钟

//映射成功返回0,否则返回错误码

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

  • addr:指文件/设备应该映射到进程空间的哪个起始地址

  • **len:**指被映射到进程空间的内存块大小

  • **prot:**指定被映射内存的访问权限,包括 PROT_READ(可读)、PROT_WRITE(可写) 等

  • **flags:**指定程序对内存块所做改变造成的影响,包括 MAP_SHARED(保存到文件) 等

  • **fd:**被映射到进程空间的文件描述符

  • **offset:**指定从文件的哪一部分开始映射

mmap 可用于跨进程通信,Linux Kernel 和 Android 中就频繁的用到了这个函数,比如 Android 的 Binder 驱动,下面分析 MemoryFile 原理时还会提到这个函数。

Copy on Write

Copy on Write(写时拷贝) 是指如果有多个调用者要请求同一资源,他们会获取到相同的指向这一资源的指针,直到某个调用者需修改资源时,系统才会复制一份副本给该调用者,而其他调用者仍使用最初的资源。

如果调用者不需要修改资源,就不会建立副本,多个调用者共享读取同一份资源。

Linux 的 fork() 函数就是 Copy on Write 的,实际开销很小,主要是给子进程创建进程描述符等,并且推迟甚至免除了数据拷贝操作。比如 fork() 后子进程需立即调用 exec() 装载新程序到进程的内存空间,即不需要父进程的任何数据,这种情况 Copy on Write 技术就避免了不必要的数据拷贝,从而提升了运行速度。

Android 内存管理


Low Memory Killer

Linux Kernel 有自己的内存监控机制,即 OOMKiller。当系统的可用内存达到临界值时,OOMKiller 就会按照优先级从低到高杀掉进程。优先级该如何衡量呢?OOMKiller 会综合进程当前消耗内存、进程占用 CPU 时间、进程类型等因素,对进程实时评分。分值存储在 /proc/{PID}/oom_score 中,可通过 cat 命令查看。分值越低的进程,优先级越高,被杀死的概率越小。

基于 Linux 内核 OOMKiller 的核心思想,Android 系统拓展出了自己的内存监控体系,相比 Linux 达到临界值才触发,Android 实现了不同梯级的 Killer。Android 系统为此开发了专门的驱动,名为 Low Memory Killer,源码在内核的 /drivers/staging/android/Lowmemorykiller.c 中。

Lowmemorykiller.c 中有如下定义:

static int lowmem_adj[6] = {0, 1, 6, 12};

static int lowmem_adj_size = 4; //页大小

static size_t lowmem_minfree[6] = { //元素使用时以 lowmem_adj_size 为单位

3 * 512, //6MB

2 * 1024, //8MB

4 * 1024, //16MB

16 * 1024,//64MB

};

lowmem_minfree 定义了可用内存容量对应的不同梯级。lowmem_adj 与 lowmem_minfree 中的梯级一一对应,表示处于某梯级时需要被处理的 adj 值。adj 值用来描述进程的优先级,取值范围为 -17~15,数字越小表示进程优先级越高,被杀死的概率越小。

比如当可用内存低于 64MB 时,即 lowmem_minfree 第 4 梯级,对应于 lowmem_adj 的 12,那就会清理掉优先级低于 12(即 adj>12)的进程。

上面这两个数组中梯级的定义只是系统的预定义值,Android 系统还提供了相应的文件供我们修改这两组值,路径为:

/sys/module/lowmemorykiller/parameters/adj

/sys/module/lowmemorykiller/parameters/minfree

可以在 init.rc(系统启动时由 init 进程解析的一个脚本) 中,这样修改:

write /sys/module/lowmemorykiller/parameters/adj 0, 8

write /sys/module/lowmemorykiller/parameters/minfree 1024, 4096

另外 ActivityManagerService 中有一个 updateOomLevels 方法也是通过修改这两个文件来实现的,AMS 在运行时会根据当前的系统配置自动调整 adj 和 minfree,以尽可能适配不同的硬件设备。

了解了 Low Memory Killer 的梯级规则后,来看下 Android 进程的 adj 值含义:

除了表格中系统的评定标准,有没有办法改变某一进程的 adj 值呢?和修改上面的 adj、minfree 梯级类似,进程的 adj 值也可以通过写文件的方式来修改,路径为 /proc/{PID}/oom_adj,比如 init.rc 中:

write /proc/1/oom_adj -16

另外还可以在 AndroidManifest.xml 中给 application 添加 “android:persistent=true” 属性。

Ashmem 驱动

Anonymous Shared Memory 匿名共享内存是 Android 特有的内存共享机制,它可以将指定的物理内存分别映射到各个进程自己的虚拟地址空间中,从而便捷的实现进程间内存共享,Ashmem 的实现依赖 Ashmem 设备节点。

怎么理解设备节点呢?Linux 抽象了对硬件的处理,所有的硬件设备都可以当作普通文件一样来看待,设备节点文件是设备驱动的逻辑文件,其中对设备的描述包括文件操作函数集合,应用程序可以通过这些函数来访问硬件设备。

除了磁盘等真正的硬件设备,还可以通过内存抽象,使用设备节点文件的方式来描述一个”设备”并使用它,Ashmem、Binder 驱动都是属于这种内存抽象的”设备”。

介绍 Ashmem 设备节点前,先了解下 ueventd 进程。ueventd 就是 Android 中负责创建和管理设备节点的进程,创建设备节点文件有两种方式:

1.静态节点文件:以预先定义的设备信息为基础,当 ueventd 进程启动后,统一创建设备节点文件

2.动态节点文件:即在系统运行中,当有设备插入 USB 端口时,ueventd 进程就会接收到这一事件,为插入的设备动态创建设备节点文件

Ashmem 设备节点就属于静态节点文件,创建过程如下:

1.Android 系统启动,解析 init.rc,启动 ueventd 进程

2.ueventd 进程会去解析 ueventd.rc,读取 ashmem 设备节点信息到系统中

其中 ueventd.rc 文件格式如下:

/dev/null 0666 root root

/dev/zero 0666 root root

/dev/random 0666 root root

/dev/ashmem 0666 root root

/dev/binder 0666 root root

可以看到包括 binder、ashmem 在内的一系列设备节点信息都会在这里读取到系统中。

随后 ashmem 会调用 ashmem.c 文件的 ashmem_init 进行初始化:

static int _init ashmem_init(void){

int ret;

ashmem_area_cachep = kmem_cache_create("ashmem_area_cache",sizeof(struct ashmem_area),0,0,NULL);

ashmem_range_cachep = kmem_cache_create("ashmem_range_cache",sizeof(struct ashmem_range),0,0,NULL);

ret = misc_register(&ashmem_misc);

...

return 0;

}

通过 kmem_cache_create() 函数创建了两个 cache,后面申请内存时需要用到。对于 kmem_cache_create() 函数,书中提及 Slab、Slub、Slob 三种机制,这里不再延伸,仅理解:kmem_cache_create() 并没有真正的分配内存,后续还要调用 kmem_cache_alloc() 。

由于 ashmem 属于 misc 杂项设备,所以调用 misc_register(&ashmem_misc) 进行设备注册。ashmem_misc 就是 Ashmem 的设备描述,定义如下:

static struct miscdevice ashmem_misc = {

.minor = MISC_DYNAMIC_MINOR, //自动分配次设备号

.name = "ashmem", //设备节点的名称

.fops = &ashmem_fops, //文件操作集合

};

.fops 就是上面提到的”文件操作函数集合”,即 Ashmem 设备的操作函数集,如下

static struct file_operations ashmem_fops = {

.owner = THIS_MODULE,

.open = ashmem_open,

.release = ashmem_release,

.read = ashmem_read,

.llseek = ashmem_llseek,

.mmap = ashmem_mmap,

.unlocked_ioctl = ashmem_ioctl,

.compat_ioctl = ashmem_ioctl,

};

其中 ashmem_open、ashmem_mmap 及 ashmem_ioctl 函数比较重要,依次来看:

1.ashmem_open

static int ashmem_open(struct inode *inode, struct file *file){

struct ashmem_area *asma;

...

asma = kmem_cache_zalloc(ashmem_area_cachep, GFP_KERNEL);

...

file->private_data = asma;

...

return 0; //申请成功

}

ashmem_open 主要做了两个工作:

1.调用 kmem_cache_zalloc 方法从 ashmem_area_cachep 分配了一块内存,这个方法和 cache 上面都提到过

2.将 ashmem_area 记录在 file 中 。

2.ashmem_mmap

static int ashmem_mmap(struct file *file, struct vm_area_struct *vma){

struct ashmem_area *asma = file->private_data;

...

mutex_lock(&ashmem_mutex);

...

if(!asma->file){

shmem_file_setup(name, asma->size, vma->vm_flags);

}

...

shmem_set_file(vma, asma->file);

...

mutex_unlock(&ashmem_mutex);

}

首先拿到在 ashmem_open 函数中创建的 ashmem_area,然后判断如果 asma->file 为空,说明这是第一个访问该共享内存的进程,调用 shmem_file_setup() 函数在 tmpfs 中创建一个临时文件,用于进程间的内存共享;如果 asma->file 不为空,直接调用 shmem_set_file 进行内存映射。

3.ashmem_ioctl

static long ashmem_ioctl(struct file *file, unsigned int cmd, unsigned long arg){

struct ashmem_area * asma = file->private_data;

switch(cmd){

case ASHMEM_SET_NAME://设置名称

set_name(asma, (void __user *) arg);

break;

case ASHMEM_GET_NAME://获取名称

get_name(asma, (void __user *) arg);

break;

case ASHMEM_SET_NAME://设置大小

if(!asma->file){

asma->size = (size_t) arg;

}

break;

...

}

}

ashmem_ioctl 即根据 ioctl 命令做相应的操作,设置或获取 size、名称等。

MemoryFile 原理

书中通过 MemoryDealer 讲解了 Ashmem 示例,触类旁通,我来分析一下 Ashmem 的另一个应用示例:MemoryFile。MemoryFile 是 Java 层对 Ashmem 的一个封装,使用方法大致如下:

进程 A 申请一块共享内存写入数据,准备好文件描述符:

MemoryFile memoryFile = new MemoryFile(name, size);

memoryFile.getOutputStream().write(data);

Method method = MemoryFile.class.getDeclaredMethod("getFileDescriptor");

FileDescriptor des = (FileDescriptor) method.invoke(memoryFile);

ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(des);

进程 B 中通过 binder 拿到 A 进程中准备好的文件描述符,然后直接读取数据:

FileDescriptor descriptor = pfd.getFileDescriptor();

FileInputStream fileInputStream = new FileInputStream(descriptor);

fileInputStream.read(data);

使用起来和文件读写一样很简单,如果不了解 Ashmem 机制,也就只能停留在仅会使用的浅显层面了。

现在有了 Ashmem 驱动知识的铺垫,来看 MemoryFile 是怎么从 Java API 调用到 Ashmem 驱动函数的,先来看 MemoryFile 的构造函数:

public MemoryFile(String name, int length) throws IOException {

try {

mSharedMemory = SharedMemory.create(name, length);

mMapping = mSharedMemory.mapReadWrite();

} catch (ErrnoException ex) {

ex.rethrowAsIOException();

}

}

可以看到构造 MemoryFile 时通过 SharedMemory create 方法申请了一块匿名共享内存,SharedMemory create 方法中调用了 nCreate native 方法:

private static native FileDescriptor nCreate(String name, int size) throws ErrnoException;

对应的 native 实现在 android_os_SharedMemory.cpp 中,源码见

androidxref.com/9.0.0_r3/xr… ,具体 native 实现如下:

static jobject SharedMemory_create(JNIEnv* env, jobject, jstring jname, jint size) {

const char* name = jname ? env->GetStringUTFChars(jname, nullptr) : nullptr;

int fd = ashmem_create_region(name, size); //创建匿名共享内存

...

return jniCreateFileDescriptor(env, fd);

}

ashmem_create_region 方法的对应实现在 ashmem-dev.cpp 中,源码见

androidxref.com/9.0.0_r3/xr… 其中 ashmem_create_region 的后续调用链如下:

#define ASHMEM_DEVICE "/dev/ashmem" //Ashmem 设备驱动

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)); //设置 Ashmem 名字

if (ret < 0) {

goto error;

}

}

}

static int __ashmem_open(){

int fd;

pthread_mutex_lock(&__ashmem_lock);

fd = __ashmem_open_locked(); //创建匿名共享内存

pthread_mutex_unlock(&__ashmem_lock);

return fd;

}

最后

简历首选内推方式,速度快,效率高啊!然后可以在拉钩,boss,脉脉,大街上看看。简历上写道熟悉什么技术就一定要去熟悉它,不然被问到不会很尴尬!做过什么项目,即使项目体量不大,但也一定要熟悉实现原理!不是你负责的部分,也可以看看同事是怎么实现的,换你来做你会怎么做?做过什么,会什么是广度问题,取决于项目内容。但做过什么,达到怎样一个境界,这是深度问题,和个人学习能力和解决问题的态度有关了。大公司看深度,小公司看广度。大公司面试你会的,小公司面试他们用到的你会不会,也就是岗位匹配度。

面试过程一定要有礼貌!即使你觉得面试官不尊重你,经常打断你的讲解,或者你觉得他不如你,问的问题缺乏专业水平,你也一定要尊重他,谁叫现在是他选择你,等你拿到offer后就是你选择他了。

另外,描述问题一定要慢!不要一下子讲一大堆,慢显得你沉稳、自信,而且你还有时间反应思路接下来怎么讲更好。现在开发过多依赖ide,所以会有个弊端,当我们在面试讲解很容易不知道某个方法怎么读,这是一个硬伤......所以一定要对常见的关键性的类名、方法名、关键字读准,有些面试官不耐烦会说“你到底说的是哪个?”这时我们会容易乱了阵脚。正确的发音+沉稳的描述+好听的嗓音决对是一个加分项!

最重要的是心态!心态!心态!重要事情说三遍!面试时间很短,在短时间内对方要摸清你的底子还是比较不现实的,所以,有时也是看眼缘,这还是个看脸的时代。

希望大家都能找到合适自己满意的工作! 如果需要PDF版本可以在GitHub中自行领取!

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)