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