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