双进程守护,驻留,杀不死服务

时间:2016-07-19 13:54:15   收藏:0   阅读:3900

这是一个轻量级的库,配置几行代码,就可以实现在Android上实现进程常驻,也就是在系统强杀下,以及360获取root权限下,clean master获取root权限下都无法杀死进程

支持系统2.3到6.0

支持大部分设备,包括三星,华为,oppo,nexus,魅族等等

可以简单对开机广播进行保护


github地址:

https://github.com/Marswin/MarsDaemon

原理分析:

Android 进程常驻(0)----MarsDaemon使用说明

Android 进程常驻(1)----开篇

Android 进程常驻(2)----细数利用android系统机制的保活手段

Android 进程常驻(3)----native保活5.0以下方案推演过程以及代码详述

Android 进程常驻(4)----native保活5.0以上方案推演过程以及代码详述

Android 进程常驻(5)----开机广播的简单守护以及总结



正文:

Marsdaemon配置需要三步:


1、明确自己需要常驻的进程service,创建一个和他同进程的receiver,然后在另外一个进程中创建一个service和一个receiver,并写在Manifest中。进程名可以自定义

见/MarsDaemon/DemoMarsdaemon/src/main/AndroidManifest.xml

技术分享

service1是应用中有业务逻辑的需要常驻进程的service,其他三个组件都是额外创建的,里面不要做任何事情,都是空实现就好了


2、用你的Application继承DaemonApplication,然后在回调方法getDaemonConfigurations中返回一个配置,将刚才注册的进程名,service类名,receiver类名传进来。

代码/MarsDaemon/DemoMarsdaemon/src/main/Java/com/marswin89/marsdaemon/demo/MyApplication1

技术分享


此时如果你想在自己的application里面复写attachBaseContext方法的话,发现他已经被写为final,因为我们需要抢时间,所以必须保证进程进入先加载Marsdaemon,如果你想在attchBaseContext中做一些事情的话,可以复写attachBaseContextByDaemon方法。


如果你的Application已经继承了其他的Application类,那么可以参考Appliation2,在Application的attachBaseContext的时候初始化一个DaemonClient,然后调用他的onAttachBaseContext同样可以实现,当然了,他同样需要一个配置来告诉他我们刚才在menifest中配的信息

代码代码/MarsDaemon/DemoMarsdaemon/src/main/java/com/marswin89/marsdaemon/demo/MyApplication2

技术分享



3、第三步就是尝试去杀掉进程

====

Android 进程常驻(5)----开机广播的简单守护以及总结


其实保活就是两个要点:

1、怎样监听到进程挂掉

2、怎样把进程拉起来

把这两个点都解决,问题就解决了。

大家把我之前的文章都看完,会发现这两个点上都有好多种策略,那么在不同的手机上,两个点的不同策略就有多种组合方式,也也是我适配手机的主要手段。

当时我适配测试的手机有

技术分享



还要说一句,有的手机会在你系统设置force close的时候,显示已经杀掉了进程,但是其实没有真的杀掉,比如魅族。。。

可以shell进去用命令 ps | grep mars来查看所有MarsDaemon的进程

如果有root权限,可以使用kill -9命令来杀进程,但是效果没有force close和360\cm 杀的好



最后要说一下,进程常驻是保证不死,但是首先要活一次才行
换句话说好多人问我是不是要开机的时候启动一次,怎么启动
答案肯定是开机广播
但是现在有第三方软件获取root权限之后可以把我们的开机广播给禁掉,那么MarsDaemon的保护活也就没有意义了

那么360/cm是怎么禁用我们的广播的呢?

我们站在他的角度来思考这个问题:

1、他阻止系统发出开机广播,开机之后立刻注入SystemService

2、系统发出广播,他让我们收不到

3、我们收到广播之后,他把我们return掉

4、他没能return掉我们,但是立马杀掉我们


ok,第一个太难,如果他能做到,我们没有root所以无解。

第四个我们没有威胁,因为MarsDaemon就是用来反被杀的

第三个他要注入我们,可以加壳之类防御

那么第二个他是怎么做的呢? 系统方法:

技术分享

没错,他可以将一个组件设置为enable或者disable,如果把我们的开机广播设置为disable,那么无疑是用不了。

可是这个他们调用不了的,需要系统签名才行。

但是他们可以用android shell中的pms 的pm命令达到同样的效果:

技术分享


只要有root权限就可以使用这个pm disable componentsName命令。第三方安全软件,我们已经默认他们有root权限了,那么我们该怎么办呢?


MarsDaemon在工程里面有这样一个类
/MarsDaemon/LibMarsdaemon/src/main/Java/com/marswin89/marsdaemon/PackageUtils

技术分享

是的,因为是我们自己的组件,所以设置他不需要任何权限,只需要在有些时机顺便重置一下开机广播的状态就好(比如每次进程重启的时候,网络变化的时候,开关屏的时候),还有一个就是注册一个关机广播,每次关机的时候重置一下开机广播的状态,从而达到保护开机广播的作用。

在CleanMaster ,给了root权限之后,禁用开机广播,然后重启手机,开机广播失效;然后加上我的这个方法,再禁用掉,开机广播ok!

很简单的一个小tip

=====

 Android 进程常驻(3)----native保活5.0以下方案推演过程以及代码详述


我们先来讲一下他的方案。

他是首先开启一个c进程,将需要保活的service名字传递进去

技术分享

然后定时给自己主进程发一个intent,如果主进程挂掉了,就可以顺利拉起来保证存活。

技术分享


所以他只是一个没有主动权的消息轮询器,说是守护其实很勉强

而且,这是要建立在保证c进程不挂的基础上,才能轮询,但是就目前来看,只有5.0以下的非国产机才会有这样的漏洞。也就是说在force close的时候,系统忽略c进程的存在,5.0以上包括5.0的哪怕源生系统也会连同c进程一起清理掉,国产机就更不用说了。就算是这样,在5.0以下的非国产机上,如果安装了获取root权限的360\cm的话,也是可以直接清理掉,也就是说会失效。


而且他不但不算守护,而且还是单向的,也就是说只能a保b,b保不了a;a保b也不是在b死了立刻拉起来,要等到了时间才会去拉。


最后,就算把刚才说的都排除掉,在很少的一部分手机,也就是低端且没有安装安全软件的手机上,他仍然无法保证时时存活。


技术关键点:开启native子进程,定时发intent
结论:单杀可以杀死,force close 5.0以上无效,5.0以下部分手机无效,第三方软件下无效,且无法保证实时常驻





==========================分割线=========================






好了,那么怎样才是双向的守护进程呢?

第一,如果a守护b,则b挂掉的一瞬间,a就应该把b启动起来
第二,a和b应该是互相守护,无论谁挂掉,对方就把他拉起来


那么怎么样才能实现双向守护呢?首先我们想到的是fork这个函数,他会创建一个子进程,然后在父进程中调用waitpid()这个函数,这是一个阻塞函数,他会一直wait到子进程挂掉,才会继续向下执行,利用这个机制,我们可以在主进程的c层fork一个子进程,然后父进程就可以监听到子进程的死亡,死亡的时候再重启子进程。


似乎可以用这个机制改进刚刚上面分析的那个工程,因为这样的话:1,无法直接杀掉子进程。2、子进程不死,他就会按时发intent给父进程。


这样做,普通杀是没有问题。但是force close不会按照你的要求先杀孩子,等你把孩子启动起来,再杀父亲,然后坐视子进程在那不管,三方软件自不必说。那么先杀父进程的话,子进程就没办法监听到父进程的死亡吗?


有朋友要说可以利用linux的进程领养机制,如果父进程挂掉,那么子进程就会被linux的init进程领养,进程所对应的父进程id也会变成1。这的确是一个很好的标示,但是要怎样监听这个状态的变化呢?轮询获取父进程id,然后判断是否等于1?那么轮询的间隔为多少合适?1秒间隔算短算长?设为1秒的话force close掉你的时候,根本不会等到你那每秒正时正点的轮询点上,会被forceclose直接干掉,我试过更短的时间,基本要小到小于10毫秒的时间间隔,才有可能再force close的时候检测到,并成功拉起父进程。注意我的关键词,小于10毫秒!才可能!对,一秒100次的检查,才有可能,只是可能!手机待机十分钟就已经可以开始烫手,三个小时电池发出了低电量警告。代码:

技术分享

(代码已经不在工程里面了,工程里的代码删除了好多,做过无数试验)

技术关键点:fork子进程 ,waitpid监听子进程 ,通过linux进程领养机制监听父进程
结论:保证单杀存活,保活效果与耗电成正比,得不偿失(5.0以上无效)





==========================分割线=========================





好,我们继续。

waitpid是阻塞函数,所以他一定是没有耗电问题的,即时性也没有问题。那么问题就集中在了子进程如何监听到父进程的死亡上面,把那个轮询替掉。

然后,我想到的是管道,linux中有多种ipc通信机制,管道是最基本的一种通信方式,且这种管道只能在父子进程间建立,于是我想是否能利用这个机制呢?在父子进程间建立管道,但是并不写入数据,只是使用阻塞方法在另一端去读取管道,这样如果对方进程挂掉,管道会被破坏,那么另一端的读取方法就会执行返回,由此确定对方挂掉然后重启对方。

代码:

技术分享


技术分享

这做样确实解决了耗电问题,父子进程两端的监听都是阻塞方法,耗电基本可以忽略不记,也基本实现双向守护。(以上代码不在项目工程里)


但是问题又来了

1、用ps命令发现fork出来的进程内存占用很大

2、fork出来的进程名字与父进程名字相同


原因:

1、fork函数调用的时候,会复制父进程的全部内存,因为父进程一定是我们需要保证常驻的Java进程,他在初始化的时候是fork的一个zygote进程,即时在应用刚初始化的时候fork,进程里面是有一个java虚拟机的内存在里面的,最少一二十兆是有了。fork出来子进程多的内存最后都会算到我们自己应用的内存中。

2、机制如此



技术关键点:fork子进程 ,waitpid监听子进程, 管道pipe监听父进程
结论:保证双向守护,无耗电问题,fork出来的进程名字与父进程同名用户体验不好,而且有内存浪费(5.0以上无效)





==========================分割线=========================




我们继续讨论内存的问题,如何不用fork也能建立管道呢。于是我想到了运行一个二进制可执行文件,这样他是一个相对独立的进程,但是又可以建立父子进程之间的管道。将真正用来实现的子进程写到一个二进制文件中(对应文件源码/MarsDaemon/LibMarsdaemon/jni/daemon.c),这样既解决了内存问题,又可以自己给新的进程命名。

问题解决了吗?没有,直接execute一个binary文件之后

1、发现代码不再继续向下执行

2、waitpid又不能用了

原因和解决方法:

1、直接运行一个二进制文件,他会占用原进程,于是我们这里仅将fork用作一个启动binary的工具,fork终于回归到了Linus希望他作用的地方

2、父子进程间的管道是单向的,于是我们可以建两根管道。ab两个进程,建12两个管道。a进程关掉管道1的写端,堵瑟调用管道2的读取方法;b进程关掉管道2的写端,堵瑟调用管道1的读取方法。这样就可以实现双向监听。任何一方监听到对方死掉就作出相应的动作,启动对方。至此,完全摒弃开始的fork方案。


代码:

此为最终方案,代码见下


技术关键点:双管道互相监听
结论:保证双向守护,无耗电问题,无内存问题,进程名自定义(5.0以上无效)




==========================分割线=========================


好了,这就是我5.0以下的最终解决方案

下面讲一下代码

二进制文件存放在assets下面,程序第一次启动的时候会将他拷贝到手机项目/data/data/...下,然后

源码/MarsDaemon/LibMarsdaemon/src/main/java/com/marswin89/marsdaemon/strategy/DaemonStrategyUnder21.java

技术分享

load对应c库,执行代码

源码/MarsDaemon/LibMarsdaemon/jni/daemon_api20.c

技术分享

技术分享


1、将对应的packagename,servicename以及二进制可执行文件的路径传进来

2、清理僵尸进程,就像最开始讲的,低端手机会忽略c进程,如果我们恰巧运行在低端手机上,那么c进程得不到释放会越来越多,我们称他为僵尸进程,需要清理一下

3、建立两条管道

4、执行二进制文件,将上面的参数传递进去

5、然后关掉自己管道1的写端和管道2的读端,然后阻塞读取管道1,如果读取到,则代表子进程挂掉


再来看二进制可执行文件的代码

源码/MarsDaemon/LibMarsdaemon/jni/daemon.c

技术分享

技术分享


二进制文件在程序启动起来的时候,将参数解析出来

关掉管道1的读端和管道2的写端,然后调用管道2的阻塞读取方法,如果执行过去,说明父进程死掉

这里用fork是为了让他的父进程id好看一些,别无他意

监听到之后的策略看下面。


==========================分割线=========================



然后说一下监听到对方进程死后的策略

你会说谁监听到对方死了,就直接拉起来就好了呀。

问题:

1、重新拉起来要重新建立双管道,子进程挂掉,父进程把他重启起来建立双管道还好说,如果父进程挂掉,子进程把父进程启动起来,他们之间就无法建立连接,而且如果中间出了差错,同步起来很费劲,于是我选择,无论谁监听到谁死了,都重启对方,然后自杀,重新初始化!

2、如果执行force close, 系统先杀父进程,子进程监听到之后拉起父进程然后自杀,但是系统杀你两个进程的间隔时间非常非常短,父进程刚起来还没来得及初始化,系统赶过来杀父进程。有的手机强杀之后很短一段时间无法拉起父进程。于是我选择用第三个进程。

第三个进程和之前的父子进程都没有任何关系,他的作用只是用做拉起常驻进程。父子进程无论谁监听到谁死,都拉起第三个进程,第三个进程负责拉起常驻进程,然后自杀。(用户实际上是看不到他的存在的,因为他可能只存活不到一秒就自杀了)

代码

/MarsDaemon/LibMarsdaemon/src/main/java/com/marswin89/marsdaemon/strategy/DaemonStrategyUnder21.java

技术分享

在常驻进程初始化的时候,初始化一个alarm,保存在内存中,以便以后使用

技术分享

监听到子进程死了时候使用闹钟拉起第三个进程,二进制文件监听到父进程死掉,直接用c代码发intent,见上面c代码

技术分享

第三进程启动起来,就是负责把常驻进程拉起来,然后自杀掉。






==========================分割线=========================





好了,这只是5.0以下的策略,5.0以上,以及6.0都不好用

下一篇我们开始分析5.0+的策略


========

 Android 进程常驻(4)----native保活5.0以上方案推演过程以及代码详述


上一篇我们通过父子进程间建立双管道,来监听进程死掉,经过测试,无耗电问题,无内存消耗问题,可以在设置中force close下成功拉起,也可以在获取到root权限的360/cleanmaster下成功存活。


可是放到5.0+的系统就不能用了,为什么呢?我们来看源码4.4系统和5.0系统在系统force close的时候都做了什么修改。


4.4.3的ActivityManagerService

技术分享

实现在这里

技术分享


然后5.0的AMS

技术分享


实现

技术分享



可以看出来5.0的源码中系统强杀的时候会连同同group中的所有进程也一起干掉,使用上一篇的策略通过打印log我们也可以看出,在父进程被杀死的时候,子进像是被冻结了一样,做不了任何事情,拿不到监听事件。那么我们该怎么办呢?

开始自己头脑风暴,想出以下几种解决策略:


1、放弃Java进程,使用两个纯native的binary进程,相互保活,确保native进程常驻,然后按时拉起需要常驻的java进程。

2、使用setsid将子进程的session改变,是否可以让系统不冻结子进程。

3、子进程创建子进程,然后自杀,重复1000次,也就是父进程和自己子进程的子进程的子进程重复一千遍的进程之间互保,是否可以让系统不冻结子进程


经过代码尝试,结果你可以猜到,没错,然并卵。这几种方案都无法阻挡系统的force close,就更不用说360/cm with root了。


于是换一个思路,不再纠结于父子进程,而是两个普通进程之间是否能互保


鉴于之前pipe管道的使用,首先想到的是fifo管道,这是linux下可以建立在两个没有关系的进程间的双向管道。同样两个进程间在c层建立fifo管道,然后阻塞读取。可问题在于,这个fifo与pipe是有本质区别的,fifo是通过一个文件做通信的,对方进程挂掉之后,自己这边既不会报错也不会返回,而是一直阻塞,直到管道里面有数据。所以,然并卵。


那么,当进程死的时候,自己往管道里面写数据,另一边阻塞读到数据不就可以得知这边死掉了吗?当时确实有这样一瞬间的可笑想法,但是这要首先要自己知道自己的死亡啊。


这个可笑的想法没有什么意义,但是他启发了我,当一个进程死掉的时候,他和另一个进程之间的什么会发生变化呢?


顺着这个思路,我想到了文件锁。不论windows还是linux都会有一套文件的进程同步机制,为了各个进程间文件的同步问题,一个进程可以给一个文件上锁,操作完了之后再解锁,其他进程如果担心同步问题,会首先检查一下文件是否有锁,如果有锁,代表别人正在操作,就会延迟对文件的操作。那么当一个进程挂掉,他给文件加的锁也自然会消失。


于是按照这个思路,进程a给文件1加锁,然后阻塞读取文件2的锁,进程b给文件2加锁,然后阻塞读取文件1的锁,如果对方进程挂掉,他所在进程所持有的文件锁会立即释放,那么另一个进程就可以读取到被释放文件锁的文件,监听到对方挂掉。


实现这个方案,首先使用的java代码,java里是有一套文件锁的api的,但是写完之后编译时报错。我就不写demo截图了,错误的信息是死锁exception,两个进程不能互相持有对方的锁。java的编译器可真多事儿,于是我想能不能用三个进程,1拿2的锁,2拿3的锁,3拿1的锁,还是不行,deadlock exception.


怀着忐忑的心情我用c来实现尝试,庆幸的是c中没有问题。原理通了,剩下的就是把代码写健壮,最难搞的问题就是同步问题,ab两个进程,如果b进程还没给文件加锁,a就开始读锁,那么就会误以为b进程挂掉了。需求:
1、需要让a进程先把文件1锁了,然后不去读文件2,等b进程做同样的操作之后,两边再同时读取对方的锁。
2、需要考虑第程序重新进入的初始化状态与单个进程等待的状态冲突问题
3、不能耗时!!时间很重要,后面会说到


这一块确实想了一晚上才想出来合适的解决方案,首先肯定不能用管道、信号这些进程间通信,连memset都不要考虑,因为耗时,java层的机制就更不用考虑了!那么就只能用一些标示位手段,比如一个文件在了就代表一个进程在了,一个文件没了,就代表一个进程锁好了。
于是最后的同步方案是:

1、4个文件,a进程文件a1,a2,b进程b1,b2


2、a进程加锁文件a1,b进程同理


3、a进程创建a2文件,然后轮询查看b2文件是否存在(这里可以轮询,因为时间很短),不存在代表b进程还没创建,b进程同理


4、a进程轮询到b2文件存在了,代表b进程已经创建并可能在对b1文件加锁,此时删除文件b2,代表a进程已经加锁完毕,允许b进程读取a进程的锁,b进程同理


5、a进程监听文件a2,如果a2被删除,代表b进程进行到了步骤4已经对b1加锁完成,可以开始读取b1文件的锁(不能直接监听a2文件删除,也就是不能跳过34步,这也是最难想的一部分,如果那样可能此时b进程还没创建,和b进程创建完成并加锁完成的状态是一样的,就会让进程a误以为进程b加锁完成),b进程同理




看代码
/MarsDaemon/LibMarsdaemon/jni/daemon_api21.c
技术分享

其中a和b两个进程都会执行这块代码,对于a进程来说,indicator_self_path对应a1,indicator_daemon_path对应b1,observer_self_path对应a2,observer_daemon_path对应b2,b进程同理

首先自己加锁,然后加入同步模块notify_and_waitfor

技术分享

以上代码就是上述同步逻辑

同步完成后,两个进程同时监听对方的锁,一方挂掉另一方立刻可以监听到。这套方案在5.0+上可以做到互相监听对方死亡状态,因为都是阻塞方法,所以无耗电效率问题。

这里要说一下,也许你看完源码你会问为什么步在5.0以下采用这种方案,因为这种方案是需要挂两个进程的,虽然与父子进程相比,在linux下都是两个进程两个pid,但是在android系统的设置里面,父子进程中的native进程是android系统统计不到的,所以不会列出来,不会让用户觉得你为啥启这么多进程。所以从用户体验角度,5.0以下还是采用上一篇博文采用的方案。



ok,监听成功,对方死亡就把对方拉起来,然后自杀,重新初始化。把方案替换进来之后发现5.0是ok的,360/cm with root也是ok的,但是在5.1又失效了。为什么?


打印log我们发现,5.1上监听死亡状态是ok的,但是却无法将对方拉起来。


好了,我们要开始第二个重点了,那就是如何启动对方。


原先我们的方案是用一个闹钟启动对方,但是在5.1上,闹钟在force close之后,闹钟同样会被废掉,之前的拉起方案行不通了,那么开始想如何将对方的进程拉起来,想了很久也没有相出什么别的方法,发送intent似乎根本不执行,但是此时这种情形只能继续研究发的intent为什么没有执行。


通过打印log,我们才发现,force close的时候,系统杀应用对应进程的时候,速度非常快,在a进程监听到b进程被杀的时候,系统的死亡镰刀已经伸向了a进程,大概只有几十毫秒的时间!而发送一个intent我们知道他是在自己进程中使用系统ActivityManagerService的一个代理类,通过一个binder将intent传给系统,虽然大部分操作是在ams中,但是我们这边执行时间居然也在百毫秒级的,难怪intent发不出去。


所以时间很重要!!!我们要跟系统抢时间



那么现在的问题是怎样才能把intent发出去,查看源码,我们可以看出intent的发送实际上是ActivityManagerNative这个类,他持有一个binder,用来与系统的ActivityManagerService通信,然后我们发送intent的时候会初始化一个Parcel,通过binder transcate过去。

技术分享


时间都消耗在了pacel的创建上。

那么我们可不可以把这些耗时操作放在进程开始的时候就完成,等到监听到进程挂掉,直接调用。

但是那个binder我们没法直接拿到,这里需要用到反射。

看代码:


代码com.marswin89.marsdaemon.strategy.DaemonStrategy22.java

技术分享


 拿到ActivityManagerNative实例然后拿binder,也就是他的一个成员变量mRemote


技术分享


  然后把Pacel初始化出来,这里注释掉的是我一行一行试验的,为了节省时间,在能达到目的的前提下,时间越少越好,所以参数能少传就少传。


技术分享


然后在检测到对方进程死掉的时候,直接调用transcate方法。

技术分享



ok,5.1上也可以实现双向守护。

但是很遗憾,6.0上又跪了。

然后经过排查,发现通过以上方式用binder transcate的方法无法启动一个service。那该怎么办呢?抱着试试看的心里尝试了broadcastreceiver,果然广播是可以的。同样的原理用ActivityManagerNative中的binder transcate一个pacel来启动一个broadcast拉起进程。

也是仿照系统源码来做的广播的Pacel

代码com.marswin89.marsdaemon.strategy.DaemonStrategy22.java

技术分享


ok,6.0也搞定。

在系统force close时成功拉起对方,但是在360\clean master with root的强杀下仍然有一定的几率会跪。通过log可以看出来,在一键清理的时候是有成功拉起的,但是360\cm均会重复杀,被拉起来的新进程还没初始化完成,就又被杀掉了。



再次,时间很重要!!!我们要跟360\cm抢时间


这里我们在进程刚创建的时候使用多线程,将文件的同步加锁监听与启动另一个进程同时进行,从来能节约100毫秒左右的时间

技术分享


大功告成!


从5.0到6.0,在force close和360\cm with root的一键清理下都可以实现互相守护了。


=======

评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!