让我们与星光探索者一起,探索Windows吧!

内核对象作业Job

在Visual Studio中,我们有时候想在编译文件中途终止编译,调试程序中途终止调试,Visual Studio是怎么做的呢?虽然这是一个简单而常见的问题,但windows没有维护线程之间的父子关系,解决起来非常难。

Windows提供了一个作业(job)内核对象,它允许你将进程组合在一起并创建一个”沙箱”来限制进程能够做什么.最好将作业对象想象成一个进程容器.但是,即使作业中只包含一个进程,也是非常有用的,因为这样可以对进程施加平时不能施加的限制.点我查看微软官方对于作业对象的介绍

下面的程序代码,将尝试将一个进程放到一个作业中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#pragma comment(lib, "user32.lib")

int _tmain()
{
BOOL bInJob = FALSE;
// 判断进程是否已被放入作业中
IsProcessInJob(GetCurrentProcess(), NULL, &bInJob);
if (bInJob)
{
MessageBox(NULL, TEXT("本进程已被放入作业"), TEXT(""), MB_OK);
return 0;
}

// 创建作业内核对象
HANDLE hJob = CreateJobObject(NULL, TEXT("MySecretJob"));

// 设置作业的基本限制事项
JOBOBJECT_BASIC_LIMIT_INFORMATION joblimit = {0};
// 设置优先级类,和设置进程的优先级类很像
joblimit.PriorityClass = IDLE_PRIORITY_CLASS;
// 设置作业的执行时间限制,单位纳米秒
joblimit.PerProcessUserTimeLimit.QuadPart = 10000;
// 设置作业的生效标志,不设置会导致不生效
joblimit.LimitFlags = JOB_OBJECT_LIMIT_PRIORITY_CLASS | JOB_OBJECT_LIMIT_JOB_TIME;

// 设置作业对象的基本用户界面控制
JOBOBJECT_BASIC_UI_RESTRICTIONS jobuir;
// 阻止作业内进程注销用户
jobuir.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_EXITWINDOWS;
// 防止与作业内进程访问同一个作业外部进程创建的内核对象
jobuir.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_HANDLES;

// 调用SetInformationJobObject,
// 依次将限制效果作用在作业对象上
SetInformationJobObject(hJob, JobObjectBasicLimitInformation, &joblimit, sizeof(joblimit));
SetInformationJobObject(hJob, JobObjectBasicUIRestrictions, &jobuir, sizeof(jobuir));

// 创建进程
STARTUPINFO si = {sizeof(si)};
PROCESS_INFORMATION pi;
// 注意:在创建进程时,这里使用了CREATE_SUSPENDED参数,这样虽然会创建新进程,
// 但不允许它执行任何代码。这样就可以防止子进程“逃离”我们的沙箱
CreateProcess(TEXT("C:\\windows\\System32\\cmd.exe"), NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);

// 将进程添加到作业,这个过程要显式设置
AssignProcessToJobObject(hJob, pi.hProcess);
// 唤醒进程的主线程
ResumeThread(pi.hThread);
// 关闭句柄
CloseHandle(pi.hThread);

// 等待进程结束或CPU时间用光
HANDLE handles[2];
handles[0] = pi.hProcess;
handles[1] = hJob;

DWORD result = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
switch (result - WAIT_OBJECT_0)
{
case 0:
MessageBox(NULL, TEXT("作业关联的进程已终止"), TEXT("提示"), MB_ICONINFORMATION);
break;
case 1:
MessageBox(NULL, TEXT("作业分配的CPU时间用尽"), TEXT("提示"), MB_ICONINFORMATION);
break;
}

// 关闭相应句柄
CloseHandle(pi.hProcess);
CloseHandle(hJob);
return 0;
}

和所有内核对象一样,第一个参数将安全信息与新的作业对象关联。调用CreateJobObject 函数可创建一个内核对象。调用CloseHandle关掉作业句柄后,并不会迫使作业内所有进程终止运行,实际上只是加了一个删除标记,只有作业中所有进程都已终止运行,才会自动销毁。

1
2
3
4
HANDLE CreateJobObject(
LPSECURITY_ATTRIBUTES lpJobAttributes, // 作业的安全描述符
LPCTSTR lpName, // 作业内核对象名称,可为NULL
)

给作业添加限制

之后,我们设置了作业的一些基本限制信息后,调用AssignProcessToJobObject函数,将一个进程放入作业。

关于设置作业的基本限制项,我们可以限制:

  • 基本限额和拓展基本限额,用于防止作业中的进程独占系统资源

  • 基本的UI限制,用于防止作业内进程修改用户界面

  • 安全限额,用于防止作业内进程访问安全资源(如文件,注册表等)

在上述示例代码中,调用了SetInformationJobObject 函数,来使设置的基本设置项生效。

1
2
3
4
5
6
BOOL SetInformationJobObject(
HANDLE hJob, // 作业对象
JOBOBJECTINFOCLASS JobObjectInformationClass, // 设置的作业信息
LPVOID lpJobObjectInformation, // 相应数据结构
DWORD cbJobObjectInformationLength // 第三个参数
);

其中第二个参数,我们可以选择下面的类,当然还有其他的

  • JOBOBJECT_BASIC_LIMIT_INFORMATION(作业基本限额)查看详细
  • JOBOBJECT_BASIC_UI_RESTRICTIONS(作业基本UI限制)查看详细
  • JOBOBJECT_CPU_RATE_CONTROL_INFORMATION(作业CPU速率限制)查看详细

在上方示例代码中,我们设置了JOB_OBJECT_UILIMIT_HANDLES,这个限制意味着作业内任何进程都不能访问作业外部进程所创建的用户对象。例如在一个作业中运行Microsoft Spy++,就只能看到Spy++自己创建的窗口。

这个UI限制只是单向的,也就是说作业外部的进程可以看到作业内部进程所创建的用户对象,即使内部指定了JOB_OBJECT_UILIMIT_HANDLES

要为作业中的进程创建一个真正安全的沙箱,对UI句柄进行限制是很强大的一个能力,不过,有时仍然需要让用户内部的一个进程同作业外部的一个进程进行通信。我们可以使用窗口消息进行通信,但是如果作业中的进程不能访问UI句柄,那么这个方法就失效了。幸运的是,我们可以用UserHandleGrantAccess 函数来通信。

1
2
3
4
5
BOOL UserHandleGrantAccess(
HANDLE hUserHandle, // 一个用户对象的句柄
HANDLE hJob, // 指定的作业对象
BOOL bGrant // 允许或拒绝作业内部进程访问此对象
);

调用TerminateJobObject 函数 ,可以把作业内的所有进程都终止。

我们可调用QueryInformationJobObject 函数来查询一个作业的相应限制信息

1
2
3
4
5
6
7
BOOL QueryInformationJobObject(
HANDLE hJob, // 作业句柄
JOBOBJECTINFOCLASS JobObjectInformationClass, // 查询信息的信息类
LPVOID lpJobObjectInformation, // 储存查询信息的结构体
DWORD cbJobObjectInformationLength,// sizeof 第三个参数
LPDWORD lpReturnLength // 已写入的字节,可传NULL
);

如果第一个参数传NULL,并且执行代码的进程被放入作业中,将查询这个作业的相关信息。对于SetInformationJobObject这样调用是失败的,目的是防止进程删掉施加给自己身上的限制。

作业通知

如果我们很想知道作业中的所有进程何时终止执行,所有已分配的CPU时间是否已经到期,我们应该怎么做呢?

作业中的进程如果尚未用完已分配的CPU时间,作业对象处于未触发状态,一旦作业用完所有已分配的CPU时间,Windows就强行“杀死”作业中的所有进程,并触发作业对象。通过调用WaitForSingleObject或一个类似的函数,我们可以轻松捕获到这个事件。我们之后还可以把作业对象重置为未触发状态,只需调用SetInformationJobObject并授予更多CPU时间即可。

如果想获得更高级的作业通知,我们必须创建一个I/O完成端口(completion port)内核对象,并将我们的作业对象与其关联,然后处理。