雙進程守護
若是從進程管理器觀察會發現新浪微博、支付寶和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,你的應用就會有一個守護進程來監視,被殺死後也會馬上從新啓動起來!是否是頗有意思呢?