Android 进程管理概述

一、task_struct

Android的进程管理建立在Linux内核的基础上。Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息。它定义在include/linux/sched.h文件中或者Android的kernel/msm-4.9/include/linux/sched.h中。包含一下的信息:


image

其中,进程的状态有以下取值:


image

二、进程创建流程

idle进程 -> init进程 -> zygote进程 -> system_server进程 →App进程
他们之间的关系如下:


image

64位下有两个zygote,zygote64和zygote。64位应用的父进程是zygote64,它的pgid也是zygote64的pid;32位应用的父进程是zygote,它的pgid却是zygote64的pid,如:com.ss.android.article.news的父进程是zygote(1112),但它的pgid是zygote64(1111),这是怎么回事呢?原来不管32位或64位的zygote,它在创建完子进程后,会调用setChildPgid()来改变子进程的pgid。


image

多个进程组还可以构成一个会话 (session),sid标识会话id,Android中进程的sid基本都是0。

一张更为详细的图:


image

ZygoteServer启动过程:


image

Zygote本身是一个Native的应用程序,刚开始的名字为“app_process”,运行过程中,通过调用setArgv0将名字改为Zygote。
ZygoteInit进程启动后,会注册一个Socket,在runSelectLoop方法中开启一个while死循环等待ActivityManagerService创建新进程的请求,其次,ZygoteInit启动了SystemServer进程,执行SystemServer的main方法。
Socket通信框架:


image

LocalSocket就是作为客户端建立于服务端的连接,发送数据。LocalServerSocket作为服务端使用,建立服务端的socket监听客户端请求,典型的C/S架构

三、如何创建一个进程

#inlucde<unistd.h>
#inlucde<stdio.h>
#inlucde<wait.h>
int main(){
  int count = 0;
  pid_t fpid = fork();
  if( fpid < 0){
    printf("创建子进程失败");
  } else if( fpid == 0){
    printf("子进程Id:%d\n",getpid());
  } else {
    printf("父进程Id:%d\n",getpid());
  }
  printf("count=%d\n",count);
  waitpid(fpid,NULL,0); // 暂时停止目前进程的执行,直到有信号来到或子进程结束
  return 0;
}
  1. fork函数执行一次,返回两次,第一次返回父进程的id,第二次返回子进程的id。
  2. count是全局变量,子进程和父进程同时操作,但是互相不受影响

fork()使用写时复制,copy-on-write,是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。如下图所示,同左到右大的方向箭头表示复制内容:


image

为这四个部分分配物理块,P2的:正文段-->PI的正文段的物理块,指的是不为P2分配正文段块,让P2的正文段指向P1的正文段块,数据段-->P2自己的数据段块(为其分配对应的块),堆-->P2自己的堆块,栈-->P2自己的栈块。

一个进程有正文段、数据段、堆和栈等段,内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟空间结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
对应到Android系统中:


image

4、Zygote进程预加载资源

android系统资源加载分两种方式,预加载和使用进程中加载。 预加载是指在zygote进程启动的时候就加载,这样系统只在zygote执行一次加载操作,所有APP用到该资源不需要再重新加载,减少资源加载时间,加快了应用启动速度,一般情况下,系统中App共享的资源会被列为预加载资源。

预加载的内容:


image

上面文件中列举的四千多个类都要通过Class.forName加载到系统中,生成字节码
preload的过程主要发生在ZygoteInit.preload()函数里面:

static void preload(TimingsTraceLog bootTimingsTraceLog) {
        Log.d(TAG, "begin preload");
        beginIcuCachePinning();
        preloadClasses();
        preloadResources();
        nativePreloadAppProcessHALs();
        preloadOpenGL();
        preloadSharedLibraries();
        preloadTextResources();
        WebViewFactory.prepareWebViewInZygote();
        endIcuCachePinning();
        warmUpJcaProviders();
        Log.d(TAG, "end preload");

        sPreloadComplete = true;
    }

会输出以下log:

Zygote  : begin preload
Zygote  : Preloading ICU data...
Zygote  : Preloading classes...
Zygote  : ...preloaded 6558 classes in 441ms.
Zygote64Timing: PreloadClasses took to complete: 545ms
Zygote  : Preloading resources...
Zygote  : ...preloaded 86 resources in 45ms.
Zygote  : ...preloaded 41 resources in 2ms.
Zygote  : Preloading shared libraries...
Zygote  : end preload

可见preload加载的有类,资源,共享库。系统中有大量的资源可以直接被App所使用,比如一个颜色,一个drawble,这些都是通过preloadResources加载的。

4、进程优先级

进程管理主要涉及到两个值:
adj:通过调整oom_score_adj来影响进程寿命(Lowmemorykiller杀进程策略);
schedGroup:影响进程的CPU资源调度与分配;
adj,即进程的优先级粗略划分如下:

image

前台进程:正在与用户进行交互的进程
可见进程:可在屏幕上显示但不在前台运行,比如一个前台进程以对话框的形式显示在该进程前面。典型的如输入法。
服务进程:正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。
后台进程:包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。
空进程:不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。

image

进程的优先级取决于进程四大组件的运行状态。例如Activity是否在前台,用户是否可见;Service正在被哪些客户端使用;ContentProvider正在被哪些客户端使用;BroadcastReceiver是否正在接受广播
ProcessRecord中有以下成员变量:

// all activities running in the process
final ArrayList<ActivityRecord> activities = new ArrayList<>();
// any tasks this process had run root activities in
final ArrayList<TaskRecord> recentTasks = new ArrayList<>();
// all ServiceRecord running in this process
final ArraySet<ServiceRecord> services = new ArraySet<>();
// services that are currently executing code (need to remain foreground).
final ArraySet<ServiceRecord> executingServices = new ArraySet<>();
// All ConnectionRecord this process holds
final ArraySet<ConnectionRecord> connections = new ArraySet<>();
// all IIntentReceivers that are registered from this process.
final ArraySet<ReceiverList> receivers = new ArraySet<>();
// class (String) -> ContentProviderRecord
final ArrayMap<String, ContentProviderRecord> pubProviders = new ArrayMap<>();
// All ContentProviderRecord process is using
final ArrayList<ContentProviderConnection> conProviders = new ArrayList<>();

activities 记录了进程中运行的Activity
services,executingServices 记录了进程中运行的Service
receivers 记录了进程中运行的BroadcastReceiver
pubProviders 记录了进程中运行的ContentProvider
而:
connections 记录了对于Service连接
conProviders 记录了对于ContentProvider的连接

连接的意义在于:连接的客户端的进程优先级会影响被使用的Service和ContentProvider所在进程的优先级。 例如:当一个后台的Service正在被一个前台的Activity使用,那么这个后台的Service就需要设置一个较高的优先级以便不会被回收。(否则后台Service进程一旦被回收,便会对前台的Activity造成影响。)

ADJ的具体数值:

级别 常量名称 简述
-1000 NATIVE_ADJ 并不被系统管理的native进程的优先级
-900 SYSTEM_ADJ 系统进程
-800 PERSISTENT_PROC_ADJ 系统级常驻进程,比如telephony
-700 PERSISTENT_SERVICE_ADJ 关联着系统进程或者常驻进程的进程
0 FOREGROUND_APP_ADJ 前台APP
100 VISIBLE_APP_ADJ 可见APP
200 PERCEPTIBLE_APP_ADJ 可感知的APP
300 BACKUP_APP_ADJ 正在备份的进程
400 HEAVY_WEIGHT_APP_ADJ 后台重量级进程,在system/rootdir.init.rc中配置
500 SERVICE_ADJ 正在运行这service的进程
600 HOME_APP_ADJ 桌面进程
700 PREVIOUS_APP_ADJ 上一个APP的进程(通过back返回)
800 SERVICE_B_ADJ List B中的service进程(区分于上一个 service list,更老、使用到的可能性更小)
900 CACHED_APP_MIN_ADJ 缓存进程的最小值
906 CACHED_APP_MAX_ADJ 缓存进程的最大值

以上各值数值越小,优先级越高

BACKUP_APP_ADJ(300):执行bindBackupAgent()过程的进程
HEAVY_WEIGHT_APP_ADJ(400): realStartActivityLocked()过程,当应用的privateFlags标识PRIVATE_FLAG_CANT_SAVE_STATE的进程;

进程由SERVICE_ADJ(500)降低到SERVICE_B_ADJ(800),有以下两种情况:
A类Service占比过高:当A类Service个数 > Service总数的1/3时,则加入到B类Service。换句话说,B Service的个数至少是A Service的2倍。

内存紧张&&A类Service占用内存较高:当系统内存紧张级别(mLastMemoryLevel)高于ADJ_MEM_FACTOR_NORMAL,且该应用所占内存lastPss大于或等于CACHED_APP_MAX_ADJ级别所对应的内存阈值的1/3(默认值阈值约等于110MB)。

Android P开始,进一步细化VISIBLE_APP_ADJ级别:


image

从Android P开始,进一步细化ADJ级别,增加了VISIBLE_APP_LAYER_ MAX(99),是指VISIBLE_APP_ADJ(100)跟PERCEPTIBLE_APP_ADJ(200)之间有99个槽,则可见级别ADJ的取值范围为[100,199]。 算法会根据其所在task的mLayerRank来调整其ADJ,100加上mLayerRank就等于目标ADJ,layer越大,则ADJ越小。

如何查看进程优先级:

  1. cat proc/[pid]/oom_score_adj
  2. adb shell dumpsys activity o/p

5、进程的回收

image

LMK基本原理

所有应用进程都是从zygote孵化出来的,记录在AMS中mLruProcesses列表中,由AMS进行统一管理,AMS中会根据进程的状态更新进程对应的oom_adj值,这个值会通过文件传递到kernel中去,kernel有个低内存回收机制,在内存达到一定阀值时会触发清理oom_adj值高的进程腾出更多的内存空间,这就是LowMemoryKiller工作原理。

LMK基本实现方案

根据不同手机的配置,就有对应的杀进程标准,这个标准用minfree和adj两个文件来定义:
/sys/module/lowmemorykiller/parameters/minfree:里面是以","分割的一组数,每个数字代表一个内存级别。
/sys/module/lowmemorykiller/parameters/adj:对应上面的一组数,每个数组代表一个进程优先级级别
例如:

$ adb shell cat /sys/module/lowmemorykiller/parameters/minfree
18432,23040,27648,32256,85296,120640
$ adb shell cat /sys/module/lowmemorykiller/parameters/adj
0,100,200,300,900,906

Minfree是一个阈值,它跟内存大小和分辨率有关系,通过ProcessList中updateOomLevels计算设置的,这里的adj并不是直接对应ProcessList中定义的。
而是adj*17/1000=ProcessList文件里面的值

对于应用进程来说,也需要有自身的adj,由AMS负责更新。定义在oom_adj和oom_score_adj文件中:
/proc/pid/oom_adj:代表当前进程的优先级,这个优先级是kernel中的优先级。
/proc/pid/oom_score_adj:这个是AMS上层的优先级,与ProcessList中的优先级对应,例如:


image

LMK回收过程

image

用户在启动一个进程之后,通常伴随着启动一个Activity游览页面或者一个Service播放音乐等等,这个时候此进程的adj被AMS提高,LMK就不会杀死这个进程,当这个进程要做的事情做完了,退出后台了,此进程的adj很快又被AMS降低。
当需要杀死一个进程释放内存时,一般先根据当前手机剩余内存的状态,在minfree节点中找到当前等级,再根据这个等级去adj节点中找到这个等级应该杀掉的进程的优先级, 之后遍历所有进程并比较进程优先级adj与优先级阈值,并杀死优先级低于阈值的进程,达到释放内存的目的


image

AMS负责更新各应用的进程优先级与阈值数组
lmkd负责接收AMS传输过来的数据然后写入到sys与proc节点
lowmemorykiller则在内存低于阈值时才会被触发并负责杀死低优先级的进程。

AMS.updateConfiguration:更新窗口配置,这个过程中,分别向/sys/module/lowmemorykiller/parameters目录下的minfree和adj节点写入相应数值;
AMS.applyOomAdjLocked:应用adj,当需要杀掉目标进程则返回false;否则返回true,这个过程中,调用setOomAdj(),向/proc/pid/oom_score_adj写入oom_adj 后直接返回;
AMS.cleanUpApplicationRecordLocked & AMS.handleAppDiedLocked:进程死亡后,调用remove(),直接返回;

时序图:


image

AMS:


image

Lmkd.c:


image
image

LMK杀进程过程:


image

入口是__init函数, register_shrinker(&lowmem_shrinker);
关键点1:其实就是确定当前低内存对应的阈值;
关键点2 :找到比该阈值优先级低,并且内存占用较多的进程(tasksize = get_mm_rss(p->mm)其实就是获取内存占用)。如何杀死的呢?很直接,通过Linux的中的信号量,发送SIGKILL信号直接将进程杀死。
到这就分析完了LomemoryKiller内核部分如何工作的。其实很简单,一句话:被动扫描,找到低优先级的进程,杀死。

onTrimMemory和onLowMemory

Activity, Service, ContentProvider和Application都实现了这个接口

应用处于Runnig状态可能收到的级别
TRIM_MEMORY_RUNNING_MODERATE 表示系统内存已经稍低
TRIM_MEMORY_RUNNING_LOW 表示系统内存已经相当低
TRIM_MEMORY_RUNNING_CRITICAL 表示系统内存已经非常低,你的应用程序应当考虑释放部分资源

应用的可见性发生变化时收到的级别
TRIM_MEMORY_UI_HIDDEN 表示应用已经处于不可见状态,可以考虑释放一些与显示相关的资源

应用处于后台时可能收到的级别
TRIM_MEMORY_BACKGROUND 表示系统内存稍低,你的应用被杀的可能性不大。但可以考虑适当释放资源
TRIM_MEMORY_MODERATE 表示系统内存已经较低,当内存持续减少,你的应用可能会被杀死
TRIM_MEMORY_COMPLETE 表示系统内存已经非常低,你的应用即将被杀死,请释放所有可能释放的资源

这里的通知也是来自ActivityManagerService,在updateOomAdjLocked的时候,ActivityManagerService会根据系统内存以及应用的状态通过app.thread.scheduleTrimMemory发送通知给应用程序。

如果你不释放资源,那么请看上面的算法:找到比该阈值优先级低,并且内存占用较多的进程,你的应用将最先被干掉

想学习更多Android知识,或者获取相关资料请加入Android技术开发交流2群:935654177。本群可免费获取Gradle,RxJava,小程序,Hybrid,移动架构,NDK,React Native,性能优化等技术教程!

推荐阅读更多精彩内容