Android NDK(C++) 雙進程守護


雙進程守護
如果從進程管理器觀察會發現新浪微博、支付寶和QQ等都有兩個以上相關進程,其中一個就是守護進程,由此可以猜到這些商業級的軟件都采用了雙進程守護的辦法。


什么是雙進程守護呢?顧名思義就是兩個進程互相監視對方,發現對方掛掉就立刻重啟!不知道應該把這樣的一對進程是叫做相依為命呢還是難兄難弟好呢,但總之,雙進程守護的確是一個解決問題的辦法!相信說到這里,很多人已經迫切的想知道如何實現雙進程守護了。這篇文章就介紹一個用NDK來實現雙進程保護的辦法,不過首先說明一點,下面要介紹的方法中,會損失不少的效率,反應到現實中就是會使手機的耗電量變大!但是這篇文章僅僅是拋磚引玉,相信看完之后會有更多高人指點出更妙的實現辦法。

需要了解些什么?
這篇文章中實現雙進程保護的方法基本上是純的NDK開發,或者說全部是用C++來實現的,需要雙進程保護的程序,只需要在程序的任何地方調用一下JAVA接口即可。下面幾個知識點是需要了解的:

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創建子進程來監視當前進程
*/

public class Watcher {

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>>");
}
}

/**
* 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,你的應用就會有一個守護進程來監視,被殺死后也會立刻重新啟動起來!是不是很有意思呢?

---------------------
作者:huaxun66
來源:CSDN
原文:https://blog.csdn.net/huaxun66/article/details/53158162
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM