Electron源碼學習:Windows下子進程跟隨父進程結束的方式

Electron源碼學習:Windows下子進程跟隨父進程結束的方式

前言

​ 最近在nodejs中使用了child_process來創建進程,驚奇的發現當使用child_process.spawn函數來創建的子進程會跟隨父進程一起被Kill掉,不管子進程處於何種狀態下(即便子進程被掛起),都會被kill掉;而使用child_process.exec就不會。

​ 基於此,研究的興趣就來了。一直以來,都認爲Windows下進程的退出機制無外乎就是,主進程主動關閉,子進程主動退出;沒見過這種無論什麼狀態下,子進程都會退出的情況,確實有點兒劉姥姥進大觀園的感覺。

技術點

child_process.spawn的實現在libuv中,跟蹤該函數的調用後,發現這項應用是因爲使用了Windows的Job內核對象來完成的。而實現的方法僅僅是將子進程放到了一個設置有特殊權限的Job對象中。然後父進程退出時,子進程就會立即跟隨退出。

​ Job對象是Windows的一個進程池(注意不是線程池)的概念實現,相關的概述可以參考《Windows核心編程》或者MSDN。除開可以關子進程,能幹的事情非常多;例如:設置子進程的運行時長,內存的申請限制,CPU資源分配等等;該內核對象也通常和完成端口配合使用。

​ **第一步:**Job對象的創建:

SECURITY_ATTRIBUTES attr;
JOBOBJECT_EXTENDED_LIMIT_INFORMATION info;

memset(&attr, 0, sizeof attr);
attr.bInheritHandle = FALSE;

memset(&info, 0, sizeof info);
info.BasicLimitInformation.LimitFlags =
    JOB_OBJECT_LIMIT_BREAKAWAY_OK |
    JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK |
    JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION |
    JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;

uv_global_job_handle_ = CreateJobObjectW(&attr, NULL);
if (uv_global_job_handle_ == NULL)
    uv_fatal_error(GetLastError(), "CreateJobObjectW");

if (!SetInformationJobObject(uv_global_job_handle_,
                             JobObjectExtendedLimitInformation,
                             &info,
                             sizeof info))
    uv_fatal_error(GetLastError(), "SetInformationJobObject");

上面的代碼比較少,值得關注的地方就是設置LimitFlags的代碼,在設置的Flags的時候;

JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION標誌: 當子進程遇到沒有處理的異常時,在沒有調試器的情況下,會關閉子進程,並將異常碼設置爲退出碼。

JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE標誌:該標誌的作用就是在Job對象關閉時子進程會跟隨退出,該操作由Windows完成;當父進程退出時,Job對象會自動銷燬,所以子進程就會跟隨退出了。

MSDN相關連接: https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_basic_limit_information?redirectedfrom=MSDN

**第二步:**libuv中將子進程分配給Job對象:

if (!CreateProcessW(application_path,
                     arguments,
                     NULL,
                     NULL,
                     1,
                     process_flags,
                     env,
                     cwd,
                     &startup,
                     &info)) {
    /* CreateProcessW failed. */
    err = GetLastError();
    goto done;
  }

  /* Spawn succeeded. Beyond this point, failure is reported asynchronously. */

  process->process_handle = info.hProcess;
  process->pid = info.dwProcessId;

  /* If the process isn't spawned as detached, assign to the global job object
   * so windows will kill it when the parent process dies. */
  if (!(options->flags & UV_PROCESS_DETACHED)) {
    uv_once(&uv_global_job_handle_init_guard_, uv__init_global_job_handle);

    if (!AssignProcessToJobObject(uv_global_job_handle_, info.hProcess)) {
      /* AssignProcessToJobObject might fail if this process is under job
       * control and the job doesn't have the
       * JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK flag set, on a Windows version
       * that doesn't support nested jobs.
       *
       * When that happens we just swallow the error and continue without
       * establishing a kill-child-on-parent-exit relationship, otherwise
       * there would be no way for libuv applications run under job control
       * to spawn processes at all.
       */
      DWORD err = GetLastError();
      if (err != ERROR_ACCESS_DENIED)
        uv_fatal_error(err, "AssignProcessToJobObject");
    }
  }

上面代碼中,在CreateProcess後,隨即調用AssignProcessToJobObject將子進程綁定到了Job對象中,其他的就不用解釋了,看代碼註釋就行。

等待子進程退出

​ 我們都知道進程的句柄是一個內核對象,那麼使用WaitForSingleObject等待該對象可以得知目標進程是否退出。這是一般的寫法,還有一種有意思的寫法,比較乾淨。

函數名稱: RegisterWaitForSingleObject,示例如下:

static void CALLBACK exit_wait_callback(void* data, BOOLEAN didTimeout) {
  uv_process_t* process = (uv_process_t*) data;
  uv_loop_t* loop = process->loop;

  assert(didTimeout == FALSE);
  assert(process);
  assert(!process->exit_cb_pending);

  process->exit_cb_pending = 1;

  /* Post completed */
  POST_COMPLETION_FOR_REQ(loop, &process->exit_req);
}

/* Setup notifications for when the child process exits. */
result = RegisterWaitForSingleObject(&process->wait_handle,
    process->process_handle, exit_wait_callback, (void*)process, INFINITE,
    WT_EXECUTEINWAITTHREAD | WT_EXECUTEONLYONCE);
if (!result) {
    uv_fatal_error(GetLastError(), "RegisterWaitForSingleObject");
}

以上的代碼就完成了一個等待進程結束的註冊,然後就什麼就不用幹了。然後當目標進程結束時,操作系統會調用線程入口爲ntdll.TppWorkerThread的線程執行來exit_wait_callback函數;

**注意:**以ntdll.TppWorkerThread爲入口的線程,是Windows中線程池的中線程。

子進程結束回調

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章