作业: 作业可以看作是一组进程的容器,把这些进程当作一个整体,对这个整体整个加入更多的限制.
因为Wi n d o w s并不维护进程之间的父/子关系。即使父进程已经终止运行,子进程仍然会继续运行。Microsoft Windoss 2000提供了一个新的作业内核对象,使你能够将进程组合在一起,并且创建一个“沙框”,以便限制进程能够进行的操作。最好将作业对象视为一个进程的容器。但是,创建包含单个进程的作业是有用的,因为这样一来,就可以对该进程加上通常情况下不能加的限制。
创建一个新作业内核对象:
HANDLE CreateJobObject( PSECURITY_ATTRIBUTES psa, PCTSTR pszName);
与所有的内核对象一样,它的第一个参数将安全信息与新作业对象关联起来,并且告诉系统,是否想要使返回的句柄成为可继承的句柄。最后一个参数用于给作业对象命名,使它可以供另一个进程通过下面所示的O p e n J o b O b j e c t函数进行访问。
HANDLE OpenJobObject( DWORD dwDesiredAccess,
BOOL bInheritHandle, PCTSTR pszName);
与平常一样,如果知道你将不再需要访问代码中的作业对象,那么就必须通过调用C l o s e H a n d l e来关闭它的句柄。
应该知道,关闭作业对象并不会迫使作业中的所有进程终止运行。该作业对象实际上做上了删除标记,只有当作业中的所有进程全部终止运行之后,该作业对象才被自动撤消。
注意,关闭作业的句柄后,尽管该作业仍然存在,但是该作业将无法被所有进程访问。
将一个进程放入一个作业,以限制该进程进行某些操作的能力。 Windows 98 Windows 98不支持作业的操作。
void StartRestrictedProcess()
{
//Create a job kernel object.
HANDLE hjob = CreateJobObject(NULL, NULL);
//Place some restrictions on processes 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 = 10000000;
//1 sec 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's 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;
CreateProcess(NULL, "CMD", NULL, NULL, FALSE,
CREATE_SUSPENDED, NULL, NULL, &si, π);
//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's thread to execute code.
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
//Wait for the process to terminate or for all the job's
//allotted 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 allotted CPU time was used
break;
}
//Clean up properly.
CloseHandle(pi.hProcess);
CloseHandle(hjob);
}
对作业进程的限制
进程创建后,通常需要设置一个沙框(设置一些限制),以便限制作业中的进程能够进行的操作。可以给一个作业加上若干不同类型的限制:
• 基本限制和扩展基本限制,用于防止作业中的进程垄断系统的资源。
• 基本的U I限制,用于防止作业中的进程改变用户界面。
• 安全性限制,用于防止作业中的进程访问保密资源(文件、注册表子关键字等)。
通过调用下面的代码,可以给作业加上各种限制:
BOOL SetInformationJobObject(
HANDLE hJob,
JOBOBJECTINFOCLASS JobObjectInformationClass,
PVOID pJobObjectInformation,
DWORD cbJobObjectInformationLength);
第一个参数用于标识要限制的作业。第二个参数是个枚举类型,用于指明要使用的限制类型。第三个参数是包含限制设置值的数据结构的地址,第四个参数用于指明该结构的大小(用于确定版本)。
可以做的限制有:(具体参数的意思参看windows核心编程)
JOB_OBJECT_BASIC_LIMIT_INFORMATION结构类似下面的样子: (基本限制)
typedef struct _JOBOBJECT_BASIC_LIMIT_INFORMATION{ LARGE_INTEGER PerProcessUserTimeLimit; LARGE_INTEGER PerJobUserTimeLimit; DWORD LimitFlags; DWORD MinimumWorkingSetSize; DWORD MaximumWorkingSetSize; DWORD ActiveProcessLimit; DWORD_PTR Affinity; DWORD PriorityClass; DWORD SchedulingClass;} JOBOBJECT_BASIC_LIMIT_INFORMATION, *PJOBOBJECT_BASIC_LIMIT_INFORMATION;
J O B O B J E C T _ E X T E N D E D _ L I M I T _ I N F O R M AT I O N结构对作业设置: (扩展限制)
typedef struct _JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
IO_COUNTERS oInfo;
SIZE_T ProcessMemoryLimit;
SIZE_T JobMemoryLimit;
SIZE_T PeakProcessMemoryUsed;
SIZE_T PeakJobMemoryUsed;
} JOBOBJECT_EXTENDED_LIMIT_INFORMATION, *PJOBOBJECT_EXTENDED_LIMIT_INFORMATION;
J O B O B J E C T _ B A S I C _ U I _ R E S T R I C T I O NS结构的样子: (其他限制)
typedef struct _JOBOBJECT_BASIC_UI_RESTRICTIONS
{
DWORD UIRestrictionsClass;
} JOBOBJECT_BASIC_UI_RESTRICTIONS, *PJOBOBJECT_BASIC_UI_RESTRICTIONS;
J O B O B J E C T _ S E C U R I T Y _ L I M I T _ I N F O R M AT I O N的结构类似下面的形式: (安全限制)
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;
当然,一旦给作业设置了限制条件,就可以查询这些限制。通过调用下面的代码,就可以进行这一操作:
BOOL QueryInformationJobObject(
HANDLE hJob,
JOBOBJECTINFOCLASS JobObjectInformationClass,
PVOID pvJobObjectInformation,
DWORD cbJobObjectInformationLength,
PDWORD pdwReturnLength);
你为该函数传递作业的句柄(就像你对SetInformationJobObject操作时那样),这些句柄包括用于指明你想要的限制信息的枚举类型,函数要进行初始化的数据结构的地址,以及包含该结构的数据块的长度。最后一个参数是pdwReturnLength,用于指向该函数填写的DWORD,它告诉你有多少字节放入了缓存。如果你愿意的话,可以(并且通常)为该参数传递N U L L。
将进程放入作业:
BOOL AssignProcessToJobObject(
HANDLE hJob,
HANDLE hProcess);
该函数告诉系统,将该进程(由hProcess标识)视为现有作业(由h J o b标识)的一部分。
注意,该函数只允许将尚未被赋予任何作业的进程赋予一个作业。一旦进程成为一个作业的组成部分,
它就不能转到另一个作业,并且不能是无作业的进程。另外,当作为作业的一部分的进程生成另一个进程的时候,
新进程将自动成为父作业的组成部分。注意:调用此函数后要调用ResumeThread();这样,
进程的线程就可以在作业的限制下执行代码。终止作业中所有进程的运行
若要撤消作业中的进程,只需要调用下面的代码:
BOOL TerminateJobObject(
HANDLE hJob,
UINT uExitCode);
这类似为作业中的每个进程调用TerminateProcess函数,将它们的所有退出代码设置为uExitCode。
查询作业统计信息
Q u e r y I n f o r m a t i o n J o b O b j e c t函数来获取对作业的当前限制信息。也可以使用它来获取关于作业的统计信息。通过传入不同的参数,可以查询到不同的作业统计信息.
例如,若要获取基本的统计信息,可以调用Q u e r y I n f o r m a t i o n J o b O b j e c t,为第二个参数传递J o b O b j e c t B a s i c A c c o u n t i n g I n f o r m a t i o n ,并传递J O B O B J E C T _ B A S I C _ A C C O U N T I N G _ I N F O R M AT I O N结构的地址:
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;
除了查询这些基本统计信息外,可以进行一次函数调用,以同时查询基本统计信息和I/O统计信息。为此,必须为第二个参数传递J o b O b j e c t B a s i c A n d I o A c c o u n t i n g I n f o r m a t i o n ,并传递J O B O B J E C T _ B A S I C _ A N D _ I O _ A C C O U N T I N G _ I N F O R M AT I O N结构的地址:
typedef struct JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION
{
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION BasicInfo;
IO_COUNTERS IoInfo;
} JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION;
如你所见,这个结构只返回一个J O B O B J E C T _ B A S I C _ A C C O U N T I N G _ I N F O R M AT I O N结构和I O _ C O U N T E R S结构:
typedef struct _IO_COUNTERS
{
ULONGLONG ReadOperationCount;
ULONGLONG WriteOperationCount;
ULONGLONG OtherOperationCount;
ULONGLONG ReadTransferCount;
ULONGLONG WriteTransferCount;
ULONGLONG OtherTransferCount;
} IO_COUNTERS;
另外,可以使用下面这个新的G e t P r o c e s s I o C o u n t e r s函数,以便获取不是这些作业中的进程的这些信息:
BOOL GetProcessIoCounters(
HANDLE hProcess,
PIO_COUNTERS pIoCounters);
也可以随时调用Q u e r y I n f o r m a t i o n J o b O b j e c t函数,以便获取当前在作业中运行的进程的一组进程I D。若要进行这项操作,首先必须确定你想在作业中看到多少进程,然后必须分配足够的内存块,来放置这些进程I D的数组,并指定J O B O B J E C T _ B A S I C _ P R O C E S S _ I D _ L I S T结构的大小:
typedef struct _JOBOBJECT_BASIC_PROCESS_ID_LIST
{
DWORD NumberOfAssignedProcesses;
DWORD NumberOfProcessIdsInList;
DWORD ProcessIdList[1];
} JOBOBJECT_BASIC_PROCESS_ID_LIST, *PJOBOBJECT_BASIC_PROCESS_ID_LIST;
因此,若要获得当前作业中的一组进程I D,必须执行类似下面的代码:
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 = _alloca(cb);
//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(int x=0; x < pjobpil -> NumberOfProcessIdsInList; x++)
{
//Use pjobpil->ProcessIdList[x]
}
//Since _alloca was used to allocate the memory,
//we don't need to free it here.
}
作业通知信息
如果关心的是分配的所有C P U时间是否已经到期,那么可以非常容易地得到这个通知信息。当作业中的进程尚未用完分配的C P U时间时,作业对象就得不到通知。一旦分配的所有C P U时间已经用完, Wi n d o w s就强制撤消作业中的所有进程,并将情况通知作业对象。通过调用Wa i t F o r S i n g l e O b j e c t (或类似的函数),可以很容易跟踪这个事件。有时,可以在晚些时候调用S e t I n f o r m a t i o n J o b O b j e c t函数,使作业对象恢复未通知状态,并为作业赋予更多的C P U时间。
当开始对作业进行操作时,我觉得当作业中没有任何进程运行时,应该将这个事件通知作业对象。毕竟当进程和线程停止运行时,进程和线程对象就会得到通知。因此,当作业停止运行时它也应该得到通知。这样,就能够很容易确定作业何时结束运行。但是, M i c r o s o f t选择在分配的C P U时间到期时才向作业发出通知,因为这显示了一个错误条件。由于许多作业启动时有一个父进程始终处于工作状态,直到它的所有子进程运行结束,因此只需要在父进程的句柄上等待,就可以了解整个作业何时运行结束。S t a r t R e s t r i c t e d P r o c e s s函数用于显示分配给作业的C P U时间何时到期,或者作业中的进程何时终止运行。
前面介绍了如何获得某些简单的通知信息,但是尚未说明如何获得更高级的通知信息,如进程创建/终止运行等。如果想要得到这些通知信息,必须将更多的基础结构放入应用程序。特别是,必须创建一个I / O完成端口内核对象,并将作业对象或多个作业对象与完成端口关联起来。然后,必须让一个或多个线程在完成端口上等待作业通知的到来,这样它们才能得到处理。
一旦创建了I / O完成端口,通过调用S e t I n f o r m a t i o n J o b O b j e c t函数,就可以将作业与该端口关联起来,如下面的代码所示:
JOBOBJECT_ASSOCIATE_COMPLETION_PORT joacp;
//Any value to uniquely identify this job
joacp.CompletionKey = 1;
//Handle of completion port that receives notifications
joacp.CompletionPort = hIOCP;
SetInformationJobObject(hJob,
jobObjectAssociateCompletionPortInformation,
&joacp, sizeof(jaocp));
当上面的代码运行时,系统将监视该作业的运行,当事件发生时,它将事件送往I / O完成端口(顺便说一下,可以调用Q u e r y I n f o r m a t i o m J o b O b j e c t函数来检索完成关键字和完成端口句柄。但是,这样做的机会很少)。线程通过调用G e t Q u e u e d C o m p l e t i o n S t a t u s函数来监控I / O完成端口:
BOOL GetQueuedCompletionStatus(
HANDLE hIOCP,
PDWORD pNumBytesTransferred,
PULONG_PTR pCompletionKey,
POVERLAPPED *pOverlapped,
DWORD dwMilliseconds);
当该函数返回一个作业事件通知时,* p C o m p l e t i o n K e y包含了调用S e t I n f o r m a t i o n J o b O b j e c t时设置的完成关键字值,用于将作业与完成端口关联起来。它使你能够知道哪个作业存在一个事件。* p N u m B y t e s Tr a n s f e r r e d中的值用于指明发生了哪个事件。