English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Android Initプロセスのシグナル処理プロセスの詳細

Android Initプロセスのシグナル処理フロー

Androidでは、プロセスが(exit())を終了すると、その親プロセスにSIGCHLDシグナルが送信されます。親プロセスがこのシグナルを受け取ると、子プロセスに割り当てられたシステムリソースを解放し、親プロセスはwait()やwaitpid()を呼び出して子プロセスの終了を待たなければなりません。親プロセスがこの処理を行わない場合、または親プロセスが初期化時にsignal(SIGCHLD, SIG_IGN)を呼び出してSIGCHLDの処理を無視するように設定していない場合、子プロセスは現在の終了状態を保持し、完全に終了しません。このような子プロセスはスケジューリングされず、プロセスリストに位置を占めて、そのプロセスのPID、終了状態、CPU使用時間などの情報を保存します。このようなプロセスを「ゾンビプロセス」と呼びます。

Linuxでは、ゾンビプロセスの設定は、子プロセスの一部の情報を保持し、親プロセスが後で参照するためにあります。特に、親プロセスが終了すると、そのすべてのゾンビ子プロセスの親プロセスはInitプロセス(PIDが1),これらのゾンビプロセスの回収をInitプロセスが担当します(Initプロセスはwait()を呼び出します)/waitpid()を呼び出し、プロセスリストからその情報をクリアします)。

ゾンビプロセスはプロセスリストに位置を占めているため、Linuxがサポートする最大プロセス数には限界があります。この限界を超えると、プロセスの作成ができません。したがって、ゾンビプロセスをクリーンアップし、システムの正常動作を確保する必要があります。

次に、InitプロセスがSIGCHLDシグナルをどのように処理するかを分析します。

Init.cppでは、signal_handler_init()を通じてSIGCHLDシグナル処理を初期化します:

void signal_handler_init() { 
  // SIGCHLDシグナルのためのシグナルメカニズムを作成します。 
  int s[2]; 
  //socketpair()は、未命名で相互に接続されたUNIXドメインソケットのペアを生成します 
  if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, s) === -1) { 
    ERROR("socketpair failed: %s\n", strerror(errno)); 
    exit(1); 
  } 
  signal_write_fd = s[0]; 
  signal_read_fd = s[1]; 
  // SIGCHLDをキャッチした場合、signal_write_fdに書き込む 
  struct sigaction act; 
  memset(&act, 0, sizeof(act)); 
  act.sa_handler = SIGCHLD_handler;//シグナルが発生した場合、上記で作成したソケットにデータを書き込むハンドラを設定します。epollがそのソケットのfdが読み取り可能であると監視中に、登録された関数がイベントを処理するようにします。 
  act.sa_flags = SA_NOCLDSTOP;//フラグを設定して、子プロセスが終了した場合にのみSIGCHIDシグナルを受け入れるようにする 
  sigaction(SIGCHLD, &act, 0);//SIGCHLDシグナルの処理方法を初期化する 
  reap_any_outstanding_children();//退出的子プロセスを処理する前に 
  register_epoll_handler(signal_read_fd, handle_signal); 
} 

sigaction()関数を使用してシグナルを初期化します。actパラメータでは、シグナルハンドラ関数を指定します:SIGCHLD_handler();シグナルが到達した場合、その関数が呼び出されます。また、actパラメータでは、SA_NOCLDSTOPフラグを設定し、子プロセスが終了した場合にのみSIGCHLDシグナルを受け入れることを示します。

Linuxでは、シグナルはソフトウェア割り込みであり、そのためシグナルの到達は現在処理中のプロセスを終了させます。したがって、登録されたシグナルハンドラ関数では、再入可能でない関数を呼び出さないようにします。また、Linuxはシグナルを並列処理しません。シグナルの処理中にどれだけ多くのシグナルが到達しても、現在のシグナル処理が完了した後、カーネルはプロセスにシグナルを再送信するだけで、シグナルの失敗が発生する可能性があります。シグナルの失敗を避けるために、登録されたシグナルハンドラ関数の操作はできるだけ効率的で早く行うべきです。

そして、SIGCHLDシグナルを処理する際には、親プロセスが待機操作を行うため、その時間は比較的长いです。この問題を解決するために、上記のシグナル初期化コードでは、スレッド間通信用の未命名で関連付けられたローカルソケットを一対で作成しました。登録されたシグナルハンドラはSIGCHLD_handler()です:

static void SIGCHLD_handler(int) { 
  if (TEMP_FAILURE_RETRY(write(signal_write_fd, "1" 1)) === -1) { 
    ERROR("write(signal_write_fd) failed: %s\n", strerror(errno)); 
  } 
}
#define TEMP_FAILURE_RETRY(exp)      \
 
 ({                    \
 
  decltype(exp) _rc;           \
 
  do {                  \
 
   _rc = (exp);             \ 
  } while (_rc == -1 && errno == EINTR); \ 
  _rc;                  \ 
 ) 

信号が到達した場合、socketにデータを書き込むだけで非常に速く、このプロセスはsocketの応答に信号処理が移行します;これにより、次の信号の処理に影響を与えません。また、write()関数はdo...whileループで囲まれており、ループ条件はwrite()がエラーが発生し、現在のエラーコードがEINTR(EINTR:この呼び出しがシグナルで中断された)である場合です、つまり現在のwrite()が中断されたシグナルによりエラーが発生した場合、操作が再実行されます;その他の場合、write()関数は一度だけ実行されます。シグナル処理が再初期化されると、reap_any_outstanding_children()が前のプロセスの終了状況を処理するために呼び出されます:

static void reap_any_outstanding_children() { 
  while (wait_for_one_process()) { 
  } 
} 

wait_for_one_process()はwaitpid()を呼び出して子プロセスの終了を待ち、サービスが再起動する必要がある場合、設定やクリーンアップを行います。
最後に、epoll_ctl()を通じてepoll_fdにローカルソケットを登録し、読み取り可能かどうかを監視し、epollイベントの処理関数を登録します:

register_epoll_handler(signal_read_fd, handle_signal); 
void register_epoll_handler(int fd, void (*fn)()) { 
  epoll_event ev; 
  ev.events = EPOLLIN;//ファイルディスクリプタが読み取り可能 
  ev.data.ptr = reinterpret_cast<void*(fn);//指定された関数ポインタを保存し、後续のイベント処理に使用します 
  if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {//epoll_fdに監視するfdを追加します、例えばproperty、keychordおよびsignalイベントの監視 
    ERROR("epoll_ctlが失敗しました: %s\n", strerror(errno)); 
  } 
} 

Zygoteプロセスの終了を例に、SIGCHLDシグナルの処理プロセスを見てみましょう。Zygoteプロセスはinit.rcでServiceとして宣言され、Initプロセスによって作成されます。Zygoteプロセスが終了すると、InitプロセスにSIGCHLDシグナルが送信されます。前のコードはシグナルの初期化操作を完了しており、シグナルが到達するとSIGCHLD_handler()関数が呼び出され、その処理はデータをsocketに書き込んですぐに戻ります;この時点で、SIGCHLDの処理はsocketイベントの応答に移行します。ローカルsocketをepoll_ctlで登録し、読み取り可能かどうかを監視しています;前のwrite()呼び出しにより、この時点でsocketにはデータが読み取れる状態になっています。そのため、登録されたhandle_signal()関数が呼び出されて処理されます:

static void handle_signal() { 
  // 未完了のリクエストをクリアします。 
  char buf[32]; 
  read(signal_read_fd, buf, sizeof(buf)); 
  reap_any_outstanding_children(); 
} 

それがsocketのデータを独立したbufに格納し、reap_any_outstanding_children()関数を呼び出して子プロセスの終了及びサービスの再起動操作を処理します:

static void reap_any_outstanding_children() { 
  while (wait_for_one_process()) { 
  } 
} 
static bool wait_for_one_process() { 
  int status; 
  pid_t pid = TEMP_FAILURE_RETRY(waitpid(-1, &status, WNOHANG));//子プロセスが終了するのを待ち、そのpidプロセス番号を取得します。WNOHANGは、プロセスが終了していない場合、すぐに戻ることを示します。 
  if (pid == 0) { 
    falseを返します; 
  } else if (pid == -1) { 
    ERROR("waitpidが失敗しました: %s\n", strerror(errno)); 
    falseを返します; 
  } 
  service* svc = service_find_by_pid(pid);//pidに基づいて、リストでこのサービス情報を見つけます 
  std::string name; 
  if (svc) { 
    name = android::base::StringPrintf("サービス '%s' (pid %d)", svc->name, pid); 
  } さもなければ { 
    name = android::base::StringPrintf("Untracked pid %d", pid); 
  } 
  NOTICE("%s %s\n", name.c_str(), DescribeStatus(status).c_str()); 
  if (!svc) { 
    return true; 
  } 
  // TODO: ここから以下のコードはserviceのメンバーファンクションであるべきです。 
  //SVC_ONESHOTフラグが設定されていないサービスプロセスまたはSVC_RESTARTフラグが設定されている場合、まず現在のプロセスを殺して新しいプロセスを再作成します; 
  //現在のサービスプロセスが既に存在するため、プロセスの再起動時にエラーが発生しないように、避けるために必要です。 
  if (!(svc->flags & SVC_ONESHOT) || (svc->flags & SVC_RESTART)) { 
    NOTICE("サービス '%s' (pid %d) プロセスグループ内のすべての子プロセスを殺します\n", svc->name, pid); 
    kill(-pid, SIGKILL); 
  } 
  // 作成した可能性のあるすべてのソケットを削除します。 
  //このサービスプロセスに対してソケットが以前に作成されていた場合、この時点でそのソケットをクリアする必要があります 
  for (socketinfo* si = svc->sockets; si; si = si->next) { 
    char tmp[128]; 
    snprintf(tmp, sizeof(tmp), ANDROID_SOCKET_DIR"/%s", si->name); 
    unlink(tmp);//このソケットデバイスファイルを削除します 
  } 
  if (svc->flags & SVC_EXEC) {////サービスが完全に終了し、すべての情報をクリアし、そのサービスをsvcから削除します-slist中移除 
    INFO("SVC_EXEC pid %d finished...\n", svc->pid); 
    waiting_for_exec = false; 
    list_remove(&svc->slist); 
    free(svc->name); 
    free(svc); 
    return true; 
  } 
  svc->pid = 0; 
  svc->flags &= (~SVC_RUNNING); 
  // Oneshotプロセスは終了時に無効化状態に入ります。 
  // 除いて手動で再起動された場合。 
  //サービスプロセスがSVC_ONESHOTフラグを持ち、SVC_RESTARTフラグを持っていない場合、そのサービスはリスタートする必要がありません 
  if ((svc->flags & SVC_ONESHOT) && !(svc->flags & SVC_RESTART)) { 
    svc->flags |= SVC_DISABLED; 
  } 
  // 無効化およびリセットされたプロセスは自動的に再起動されません。 
  //サービスがSVC_RESETフラグを持っている場合、サービスはリスタートする必要がありません 
  if (svc->flags & (SVC_DISABLED | SVC_RESET)) {//結果から見ると、SVC_RESETフラグの判断優先度が最高です 
    svc->NotifyStateChange("stopped"); 
    return true; 
  } 
  //ここで、サービスプロセスがinit.rcでSVC_ONESHOTおよびSVC_RESETフラグを宣言していない限り、そのプロセスが死亡すると、自動的にリスタートされることが分かります; 
  //ただし、サービスプロセスがSVC_CRITICALフラグを持ち、SVC_RESTARTフラグを持っていない場合、そのプロセスがクラッシュ、リスタートする回数が4その時、システムは自動的にリスタートし、リカバリーモードに進入します。 
  time_t now = gettime(); 
  if ((svc->flags & SVC_CRITICAL) && !(svc->flags & SVC_RESTART)) { 
    if (svc->time_crashed + CRITICAL_CRASH_WINDOW >= now) { 
      if (++svc->nr_crashed > CRITICAL_CRASH_THRESHOLD) { 
        ERROR("critical process '%s' exited %d times in %d minutes; " 
           "rebooting into recovery mode\n", svc->name, 
           CRITICAL_CRASH_THRESHOLD, CRITICAL_CRASH_WINDOW / 60); 
        android_reboot(ANDROID_RB_RESTART2, 0, "recovery"); 
        return true; 
      } 
    } さもなければ { 
      svc->time_crashed = now; 
      svc->nr_crashed = 1; 
    } 
  } 
  svc->flags &= (~SVC_RESTART); 
  svc->flags |= SVC_RESTARTING;//サービスに再起動フラグを追加し、再起動が必要であることを示し、その後の作業ではこの判定に基づく必要があります。 
  // このサービスのすべてのonrestartコマンドを実行します。 
  struct listnode* node; 
  list_for_each(node, &svc->onrestart.commands) {//サービスがonrestartオプションを持っている場合、プロセスが再起動時に実行するコマンドリストをループして実行します。 
    command* cmd = node_to_item(node, struct command, clist); 
    cmd->func(cmd->nargs, cmd->args); 
  } 
  svc->NotifyStateChange("restarting"); 
  return true; 
} 

この関数の処理は主に以下のポイントです:

  1. waitpid()を呼び出して子プロセスの終了を待ちます。waitpid()の返値は子プロセスのプロセス番号です。子プロセスが退出していない場合、WONHANGフラグが設定されているため、waitpid()はすぐに返り値を返し、待機しません。TEMP_FAILURE_RETRY()のネストされた意味は、先に紹介したものと似ています。waitpid()がエラーを返し、エラーコードがEINTRの場合、waitpid()は再び呼び出されます。
  2. pidに基づいて、service_listリストから対応するプロセスの対応するサービス情報を見つけます。そのサービスプロセスの定義がinit.rcにSVC_ONESHOTフラグが設定されていないか、またはSVC_RESTARTフラグが設定されている場合、まず現在のプロセスを殺し、新しいプロセスを再作成します;後でプロセスを再作成する際に、現在のサービスプロセスが既に存在しているためにエラーが発生することを避けるためにです。
  3. 現在のサービスにソケットが作成されている場合、そのソケットをクリアします。
  4. サービスプロセスがSVC_ONESHOTフラグを持ち、SVC_RESTARTフラグを持っていない場合、そのサービスは再起動する必要がありません。
  5. サービスにSVC_RESETフラグがある場合、サービスは再起動する必要がありません。
  6. サービスプロセスがSVC_CRITICALフラグを持ち、SVC_RESTARTフラグを持っていない場合、それがクラッシュしたり再起動したりする回数が4この時、システムは自動的に再起動し、リカバリーモードに入ります。
  7. サービスが再起動が必要と判定された場合、そのサービスに再起動フラグSVC_RESTARTINGを追加し、再起動が必要であることを示し、その後の作業ではこの判定に基づく必要があります。//重要
  8. 最後に、サービスにonrestartオプションがある場合、サービス再起動時に実行するコマンドリストを巡回し、これらのコマンドを実行します

この子プロセスが表すサービスが再起動する必要がある場合、そのサービスにSVC_RESTARTINGフラグが追加されます。

以前にInitプロセスの初期化プロセスについて紹介した際に、Initプロセスが処理を完了すると、それがデーモンプロセスとしてループに入り、signal、property、keychordなどのサービスを処理するようになります:

while (true) { 
   if (!waiting_for_exec) { 
     execute_one_command();//コマンドリストに含まれるコマンドを実行します 
     restart_processes();//サービスリストに含まれるプロセスを起動します 
   } 
   int timeout = -1; 
   if (process_needs_restart) { 
     timeout = (process_needs_restart - gettime()) * 1000; 
     if (timeout < 0) 
       timeout = 0; 
   } 
   if (!action_queue_empty() || cur_action) { 
     timeout = 0; 
   } 
   bootchart_sample(&timeout);//bootchartは、起動プロセスを視覚化してパフォーマンス分析を行うツールです;プロセスを定期的に唤醒する必要があります 
   epoll_event ev; 
   int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, timeout));//开始轮询,epoll_wait()等待事件产生 
   if (nr == -1) { 
     ERROR("epoll_wait failed: %s\n", strerror(errno)); 
   } else if (nr == 1) { 
     ((void (*)()) ev.data.ptr)();//epoll_eventイベントの関数ポインタを処理する関数を呼び出します 
   } 
 } 

その中で、restart_processes()をループして呼び出し、service_listリストにSVC_RESTARTINGフラグ(wait_for_one_process()の処理で設定されたフラグ)を持つすべてのサービスを再起動します:

static void restart_processes() 
{ 
  process_needs_restart = 0; 
  service_for_each_flags(SVC_RESTARTING,) 
              restart_service_if_needed); 
} 
void service_for_each_flags(unsigned matchflags, 
              void (*func)(struct service *svc)) 
{ 
  struct listnode *node; 
  struct service *svc; 
  list_for_each(node, &service_list) { 
    svc = node_to_item(node, struct service, slist); 
    if (svc->flags & matchflags) { 
      func(svc); 
    } 
  } 
} 

static void restart_service_if_needed(struct service *svc) 
{ 
  time_t next_start_time = svc->time_started + 5; 
  if (next_start_time <= gettime()) { 
    svc->flags &= (~SVC_RESTARTING); 
    service_start(svc, NULL); 
    return; 
  } 
  if ((next_start_time < process_needs_restart) || 
    (process_needs_restart == 0)) { 
    process_needs_restart = next_start_time; 
  } 
} 

最終的には、退出的サービスを再起動するためにservice_start()関数が呼び出されます。service_start()の処理プロセスは、Initプロセスの処理フローを紹介した際に分析しましたので、ここでは詳細は省略します。

ご読覧ありがとうございます。皆様のサポートに感謝します!

基本教程