Electron源码学习:Windows下子进程跟随父进程结束的方式

    技术2022-07-10  145

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

    Processed: 0.012, SQL: 9