作业
概述
- 作业可以让我们将一组进程当作一个实体来处理。
- 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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
// 示例代码 void StartRestrictedProcess() { // 检查我们是否已经与一个作业关联了 // 如果是,没有方法切换到另一个作业 BOOL bInJob = FALSE; IsProcessInJob(GetCurrentProcess(), NULL, &bInJob); if (bInJob) { MessageBox(NULL, TEXT("process already in a job"), TEXT(""), MB_ICONINFORMATION | MB_OK); return; } // 创建一个作业(job)内核对象 HANDLE job = CreateJobObject(NULL, TEXT("Wintellect_RestrictedProcessJob")); // 下面是对作业中的进程施加一些限制 // 首先,设置一些基本的限制 JOBOBJECT_BASIC_LIMIT_INFORMATION jobli = { 0 }; // 让进程总是以空闲优先级运行 jobli.PriorityClass = IDLE_PRIORITY_CLASS; // 作业不能使用超过多余1秒的CPU时间 jobli.PerJobUserTimeLimit.QuadPart = 10000; // 对作业(进程)添加了如下两个限制 jobli.LimitFlags = JOB_OBJECT_LIMIT_PRIORITY_CLASS | JOB_OBJECT_LIMIT_JOB_TIME; SetInformationJobObject(hjob, JobObjectBasicLimitInformation, &jobli, sizeof(jobli)); // 其次,设置一些UI限制 JOBOBJECT_BASIC_UI_RESTRICTIONS jobuir; jobuir.UIRestrictionsClass = JOB_OBJECT_UILIMIT_NONE; // 进程不能注销系统 jobuir.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_EXITWINDOWS; // 进程无法访问系统中的其他Windows用户对象 jobuir.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_HANDLES; SetInfomationJobObject(hjob, JobObjectBasicUIRestrictions, &jobuir, sizeof(jobuir)); // 下面生成一个在作业中的进程 // 需要注意的是:必须先生成进程,再把线程置入作业中,顺序不能错。 // 意味着,这个进程的线程的最初状态必须是暂停的, // 因为,暂停了,它就不能执行作业限制之外的任何代码。 STARTUPINFO si = { sizeof(si)}; PROCESS_INFORMATION pi; TCHAR szCmdLine[8]; _tcscpy_s(szCmdLine, _countof(szCmdLine), TEXT("CMD")); BOOL bResult = CreateProcess(NULL, szCmdLine, NULL, NULL, FALSE, CREATE_SUSPENDED | CREATA_NEW_CONSOLE, NULL, NULL, &si, &pi); // 下面是把进程放置到作业里面 // 如果进程生成了任何的子进程,子进程会自动成为同一作业的一部分。 AssignProcessToJobObject(hjob, pi.hProcess); // 现在允许子进程的线程执行代码 ResumeThread(pi.hThread); CloseHandle(pi.hThread); // 等待进程终止或所有为作业分配的CPU时间耗尽 HANDLE h[2]; h[0] = pi.hProcess; h[1] = hjob; DWORD dw = WaitForMultipleObjects(2, h, FALSE, INFINITE); switch (dw - WAIT_OBJECT_0) { case 0: // 进程终止了 break; case 1: // 给作业分配的CPU时间耗尽了 break; } FILETIME CreationTime; FILETIME ExitTime; FILETIME KernelTime; FILETIME UserTime; TCHAR szInfo[MAX_PATH]; GetProcessTimes(pi.hProcess, &CreationTime, &ExitTime, &KernelTime, &UserTime); StringCchPrintf(szInfo, _countof(szInfo), TEXT("Kernel = %u | User = %u\n"), KernelTime.dwLowDataTime / 10000, UserTime.dwLowDataTime / 10000); MessageBox(GetActiveWindow(), szInfo, TEXT("Restricted Process times"), MB_ICONINFORMATION | MB_OK); // 正确释放资源 CloseHandle(pi.hProcess); CloseHandle(hjob); } |
解析
- 首先获取了当前进程,然后将NULL作为第二个参数传给了IsProcessInJob函数,用于验证当前进程是否在一个现有的作业控制之下运行。
1 2 3 |
BOOL IsProcessInJob(HANDLE hProcess, HANDLE hJob, PBOOL pbInJob); |
- 如上面注释中所言,如果一个进程已经与一个作业相关联了,那么是无法将当前进程或它的任何子进程从这个作业中去除的。
这个特性确保了进程无法摆脱对它施加的限制。 - 然后创建一个新的作业内核对象
和所有内核对象一样,第一个参数是将安全信息将新的作业内核对象关联,然后告诉系统,是否希望返回的句柄可被继承。
第二个参数是对此作业对象进行命名,使其能够被另一个进程通过OpenJobObject函数进行访问。
1 2 |
HANDLE CreateJobObject(PSECURITY_ATTRIBUTES psa, PCTSTR pszName); |
- 如果确定在自己的这个进程中不会再访问作业内核对象了,调用CloseHandle来关闭它的句柄。
需要了解的是:
关闭一个作业的内核对象句柄,并不会导致作业中的所有进程都终止运行。
作业对象只是加了一个删除标记,只有再作业中的所有进程都终止运行之后,才会自动销毁作业。
但是,关闭了作业的句柄,会导致所有进程都不能再访问这个作业,即使这个作业仍然存在也不能访问。
对作业中的进程施加限制
- 创建好一个进程之后,一般会根据作业中的进程能够执行哪些操作来建立一个沙箱(限制)。
- 可向作业应用以下几种类型的限制:
- 基本限制和扩展基本限制
用于防止作业中的进程独占系统资源。 - 基本的UI限制
用于防止作业内的进程更改用户界面。 - 安全限制
用于防止作业内的进程访问安全资源(如文件、注册表子项等)。
- 基本限制和扩展基本限制
- 可以调用以下函数向作业施加限制
- 第一个参数是要添加限制的作业的句柄
- 第二个参数是一个枚举类型,指定了施加限制的类型
- 第三个参数是数据结构的地址,该结构中包含了具体的限制设置
- 第四个参数指出了此数据结构的大小
1 2 3 4 |
BOOL SetInfomationJobObject(HANDLE hJob, JOBOBJECTINFOCLASS JobObjectInformationClass, PVOID pJobObjectInformation, DWORD cbJobObjectInformationSize); |
限制类型 | 第二个参数的值 | 第三个参数对应的数据结构 |
基本限制 | JobObjectBasicLimitInformation | JOBOBJECT_BASIC_LIMIT_INFORMATION |
扩展后的基本限制 | JobObjectExtendedLimitInformation | JOBOBJECT_EXTENDED_LIMIT_INFORMATION |
基本的UI限制 | JobObjectBasicUIRestrictions | JOBOBJECT_BASIC_UI_RESTRICTIONS |
安全限制 | JobObjectSecurityLimitInformation | JOBOBJECT_SECURITY_LIMIT_INFORMATION |
- 查询限制
1 2 3 4 5 |
BOOL QueryInformationJobObject(HANDLE hObj, JOBOBJECTINFOCLASS ,JobObjectInformationClass, PVOID pvJobObjectInformation, DWORD cvJobObjectInformationSize, PDWORD pdwReturnSize); |
将进程放入作业中
- 向系统表明将此进程当作现有作业的一部分。
- 需要注意的是,这个函数只允许将一个尚未分配给作业的一个进程分配给一个作业。
- 一旦一个进程成为了一个作业的一部分,那它就不能再成为其他作业的一部分了,也不能成为“无业的”了。
1 2 |
BOOL AssignProcessToJobObject(HANDLE hJob, HANDLE hProcess); |
-
还有一个点,就是说作业中的一个进程生成了一个子进程,新进程将会自动成为作业的一部分。
但是,有方法改变这种行为。 -
方法一
打开JOBOBJECT_BASIC_LIMIT_INFORMATION的LimitFlags成员的JOB_OBJECT_LIMIT_BREAKAWAY_OK标志,告诉系统新生成的进程可以在作业外部执行。
为此,必须调用CreateProcess的时候,指定JOB_OBJECT_LIMIT_BREAKAWAY_OK限额标志。
如果调用CreateProcess的时候指定了这个限额标志,但是作业没有打开JOB_OBJECT_LIMIT_BREAKAWAY_OK标志,CreateProcess就会调用失败。如果希望新生成的进程控制作业,这个标志就比较有用。
-
方法二
打开JOBOBJECT_BASIC_LIMIT_INFORMATION的LimitFlags成员的JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK标志。
比标志也告诉系统新生成的子进程不应该是作业的一部分。
这个就没有必要向CreateProcess传递任何额外的标志。
事实上,此标志会强制新生成的进程脱离当前作业。如果进程对象在设立之初就对作业一无所知,这个标志就比较有用。
终止作业中的所有进程
- 杀死作业内部的所有进程
效果类似为作业内的每一个进程调用TerminateProcess,将所有退出代码设为uExitCode。
1 2 |
BOOL TerminateJobObject(HANDLE hJob, UINT uExitCode); |
查询作业统计信息
- 这个函数也可以获取作业的统计信息。向第二个参数传递JobObjectBasicAccountingInformation和一个JOBOBJECT_BASIC_ACCOUNTING_INFORMATION结构的地址。
1 2 3 4 5 |
BOOL QueryInformationJobObject(HANDLE hObj, JOBOBJECTINFOCLASS ,JobObjectInformationClass, PVOID pvJobObjectInformation, DWORD cvJobObjectInformationSize, PDWORD pdwReturnSize); |
- 对于那些未放在作业中的进程,可以使用下面这个函数来获取它的信息。
1 2 |
BOOL GetProcessIoCounters(HANDLE hProcess, PIO_COUNTERS pIoCounters); |
作业通知
- 作业中的进程如何尚未用完已分配的CPU时间,作业对象就处于未触发状态的。
- 通过调用WaitForSingleObject,可以捕获到这个事件。
- 还可以通过调用SetInformationJobObject把作业对象重置为未触发状态,授予作业更多的CPU时间。
额外的通知
- 要获得额外的通知,要必须创建一个I/O完成端口内核对象,并将我们的作业对象与完成端口关联起来。
- 然后,必须要有一个或多个线程等待作业通知到达完成端口,以便对它们进行处理。
1 2 3 4 5 6 7 |
JOBOBJECT_ASSOCIATE_COMPLETION_PORT joacp; // joacp.CompletionKey = 1; // joacp.CompletionPort = hIOCP; SetInfomationJobObject(hJob, JobObjectAssociateCompletionPortInformation, &joacp, sizeof(joacp)); |
- 执行上面代码后,系统将监视作业,只要有事件发生,就会把它们投递到I/O完成端口。
QueryInformationJobObject函数也可以获取完成键和完成端口句柄,但很少有必要这么做。 - 线程通过调用GetQueuedCompletionStatue来监视完成端口。
1 2 3 4 5 |
BOOL GetQueuedCompletionStatue(HANDLE hIOCP, PDWORD pNumBytesTransferrd, PULONG_PTR pCompletionKey, POVERLAPPED * pOverlapped, DWORD dwMilliseconds); |
- 当这个函数返回一个作业事件通知的时候,在pCompletionKey中,将包含完成键的值。这个值是在调用SetInformationJobObject将作业与完成端口关联时设置的。这样一来,我们就可以知道是哪个作业有事件发生。
- 而pNumBytesTransferred的值指出具体发生了什么事件。
根据事件的不同,pOverlapped的值所表示的数据类型也不同。比如在作业事件通知中,它表示的是一个对应的进程ID(而不是地址)。
事件 | 描述 |
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO | 作业中没有进程在运行时,就投递这个通知 |
JOB_OBJECT_MSG_END_OF_PROCESS_TIME | 进程已分配的CPU时间到期时,就投递这个通知,进程将终止运行,给出进程ID |
JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT | 试图超过作业中的活动进程数时,就投递这个通知 |
JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT | 进程试图调拨的存储超过进程的限额时,就投递这个通知,并给出进程ID |
JOB_OBJECT_MSG_NEW_PROCESS | 一个进程添加到一个作业时,就投递这个通知,同时给出进程ID |
JOB_OBJECT_MSG_EXIT_PROCESS | 一个进程终止运行时,就头地址这个通知,并给出进程ID |
JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS | 一个进程由于未处理的异常而终止运行时,就头地址这个通知,并给出进程ID |
JOB_OBJECT_MSG_END_OF_JOB_TIME | 作业分配的CPU时间到期时,就投递这个通知。但其中的进程不会自动终止。我们可以允许进程继续运行,可以设置一个新的时间限额,还可以自己调用TerminateJobObject。 |
作业对象的默认配置
- 在默认的情况下,作业对象是这样配置的:
- 当分配作业的CPU时间到期时,它的所有进程都会自动终止,而且不会投递JOB_OBJECT_MSG_END_OF_JOB_TIME通知。
- 如果向阻住作业对象“杀死”进程,只是简单的通知我们CPU时间到期,必须执行下面的代码。
1 2 3 4 |
JOBOBJECT_END_OF_TIME_INFORMATION joeojti; joeojti.EndOfJobTimeAction = JOB_OBJECT_POST_AT_END_OF_JOB; SetInformationJobObject(hJob, JobObjectEndOfJobTimeInformation, &joeojti, sizeof(joeojti)); |
- 针对EndOfJobTimeAction这个成员,唯一能指定的另一个值是JOB_OBJECT_TERMINATE_AT_END_OF_JOB,这是创建作业时的默认值。
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Windows 核心编程 _ 线程优先级与关联性07/09
- ♥ Windows 核心编程 _ 用户模式:线程同步一07/15
- ♥ Windows 核心编程 _ 线程调度07/07
- ♥ X86_64汇编学习记述三08/08
- ♥ Windows API11/11
- ♥ Windows系统学习一03/21