(4.6.17.6)进程保活(Android的5.0分界线):Android5.0以上版本的force close到底发生了什么改变?

来源:互联网 发布:杀破狼贪狼 知乎 编辑:程序博客网 时间:2024/04/28 06:09

在阅读本篇之前,你首先需要大概清楚一点,无论是系统杀(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.pocketdogu0_a315   28653 1     9248   568   ffffffff 00000000 S /data/data/com.XXXX.pocketdog/files/daemon07-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: 第一个子进程: 2865207-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 forceStopPackage07-27 15:03:47.229 1197-3392/? I/ActivityManager: Force stopping com.XXXX.pocketdog appid=10315 user=0: from pid 254507-27 15:03:47.229 1197-3392/? I/ActivityManager: Killing 28453:com.sangfor.pocketdog/u0a315 (adj 0): stop com.XXXX.pocketdog07-27 15:03:47.230 1197-3392/? W/ActivityManager: Scheduling restart of crashed service com.sangfor.pocketdog/.ForeverService in 1000ms07-27 15:03:47.231 1197-1518/? I/libprocessgroup: Killing pid 28653 in uid 10315 as part of process group 2845307-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来管理进程
关于cgroup的参考资料:

  • http://lxr.free-electrons.com/source/Documentation/cgroups/cgroups.txt
  • http://www.linux-kongress.org/2010/slides/seyfried-cgroups-linux-kongress-2010-presentation.pdf

    它会把进程以及它的所有子进程都绑到了一个组里面管理,这样就能做到将所有子进程都杀了此处,买一个关子,你要知道,这个组的概念和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.mmu0_a132   6550  362   1707116 43600 ffffffff 00000000 S com.tencent.mm:exdeviceu0_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.democgroup.procs--------21068u0_a179   21098 490   1498412 20400 ffffffff 00000000 S com.yhf.demo1cgroup.procs---------21068u0_a179   21126 490   1498412 20420 ffffffff 00000000 S com.yhf.demo2cgroup.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.demou0_a324   27647 362   1545432 38436 ffffffff 00000000 S com.marswin89.marsdaemon.demo:process1u0_a324   27677 362   1545432 38572 ffffffff 00000000 S com.marswin89.marsdaemon.demo:process207-28 17:20:46.718 1197-1220/? I/ActivityManager: Killing 27615:com.marswin89.marsdaemon.demo/u0a324 (adj 9): remove task07-28 17:20:46.729 2545-2667/? W/recents.Proxy: packageName = com.marswin89.marsdaemon.demo is other will be forceStopPackage07-28 17:20:46.731 1197-3457/? I/ActivityManager: Force stopping com.marswin89.marsdaemon.demo appid=10324 user=0: from pid 254507-28 17:20:46.737 1197-3457/? I/ActivityManager: Killing 27647:com.marswin89.marsdaemon.demo:process1/u0a324 (adj 0): stop com.marswin89.marsdaemon.demo07-28 17:20:46.738 1197-3457/? W/ActivityManager: Scheduling restart of crashed service com.marswin89.marsdaemon.demo/.Service1 in 1000ms07-28 17:20:46.739 1197-3457/? I/ActivityManager: Killing 27677:com.marswin89.marsdaemon.demo:process2/u0a324 (adj 0): stop com.marswin89.marsdaemon.demo07-28 17:20:46.740 1197-3457/? W/ActivityManager: Scheduling restart of crashed service com.marswin89.marsdaemon.demo/.Service2 in 10998ms07-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.demou0_a324   27647 362   1545432 38436 ffffffff 00000000 S com.yhf.demo1u0_a324   27677 362   1545432 38572 ffffffff 00000000 S com.yhf.demo2//还是被清除了  日志忘了记录 哈哈  不信的自己试试哈

5.2 同task杀

task就不多讲了

不懂
看这里here

0 0
原创粉丝点击