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
版權宣告:本文為博主原創文章,轉載請附上博文連結!