Skip to content
Published at:

第4章:作业

作业简介

作业对象(Job Object)自 Windows 2000 起就已存在,用于管理一个或多个进程。其核心功能是通过各种限制(Limits)来控制进程的行为。

在 Windows 8 之前,一个进程只能属于一个作业;Windows 8 及以后,进程可以与多个作业相关联,形成嵌套的层次结构。

如果进程隶属于某个作业,在进程资源管理器(Process Explorer)中可以看到"作业"选项卡。默认情况下,作业中的进程在资源管理器中以棕色显示。

一旦进程与作业关联,它就无法脱离该作业——这是设计使然,否则作业的限制功能将失去意义。

创建作业

创建作业使用 CreateJobObject API:

cpp
HANDLE CreateJobObject(
    _In_opt_ LPSECURITY_ATTRIBUTES lpJobAttributes,
    _In_opt_ LPCWSTR             lpName
);

参数包括安全属性指针(通常为 NULL)和可选名称。如果同名作业已存在,会返回指向现有作业的句柄,可通过 GetLastError 检查 ERROR_ALREADY_EXISTS

打开现有作业使用 OpenJobObject

cpp
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_QUOTAPROCESS_TERMINATE 权限。受保护进程(Protected Process)无法成为作业的一部分。

子进程默认会继承作业归属,除非满足以下条件之一:

  • 调用 CreateProcess 时指定 CREATE_BREAKAWAY_FROM_JOB 标志,且作业允许脱离(设置了 JOB_OBJECT_LIMIT_BREAKAWAY_OK
  • 作业设置了 JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK,子进程无需特殊标志即可自动脱离

以下是通过 PID 打开进程并添加到作业的示例:

cpp
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

层次结构示例,按以下步骤构建:

  1. 将进程 P1 分配到作业 J1
  2. 将 P1 分配到作业 J2(此时 J2 成为 J1 的子作业,形成层次)
  3. 将进程 P2 分配到作业 J2(P2 同时受 J1 和 J2 的限制影响)
  4. 将进程 P3 分配到作业 J1

查看作业层次结构并不容易,因为缺少公开的枚举 API。开发者可以借助第三方工具(如 Job Explorer,GitHub: zodiacon/jobexplorer)来查看和浏览作业的层次关系。

以下代码展示了构建嵌套作业层次结构的完整过程:

cpp
#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

cpp
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 结构:

cpp
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 操作计数和传输大小:

cpp
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):

cpp
BOOL TerminateJobObject(
    _In_ HANDLE hJob,
    _In_ UINT   uExitCode
);

查询作业进程列表

使用 JobObjectBasicProcessIdList 信息类,返回 JOBOBJECT_BASIC_PROCESS_ID_LIST 结构,其中包含一个进程 ID 数组:

cpp
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 不同。

由于结构大小可变,需要动态分配缓冲区。以下示例展示了安全地获取进程列表的方式:

cpp
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 设置限制:

cpp
BOOL SetInformationJobObject(
    _In_ HANDLE             hJob,
    _In_ JOBOBJECTINFOCLASS JobObjectInformationClass,
    _In_ LPVOID             lpJobObjectInformation,
    _In_ DWORD              cbJobObjectInformationLength
);

作业句柄需要 JOB_OBJECT_SET_ATTRIBUTES 访问掩码。

基本与扩展限制

两个核心结构用于设置限制:

JOBOBJECT_BASIC_LIMIT_INFORMATION

cpp
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_WORKINGSETMinimumWorkingSetSize / MaximumWorkingSetSize
JOB_OBJECT_LIMIT_PROCESS_TIMEPerProcessUserTimeLimit
JOB_OBJECT_LIMIT_JOB_TIMEPerJobUserTimeLimit
JOB_OBJECT_LIMIT_ACTIVE_PROCESSActiveProcessLimit
JOB_OBJECT_LIMIT_AFFINITYAffinity
JOB_OBJECT_LIMIT_PRIORITY_CLASSPriorityClass
JOB_OBJECT_LIMIT_SCHEDULING_CLASSSchedulingClass
JOB_OBJECT_LIMIT_PROCESS_MEMORYProcessMemoryLimit(扩展结构)
JOB_OBJECT_LIMIT_JOB_MEMORYJobMemoryLimit(扩展结构)

以下示例设置作业中所有进程的优先级类别为"低于正常":

cpp
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 结构:

cpp
typedef struct _JOBOBJECT_CPU_RATE_CONTROL_INFORMATION {
    DWORD ControlFlags;
    union {
        DWORD CpuRate;   // 基于比率的硬上限
        DWORD Weight;    // 基于权重
        struct {
            DWORD MinRate; // 最小速率
            DWORD MaxRate; // 最大速率
        };
    };
} JOBOBJECT_CPU_RATE_CONTROL_INFORMATION;

三种控制方式:

  1. 基于比率的硬上限(Hard Cap)CpuRate 相对于 10000 指定百分比。例如,1500 表示 CPU 使用率上限为 15%。使用 JOB_OBJECT_CPU_RATE_CONTROL_ENABLE | JOB_OBJECT_CPU_RATE_CONTROL_HARD_CAP 标志
  2. 基于权重(Weight-Based)Weight 成员指定相对权重,使用 JOB_OBJECT_CPU_RATE_CONTROL_ENABLE | JOB_OBJECT_CPU_RATE_CONTROL_WEIGHT_BASED 标志
  3. 最小/最大速率(Min/Max Rate)MinRateMaxRate 指定范围,使用 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%:

cpp
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 字段控制:

cpp
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 由作业外部的进程授予对特定用户对象句柄的访问权限:

cpp
BOOL UserHandleGrantAccess(
    _In_ HANDLE hUserHandle,   // 要授予访问权限的用户对象句柄
    _In_ HANDLE hJob,          // 作业句柄
    _In_ BOOL   bGrant         // TRUE 授予访问,FALSE 撤销
);

以下示例为作业设置 UI 限制——禁止读取剪贴板、修改显示设置和关机:

cpp
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 时间违规时会变为已通知状态,但更灵活的方式是关联完成端口。

关联完成端口的步骤:

  1. 调用 CreateIoCompletionPort 创建完成端口(文件句柄传 INVALID_HANDLE_VALUE
  2. 填充 JOBOBJECT_ASSOCIATE_COMPLETION_PORT 结构:
cpp
typedef struct _JOBOBJECT_ASSOCIATE_COMPLETION_PORT {
    PVOID  CompletionKey;  // 应用程序定义的键值
    HANDLE CompletionPort; // 完成端口句柄
} JOBOBJECT_ASSOCIATE_COMPLETION_PORT;
  1. 调用 SetInformationJobObject(信息类为 JobObjectAssociateCompletionPortInformation
  2. 创建工作线程,循环调用 GetQueuedCompletionStatus 等待通知

通知类型(通过 GetQueuedCompletionStatusdwNumberOfBytesTransferred 参数传递类型,lpOverlapped 包含附加数据):

通知附加数据说明
JOB_OBJECT_MSG_END_OF_JOB_TIMENULL作业时间限制用尽(时间限制被取消)
JOB_OBJECT_MSG_END_OF_PROCESS_TIMEPID某进程超出 CPU 时间限制(正在被终止)
JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMITNULL活动进程数量超限
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERONULL活动进程数为零
JOB_OBJECT_MSG_NEW_PROCESSPID新进程加入作业
JOB_OBJECT_MSG_EXIT_PROCESSPID进程退出
JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESSPID进程异常退出
JOB_OBJECT_MSG_PROCESS_MEMORY_LIMITPID进程超出内存限制
JOB_OBJECT_MSG_JOB_MEMORY_LIMITPID作业超出全局内存限制
JOB_OBJECT_MSG_NOTIFICATION_LIMITPID超出通知限制

以下示例展示了如何设置完成端口并关联到作业:

cpp
#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,则仅发送通知而不终止进程(需要完成端口已关联):

cpp
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)可以将普通作业升级为隔离仓。

两种类型:

  1. 应用程序隔离仓(Application Silos) — 用于通过桌面桥接技术(Desktop Bridge)转换为 UWP 的应用程序,功能相对有限
  2. 服务器隔离仓(Server Silos) — 仅 Windows Server 支持,用于实现 Windows 容器(Windows Containers) 功能,对进程进行沙盒化(Sandboxing),重定向文件系统、注册表和对象命名空间

每个隔离仓有一个唯一的隔离仓 ID,用于内核内部进行分辨。借助 Job Explorer 等工具可以查看隔离仓的类型和相关信息。

练习

  1. 编写 MemLimit 工具,接受进程 ID 和最大提交内存数字(以 MB 为单位),通过作业设置该进程的内存限制。
  2. 扩展 JobMon 工具,使其能够显示所有剩余的限制信息(如 I/O 限制和网络限制)。

总结

作业(Job Object)提供了多种由内核实现的控制和限制进程的方式。通过作业,可以限制进程的 CPU 使用率、内存消耗、活动进程数、优先级、UI 访问等各个方面,还可以通过完成端口机制接收进程状态变化的通知。

Windows 8 引入的嵌套作业使作业更加灵活实用——进程可以属于多个作业,形成层次化的限制体系,子作业可以在父作业的限制基础上施加更严格的约束。Windows 10 进一步引入了隔离仓的概念,为容器化和沙盒化提供了底层支持。

下一章将转向线程——进程是资源管理对象,而线程才是实际分配到处理器上执行工作的实体。