在阅读本篇之前,你首先需要大概清楚一点,无论是系统杀(android机型上长按home键中清除),或者是他杀(第三方管理软件,如360,腾讯等)。其实现方法,基本都是借助ActivityManagerService的removeLruProcessLocked,改变主要也是在这里
一、 先看代码有啥不同
5.0以下我们先来看看Android4.4的源码,ActivityManagerService(源码/frameworks/base/services/core/Java/com/Android/server/am/ActivityManagerService.java)是如何关闭在应用退出后清理内存的:
final void removeLruProcessLocked(ProcessRecord app) {
/.......
if (lrui >= 0) {
if (!app.killed) {
Slog.wtfStack(TAG, "Removing process that hasn't been killed: " + app);
Process.killProcessQuiet(app.pid);
}
/.......
}
}
killProcessQuiet我们暂时不向下层深究,从字面看就可以,就是kill了一个pid指向的进程
5.0以上 final void removeLruProcessLocked(ProcessRecord app) {
/.......
if (lrui >= 0) {
if (!app.killed) {
Slog.wtfStack(TAG, "Removing process that hasn't been killed: " + app);
Process.killProcessQuiet(app.pid);
Process.killProcessGroup(app.info.uid, app.pid);
}
/.......
}
}
可以看到,明显多了一句killProcessGroup,也就是把和pid同组的进程全部杀死
最大的机制改变,就是这个“同组杀”,我们接下来研究这个“同组杀”,到底是如何实现的
二、 你不信这个改变? 那测试下吧
注意,本篇的分析,都建立在你基本了解了linux的命令终端、用户、会话、进程组、进程这些概念的基础上,如果你这些词都还没听过
2.1 fork一个子进程
pid_t pid = fork();
if(pid < 0){
return;
}else if(pid > 0){
return;
}else{
}
我们在 adb shell中 使用命令 ps| grep test,可以看到进程信息:
USER PID PPID VSIZE RSS WCHAN PC NAME
u0_a64 16992 317 1536924 52588 ffffffff 00000000 S com.example.testndk2
u0_a64 17011 16992 1504092 34508 ffffffff 00000000 S com.example.testndk2
//可以看到有两个进程,17011便是在JNI中通过fork创建出的子进程。它的父进程是16992.
//在5.0+上只要kill 16992,17011也会被kill.而在4.4上,则不会。
//打印出来他们的进程组 getpgrp() 都是317 ps:也就是主进程的父进程的组长
ps命令参数
user对应着linux的用户,其实也能看出来uid,例如u0_a64的id就是10164 pid当前进程id号 PPID 父进程的id号 VSIZE : virtual size,进程虚拟地址空间大小; RSS : 进程正在使用的物理内存的大小; WCHAN :进程如果处于休眠状态的话,在内核中的地址; PC : program counter, NAME: process name,进程的名称
2.2 init领养了,还算是同组么?
if(pid=fork()>0) {
//父进程,让它活着
}
else if(pid< 0){
perror("fail to fork1");
exit(1);//fork失败,退出
}else{//第一个子进程
if(pid=fork()>0)
exit(0);//【2.1】是第一子进程,结束第一子进程
else if(pid< 0)
exit(1);//fork失败,退出
else{//第二个子进程
}
}
//可以看到,第二个子进程的ppid为1,证明它在它父进程死亡后,被init收养了
// 测试发现若kill 18602,18650也会一同被kill
//打印出来他们的进程组 getpgrp() 都是317 ps:也就是主进程的父进程的组长
USER PID PPID VSIZE RSS WCHAN PC NAME
u0_a64 18602 317 1538796 53848 ffffffff 00000000 S com.example.testndk2
u0_a64 18650 1 1504092 34508 ffffffff 00000000 S com.example.testndk2
2.3 守护进程实现了跨组,跨会话,怎么样?
我们知道setsid(),会让当前的子进程 跳到新的会话里,并成为新的组的组长
pid = fork();
if(pid<0){
LOGI(LOG_TAG, "第一次fork()失败");
}
else if (pid == 0) {//第一个子进程
LOGI(LOG_TAG, "第一个子进程: %d",getpid());
if(setsid() == -1){ LOGI(LOG_TAG, "第一个子进程独立到新会话:失败");}//【2】第一子进程成为新的会话组长和进程组长
pid = fork();
if (pid == 0) {//第二个子进程
LOGI(LOG_TAG, "第二个子进程ID:%d,实际用户UID为:%d,有效用户UID为:%d,进程组ID:%d",getpid(),getuid(),geteuid(),getpgrp());
chdir(s_work_dir);//【3】改变工作目录到s_work_dir
umask(0);;//【4】重设文件创建掩模
for(i=0;i< 5;++i)//【5】关闭打开的文件描述符 TODO 数目
close(i);
//将真正用来实现的子进程写到一个二进制文件中(对应文件源码/MarsDaemon/LibMarsdaemon/jni/daemon.c),这样既解决了内存问题,又可以自己给新的进程命名
//直接运行一个二进制文件,他会占用原进程,于是我们这里仅将fork用作一个启动binary的工具
LOGI(LOG_TAG, "开始运行二进制文件,启动守护进程,当前进程ID:%d,实际用户UID为:%d,有效用户UID为:%d,进程组ID:%d",getpid(),getuid(),geteuid(),getpgrp());
execl(daemon, daemon, workdir, service, (char *)0);//二进制文件启动服务
}
exit(0);//【2.1】是第一子进程,结束第一子进程 ,使得会话不会申请控制终端
} else {//主进程
// 等待第一个子进程退出,应该会立即退出,让它继续活着
waitpid(pid, &status, 0);
//一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程
//exit(EXIT_SUCCESS); //【1】父进程直接退出,从而实现子进程的脱离控制终端
}
这次我们在一个完整项目里实验:
28653 是在fork后的子进程中,使用execlp()开启了一个新进程,它会替代原进程(kill原进程,执行自己占有原进程),它是一个守护进程
u0_a315 28453 361 2179636 60772 ffffffff 00000000 S com.XXXX.pocketdog
u0_a315 28653 1 9248 568 ffffffff 00000000 S /data/data/com.XXXX.pocketdog/files/daemon
07-27 15:01:58.670 28453-28651/com.XXXX.pocketdog I/packetdog: 开启守护进程,主进程id为:28453,实际用户UID为:10315,有效用户UID为:10315,进程组ID:361
..........
07-27 15:01:58.685 28652-28652/? I/packetdog: 第一个子进程: 28652
07-27 15:01:58.700 28653-28653/? I/packetdog: 开始运行二进制文件,启动守护进程,当前进程ID:28653,实际用户UID为:10315,有效用户UID为:10315,进程组ID:28652
VSIZE可以看内存大小 主进程有虚拟机 多一二十兆 子进程没有
我擦,好激动呀,真的实现了进程不同组了,子进程和守护进程不在同一个组里
赶紧试一下,forc kill
07-27 15:03:47.227 2545-2667/? W/recents.Proxy: packageName = com.XXXX.pocketdog is other will be forceStopPackage
07-27 15:03:47.229 1197-3392/? I/ActivityManager: Force stopping com.XXXX.pocketdog appid=10315 user=0: from pid 2545
07-27 15:03:47.229 1197-3392/? I/ActivityManager: Killing 28453:com.sangfor.pocketdog/u0a315 (adj 0): stop com.XXXX.pocketdog
07-27 15:03:47.230 1197-3392/? W/ActivityManager: Scheduling restart of crashed service com.sangfor.pocketdog/.ForeverService in 1000ms
07-27 15:03:47.231 1197-1518/? I/libprocessgroup: Killing pid 28653 in uid 10315 as part of process group 28453
07-27 15:03:47.240 1197-3392/? I/ActivityManager: Force stopping service ServiceRecord{e13adf u0 com.sangfor.pocketdog/.ForeverService}
Killing pid 28653 in uid 10315 as part of process group 28453
我去,28653 这个子守护进程已经被我搞成真的“守护进程”,并且移动到28652的新会话里的新进程组了,已经和主进程28453脱离了呀,怎么还是同组?
三、同组杀? 历史同组杀?
上边,我们讲到,如果是fork的子进程,或者是init领养的子进程,ActivityManagerService你给我“组杀”了,我还能理解,毕竟他们的确还在一个 进程组里
但是,守护进程我已经跳出该组了, 你还以“组杀”的名义,把我的守护进程干掉,太不讲理了吧?
你这是什么“组杀”?
3.1 组杀的实现方式
5.0+上开启了Control Group来管理进程
它会把进程以及它的所有子进程都绑到了一个组里面管理,这样就能做到将所有子进程都杀了此处,买一个关子,你要知道,这个组的概念和linux的实际进程组是有不同之处的
对代码进行分析:
在AMS中杀App时,会调用到processgroup.cpp的killProcessGroup函数,看下该函数会做什么:
int killProcessGroup(uid_t uid, int initialPid, int signal)
{
..........
while ((processes = killProcessGroupOnce(uid, initialPid, signal)) > 0) {
if (retry > 0) {
usleep(sleep_us);
--retry;
} else {
break;
}
}
..........
}
可以看到在killProcessGroup中只是循环调用killProcessGroupOnce,再看看该函数又是做什么的:
static int killProcessGroupOnce(uid_t uid, int initialPid, int signal)
{
while ((pid = getOneAppProcess(uid, initialPid, &ctx) >= 0) {
processes++;
......
int ret = kill(pid, signal);
if (ret == -1) {
SLOGW("failed to kill pid %d: %s", pid, strerror(errno));
}
}
它通过getOneAppProcess循环获取子进程id,再kill掉。
进入getOneAppProcess查看,最终发现子进程是从下面这个函数组成的文件名中读取到的:
static int convertUidPidToPath(char *path, size_t size, uid_t uid, int pid)
{
return snprintf(path, size, "%s/%s%d/%s%d",
PROCESSGROUP_CGROUP_PATH,
PROCESSGROUP_UID_PREFIX,
uid,
PROCESSGROUP_PID_PREFIX,
pid);
}
上面几个常量的定义如下:
#define PROCESSGROUP_CGROUP_PATH "/acct"
#define PROCESSGROUP_UID_PREFIX "uid_"
#define PROCESSGROUP_PID_PREFIX "pid_"
#define PROCESSGROUP_CGROUP_PROCS_FILE "/cgroup.procs"
所以上面的函数组成的文件名(路径)是这样的:
/acct/uid_XX/pid_XX/cgroup.procs
该文件存储了,同组的进程的信息
//使用shell命令去查看
//adb shell
//su 手机一定要root,不然看不了 uid_XX的目录
//cd uid_XX 进入进程所属的用户目录
//cat pid_/cgroup.procs pid_是某个进程的目录,cgroup存储该进程的同组进程信息
3.2 cgroup的记录是如何生成的
到现在你应该明白了,5.0的force kill是根据cgroup杀进程的,那么cgroup记录是怎么生成的呢,是linxu的同组进程的id么?
其实,基本是这样子:
如果当前进程是非系统进程,非白名单进程。那么这个进程的所fork()或execlp出来的任何进程,都是该进程的同组进程,会在这里记录 那我把新创建的进程移到新的进程组呢? 对不起,只要你曾经在这个组里,那我就会在这里记录。哪怕是你移到一个新组,再setsid再开一个新会话进一个新组,我都会记录你 如果是系统进程,或者白名单进程呢?
系统进程或白名单创建的新进程,会创建一个pid_xx的文件夹,相当于根进程又启动了几个进程,他们不算同组进程,也不会记录在这里边
所以,我们明白了 5.0 实现的是“历史同组杀”
四、微信,qq怎么在5.0 以上的保活的
好啦,此处我们加入讲解下,为什么微信或者qq之类的一些软件能持久保活
看下微信的:
u0_a132 2990 362 2306772 300900 ffffffff 00000000 S com.tencent.mm
u0_a132 6550 362 1707116 43600 ffffffff 00000000 S com.tencent.mm:exdevice
u0_a132 6607 362 1715528 53760 ffffffff 00000000 S com.tencent.mm:push
注意到没有,我们自己的应用在 魅族手机上看进程,尼玛全是10000以上的id号,微信的呢?
2990 6550 6607 都是10000以下
我们还知道 进程号里 0-300 是守护进程
那么很有可能在不同定制版本的手机上,区分了 300-XX是系统进程,XX1-XX2是白名单进程
你问白名单有什么好处?
看 3.2的第二项吧 这就是好处
我们现在看看java层启动服务的cgroup文件(5.1中配置AndroidManifest.xml的service中配置android:process):
u0_a179 21068 490 1515144 31516 ffffffff 00000000 S com.marswin89.marsdaemon.demo
cgroup.procs--------21068
u0_a179 21098 490 1498412 20400 ffffffff 00000000 S com.yhf.demo1
cgroup.procs---------21068
u0_a179 21126 490 1498412 20420 ffffffff 00000000 S com.yhf.demo2
cgroup.procs----------21126
据此我们可以看出 android层启动的进程 由于是系统启动的 都会创建pid_文件夹
同时 其中的记录文件没有记录同组的其他id(如上文所述,该记录文件记录同组历史,哪怕你跳出去新的进程组,还是会一样在这里显示)
测试记录:\进程保护\MARS\进程保护_魅族A5.1
由此可见,如果想要解决 5.0的保护问题,势必要从android层开始
android层解决“历史同组”问题,native层再修复“同包名杀”和“task杀”的问题,就是整体的解决方向
五、编外:为什么保活大部分都在native层实现?
其实这个很好理解,在android最开始的版本里,就引入了“同包名杀”和“同task杀”
5.1 同包名杀
u0_a324 27615 362 1557944 50628 ffffffff 00000000 S com.marswin89.marsdaemon.demo
u0_a324 27647 362 1545432 38436 ffffffff 00000000 S com.marswin89.marsdaemon.demo:process1
u0_a324 27677 362 1545432 38572 ffffffff 00000000 S com.marswin89.marsdaemon.demo:process2
0
7-28 17:20:46.718 1197-1220/? I/ActivityManager: Killing 27615:com.marswin89.marsdaemon.demo/u0a324 (adj 9): remove task
07-28 17:20:46.729 2545-2667/? W/recents.Proxy: packageName = com.marswin89.marsdaemon.demo is other will be forceStopPackage
07-28 17:20:46.731 1197-3457/? I/ActivityManager: Force stopping com.marswin89.marsdaemon.demo appid=10324 user=0: from pid 2545
07-28 17:20:46.737 1197-3457/? I/ActivityManager: Killing 27647:com.marswin89.marsdaemon.demo:process1/u0a324 (adj 0): stop com.marswin89.marsdaemon.demo
07-28 17:20:46.738 1197-3457/? W/ActivityManager: Scheduling restart of crashed service com.marswin89.marsdaemon.demo/.Service1 in 1000ms
07-28 17:20:46.739 1197-3457/? I/ActivityManager: Killing 27677:com.marswin89.marsdaemon.demo:process2/u0a324 (adj 0): stop com.marswin89.marsdaemon.demo
07-28 17:20:46.740 1197-3457/? W/ActivityManager: Scheduling restart of crashed service com.marswin89.marsdaemon.demo/.Service2 in 10998ms
07-28 17:20:46.743 1197-3457/? I/ActivityManager: Force stopping service ServiceRecord{378476d4 u0 com.marswin89.marsdaemon.demo/.Service1}
07-28 17:20:46.743 1197-3457/? I/ActivityManager: Force stopping service ServiceRecord{1e1c2c40 u0 com.marswin89.marsdaemon.demo/.Service2}
packageName = com.marswin89.marsdaemon.demo is other will be forceStopPackage
杀进程的时候,同一个包名下的进程都会被kill掉
咦,那你说我在AndroidManifest.xml的service中配置android:process=”com.yhf.demo1”,这样包名不一样了吧,而且都是系统进程开启的,跟主进程不属“历史同组进程”,能实现保活?
别天真了,你的确实现了避免“历史同组进程”,但是系统还是认为你是同包名的进程的,最重要,还是“同task杀”
u0_a324 27615 362 1557944 50628 ffffffff 00000000 S com.marswin89.marsdaemon.demo
u0_a324 27647 362 1545432 38436 ffffffff 00000000 S com.yhf.demo1
u0_a324 27677 362 1545432 38572 ffffffff 00000000 S com.yhf.demo2
//还是被清除了 日志忘了记录 哈哈 不信的自己试试哈
5.2 同task杀
task就不多讲了