事实证明这是因为我们使用JobObjects来确保所有子进程在当前进程退出时使用 C 中的此代码退出(我们实际上是从 C# 调用的):
HANDLE h = ::CreateJobObject(NULL, NULL);
JOBOBJECT_EXTENDED_LIMIT_INFORMATION info;
::ZeroMemory(&info, sizeof(info));
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
::SetInformationJobObject(h, JobObjectExtendedLimitInformation, &info, sizeof(info));
::AssignProcessToJobObject(h, ::GetCurrentProcess());
...
::CloseHandle(h);
return -1;
此代码将当前进程及其所有子进程添加到将在当前进程退出时关闭的作业对象。但是它在被调用时会产生副作用,CloseHandle
它会杀死当前进程而不会到达 line return -1
。而且由于JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
flag 会自动杀死所有进程,因此无法为所有进程设置退出代码,因此 OS 以退出代码 0 退出进程。
在 C# 中,我们遵循标准准则来清理资源并使用 -SafeHandle
派生类来确保CloseHandle
调用它并且发生完全相同的情况 - 在 CLR 实际退出之前,它::CloseHandle
为所有SafeHandle
s 调用,忽略返回值和Environment.Exit
.
然而更有趣的是,如果CloseHandle
在 C# 和 C++ 中都删除了显式(或不那么显式)调用,操作系统仍将在 CLR/CRT 退出后关闭进程退出时的所有句柄,并且实际的退出代码将被退回。所以有时最好不要清理资源:-) 或者换句话说,在::ExitProcess
调用本机之前,你不能保证退出代码是完整的。
因此,为了解决这个特定问题,我可以AssignProcessToJobObject
在子进程启动时调用,也可以删除对CloseHandle
. 我选择了第一种方法。