第4章:作业
作业简介
作业对象(Job Object)自 Windows 2000 起就已存在,用于管理一个或多个进程。其核心功能是通过各种限制(Limits)来控制进程的行为。
在 Windows 8 之前,一个进程只能属于一个作业;Windows 8 及以后,进程可以与多个作业相关联,形成嵌套的层次结构。
如果进程隶属于某个作业,在进程资源管理器(Process Explorer)中可以看到"作业"选项卡。默认情况下,作业中的进程在资源管理器中以棕色显示。
一旦进程与作业关联,它就无法脱离该作业——这是设计使然,否则作业的限制功能将失去意义。
创建作业
创建作业使用 CreateJobObject API:
HANDLE CreateJobObject(
_In_opt_ LPSECURITY_ATTRIBUTES lpJobAttributes,
_In_opt_ LPCWSTR lpName
);参数包括安全属性指针(通常为 NULL)和可选名称。如果同名作业已存在,会返回指向现有作业的句柄,可通过 GetLastError 检查 ERROR_ALREADY_EXISTS。
打开现有作业使用 OpenJobObject:
HANDLE OpenJobObject(
_In_ DWORD dwDesiredAccess,
_In_ BOOL bInheritHandle,
_In_ LPCWSTR lpName
);需要指定访问掩码(Access Mask)、继承句柄标志和名称。
关键访问掩码:
| 掩码 | 说明 |
|---|---|
JOB_OBJECT_QUERY | 执行查询操作 |
JOB_OBJECT_ASSIGN_PROCESS | 允许添加进程 |
JOB_OBJECT_SET_ATTRIBUTES | 调用 SetInformationJobObject 所需 |
JOB_OBJECT_TERMINATE | 调用 TerminateJobObject 所需 |
AssignProcessToJobObject 将进程关联到作业。作业句柄需要 JOB_OBJECT_ASSIGN_PROCESS 权限,进程句柄需要 PROCESS_SET_QUOTA 和 PROCESS_TERMINATE 权限。受保护进程(Protected Process)无法成为作业的一部分。
子进程默认会继承作业归属,除非满足以下条件之一:
- 调用
CreateProcess时指定CREATE_BREAKAWAY_FROM_JOB标志,且作业允许脱离(设置了JOB_OBJECT_LIMIT_BREAKAWAY_OK) - 作业设置了
JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK,子进程无需特殊标志即可自动脱离
以下是通过 PID 打开进程并添加到作业的示例:
bool AddProcessToJob(HANDLE hJob, DWORD pid) {
HANDLE hProcess = ::OpenProcess(
PROCESS_SET_QUOTA | PROCESS_TERMINATE, FALSE, pid);
if (!hProcess)
return false;
BOOL success = ::AssignProcessToJobObject(hJob, hProcess);
::CloseHandle(hProcess);
return success ? true : false;
}嵌套作业
Windows 8 引入了嵌套作业(Nested Job)功能,允许一个进程与多个作业相关联。当进程被分配到第二个作业时,会形成层次结构(Hierarchy):第二个作业成为第一个的子作业。
核心规则:
- 父作业的限制会向下传播到所有子作业及其进程
- 子作业无法取消父作业的限制,但可以设置更严格的限制。例如,父作业设置了 200MB 内存限制,子作业可以设为 150MB,但不能设为 250MB
层次结构示例,按以下步骤构建:
- 将进程 P1 分配到作业 J1
- 将 P1 分配到作业 J2(此时 J2 成为 J1 的子作业,形成层次)
- 将进程 P2 分配到作业 J2(P2 同时受 J1 和 J2 的限制影响)
- 将进程 P3 分配到作业 J1
查看作业层次结构并不容易,因为缺少公开的枚举 API。开发者可以借助第三方工具(如 Job Explorer,GitHub: zodiacon/jobexplorer)来查看和浏览作业的层次关系。
以下代码展示了构建嵌套作业层次结构的完整过程:
#include <windows.h>
#include <stdio.h>
int main() {
// 创建两个命名作业
HANDLE hJob1 = ::CreateJobObject(nullptr, L"Job1");
HANDLE hJob2 = ::CreateJobObject(nullptr, L"Job2");
// 创建进程 P1 (mspaint),默认继承当前作业(如果有的话)
// 这里假设当前进程不在作业中
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
// P1 -> J1
::CreateProcess(L"c:\\windows\\system32\\mspaint.exe",
nullptr, nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi);
::AssignProcessToJobObject(hJob1, pi.hProcess);
::CloseHandle(pi.hThread);
// P1 -> J2(此时 J2 成为 J1 的子作业)
::AssignProcessToJobObject(hJob2, pi.hProcess);
::CloseHandle(pi.hProcess);
// 创建 P2 (mstsc),并分配到 J2
// P2 需要 BREAKAWAY 标志以脱离潜在的默认作业
::CreateProcess(L"c:\\windows\\system32\\mstsc.exe",
nullptr, nullptr, nullptr, FALSE,
CREATE_BREAKAWAY_FROM_JOB, nullptr, nullptr, &si, &pi);
::AssignProcessToJobObject(hJob2, pi.hProcess);
::CloseHandle(pi.hThread);
::CloseHandle(pi.hProcess);
// 创建 P3 (cmd),分配到 J1
::CreateProcess(L"c:\\windows\\system32\\cmd.exe",
nullptr, nullptr, nullptr, FALSE,
CREATE_BREAKAWAY_FROM_JOB, nullptr, nullptr, &si, &pi);
::AssignProcessToJobObject(hJob1, pi.hProcess);
::CloseHandle(pi.hThread);
::CloseHandle(pi.hProcess);
printf("Job hierarchy created. Press Enter to terminate...\n");
getchar();
// 关闭作业句柄,终止所有关联进程
// (前提是作业设置了 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)
::CloseHandle(hJob1);
::CloseHandle(hJob2);
return 0;
}查询作业信息
主要 API 是 QueryInformationJobObject:
BOOL QueryInformationJobObject(
_In_opt_ HANDLE hJob,
_In_ JOBOBJECTINFOCLASS JobObjectInformationClass,
_Out_ LPVOID lpJobObjectInformation,
_In_ DWORD cbJobObjectInformationLength,
_Out_opt_ LPDWORD lpReturnLength
);作业句柄需要 JOB_OBJECT_QUERY 访问掩码。一个有趣的特性是:可以将 hJob 设为 NULL,此时查询的是调用进程所属的直接作业。
JOBOBJECTINFOCLASS 枚举定义了可查询的信息类型,包括:
JobObjectBasicAccountingInformation— 基本记账信息JobObjectBasicLimitInformation— 基本限制信息JobObjectBasicProcessIdList— 进程 ID 列表JobObjectBasicUIRestrictions— 用户界面限制JobObjectExtendedLimitInformation— 扩展限制信息JobObjectCpuRateControlInformation— CPU 速率控制JobObjectNetRateControlInformation— 网络速率控制
作业记账信息
无论是否设置限制,作业都会跟踪基本统计信息(Accounting Information)。使用 JobObjectBasicAccountingInformation 信息类查询,返回 JOBOBJECT_BASIC_ACCOUNTING_INFORMATION 结构:
typedef struct _JOBOBJECT_BASIC_ACCOUNTING_INFORMATION {
LARGE_INTEGER TotalUserTime; // 总用户模式 CPU 时间(100 纳秒单位)
LARGE_INTEGER TotalKernelTime; // 总内核模式 CPU 时间(100 纳秒单位)
LARGE_INTEGER ThisPeriodTotalUserTime; // 本周期用户模式 CPU 时间
LARGE_INTEGER ThisPeriodTotalKernelTime; // 本周期内核模式 CPU 时间
DWORD TotalPageFaultCount; // 页面错误计数
DWORD TotalProcesses; // 曾经存在的总进程数
DWORD ActiveProcesses; // 当前活动进程数
DWORD TotalTerminatedProcesses; // 因限制违规而终止的进程数
} JOBOBJECT_BASIC_ACCOUNTING_INFORMATION;"本周期"(This Period)时间计数器从最近一次设置每个作业的时间限制时开始累计。
扩展记账信息使用 JobObjectBasicAndIoAccountingInformation 信息类,返回 JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION 结构,额外包含 I/O 操作计数和传输大小:
typedef struct JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION {
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION BasicInfo;
IO_COUNTERS IoInfo; // 读取/写入/其他操作计数与字节数
} JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION;TerminateJobObject 可以一次性终止作业中的所有进程,行为如同对每个进程调用 TerminateProcess,所有进程获得相同的退出代码(Exit Code):
BOOL TerminateJobObject(
_In_ HANDLE hJob,
_In_ UINT uExitCode
);查询作业进程列表
使用 JobObjectBasicProcessIdList 信息类,返回 JOBOBJECT_BASIC_PROCESS_ID_LIST 结构,其中包含一个进程 ID 数组:
typedef struct _JOBOBJECT_BASIC_PROCESS_ID_LIST {
DWORD NumberOfAssignedProcesses; // 已分配的进程数
DWORD NumberOfProcessIdsInList; // 列表中实际返回的进程 ID 数
ULONG_PTR ProcessIdList[1]; // 进程 ID 数组(可变长度)
} JOBOBJECT_BASIC_PROCESS_ID_LIST;需要注意,结构中返回的进程 ID 类型为 ULONG_PTR(在 64 位系统上是 64 位值),这与通常的 32 位 DWORD 类型的进程 ID 不同。
由于结构大小可变,需要动态分配缓冲区。以下示例展示了安全地获取进程列表的方式:
std::vector<DWORD> GetJobProcessList(HANDLE hJob) {
std::vector<DWORD> pids;
DWORD size = 1 << 16; // 64KB 初始缓冲区
auto buffer = std::make_unique<BYTE[]>(size);
auto info = reinterpret_cast<JOBOBJECT_BASIC_PROCESS_ID_LIST*>(buffer.get());
while (!::QueryInformationJobObject(hJob,
JobObjectBasicProcessIdList, buffer.get(), size, nullptr)) {
if (::GetLastError() == ERROR_MORE_DATA) {
size *= 2;
buffer = std::make_unique<BYTE[]>(size);
info = reinterpret_cast<JOBOBJECT_BASIC_PROCESS_ID_LIST*>(buffer.get());
} else {
return pids; // 查询失败
}
}
for (DWORD i = 0; i < info->NumberOfProcessIdsInList; i++) {
pids.push_back((DWORD)info->ProcessIdList[i]);
}
return pids;
}设置作业限制
使用 SetInformationJobObject 设置限制:
BOOL SetInformationJobObject(
_In_ HANDLE hJob,
_In_ JOBOBJECTINFOCLASS JobObjectInformationClass,
_In_ LPVOID lpJobObjectInformation,
_In_ DWORD cbJobObjectInformationLength
);作业句柄需要 JOB_OBJECT_SET_ATTRIBUTES 访问掩码。
基本与扩展限制
两个核心结构用于设置限制:
JOBOBJECT_BASIC_LIMIT_INFORMATION:
typedef struct _JOBOBJECT_BASIC_LIMIT_INFORMATION {
LARGE_INTEGER PerProcessUserTimeLimit; // 每个进程的用户时间限制
LARGE_INTEGER PerJobUserTimeLimit; // 整个作业的用户时间限制
DWORD LimitFlags; // 控制哪些限制生效
SIZE_T MinimumWorkingSetSize; // 最小工作集大小
SIZE_T MaximumWorkingSetSize; // 最大工作集大小
DWORD ActiveProcessLimit; // 活动进程数量上限
ULONG_PTR Affinity; // CPU 亲和力
DWORD PriorityClass; // 优先级类别
DWORD SchedulingClass; // 调度类别(0-9,默认 5)
} JOBOBJECT_BASIC_LIMIT_INFORMATION;JOBOBJECT_EXTENDED_LIMIT_INFORMATION 在基本限制的基础上增加了:
- I/O 计数器
- 每个进程 / 整个作业的内存限制
- 峰值内存使用
LimitFlags 控制哪些限制生效,标志分为两类:
无关联成员的标志(仅标志自身即可生效):
| 标志 | 说明 |
|---|---|
JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION | 阻止未处理异常的对话框弹出 |
JOB_OBJECT_LIMIT_BREAKAWAY_OK | 允许进程通过 CREATE_BREAKAWAY_FROM_JOB 脱离 |
JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK | 子进程自动脱离,无需特殊标志 |
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | 最后一个作业句柄关闭时终止所有进程 |
JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME | 保留之前的作业时间设置 |
有关联成员的标志(需同时设置对应的字段值):
| 标志 | 关联成员 |
|---|---|
JOB_OBJECT_LIMIT_WORKINGSET | MinimumWorkingSetSize / MaximumWorkingSetSize |
JOB_OBJECT_LIMIT_PROCESS_TIME | PerProcessUserTimeLimit |
JOB_OBJECT_LIMIT_JOB_TIME | PerJobUserTimeLimit |
JOB_OBJECT_LIMIT_ACTIVE_PROCESS | ActiveProcessLimit |
JOB_OBJECT_LIMIT_AFFINITY | Affinity |
JOB_OBJECT_LIMIT_PRIORITY_CLASS | PriorityClass |
JOB_OBJECT_LIMIT_SCHEDULING_CLASS | SchedulingClass |
JOB_OBJECT_LIMIT_PROCESS_MEMORY | ProcessMemoryLimit(扩展结构) |
JOB_OBJECT_LIMIT_JOB_MEMORY | JobMemoryLimit(扩展结构) |
以下示例设置作业中所有进程的优先级类别为"低于正常":
JOBOBJECT_BASIC_LIMIT_INFORMATION info = {};
info.LimitFlags = JOB_OBJECT_LIMIT_PRIORITY_CLASS;
info.PriorityClass = BELOW_NORMAL_PRIORITY_CLASS;
::SetInformationJobObject(hJob, JobObjectBasicLimitInformation,
&info, sizeof(info));设置后,通过任务管理器尝试更改优先级会无效,因为作业限制优先于手动设置。
CPU 速率限制
Windows 8 新增的独立功能,使用 JobObjectCpuRateControlInformation 信息类和 JOBOBJECT_CPU_RATE_CONTROL_INFORMATION 结构:
typedef struct _JOBOBJECT_CPU_RATE_CONTROL_INFORMATION {
DWORD ControlFlags;
union {
DWORD CpuRate; // 基于比率的硬上限
DWORD Weight; // 基于权重
struct {
DWORD MinRate; // 最小速率
DWORD MaxRate; // 最大速率
};
};
} JOBOBJECT_CPU_RATE_CONTROL_INFORMATION;三种控制方式:
- 基于比率的硬上限(Hard Cap) —
CpuRate相对于 10000 指定百分比。例如,1500 表示 CPU 使用率上限为 15%。使用JOB_OBJECT_CPU_RATE_CONTROL_ENABLE | JOB_OBJECT_CPU_RATE_CONTROL_HARD_CAP标志 - 基于权重(Weight-Based) —
Weight成员指定相对权重,使用JOB_OBJECT_CPU_RATE_CONTROL_ENABLE | JOB_OBJECT_CPU_RATE_CONTROL_WEIGHT_BASED标志 - 最小/最大速率(Min/Max Rate) —
MinRate和MaxRate指定范围,使用JOB_OBJECT_CPU_RATE_CONTROL_ENABLE | JOB_OBJECT_CPU_RATE_CONTROL_MIN_MAX_RATE标志
控制标志一览:
| 标志 | 说明 |
|---|---|
JOB_OBJECT_CPU_RATE_CONTROL_ENABLE | 启用 CPU 速率控制 |
JOB_OBJECT_CPU_RATE_CONTROL_WEIGHT_BASED | 使用基于权重的模式 |
JOB_OBJECT_CPU_RATE_CONTROL_HARD_CAP | 使用硬上限模式 |
JOB_OBJECT_CPU_RATE_CONTROL_NOTIFY | 在 CPU 使用超出限制时发送通知 |
JOB_OBJECT_CPU_RATE_CONTROL_MIN_MAX_RATE | 使用最小/最大速率模式 |
使用 HARD_CAP 时,即使系统有可用 CPU 资源,作业也不会获得超出限制的 CPU 周期。内核以 300 毫秒为间隔测量 CPU 消耗来实施限制。
以下示例将作业的 CPU 使用率限制为 20%:
JOBOBJECT_CPU_RATE_CONTROL_INFORMATION cpuInfo = {};
cpuInfo.ControlFlags = JOB_OBJECT_CPU_RATE_CONTROL_ENABLE
| JOB_OBJECT_CPU_RATE_CONTROL_HARD_CAP;
cpuInfo.CpuRate = 2000; // 2000 / 10000 = 20%
::SetInformationJobObject(hJob, JobObjectCpuRateControlInformation,
&cpuInfo, sizeof(cpuInfo));用户界面限制
使用 JobObjectBasicUIRestrictions 信息类,由 JOBOBJECT_BASIC_UI_RESTRICTIONS 结构的单个 UIRestrictionsClass 字段控制:
typedef struct _JOBOBJECT_BASIC_UI_RESTRICTIONS {
DWORD UIRestrictionsClass;
} JOBOBJECT_BASIC_UI_RESTRICTIONS;可用的 UI 限制标志:
| 标志 | 说明 |
|---|---|
JOB_OBJECT_UILIMIT_HANDLES | 无法访问其他进程的用户句柄(窗口等) |
JOB_OBJECT_UILIMIT_READCLIPBOARD | 无法从剪贴板读取 |
JOB_OBJECT_UILIMIT_WRITECLIPBOARD | 无法写入剪贴板 |
JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS | 无法更改系统参数 |
JOB_OBJECT_UILIMIT_DISPLAYSETTINGS | 无法更改显示设置 |
JOB_OBJECT_UILIMIT_GLOBALATOMS | 无法访问全局原子表(作业有自己的原子表) |
JOB_OBJECT_UILIMIT_DESKTOP | 无法创建或切换桌面 |
JOB_OBJECT_UILIMIT_EXITWINDOWS | 无法调用 ExitWindows / ExitWindowsEx |
重要限制: 设置了 UI 限制的作业不能成为作业层次结构的一部分。
HANDLES 标志下,作业内部的进程可通过 UserHandleGrantAccess 由作业外部的进程授予对特定用户对象句柄的访问权限:
BOOL UserHandleGrantAccess(
_In_ HANDLE hUserHandle, // 要授予访问权限的用户对象句柄
_In_ HANDLE hJob, // 作业句柄
_In_ BOOL bGrant // TRUE 授予访问,FALSE 撤销
);以下示例为作业设置 UI 限制——禁止读取剪贴板、修改显示设置和关机:
JOBOBJECT_BASIC_UI_RESTRICTIONS uiRestrictions = {};
uiRestrictions.UIRestrictionsClass =
JOB_OBJECT_UILIMIT_READCLIPBOARD |
JOB_OBJECT_UILIMIT_DISPLAYSETTINGS |
JOB_OBJECT_UILIMIT_EXITWINDOWS;
::SetInformationJobObject(hJob, JobObjectBasicUIRestrictions,
&uiRestrictions, sizeof(uiRestrictions));作业通知
作业可以通过 I/O 完成端口(I/O Completion Port)发送通知。虽然作业本身是一个可等待对象(Kernel Object),在发生 CPU 时间违规时会变为已通知状态,但更灵活的方式是关联完成端口。
关联完成端口的步骤:
- 调用
CreateIoCompletionPort创建完成端口(文件句柄传INVALID_HANDLE_VALUE) - 填充
JOBOBJECT_ASSOCIATE_COMPLETION_PORT结构:
typedef struct _JOBOBJECT_ASSOCIATE_COMPLETION_PORT {
PVOID CompletionKey; // 应用程序定义的键值
HANDLE CompletionPort; // 完成端口句柄
} JOBOBJECT_ASSOCIATE_COMPLETION_PORT;- 调用
SetInformationJobObject(信息类为JobObjectAssociateCompletionPortInformation) - 创建工作线程,循环调用
GetQueuedCompletionStatus等待通知
通知类型(通过 GetQueuedCompletionStatus 的 dwNumberOfBytesTransferred 参数传递类型,lpOverlapped 包含附加数据):
| 通知 | 附加数据 | 说明 |
|---|---|---|
JOB_OBJECT_MSG_END_OF_JOB_TIME | NULL | 作业时间限制用尽(时间限制被取消) |
JOB_OBJECT_MSG_END_OF_PROCESS_TIME | PID | 某进程超出 CPU 时间限制(正在被终止) |
JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT | NULL | 活动进程数量超限 |
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO | NULL | 活动进程数为零 |
JOB_OBJECT_MSG_NEW_PROCESS | PID | 新进程加入作业 |
JOB_OBJECT_MSG_EXIT_PROCESS | PID | 进程退出 |
JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS | PID | 进程异常退出 |
JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT | PID | 进程超出内存限制 |
JOB_OBJECT_MSG_JOB_MEMORY_LIMIT | PID | 作业超出全局内存限制 |
JOB_OBJECT_MSG_NOTIFICATION_LIMIT | PID | 超出通知限制 |
以下示例展示了如何设置完成端口并关联到作业:
#include <windows.h>
#include <stdio.h>
// 工作线程,持续等待作业通知
DWORD WINAPI JobMonitorThread(PVOID param) {
HANDLE hCompPort = (HANDLE)param;
while (true) {
DWORD msgId;
ULONG_PTR completionKey;
LPOVERLAPPED pOverlapped;
BOOL ok = ::GetQueuedCompletionStatus(
hCompPort, &msgId, &completionKey, &pOverlapped, INFINITE);
if (!ok) {
// 完成端口可能已关闭
break;
}
DWORD pid = PtrToUlong(pOverlapped);
switch (msgId) {
case JOB_OBJECT_MSG_NEW_PROCESS:
printf("进程 %lu 加入作业\n", pid);
break;
case JOB_OBJECT_MSG_EXIT_PROCESS:
printf("进程 %lu 退出\n", pid);
break;
case JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS:
printf("进程 %lu 异常退出\n", pid);
break;
case JOB_OBJECT_MSG_END_OF_JOB_TIME:
printf("作业时间限制用尽\n");
break;
case JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO:
printf("作业中无活动进程\n");
break;
case JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT:
printf("进程 %lu 超出内存限制\n", pid);
break;
default:
printf("未知通知: %lu\n", msgId);
break;
}
}
return 0;
}
void SetupJobNotifications(HANDLE hJob) {
// 创建完成端口
HANDLE hCompPort = ::CreateIoCompletionPort(
INVALID_HANDLE_VALUE, nullptr, 0, 0);
// 关联完成端口到作业
JOBOBJECT_ASSOCIATE_COMPLETION_PORT portInfo = {};
portInfo.CompletionKey = (PVOID)0x1234; // 应用程序自定义
portInfo.CompletionPort = hCompPort;
::SetInformationJobObject(hJob,
JobObjectAssociateCompletionPortInformation,
&portInfo, sizeof(portInfo));
// 启动监控线程
HANDLE hThread = ::CreateThread(nullptr, 0,
JobMonitorThread, hCompPort, 0, nullptr);
// 实际应用中应保存线程句柄以便后续管理
}对于时间限制违规,默认操作是终止所有进程(退出码设为 ERROR_NOT_ENOUGH_QUOTA)。可以通过 JOBOBJECT_END_OF_JOB_TIME_INFORMATION 结构来改变这一行为——将 EndOfJobTimeAction 设为 JOB_OBJECT_POST_AT_END_OF_JOB,则仅发送通知而不终止进程(需要完成端口已关联):
JOBOBJECT_END_OF_JOB_TIME_INFORMATION endInfo = {};
endInfo.EndOfJobTimeAction = JOB_OBJECT_POST_AT_END_OF_JOB;
::SetInformationJobObject(hJob, JobObjectEndOfJobTimeInformation,
&endInfo, sizeof(endInfo));隔离仓(Silos)
隔离仓(Silo)是 Windows 10 1607 和 Windows Server 2016 引入的增强型作业。通过 SetInformationJobObject 使用未文档化的信息类 JobObjectCreateSilo(值为 35)可以将普通作业升级为隔离仓。
两种类型:
- 应用程序隔离仓(Application Silos) — 用于通过桌面桥接技术(Desktop Bridge)转换为 UWP 的应用程序,功能相对有限
- 服务器隔离仓(Server Silos) — 仅 Windows Server 支持,用于实现 Windows 容器(Windows Containers) 功能,对进程进行沙盒化(Sandboxing),重定向文件系统、注册表和对象命名空间
每个隔离仓有一个唯一的隔离仓 ID,用于内核内部进行分辨。借助 Job Explorer 等工具可以查看隔离仓的类型和相关信息。
练习
- 编写 MemLimit 工具,接受进程 ID 和最大提交内存数字(以 MB 为单位),通过作业设置该进程的内存限制。
- 扩展 JobMon 工具,使其能够显示所有剩余的限制信息(如 I/O 限制和网络限制)。
总结
作业(Job Object)提供了多种由内核实现的控制和限制进程的方式。通过作业,可以限制进程的 CPU 使用率、内存消耗、活动进程数、优先级、UI 访问等各个方面,还可以通过完成端口机制接收进程状态变化的通知。
Windows 8 引入的嵌套作业使作业更加灵活实用——进程可以属于多个作业,形成层次化的限制体系,子作业可以在父作业的限制基础上施加更严格的约束。Windows 10 进一步引入了隔离仓的概念,为容器化和沙盒化提供了底层支持。
下一章将转向线程——进程是资源管理对象,而线程才是实际分配到处理器上执行工作的实体。