第五章 作业
本章内容
5.1 对作业中的进程施加限制
5.2 将进程放入作业中
5.3 终止作业中的所有进程
5.4 作业通知
5.5 Job Lab示例程序
有时候为了完成某些任务需要执行一组进程,需要将一组进程作为一个组来管理。并且可以加以限制。
windows提供一个job内核对象,将应用程序组合在一起并创建一个“沙盒”来限制进程能做什么。
一个StartRestrictedProcess的例子
void StartRestrictedProcess() {
// Check if we are not already associated with a job.
// If this is the case ,there is no way to switch to
// another job.
BOOL bInJob = FALSE;
IsProcessInJob(GetCurrentProcess(), NULL, &bInJob);
if (bInJob) {
MessageBox(NULL, TEXT("Process already in a job"),
TEXT(""), MB_ICONINFORMATION | MB_OK);
return;
}
// Create a job kernel object.
HANDLE hjob = CreateJobObject(NULL,
TEXT("Wintellect_RestrictedProcessJob"));
// Place some restrictions on process in the job.
// First, set some basic restrictions.
JOBOBJECT_BASIC_LIMIT_INFORMATION jobli = { 0 };
// The process always runs in the idle priority class.
jobli.PriorityClass = IDLE_PRIORITY_CLASS;
// The job cannot use more than 1 second of CPU time.
jobli.PerJobUserTimeLimit.QuadPart = 10000; // 1sec in 100-ns intervals
// These are the only 2 restrictions I want placed on the job (process.
jobli.LimitFlags = JOB_OBJECT_LIMIT_PRIORITY_CLASS
| JOB_OBJECT_LIMIT_JOB_TIME;
SetInformationJobObject(hjob, JobObjectBasicLimitInformation, &jobli,
sizeof(jobli));
// Second, set some UI restrictions.
JOBOBJECT_BASIC_UI_RESTRICTIONS jobuir;
jobuir.UIRestrictionsClass = JOB_OBJECT_UILIMIT_NONE; // A fancy zero
// The process can't log off the system.
jobuir.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_EXITWINDOWS;
// The process can't access USER objects(such as other windows)
// in the system.
jobuir.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_HANDLES;
SetInformationJobObject(hjob, JobObjectBasicUIRestrictions, &jobuir,
sizeof(jobuir));
// Spawn the process that is to be in the job.
// Note: you must first spawn the process and then place the process in
// the job. This means that the process' thread must be initially
// suspended so that it can't execute any code outside of the job's
// restrictions.
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 | CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
// Place the process in the job.
// Note: if this process spawns any children, the children are
// automatically part of the same job.
AssignProcessToJobObject(hjob, pi.hProcess);
// Now we can allow the child process' thread to execute code.
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
// Wait for the process to terminate or
// for all the job's allocated CPU time to be used.
HANDLE h[2];
h[0] = pi.hProcess;
h[1] = hjob;
DWORD dw = WaitForMultipleObjects(2, h, FALSE, INFINITE);
switch (dw - WAIT_OBJECT_0) {
case 0:
// The process has terminated...
break;
case 1:
// All of the job's allocated CPU time was used...
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.dwLowDateTime / 10000, UserTime.dwLowDateTime / 10000);
MessageBox(GetActiveWindow(), szInfo, TEXT("Restricted Process times"),
MB_ICONINFORMATION | MB_OK);
// Clean up properly.
CloseHandle(pi.hProcess);
CloseHandle(hjob);
}
该函数运行机制
首先调用一下函数判断当前进程是否在一个现有的作业之下运行。
WINBASEAPI
BOOL
WINAPI
IsProcessInJob(
_In_ HANDLE ProcessHandle,
_In_opt_ HANDLE JobHandle,
_Out_ PBOOL Result
);
接下来创建一个Job内核对象
WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateJobObjectW(
_In_opt_ LPSECURITY_ATTRIBUTES lpJobAttributes,
_In_opt_ LPCWSTR lpName
);
如果自己的代码不再访问作业对象应该调用CloseHandle来关闭它的句柄。
注意:关闭一个作业对象不会迫使其下运行的进程终止。作业对象只是加了一个删除标记,只有在其下的所有进程都终止运行后,才会自动销毁。
关闭作业对象会导致所有进程都无法访问此作业,即使该作业仍然存在。
例如一下代码
HANDLE hJob = CreateJobObject(NULL, TEXT("Jeff"));
AssignProcessToJobObject(hJob, GetCurrentProcess());
CloseHandle(hJob);
hJob = OpenJobObject(JOB_OBJECT_ALL_ACCESS, FALSE, TEXT("Jeff"));
// OpenJobObject fails and returns NULL here because the name "Jeff
// was disassociated from the job when CloseHandle was called.
// There is no way to get a handle to this job now.
5.1 对作业中的进程施加限制
基本限额和扩展基本限额,用于防止作业中的进程独占系统资源。
基本UI限制,用于防止作业内的进程更改用户界面
安全限额,用于防止作业内的进程访问安全资源(文件,注册表子项等)
通过以下函数向作业施加限制
WINBASEAPI
BOOL
WINAPI
SetInformationJobObject(
_In_ HANDLE hJob,
_In_ JOBOBJECTINFOCLASS JobObjectInformationClass,
_In_reads_bytes_(cbJobObjectInformationLength) LPVOID lpJobObjectInformation,
_In_ DWORD cbJobObjectInformationLength
);
第一个参数指定要限制的作业。
第二个参数指定了要限制的类型
第三个参数指定了一个数据结构的地址
第四个参数指出了此数据结构的大小(用于版本控制)
以上例子只做了一个基本限制设置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;
DWORD PriorityClass;
DWORD SchedulingClass;
} JOBOBJECT_BASIC_LIMIT_INFORMATION, *PJOBOBJECT_BASIC_LIMIT_INFORMATION;
SchedulingClass 成员将设定在相同优先级的作业下进程的调度优先级(0~9)数值越大进程获得的cpu时间越多。
还有一个扩展的结构体用于设置作业的信息
typedef struct _JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
IO_COUNTERS IoInfo;
SIZE_T ProcessMemoryLimit;
SIZE_T JobMemoryLimit;
SIZE_T PeakProcessMemoryUsed;
SIZE_T PeakJobMemoryUsed;
} JOBOBJECT_EXTENDED_LIMIT_INFORMATION, *PJOBOBJECT_EXTENDED_LIMIT_INFORMATION;
他包含一个JOBOBJECT_BASIC_LIMIT_INFORMATION结构
InInfo保留用
PeakProcessMemoryUsed和PeakJobMemoryUsed是只读的。
ProcessMemoryLimit和JobMemoryLimit限制作业中任何一个进程或全部进程所使用的已调拨的存储空间。
需要在LimitFlags中设定JOB_OBJECT_LIMIT_JOB_MEMORY和JOB_OBJECT_LIMIT_PROCESS_MEMORY标志。
再看看JOBOBJECT_BASIC_UI_RESTRICTIONS
typedef struct _JOBOBJECT_BASIC_UI_RESTRICTIONS {
DWORD UIRestrictionsClass;
} JOBOBJECT_BASIC_UI_RESTRICTIONS, *PJOBOBJECT_BASIC_UI_RESTRICTIONS;
例如在一个限制了访问UI句柄的作业中启动spy++ ,spy++将无法访问其他进程的窗口句柄。
可以看到spy++在作业中访问不了其他进程的句柄,只能访问自己创建的窗口句柄。
但是在作业外的进程是可以看见spy++的窗口句柄的。
有时候如果限制了作业内的进程访问窗口句柄,则作业内的进程将无法对外发送窗口消息。可以使用函数解决。
WINUSERAPI
BOOL
WINAPI
UserHandleGrantAccess(
_In_ HANDLE hUserHandle,
_In_ HANDLE hJob,
_In_ BOOL bGrant);
授权作业可以访问或拒绝访问哪些对象。单该函数如果被一个受限的作业内的进程中的线程调用会返回失败
可以向作业施加最后一种安全限制。一旦应用,安全限制就不能撤销。
typedef struct _JOBOBJECT_SECURITY_LIMIT_INFORMATION {
DWORD SecurityLimitFlags ;
HANDLE JobToken ;
PTOKEN_GROUPS SidsToDisable ;
PTOKEN_PRIVILEGES PrivilegesToDelete ;
PTOKEN_GROUPS RestrictedSids ;
} JOBOBJECT_SECURITY_LIMIT_INFORMATION, *PJOBOBJECT_SECURITY_LIMIT_INFORMATION ;
通过以下函数查询作业的安全限制
WINBASEAPI
BOOL
WINAPI
QueryInformationJobObject(
_In_opt_ HANDLE hJob,
_In_ JOBOBJECTINFOCLASS JobObjectInformationClass,
_Out_writes_bytes_to_(cbJobObjectInformationLength, *lpReturnLength) LPVOID lpJobObjectInformation,
_In_ DWORD cbJobObjectInformationLength,
_Out_opt_ LPDWORD lpReturnLength
);
该函数也可以用于作用中的进程来调用查询自己被施加了哪些限制。
5.2 将进程放入作业中
CreateProcess创建需要放入作业的进程,并使用CREATE_SUSPENDED标志。
在子进程执行代码以前将其加入作业
WINBASEAPI
BOOL
WINAPI
AssignProcessToJobObject(
_In_ HANDLE hJob,
_In_ HANDLE hProcess
);
并且一个进程只能加入一个作业。
给作业设置
JOBOBJECT_BASIC_LIMIT_INFORMATION 的limitFlags成员设置JOB_OBJECT_LIMIT_BREAKWAWY_OK标志告知系统新生产的进程可以在作业外部执行。
并且CreateProcess函数时指定CREATE_BREAKAWAY_FROM_JOB标志,如果不加此标志CreateProcess会失败。
JOBOBJECT_BASIC_LIMIT_INFORMATION的limitFlags成员 JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK .这样在CreateProcess时并不需要额外传递任何标志。
最后在调用完AssignProcessToJobObject以后调用ResumeThread使得进程可以在作业限制下执行代码。
5.3 终止作业中的所有进程。
WINBASEAPI
BOOL
WINAPI
TerminateJobObject(
_In_ HANDLE hJob,
_In_ UINT uExitCode
);
类似TerminateProcess
查询作业统计信息
QueryInformationJobObject
WINBASEAPI
BOOL
WINAPI
QueryInformationJobObject(
_In_opt_ HANDLE hJob,
_In_ JOBOBJECTINFOCLASS JobObjectInformationClass,
_Out_writes_bytes_to_(cbJobObjectInformationLength, *lpReturnLength) LPVOID lpJobObjectInformation,
_In_ DWORD cbJobObjectInformationLength,
_Out_opt_ LPDWORD lpReturnLength
);
typedef struct _JOBOBJECT_BASIC_ACCOUNTING_INFORMATION {
LARGE_INTEGER TotalUserTime;
LARGE_INTEGER TotalKernelTime;
LARGE_INTEGER ThisPeriodTotalUserTime;
LARGE_INTEGER ThisPeriodTotalKernelTime;
DWORD TotalPageFaultCount;
DWORD TotalProcesses;
DWORD ActiveProcesses;
DWORD TotalTerminatedProcesses;
} JOBOBJECT_BASIC_ACCOUNTING_INFORMATION, *PJOBOBJECT_BASIC_ACCOUNTING_INFORMATION;
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION jobAccountInfo = { 0 };
QueryInformationJobObject(hjob, JobObjectBasicAccountingInformation,
&jobAccountInfo, sizeof(jobAccountInfo), NULL);
还可以传入一个一下结构来查询IO信息
typedef struct _JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION {
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION BasicInfo;
IO_COUNTERS IoInfo;
} JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION, *PJOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION;
typedef struct _IO_COUNTERS {
ULONGLONG ReadOperationCount;
ULONGLONG WriteOperationCount;
ULONGLONG OtherOperationCount;
ULONGLONG ReadTransferCount;
ULONGLONG WriteTransferCount;
ULONGLONG OtherTransferCount;
} IO_COUNTERS;
typedef IO_COUNTERS *PIO_COUNTERS;
对于不属于作业的进程可以调用GetProcessIoCounters函数来获得未放入作业那些进程的信息。
枚举当前作业中的进程数和id
void EnumProcessIdsInJob(HANDLE hjob) {
// I assume that there will never be more
// than 10 processes in this job.
#define MAX_PROCESS_IDS 10
// calculate the number of bytes needed for structure & process IDs.
DWORD cb = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST) +
(MAX_PROCESS_IDS - 1) * sizeof(DWORD);
// Allocate the block of memory.
PJOBOBJECT_BASIC_PROCESS_ID_LIST pjobpil =
(PJOBOBJECT_BASIC_PROCESS_ID_LIST)_alloca(cb); // alloc the memory in stack so we don't need to free when return.
// Tell the function the maximum number of processes
// that we allocated space for.
pjobpil->NumberOfAssignedProcesses = MAX_PROCESS_IDS;
// Request the current set of process IDs.
QueryInformationJobObject(hjob, JobObjectBasicProcessIdList,
pjobpil, cb, &cb);
// Enumerate the process IDs.
for (DWORD x = 0; x < pjobpil->NumberOfProcessIdsInList; x++) {
// Use pjobpil->ProcessIdList[x]...
}
// Since _alloca was used to allocate the memory,
// w don't need to free it here.
}
可以利用Performance Data Helper来获取更多作业相关的统计信息
利用Process Explorer可以观察作业
5.4 作业通知
如果作业进程使用完所有分配的cpu使用,作业会强行杀死进程并触发作业对象。
之后还可以调用SetInformationJobObject设置为未触发状态并授予更多的cpu时间。
注意作业并不会在其内部的进程都执行完毕而置于触发状态,而是取决于释放用完了分配的cpu时间。
因此想判断作业是否执行完毕可以等待主进程的进程对象句柄触发。
为了获得一些更高级的通知,可以创建一些IO完成端口内核对象,并将其与作业关联。然后有一个或多个线程等待作业通知到达完成端口,以对他们进行处理
创建了IO完成端口,可以调用SetInformationJobObject将其与作业关联。
HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0,
0);
//...
JOBOBJECT_ASSOCIATE_COMPLETION_PORT jacp;
jacp.CompletionKey = hjob;
jacp.CompletionPort = hIOCP; // handle of completion port that recieves notifications
SetInformationJobObject(hjob, JobObjectAssociateCompletionPortInformation,
&jacp, sizeof(jacp));
线程调用GetQueuedCompletionStatus来监视完成端口:
WINBASEAPI
BOOL
WINAPI
GetQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_Out_ LPDWORD lpNumberOfBytesTransferred,
_Out_ PULONG_PTR lpCompletionKey,
_Out_ LPOVERLAPPED * lpOverlapped,
_In_ DWORD dwMilliseconds
);
lpNumberOfBytesTransferred参见下表。
注意:默认情况下,如果作业分配的CPU时间到期,他会终止所有的进程,但不会投递 JOB_OBJECT_MSG_END_OF_JOB_TIME
需要执行以下类似的代码:
// Create a JOBOBJECT_END_OF_JOB_TIME_INFORMATION structure
// and initialize its only member.
JOBOBJECT_END_OF_JOB_TIME_INFORMATION joeojti;
joeojti.EndOfJobTimeAction = JOB_OBJECT_POST_AT_END_OF_JOB;
// Tell the job object what we want it to do when the job time is execeeded.
SetInformationJobObject(hJob, JobObjectEndOfJobTimeInformation,
&joeojti, sizeof(joeojti));
对于EndOfJobTimeAction成员,创建作业时候的默认值是 JOB_OBJECT_TERMINATE_AT_END_OF_JOB
5.5 Job Lab示例程序
该示例创建了一个作业对象,一个IO完成端口并与之关联。面板上可以进行各种限额设置,然后在此作业环境下创建进程观察进程的运行。
并且可以收到IO完成端口收到的各种通知并显示相关统计信息。