Android NDK(C++) 双进程守护

双进程守护
若是从进程管理器观察会发现新浪微博、支付宝和QQ等都有两个以上相关进程,其中一个就是守护进程,由此能够猜到这些商业级的软件都采用了双进程守护的办法。java


什么是双进程守护呢?顾名思义就是两个进程互相监视对方,发现对方挂掉就马上重启!不知道应该把这样的一对进程是叫作相依为命呢仍是难兄难弟好呢,但总之,双进程守护的确是一个解决问题的办法!相信说到这里,不少人已经迫切的想知道如何实现双进程守护了。这篇文章就介绍一个用NDK来实现双进程保护的办法,不过首先说明一点,下面要介绍的方法中,会损失很多的效率,反应到现实中就是会使手机的耗电量变大!可是这篇文章仅仅是抛砖引玉,相信看完以后会有更多高人指点出更妙的实现办法。linux

须要了解些什么?
这篇文章中实现双进程保护的方法基本上是纯的NDK开发,或者说所有是用C++来实现的,须要双进程保护的程序,只须要在程序的任何地方调用一下JAVA接口便可。下面几个知识点是须要了解的:android

1.linux中多进程;
2.unix domain套接字实现跨进程通讯;
3.linux的信号处理;
4.exec函数族的用法;
其实这些东西自己并非多复杂的技术,只是咱们把他们组合起来实现了一个双进程守护而已,没有想象中那么神秘!在正式贴出代码以前,先来讲说几个实现双进程守护时的关键点:编程

1.父进程如何监视到子进程(监视进程)的死亡?
很简单,在linux中,子进程被终止时,会向父进程发送SIG_CHLD信号,因而咱们能够安装信号处理函数,并在此信号处理函数中从新启动建立监视进程;
2.子进程(监视进程)如何监视到父进程死亡?
当父进程死亡之后,子进程就成为了孤儿进程由Init进程领养,因而咱们能够在一个循环中读取子进程的父进程PID,当变为1就说明其父进程已经死亡,因而能够重启父进程。这里由于采用了循环,因此就引出了以前提到的耗电量的问题。
3.父子进程间的通讯
有一种办法是父子进程间创建通讯通道,而后经过监视此通道来感知对方的存在,这样不会存在以前提到的耗电量的问题,在本文的实现中,为了简单,仍是采用了轮询父进程PID的办法,可是仍是留出了父子进程的通讯通道,虽然暂时没有用到,但可备不时之需!服务器


OK, 下面就贴上代码!首先是Java部分,这一部分太过简单,只是一个类,提供了给外部调用的API接口用于建立守护进程,全部的实现都经过native方法在C++中完成!网络

/**
* 监视器类,构造时将会在Native建立子进程来监视当前进程
*/dom

public class Watcher {socket

public void createAppMonitor(String userId) {
if (!createWatcher(userId)) {
MainActivity.showlog("<<Monitor created failed>>");
} else {
MainActivity.showlog("<<Monitor created success>>");
}
if (!connectToMonitor()) {
MainActivity.showlog("<<Connect To Monitor failed>>");
} else {
MainActivity.showlog("<<Connect To Monitor success>>");
}
}ide

/**
* Native方法,建立一个监视子进程.
*
* @param userId
* 当前进程的用户ID,子进程重启当前进程时须要用到当前进程的用户ID.
* @return 若是子进程建立成功返回true,不然返回false
*/
private native boolean createWatcher(String userId);函数

/**
* Native方法,让当前进程链接到监视进程.
*
* @return 链接成功返回true,不然返回false
*/
private native boolean connectToMonitor();

/**
* Native方法,向监视进程发送任意信息
*
* @param 发给monitor的信息
* @return 实际发送的字节
*/
private native int sendMsgToMonitor(String msg);

static {
System.loadLibrary("monitor");
}
}
只须要关心createAppMonitor这个对外接口就能够了,它要求传入一个当前进程的用户ID,而后会调用createWatcher本地方法来建立守护进程。还有两个方法connectToMonitor用于建立和监视进程的socket通道,sendMsgToMonitor用于经过socket向子进程发送数据。因为暂时不须要和子进程进行数据交互,因此这两个方法就没有添加对外的JAVA接口,可是要添加简直是垂手可得的事!

JAVA只是个壳,内部的实现还得是C++,为了让程序更加的面向对象,在实现native时,咱们用一个ProcessBase基类来对父子进程进行一个抽象,把父子进程都会有的行为抽象出来,而父子进程能够根据须要用本身的方式去实现其中的接口,先来看看这个抽象了父子进程共同行为的ProcessBase基类:

#ifndef _PROCESS_H
#define _PROCESS_H

#include <jni.h>
#include <sys/select.h>
#include <unistd.h>
#include <sys/socket.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
#include <android/log.h>
#include <sys/types.h>
#include <sys/un.h>
#include <errno.h>
#include <stdlib.h>
//#include "constants.h"

#define LOG_TAG "Native"

#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

/**
* 功能:对父子进程的一个抽象
* @author wangqiang
* @date 2014-03-14
*/
class ProcessBase {
public:

ProcessBase();

/**
* 父子进程要作的工做不相同,留出一个抽象接口由父子进程
* 本身去实现.
*/
virtual void do_work() = 0;

/**
* 进程能够根据须要建立子进程,若是不须要建立子进程,能够给
* 此接口一个空实现便可.
*/
virtual bool create_child() = 0;

/**
* 捕捉子进程死亡的信号,若是没有子进程此方法能够给一个空实现.
*/
virtual void catch_child_dead_signal() = 0;

/**
* 在子进程死亡以后作任意事情.
*/
virtual void on_child_end() = 0;

/**
* 建立父子进程通讯通道.
*/
bool create_channel();

/**
* 给进程设置通讯通道.
* @param channel_fd 通道的文件描述
*/
void set_channel(int channel_fd);

/**
* 向通道中写入数据.
* @param data 写入通道的数据
* @param len 写入的字节数
* @return 实际写入通道的字节数
*/
int write_to_channel(void* data, int len);

/**
* 从通道中读数据.
* @param data 保存从通道中读入的数据
* @param len 从通道中读入的字节数
* @return 实际读到的字节数
*/
int read_from_channel(void* data, int len);

/**
* 获取通道对应的文件描述符
*/
int get_channel() const;

virtual ~ProcessBase();

protected:

int m_channel;
};

只是很简单的一个类,相信看看注释就知道是什么意思了,好比父子进程可能都须要捕获他的子孙死亡的信号,因而给一个catch_child_dead_signal函数,若是对子进程的死活不感兴趣,能够给个空实现,忽略掉就能够了。因为用了纯虚函数,因此ProcessBase是一个抽象类,也就是说它不能有本身的实例,只是用来继承的,它的子孙后代能够用不一样的方式实现它里面的接口从而表现出不同的行为,这里父进程和子进程的行为就是有区别的,下面就先为诸君奉上父进程的实现:

/**
* 功能:父进程的实现
* @author wangqiang
* @date 2014-03-14
*/
class Parent: public ProcessBase {
public:

Parent(JNIEnv* env, jobject jobj);

virtual bool create_child();

virtual void do_work();

virtual void catch_child_dead_signal();

virtual void on_child_end();

virtual ~Parent();

bool create_channel();

/**
* 获取父进程的JNIEnv
*/
JNIEnv *get_jni_env() const;

/**
* 获取Java层的对象
*/
jobject get_jobj() const;

private:

JNIEnv *m_env;

jobject m_jobj;

};

以上是定义部分,其实JNIEnv和jobject基本上没用到,彻底能够给剃掉的,你们就当这两个属性不存在就是了!实现部分以下:

#include "process.h"

extern ProcessBase *g_process;

extern const char* g_userId;

extern JNIEnv* g_env;

//子进程有权限访问父进程的私有目录,在此创建跨进程通讯的套接字文件
static const char* PATH = "/data/data/com.hx.doubleprocess/my.sock";

//服务名称
static const char* SERVICE_NAME = "com.hx.doubleprocess/com.hx.doubleprocess.MyService";

bool ProcessBase::create_channel() {
}

int ProcessBase::write_to_channel(void* data, int len) {
return write(m_channel, data, len);
}

int ProcessBase::read_from_channel(void* data, int len) {
return read(m_channel, data, len);
}

int ProcessBase::get_channel() const {
return m_channel;
}

void ProcessBase::set_channel(int channel_fd) {
m_channel = channel_fd;
}

ProcessBase::ProcessBase() {

}

ProcessBase::~ProcessBase() {
close(m_channel);
}

Parent::Parent(JNIEnv *env, jobject jobj) :
m_env(env) {
LOGE("<<new parent instance>>");

m_jobj = env->NewGlobalRef(jobj);
}

Parent::~Parent() {
LOGE("<<Parent::~Parent()>>");

g_process = NULL;
}

void Parent::do_work() {
}

JNIEnv* Parent::get_jni_env() const {
return m_env;
}

jobject Parent::get_jobj() const {
return m_jobj;
}

/**
* 父进程建立通道,这里实际上是建立一个客户端并尝试
* 链接服务器(子进程)
*/
bool Parent::create_channel() {
int sockfd;

sockaddr_un addr;

while (1) {
sockfd = socket(AF_LOCAL, SOCK_STREAM, 0);

if (sockfd < 0) {
LOGE("<<Parent create channel failed>>");

return false;
}

memset(&addr, 0, sizeof(addr));

addr.sun_family = AF_LOCAL;

strcpy(addr.sun_path, PATH);

if (connect(sockfd, (sockaddr*) &addr, sizeof(addr)) < 0) {
close(sockfd);

sleep(1);

continue;
}

set_channel(sockfd);

LOGE("<<parent channel fd %d>>", m_channel);

break;
}

return true;
}

/**
* 子进程死亡会发出SIGCHLD信号,经过捕捉此信号父进程能够
* 知道子进程已经死亡,此函数即为SIGCHLD信号的处理函数.
*/
static void sig_handler(int signo) {
pid_t pid;

int status;

//调用wait等待子进程死亡时发出的SIGCHLD
//信号以给子进程收尸,防止它变成僵尸进程
pid = wait(&status);

if (g_process != NULL) {
g_process->on_child_end();
}
}

void Parent::catch_child_dead_signal() {
LOGE("<<process %d install child dead signal detector!>>", getpid());

struct sigaction sa;

sigemptyset(&sa.sa_mask);

sa.sa_flags = 0;

sa.sa_handler = sig_handler;

sigaction(SIGCHLD, &sa, NULL);
}

void Parent::on_child_end() {
LOGE("<<on_child_end:create a new child process>>");

create_child();
}

bool Parent::create_child() {
pid_t pid;

if ((pid = fork()) < 0) {
return false;
} else if (pid == 0) //子进程
{
LOGE("<<In child process,pid=%d>>", getpid());

Child child;

ProcessBase& ref_child = child;

ref_child.do_work();
} else if (pid > 0) //父进程
{
LOGE("<<In parent process,pid=%d>>", getpid());
}

return true;
}

这里先要说明一下三个全局变量:

g_process是父进程的指针;
g_userId是父进程用户ID,由Java侧传递过来,咱们须要把它用全局变量保存起来,由于子进程在重启父进程的时候须要用到用户ID,不然会有问题,固然这里也得益于子进程可以继承父进程的全局变量这个事实!
g_env是JNIEnv的指针,把这个变量也做为一个全局变量,是保留给子进程用的;
父进程在create_child中用fork建立了子进程,其实就是一个fork调用,而后父进程什么都不作,子进程建立一个Child对象并调用其do_work开始作本身该作的事!

父进程实现了catch_child_dead_signal,在其中安装了SIG_CHLD信号处理函数,由于他很爱他的儿子,时刻关心着他。而在信号处理函数sig_handler中,咱们留意到了wait调用,这是为了防止子进程死了之后变成僵尸进程,因为咱们已经知道父进程最多只会建立一个子监视进程,因此wait就足够了,不须要waitpid函数亲自出马!而信号处理函数很简单,从新调用一下on_child_end,在其中再次create_child和他亲爱的夫人在make一个小baby就能够了!

最后要说说create_channel这个函数,他用来建立和子进程的socket通道,这个编程模型对于有网络编程经验的人来讲显得很是亲切和熟悉,他遵循标准的网络编程客户端步骤:建立socket,connect,以后收发数据就OK了,只是这里的协议用的是AF_LOCAL,代表咱们是要进行跨进程通讯。因为域套接字用的不是IP地址,而是经过指定的一个文件来和目标进程通讯,父子进程都须要这个文件,因此这个文件的位置指定在哪里也须要注意一下:在一个没有root过的手机上,几乎全部的文件都是没有写入权限的,可是很幸运的是linux的子进程共享父进程的目录,因此把这个位置指定到/data/data/下应用的私有目录就能够作到让父子进程都能访问这个文件了!

接下来是子进程的实现了,它的定义以下:

/**
* 子进程的实现
* @author wangqiang
* @date 2014-03-14
*/
class Child: public ProcessBase {
public:

Child();

virtual ~Child();

virtual void do_work();

virtual bool create_child();

virtual void catch_child_dead_signal();

virtual void on_child_end();

bool create_channel();

private:

/**
* 处理父进程死亡事件
*/
void handle_parent_die();

/**
* 侦听父进程发送的消息
*/
void listen_msg();

/**
* 从新启动父进程.
*/
void restart_parent();

/**
* 处理来自父进程的消息
*/
void handle_msg(const char* msg);

/**
* 线程函数,用来检测父进程是否挂掉
*/
void* parent_monitor();

void start_parent_monitor();

/**
* 这个联合体的做用是帮助将类的成员函数作为线程函数使用
*/
union {
void* (*thread_rtn)(void*);

void* (Child::*member_rtn)();
} RTN_MAP;
};
#endif

注意到里面有个union,这个联合体的做用是为了辅助把一个类的成员函数做为线程函数来传递给pthread_create,不少时候咱们都但愿线程可以像本身人同样访问类的私有成员,就像一个成员函数那样,用friend虽然能够作到这一点,但总感受不够优美,因为成员函数隐含的this指针,使咱们彻底能够将一个成员函数做为线程函数来用。只是因为编译器堵死了函数指针的类型转换,因此这里就只好用一个结构体。

废话很少说,看看子进程的实现:

bool Child::create_child() {
//子进程不须要再去建立子进程,此函数留空
return false;
}

Child::Child() {
RTN_MAP.member_rtn = &Child::parent_monitor;
}

Child::~Child() {
LOGE("<<~Child(), unlink %s>>", PATH);

unlink (PATH);
}

void Child::catch_child_dead_signal() {
//子进程不须要捕捉SIGCHLD信号
return;
}

void Child::on_child_end() {
//子进程不须要处理
return;
}

void Child::handle_parent_die() {
//子进程成为了孤儿进程,等待被Init进程收养后在进行后续处理
while (getppid() != 1) {
usleep(500); //休眠0.5ms
}

close (m_channel);

//重启父进程服务
LOGE("<<parent died,restart now>>");

restart_parent();
}

void Child::restart_parent() {
LOGE("<<restart_parent enter>>");

/**
* TODO 重启父进程,经过am启动Java空间的任一组件(service或者activity等)便可让应用从新启动
*/
execlp("am", "am", "startservice", "--user", g_userId, "-n", SERVICE_NAME, //注意此处的名称
(char *) NULL);
}

void* Child::parent_monitor() {
handle_parent_die();
}

void Child::start_parent_monitor() {
pthread_t tid;

pthread_create(&tid, NULL, RTN_MAP.thread_rtn, this);
}

bool Child::create_channel() {
int listenfd, connfd;

struct sockaddr_un addr;

listenfd = socket(AF_LOCAL, SOCK_STREAM, 0);

unlink (PATH);

memset(&addr, 0, sizeof(addr));

addr.sun_family = AF_LOCAL;

strcpy(addr.sun_path, PATH);

if (bind(listenfd, (sockaddr*) &addr, sizeof(addr)) < 0) {
LOGE("<<bind error,errno(%d)>>", errno);

return false;
}

listen(listenfd, 5);

while (true) {
if ((connfd = accept(listenfd, NULL, NULL)) < 0) {
if (errno == EINTR)
continue;
else {
LOGE("<<accept error>>");

return false;
}
}

set_channel(connfd);

break;
}

LOGE("<<child channel fd %d>>", m_channel);

return true;
}

void Child::handle_msg(const char* msg) {
//TODO How to handle message is decided by you.
}

void Child::listen_msg() {
fd_set rfds;

int retry = 0;

while (1) {
FD_ZERO(&rfds);

FD_SET(m_channel, &rfds);

timeval timeout = { 3, 0 };

int r = select(m_channel + 1, &rfds, NULL, NULL, &timeout);

if (r > 0) {
char pkg[256] = { 0 };

if (FD_ISSET(m_channel, &rfds)) {
read_from_channel(pkg, sizeof(pkg));

LOGE("<<A message comes:%s>>", pkg);

handle_msg((const char*) pkg);
}
}
}
}

void Child::do_work() {
start_parent_monitor(); //启动监视线程

if (create_channel()) //等待而且处理来自父进程发送的消息
{
listen_msg();
}
}

子进程在他的do_work中先建立了一个线程轮询其父进程的PID,若是发现变成了1,就会调用restart_parent,在其中调用execlp,执行一下am指令启动JAVA侧的组件,从而实现父进程的重启!这里请留意一下execlp中给am传入的参数,带了–user并加上了以前咱们在全局变量中保存的user id,若是不加这个选项,就没法重启父进程,我在这花费了好长时间哦!

子进程剩余的工做很简单,建立通道,监听来自父进程的消息,这里咱们用select来监听,因为实际上只有一个客户端(父进程),因此用select有点脱裤子放屁,把简单问题复杂化的嫌疑,可是实际上也没啥太大影响!

有了以上的实现,JNI的实现就至关的简单了:

#include "process.h"

/**
* 全局变量,表明应用程序进程.
*/
ProcessBase *g_process = NULL;

/**
* 应用进程的UID.
*/
const char* g_userId = NULL;

/**
* 全局的JNIEnv,子进程有时会用到它.
*/
JNIEnv* g_env = NULL;

extern "C" {
JNIEXPORT jboolean JNICALL Java_com_hx_doubleprocess_Watcher_createWatcher(
JNIEnv*, jobject, jstring);

JNIEXPORT jboolean JNICALL Java_com_hx_doubleprocess_Watcher_connectToMonitor(
JNIEnv*, jobject);

JNIEXPORT jint JNICALL Java_com_hx_doubleprocess_Watcher_sendMsgToMonitor(
JNIEnv*, jobject, jstring);

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM*, void*);
};

JNIEXPORT jboolean JNICALL Java_com_hx_doubleprocess_Watcher_createWatcher(
JNIEnv* env, jobject thiz, jstring user) {
g_process = new Parent(env, thiz);//建立父进程
g_userId = (const char*) env->GetStringUTFChars(user,0);//用户ID
g_process->catch_child_dead_signal();//接收子线程死掉的信号

if (!g_process->create_child()) {
LOGE("<<create child error!>>");
return JNI_FALSE;
}

return JNI_TRUE;
}

JNIEXPORT jboolean JNICALL Java_com_hx_doubleprocess_Watcher_connectToMonitor(
JNIEnv* env, jobject thiz) {
if (g_process != NULL) {
if (g_process->create_channel()) {
return JNI_TRUE;
}
return JNI_FALSE;
}
}

把上面这些代码整合起来,一个双进程守护的实现就完成了,只须要调用一下Watcher.java的createAppMonitor,你的应用就会有一个守护进程来监视,被杀死后也会马上从新启动起来!是否是颇有意思呢?

相关文章
相关标签/搜索