上一篇我们通过父子进程间建立双管道,来监听进程死掉,经过测试,无耗电问题,无内存消耗问题,可以在设置中force close下成功拉起,也可以在获取到root权限的360/cleanmaster下成功存活。
开始自己头脑风暴,想出以下几种解决策略:
1、放弃Java进程,使用两个纯native的binary进程,相互保活,确保native进程常驻,然后按时拉起需要常驻的java进程。
2、使用setsid将子进程的session改变,是否可以让系统不冻结子进程。
3、子进程创建子进程,然后自杀,重复1000次,也就是父进程和自己子进程的子进程的子进程重复一千遍的进程之间互保,是否可以让系统不冻结子进程经过代码尝试,结果你可以猜到,没错,然并卵。这几种方案都无法阻挡系统的force close,就更不用说360/cm with root了。
于是换一个思路,不再纠结于父子进程,而是两个普通进程(android层)之间是否能互保
鉴于之前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进程同理
1. 实际上是 * a. 主进程就是 service1在的服务 process1 * b. 守护进程就 辅助进程 process2 * 没有native层的damon进程了, 主进程和守护进程跑相同的逻辑,锁上文件,然后等待对方的文件锁
看代码
/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的一键清理下都可以实现互相守护了。