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中线程池的中线程。

electron 主进程使用 axios electron关闭子进程_子进程