当设计一个服务器时,也必须将一组进程作为单个进程组来处理。例如,客户机可能要求服务器执行一个应用程序(这可以生成它自己的子应用程序),并给客户机返回其结果。由于可能有许多客户机与该服务器相连接,如果服务器能够限制客户机的要求,即用什么手段来防止任何一个客户机垄断它的所有资源,那么这是非常有用的。这些限制包括:可以分配给客户机请求的最大C P U时间,最小和最大的工作区的大小,防止客户机的应用程序关闭计算机,以及安全性限制等。
Microsoft Windoss 2000提供了一个新的作业内核对象,使你能够将进程组合在一起,并且创建一个“沙框”,以便限制进程能够进行的操作。最好将作业对象视为一个进程的容器。但是,创建包含单个进程的作业是有用的,因为这样一来,就可以对该进程加上通常情况下不能加的限制。
我的S t a r t R e s t r i c t e d P r o c e s s函数(见清单5 - 1)将一个进程放入一个作业,以限制该进程进行某些操作的能力。
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,
JobObjectBasicLimitInfor mation,
&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,JobObjectBasicUIRestrict ions,
&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);
}
HANDLE CreateJobObject( PSECURITY_ATTRIBUTES psa, PCTSTR pszName);
HANDLE OpenJobObject( DWORD dwDesiredAccess,
BOOL bInheritHandle, PCTSTR pszName);
注意,关闭作业的句柄后,尽管该作业仍然存在,但是该作业将无法被所有进程访问。请看下面的代码:
//Create a named job object.
HANDLE hjob = CreateJobObject(NULL, TEXT("Jeff"));
//Put our own process in the job.
AssignProcessToJobObject (hjob,GetCurrentProcess());
//Closing the job does not kill our process or the job.
//But the name ("Jeff") is immediately disassociated with the job.
CloseHandle(hjob);
//Try to open the existing job.
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 对作业进程的限制
进程创建后,通常需要设置一个沙框(设置一些限制),以便限制作业中的进程能够进行的操作。可以给一个作业加上若干不同类型的限制:
• 基本限制和扩展基本限制,用于防止作业中的进程垄断系统的资源。
• 基本的U I限制,用于防止作业中的进程改变用户界面。
• 安全性限制,用于防止作业中的进程访问保密资源(文件、注册表子关键字等)。
通过调用下面的代码,可以给作业加上各种限制:
BOOL SetInformationJobObject(
HANDLE hJob,
JOBOBJECTINFOCLASS JobObjectInformationClas s,
PVOID pJobObjectInformation,
DWORD cbJobObjectInformationLe ngth);
表5-1 设置限制条件
限制类型 | 第二个参数的值 | 第三个参数的结构 |
基本限制 | JobObjectBasicLimitInfor | JOBOBJECT_BASIC_LIMIT_INFORMATION |
扩展基本限制 | JobObjectExtendedLimitIn | JOBOBJECT_EXTENDED_LIMIT_INFORMATION |
基本UI限制 | JobObjectBasicUIRestrict | JOBOBJECT_BASIC_UI_RESTRICTIONS |
安全性限制 | JobObjectSecurityLimitIn | JOBOBJECT_SECURITY_LIMIT_INFORMATION |
在StartRestrictedProcess函数中,我只对作业设置了一些最基本的限制。指定了一个JOB_OBJECT_BASIC_LIMIT_INFORMATION结构,对它进行了初始化,然后调用SetInformationJobObject函数。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;
表5-2 JOB_OBJECT_BASIC_LIMIT_INFORMATION结构的成员
成 员 | 描 述 | 说 明 |
PerProcessUser-TimeLimit | 设定分配给每个进程的用户方式的最大时间(以1 0 0 n s为间隔时间) | 任何进程占用的时间如果超过了分配给它的时间,系统将自动终止它的运行。若要设置这个限制条件,请在LimitFlags成员中设定JOB_OBJECT_LIMIT_PROCESS_TIME |
PerJobUser-TimeLimit | 设定该作业中可以使用多少用户方式的时间(以1 0 0 n s为间隔时间) | 按照默认设置,当达到该时间限制时,系统将自动终止所有进程的运行。可以在作业运行时定期改变这个值。若要设置该限制条件,请在LimitFlags成员中设定JOB_OBJECT_LIMIT_JOB_TIME |
LimitFlags | 指明哪些限制适用于该作业 | 详细说明参见本表下面的一段 |
Minimum Working SetSize/Maximum Working SetSize | 设定每个进程(不是作业中的所有进程)的最小和最大工作区的大小 | 通常,进程的工作区可能扩大而超过它的最小值。设置MaximumWorkingSetSize后,就可以实施硬限制。一旦进程的工作区达到该限制值,进程就会对此作出页标记。各个进程对SetProcessWorking-SetSize的调用将被忽略,除非该进程只是试图清空它的工作区。若要设置该限制,请在LimitFlags成员中设定JOB_OBJECT_LIMIT_WORKINGSET标志 |
ActiveProcessLimit | 设定作业中可以同时运行的进程的最大数量 | 超过这个限制的任何尝试都会导致新进程被迫终止运行,并产生一个“定额不足”的错误。若要设置这个限制,请在LimitFlags成员中设定JOB_OBJECT_LIMIT_ACTIVE_PROCESS |
Affinity | 设定能够运行的进程的CPU子集 | 单个进程甚至能够进一步对此作出限制。若要设置这个限制,请在LimitFlags成员中设定JOB_OBJECT_LIMIT_AFFINITY |
PriorityClass | 设定所有进程使用的优先级 | 如果进程调用SetPriorityClass函数,即使该函数调用失败,它也能成功地返回。如果进程调用GetPriorityClass函数,该函数将返回进程已经设置的优先级类,尽管这可能不是进程的实际优先级类。此外,SetThreadPriority无法将线程的优先级提高到正常的优先级之上,不过它可以用于降低线程的优先级。若要设置这个限制,请在LimitFlags成员中设定JOB_OBJECT_LIMIT_PRIORITY_CLASS |
SchedulingClass | 设定分配给作业中的线程的相对时段差 | 它的值可以在0到9之间(包括0和9),默认值是5。详细说明参见本表后面的文字。若要设置这个限制,请在LimitFlags成员中设定JOB_OBJECT_LIMIT_SCHEDULING_CLASS |
关于这个结构的某些问题在Platform SDK文档中并没有说清楚,因此在这里作一些说明。你在L i m i t F l a g s 成员中设置了一些信息,来指明想用于作业的限制条件。我设置了J O B _ O B J E C T _ L I M I T _ P R I O R I T Y _ C L A S S和J O B _ O B J E C T _ L I M I T _ J O B _ T I M E这两个标志。这意味着它们是我用于该作业的唯一的两个限制条件。我没有对C P U的亲缘关系、工作区的大小、每个进程占用的C P U时间等作出限制。
当作业运行时,它会维护一些统计信息,比如作业中的进程已经使用了多少C P U时间。每次使用J O B _ O B J E C T _ L I M I T _ J O B _ T I M E标志来设置基本限制时,作业就会减去已经终止运行的进程的C P U时间的统计信息。这显示当前活动的进程使用了多少C P U时间。如果想改变作业运行所在的C P U的亲缘关系,但是没有重置C P U时间的统计信息,那将要如何处理呢?为了处理这种情况,必须使用JOB_OBJECT_LIMIT_AFFINITY 标志来设置新的基本限制条件,并且必须退出J O B _ O B J E C T _ L I M I T _ J O B _ T I M E标志的设置。这样一来, 就告诉作业, 不再想要使用C P U的时间限制。这不是你想要的。
你想要的是改变C P U亲缘关系的限制,保留现有的C P U时间限制。你只是不想减去已终止运行的进程的C P U 时间的统计信息。为了解决这个问题,可以使用一个特殊标志,即J O B _ O B J E C T _ L I M I T _ P R E S E RV E _ J O B _ T I M E。这个标志与J O B _ O B J E C T _ L I M I T _ J O B _ T I M E标志是互斥的。J O B _ O B J E C T _ L I M I T _ P R E S E RV E _ J O B _ T I M E标志表示你想改变限制条件,而不减去已经终止运行的进程的C P U时间的统计信息。
现在介绍一下J O B O B J E C T _ B A S I C _ L I M I T _ I N F O R M AT I O N结构的S c h e d u l i n g C l a s s成员。假如你有两个正在运行的作业,你将两个作业的优先级类都设置为N O R M A L _ P R I O R I T Y _C L A S S。但是你还想让一个作业中的进程获得比另一个进程多的C P U 时间。可以使用S c h e d u l i n g C l a s s成员来改变拥有相同优先级的作业的相对调度关系。可以设置一个0至9之间的值(包括0和9),5是默认值。在Windows 2000上,如果这个设置值比较大,那么系统就会给某个作业的进程中的线程提供较长的C P U时间量。如果设置的值比较小,就减少该线程的C P U时间量。
例如,我有两个拥有正常优先级类的作业。每个作业包含一个进程,每个进程只有一个(拥有正常优先级的)线程。在正常环境下,这两个线程将按循环方式进行调度,每个线程获得相同的C P U时间量。但是,如果将第一个作业的S c h e d u l i n g C l a s s成员设置为3,那么,当该作业中的线程被安排C P U时间时,它得到的时间量将比第二个作业中的线程少。
如果使用S c h e d u l i n g C l a s s成员,应该避免使用大数字即较大的时间量,因为较大的时间量会降低系统中的其他作业、进程和线程的总体响应能力。另外,我只是介绍了Windows 2000中的情况。M i c r o s o f t计划在将来的Wi n d o w s版本中对线程调度程序进行更重要的修改,因为它认为操作系统应该为作业、进程和线程提供更宽松的线程调度环境。
需要特别注意的最后一个限制是J O B _ O B J E C T _ L I M I T _ D I E _ O N _ U N H A N D L E D _E X C E P T I O N限制标志。这个限制可使系统为与作业相关的每个进程关闭“未处理的异常情况”对话框。系统通过调用S e t E r r o r M o d e函数,将作业中的每个进程的S E M _ N O G P FA U LT E R R O R B O X标志传递给它。作业中产生未处理的异常情况的进程会立即终止运行,不显示任何用户界面。对于服务程序和其他面向批处理的作业来说,这是个非常有用的限制标志。如果没有这个标志,作业中的进程就会产生一个异常情况,并且永远不会终止运行,从而浪费了系统资源。
除了基本限制外,还可以使用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;
另外两个成员P r o c e s s M e m o r y L i m i t和J o b M e m o r y L i m i t分别用于限制作业中的任何一个进程和所有进程使用的已确认的内存量。若要设置这些限制值,可以在L i m i t F l a g s成员中分别设定J O B _ O B J E C T _ L I M I T _ J O B _ M E M O RY和J O B _ O B J E C T _ L I M I T _ P R O C E S S _ M E M O RY两个标志。
现在看一下可以对作业设置的另一些限制。下面是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 _ U I L I M I T _ H A N D L E S是特别有趣的。这个限制意味着作业中没有一个进程能够访问该作业外部的进程创建的U S E R对象。因此,如果试图在作业内部运行Microsoft Spy++,那么除了S p y + +自己创建的窗口外,你看不到任何别的窗口。图5 - 1显示的S p y + +中打开了两个M D I子窗口。注意,Threads 1的窗口包含一个系统中的线程列表。这些线程中只有一个线程,即000006AC SPYXX似乎创建了一些窗口。这是因为我是在它自己的作业中运行S p y + +的,并且限制了它对U I句柄的使用。在同一个窗口中,可以看到M S D E V和E X P L O R E R两个线程,但是看来它们尚未创建任何窗口。可以保证,这些线程肯定创建了窗口,但是S p y + +无法访问它们。在对话框的右边,可以看到Windows 3窗口,在这个窗口中,S p y + +显示了桌面上存在的所有窗口的层次结构。注意,它只有一个项目,即0 0 0 0 0 0 0 0。S p y + +必须将它作为占位符放在这里。
表5-3 用于作业对象的基本用户界面限制的位标志
标志 | 描述 |
JOB_OBJECT_UILIMIT_EXITWINDOWS | 用于防止进程通过ExitWindowsEx函数退出、关闭、重新引导或关闭系统电源 |
JOB_OBJECT_UILIMIT_READCLIPBOARD | 防止进程读取剪贴板的内容 |
JOB_OBJECT_UILIMIT_WRITECLIPBOARD | 防止进程删除剪贴板的内容 |
JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS | 防止进程通过SystemParametersInfor函数来改变系统参数 |
JOB_OBJECT_UILIMIT_DISPLAYSETTINGS | 防止进程通过ChangeDisplaySettings函数来改变显示设置 |
JOB_OBJECT_UILIMIT_GLOBALATOMS | 为作业赋予它自己的基本结构表,使作业中的进程只能访问该作业的表 |
JOB_OBJECT_UILIMIT_DESKTOP | 防止进程使用CreateDesktop或SwitchDesktop函数创建或转换桌面 |
JOB_OBJECT_UILIMIT_HANDLES | 防止作业中的进程使用同一作业外部的进程创建的USER对象(如HWND) |
图5-1 在作业中运行的Microsoft Spy++可以限制对UI句柄的访问
注意,这个UI限制是单向的。这就是说,作业外部的进程能够看到作业内部的进程创建的U S D R对象。例如,如果我在一个作业中运行N o t e p a d,并在作业的外部运行S p y + +,那么,如果N o t e p a d所在的作业设定了J O B _ O B J E C T _ U I L I M I T _ H A N D L E S标志, S p y + +将能够看到N o t e p a d的窗口。同样,如果S p y + +在它自己的作业中,那么它也可以看到N o t e p a d的窗口,只要它设定了J O B _ O B J E C T _ U I L I M I T _ H A N D L E S标志。
如果想为作业进程的操作创建真正的沙框,那么限制U I句柄是可怕的。但是,如果作为作业组成部分的一个进程要与作业外部的进程进行通信,就可以使用这种限制。
实现这个目的有一个简便的方法,那就是使用窗口消息,但是,如果作业的进程不能访问U I句柄,那么作业中的进程就无法将窗口消息发送或显示在作业外部的进程创建的窗口中。不过,可以使用下面这个新函数来解决这个问题:
BOOL UserHandleGrantAccess(
HANDLE hUserObj,
HANDLE hjob,
BOOL fGrant);
对作业施加的最后一种限制类型与安全性相关(注意,一旦使用这种限制,就无法取消安全性限制)。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;
表5-4 JOBOBJECT_SECURITY_LIMIT_INFORMATIDN 的成员
成员 | 描述 |
SecurityLimitFlags | 指明是否不允许管理员访问、不允许无限制的标记访问、强制使用特定的访问标记,或者停用某些安全性标识符和优先权 |
JobToken | 作业中的所有进程使用的访问标记 |
SidsToDisable | 指明为访问检查停用哪些SID |
PrivilegesToDelete | 指明要从访问标记中删除哪些优先权 |
RestrictedSids | 指明应该添加给访问标记的一组仅为拒绝(deny only)的SID |
当然,一旦给作业设置了限制条件,就可以查询这些限制。通过调用下面的代码,就可以进行这一操作
BOOL QueryInformationJobObjec t(
HANDLE hJob,
JOBOBJECTINFOCLASS JobObjectInformationClas s,
PVOID pvJobObjectInformation,
DWORD cbJobObjectInformationLe ngth,
PDWORD pdwReturnLength);
注意作业中的进程可以调用QueryInformationJobObjec
上面介绍的是设置和查询限制方面的信息。现在回到StartRestrictedProcess这个函数的操作上来。当对作业实施一些限制之后,通过调用CreateProcess,生成了一个进程,我想将它放入作业。但是,注意,当调用CreateProcess时,我使用了CREATE_SUSPENDED标志。这样,创建了一个新进程,但是不允许它执行任何代码。由于Start-ReatrictedProcess函数是从不属于作业组成部分的进程来执行的,因此子进程也不属于作业的组成部分。如果准备立即允许子进程开始执行代码,那么它将跑出我的沙框,并且能够成功地执行我想限制它做的工作。因此,当创建子进程之后,在我允许它开始运行之前,我必须显式地将该进程放入我新创建的作业,方法是调用下面的代码:
BOOL AssignProcessToJobObject (
HANDLE hJob,
HANDLE hProcess);
• 打开JOBOBJECT_BASIC_LIMIT_INFORMATION的LimitFlags成员中的JOB_OBJECT_BREAKAWAY_OK标志,告诉系统,新生成的进程可以在作业外部运行。若要做到这一点,必须用新的CREATE_BREAKAWAY_FROM_JOB标志来调用CreateProcess。如果用CREATE_BREAKAWAY_FROM_JOB标志调用CreateProcess函数,但是该作业并没有打开CREATE_BREAKAWAY_FROM_JOB这个标志,那么CreateProcess函数运行就会失败。如果新生成的进程也能控制作业,那么这个机制是有用的。
• 打开JOBOBJECT_BASIC_LIMIT_INFORMATION的LimitFlags成员中的JOB_OBJECT_SILENT_BREAKAWAY_OK标志。该标志也告诉系统,新生成的进程不应该是作业的组成部分。但是没有必要将任何其他标志传递给CreateProcess。实际上,该标志将使新进程不能成为作业的组成部分。该标志可以用于原先对作业对象一无所知的进程。
至于StartRestrictedProcess函数,当调用AssignProcessToJobObject
当然,想对作业进行的最经常的操作是撤消作业中的所有进程。本章开头讲过, DeveloperStudio没有配备任何便于使用的方法,来停止进程中的某个操作,因为它不知道哪个进程是由第一个进程生成的(这非常复杂。我在Microsoft Systems Journal期刊1 9 9 8年9月号上Win32 问与答栏中介绍了Developer Studio是如何做到这一点的)。我认为,Developer Studio的将来版本将会改用作业来进行操作,因为代码的编写要容易得多,可以用它做更多的工作。
若要撤消作业中的进程,只需要调用下面的代码:
BOOL TerminateJobObject(
HANDLE hJob,
UINT 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 ThisPeriodTotalKernelTim e;
DWORD TotalPageFaultCount;
DWORD TotalProcesses;
DWORD ActiveProcesses;
DWORD TotalTerminatedProcesses ;
} JOBOBJECT_BASIC_ACCOUNTING_INFORMATION, *PJOBOBJECT_BASIC_ACCOUNTING_INFORMATION;
表5-5 JOBOBJECT_BASIC_ACCOUNTING_INFORMATION的成员
成员 | 描述 |
TotalUserTime | 设定作业中的进程已经使用多少用户方式C P U时间 |
TotalKernelTime | 设定作业中的进程已经使用多少内核方式C P U时间 |
ThisPeriodTotalUserTime | 与TotalUserTime的作用相同,差别是,当调用SetInformation-JobObject以便改变基本限制信息并且不使用JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME限制标记时,本值复置为0 |
ThisPeriodTotalKernelTim | 与ThisPeriodTotalUserTime相同,差别是,本值显示的是内核方式时间 |
TotalPageFaultCount | 设定作业中的进程已经产生的页面故障数量 |
TotalProcesses | 设定曾经成为作业组成部分的进程总数 |
ActiveProcesses | 设定当前作为作业的组成部分的进程的数量 |
TotalTerminatedProcesses | 设定由于超过分配给它们的C P U时间限制而被撤消的进程的数量 |
除了查询这些基本统计信息外,可以进行一次函数调用,以同时查询基本统计信息和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;
typedef struct _IO_COUNTERS
{
ULONGLONG ReadOperationCount;
ULONGLONG WriteOperationCount;
ULONGLONG OtherOperationCount;
ULONGLONG ReadTransferCount;
ULONGLONG WriteTransferCount;
ULONGLONG OtherTransferCount;
} IO_COUNTERS;
BOOL GetProcessIoCounters(
HANDLE hProcess,
PIO_COUNTERS pIoCounters);
typedef struct _JOBOBJECT_BASIC_PROCESS_ID_LIST
{
DWORD NumberOfAssignedProcesse s;
DWORD NumberOfProcessIdsInList ;
DWORD ProcessIdList[1];
} JOBOBJECT_BASIC_PROCESS_ID_LIST, *PJOBOBJECT_BASIC_PROCESS_ID_LIST;
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->NumberOfAssignedProcesse s = MAX_PROCESS_IDS;
//Request the current set of process IDs.
QueryInformationJobObjec t(hjob, JobObjectBasicProcessIdL ist,
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.
}
图5-2 MMC的性能监控器:作业对象计数器
图5-3 MMC 的性能监控器:作业对象明细计数器
注意,当调用C r e a t e J o b O b j e c t函数时,只能为已经赋予名字的作业获取性能计数器信息。由于这个原因,即使不打算按名字来共享跨越进程的作业对象,也应该创建带有名字的这些对象。
现在,已经知道了关于作业对象的基本知识,剩下要介绍的内容是关于通知的问题。例如,是否想知道作业中的所有进程何时终止运行或者分配的全部C P U时间是否已经到期呢?也许想知道作业中何时生成新进程或者作业中的进程何时终止运行。如果不关心这些通知信息(而且许多应用程序也不关心这些信息),作业的操作非常容易。如果关心这些事件,那么还有一些工作要做。
如果关心的是分配的所有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,
jobObjectAssociateComple tionPortInformation,
&joacp, sizeof(jaocp));
BOOL GetQueuedCompletionStatu s(
HANDLE hIOCP,
PDWORD pNumBytesTransferred,
PULONG_PTR pCompletionKey,
POVERLAPPED *pOverlapped,
DWORD dwMilliseconds);
表5-6 系统可以发送给作业的相关完成端口的作业事件通知
事件 | 描述 |
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO | 当作业中没有进程运行时发送 |
JOB_OBJECT_MSG_END_OF_PROCESS_TIME | 当超过分配给进程的C P U时间时发送。进程终止运行,并赋予进程的I D |
JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT | 当试图超过作业中运行的进程数量时发送 |
JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT | 当进程试图占用超过限额的内存时发送。给出进程的I D |
JOB_OBJECT_MSG_JOB_MEMORY_LIMIT | 当进程试图占用的内存超过作业的内存限制时发送。给出进程的I D |
JOB_OBJECT_MSG_NEW_PROCESS | 当一个进程添加给作业时发送。给出进程的I D |
JOB_OBJECT_MSG_EXIT_PROCESS | 当进程终止运行时发送。给出进程的I D |
JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS | 当进程由于未处理的异常事件而终止运行时发送。给出进程的I D |
JOB_OBJECT_MSG_END_OF_JOB_TIME | 当超过分配给作业的C P U时间时发送。这些进程没有终止运行。可以允许它们继续运行,设置一个新的时间限制,或者自己调用Te r m i n a t e J o b O b j e c t函数 |
最后要说明的一点是,按照默认设置,作业对象是这样配置的:当分配给作业的C P U时间已经到期时,作业的所有进程均自动停止运行,而J O B _ O B J E C T _ M S G _ E N D _ O F _ J O B _ T I M E通知尚未发送。如果想要防止作业对象撤消进程而只是通知你时间已经超过,必须执行下面这样的代码:
//Create a JOBOBJECT_END_OF_JOB_TIME_INFORMATION structure
//and initialize its only member.
JOBOBJECT_END_OF_JOB_TIME_INFORMATION joeojti;
joeojti.EndOfJobTimeAction = OB_OBJECT_POST_AT_END_OF_JOB;
//Tell the job object what we want it to do when the job time is exceeded.
SetInformationJobObject(hJob, JobObjectEndOfJobTimeInf ormation,
&joeojti, sizeof(joeojti));
J o b L a b应用程序“0 5 J o b L a b . e x e”(在本章未尾处清单5 - 2中列出)使你能够很容易地对作业进行实验性操作。该应用程序的源代码和资源文件放在本书所附光盘上0 5 - J o b L a b目录中。当启动该程序时,出现图5 -4所示的窗口。
当进程被初始化时,它创建一个作业对象。我创建的这个作业对象的名字是J o b L a b,这样,就可以使用M M C的Performance Monitor Snap-In来观察和监控它的性能。该应用程序还创建了一个I / O完成端口,并将作业对象与它相关联。这样就可以对来自作业的通知进行监控,并可显示在窗口底部的列表框中。
开始时,该作业不包含进程,也没有各种限制条件。顶部的各个域用于设定对作业的基本限制和扩展限制条件。要做的工作是用有效值填写这些域,然后点击Apply Limits按钮。如果将一个域置空,那么该限制条件就不起作用。除了基本限制和扩展限制条件外,还可以打开和关闭各种U I限制。注意, Preserve Job Time When Applying Limits(当运用各个限制时保留作业时间)复选框并不用于设置限制条件。它只是让你在查询基本统计信息时可以改变作业的限制条件,而不重置T h i s P e r i o d To t a l U s e r Ti m e和T h i s P e r i o d - To t a l K e r n e l Ti m e成员。当运用单个作业的时间限制时,该复选框不起作用。
图5-4 JobLab示例应用程序
其余的按钮供你用其他方式对作业进行操作。Terminate Processes按钮用于撤消作业中的所有进程。Spawn CMD In Job按钮用于生成与作业相关的命令外壳进程。从该命令外壳程序中,可以生成更多的子进程,并且可以看到它们如何作为作业的组成部分来运行。我发现这对试验操作是非常有用的。最后一个按钮是Put PID In Job,它用于将现有的无作业进程与作业相关联。
窗口底部的列表框显示了更新的关于作业的状态信息。每隔1 0 s,该窗口显示一次基本统计信息和I / O统计信息,以及进程/作业的内存峰值使用量。同时也显示作业中当前的每个进程的I D。
最后要说明的是,如果修改了源代码,并且创建一个没有名字的作业内核对象,那么可以运行该应用程序的多个拷贝,以便在同一台机器上创建两个或多个作业对象,并且进行更多的试验。
就源代码而言,没有什么特殊的东西需要介绍,因为源代码已经做了非常完善的说明。不过,我创建了一个J o b . h文件,它定义了一个Cjob C++类,用于封装操作系统的作业对象。这
使得操作起来更加容易,因为不必到处传递作业的句柄。这个类还减少了平常调用Q u e r yI n f o r m a t i o n J o b O b j e c t和S e t I n f o r m a t i o m J o b O b j e c t函数时需要进行的转换工作量。
清单5-2 JobLab示例应用程序
#include "..\CmnHdr.h"
#include <windowsx.h>
#include <process.h> // for _beginthreadex
#include <tchar.h>
#include <stdio.h>
#include "Resource.h"
#include "Job.h"
///////////////////////////////////////////////////////////////////////////////
CJob g_job; // Job object
HWND g_hwnd; // Handle to dialog box (accessible by all threads)
HANDLE g_hIOCP; // Completion port that receives Job notifications
HANDLE g_hThreadIOCP; // Completion port thread
// Completion keys for the completion port
#define COMPKEY_TERMINATE ((UINT_PTR) 0)
#define COMPKEY_STATUS ((UINT_PTR) 1)
#define COMPKEY_JOBOBJECT ((UINT_PTR) 2)
///////////////////////////////////////////////////////////////////////////////
DWORD WINAPI JobNotify(PVOID)
{
TCHAR sz[2000];
BOOL fDone = FALSE;
while (!fDone)
{
DWORD dwBytesXferred;
ULONG_PTR CompKey;
LPOVERLAPPED po;
GetQueuedCompletionStatu s(g_hIOCP,
&dwBytesXferred, &CompKey, &po, INFINITE);
// The app is shutting down, exit this thread
fDone = (CompKey == COMPKEY_TERMINATE);
HWND hwndLB = FindWindow(NULL, TEXT("Job Lab"));
hwndLB = GetDlgItem(hwndLB, IDC_STATUS);
if (CompKey == COMPKEY_JOBOBJECT)
{
lstrcpy(sz, TEXT("--> Notification: "));
PTSTR psz = sz + lstrlen(sz);
switch (dwBytesXferred)
{
case JOB_OBJECT_MSG_END_OF_JOB_TIME:
wsprintf(psz, TEXT("Job time limit reached"));
break;
case JOB_OBJECT_MSG_END_OF_PROCESS_TIME:
wsprintf(psz, TEXT("Job process (Id=%d) time limit reached"), po);
break;
case JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT:
wsprintf(psz, TEXT("Too many active processes in job"));
break;
case JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO:
wsprintf(psz, TEXT("Job contains no active processes"));
break;
case JOB_OBJECT_MSG_NEW_PROCESS:
wsprintf(psz, TEXT("New process (Id=%d) in Job"), po);
break;
case JOB_OBJECT_MSG_EXIT_PROCESS:
wsprintf(psz, TEXT("Process (Id=%d) terminated"), po);
break;
case JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS:
wsprintf(psz, TEXT("Process (Id=%d) terminated abnormally"), po);
break;
case JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT:
wsprintf(psz, TEXT("Process (Id=%d) exceeded memory limit"), po);
break;
case JOB_OBJECT_MSG_JOB_MEMORY_LIMIT:
wsprintf(psz,
TEXT("Process (Id=%d) exceeded job memory limit"), po);
break;
default:
wsprintf(psz, TEXT("Unknown notification: %d"), dwBytesXferred);
break;
}
ListBox_SetCurSel(hwndLB, ListBox_AddString(hwndLB, sz));
CompKey = 1; // Force a status update when a notification arrives
}
if (CompKey == COMPKEY_STATUS)
{
static int s_nStatusCount = 0;
_stprintf(sz, TEXT("--> Status Update (%u)"), s_nStatusCount++);
ListBox_SetCurSel(hwndLB, ListBox_AddString(hwndLB, sz));
// Show the basic accounting information
JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION jobai;
g_job.QueryBasicAccountingInfo (&jobai);
_stprintf(sz, TEXT("Total Time: User=%I64u, Kernel=%I64u ")
TEXT("Period Time: User=%I64u, Kernel=%I64u"),
jobai.BasicInfo.TotalUserTime.QuadPart,
jobai.BasicInfo.TotalKernelTime.QuadPart,
jobai.BasicInfo.ThisPeriodTotalUserTime.QuadPart,
jobai.BasicInfo.ThisPeriodTotalKernelTim e.QuadPart);
ListBox_SetCurSel(hwndLB, ListBox_AddString(hwndLB, sz));
_stprintf(sz, TEXT("Page Faults=%u, Total Processes=%u, ")
TEXT("Active Processes=%u, Terminated Processes=%u"),
jobai.BasicInfo.TotalPageFaultCount,
jobai.BasicInfo.TotalProcesses,
jobai.BasicInfo.ActiveProcesses,
jobai.BasicInfo.TotalTerminatedProcesses );
ListBox_SetCurSel(hwndLB, ListBox_AddString(hwndLB, sz));
// Show the I/O accounting information
_stprintf(sz, TEXT("Reads=%I64u (%I64u bytes), ")
TEXT("Write=%I64u (%I64u bytes), Other=%I64u (%I64u bytes)"),
jobai.IoInfo.ReadOperationCount, jobai.IoInfo.ReadTransferCount,
jobai.IoInfo.WriteOperationCount, jobai.IoInfo.WriteTransferCount,
jobai.IoInfo.OtherOperationCount, jobai.IoInfo.OtherTransferCount);
ListBox_SetCurSel(hwndLB, ListBox_AddString(hwndLB, sz));
// Show the peak per-process and job memory usage
JOBOBJECT_EXTENDED_LIMIT_INFORMATION joeli;
g_job.QueryExtendedLimitInfo(&joeli);
_stprintf(sz, TEXT("Peak memory used: Process=%I64u, Job=%I64u"),
(__int64) joeli.PeakProcessMemoryUsed,
(__int64) joeli.PeakJobMemoryUsed);
ListBox_SetCurSel(hwndLB, ListBox_AddString(hwndLB, sz));
// Show the set of Process IDs
DWORD dwNumProcesses = 50, dwProcessIdList[50];
g_job.QueryBasicProcessIdList(dwNumProcesses,
dwProcessIdList, &dwNumProcesses);
_stprintf(sz, TEXT("PIDs: %s"),
(dwNumProcesses == 0) ? TEXT("(none)") : TEXT(""));
for (DWORD x = 0; x < dwNumProcesses; x++) {
_stprintf(_tcschr(sz, 0), TEXT("%d "), dwProcessIdList[x]);
}
ListBox_SetCurSel(hwndLB, ListBox_AddString(hwndLB, sz));
}
}
return(0);
}
///////////////////////////////////////////////////////////////////////////////
BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam)
{
chSETDLGICONS(hwnd, IDI_JOBLAB);
// Save our window handle so that the completion port thread can access it
g_hwnd = hwnd;
HWND hwndPriorityClass = GetDlgItem(hwnd, IDC_PRIORITYCLASS);
ComboBox_AddString(hwndPriorityClass, TEXT("No limit"));
ComboBox_AddString(hwndPriorityClass, TEXT("Idle"));
ComboBox_AddString(hwndPriorityClass, TEXT("Below normal"));
ComboBox_AddString(hwndPriorityClass, TEXT("Normal"));
ComboBox_AddString(hwndPriorityClass, TEXT("Above normal"));
ComboBox_AddString(hwndPriorityClass, TEXT("High"));
ComboBox_AddString(hwndPriorityClass, TEXT("Realtime"));
ComboBox_SetCurSel(hwndPriorityClass, 0); // Default to "No Limit"
HWND hwndSchedulingClass = GetDlgItem(hwnd, IDC_SCHEDULINGCLASS);
ComboBox_AddString(hwndSchedulingClass, TEXT("No limit"));
for (int n = 0; n <= 9; n++) {
TCHAR szSchedulingClass[2] = { (TCHAR) (TEXT('0') + n), 0 };
ComboBox_AddString(hwndSchedulingClass, szSchedulingClass);
}
ComboBox_SetCurSel(hwndSchedulingClass, 0); // Default to "No Limit"
SetTimer(hwnd, 1, 10000, NULL); // 10 second accounting update
return(TRUE);
}
///////////////////////////////////////////////////////////////////////////////
void Dlg_ApplyLimits(HWND hwnd)
{
const int nNanosecondsPerSecond = 100000000;
const int nMillisecondsPerSecond = 1000;
const int nNanosecondsPerMilliseco nd =
nNanosecondsPerSecond / nMillisecondsPerSecond;
BOOL f;
__int64 q;
SIZE_T s;
DWORD d;
// Set Basic and Extended Limits
JOBOBJECT_EXTENDED_LIMIT_INFORMATION joeli = { 0 };
joeli.BasicLimitInformation.LimitFlags = 0;
q = GetDlgItemInt(hwnd, IDC_PERPROCESSUSERTIMELIMIT, &f, FALSE);
if (f)
{
joeli.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_PROCESS_TIME;
joeli.BasicLimitInformation.PerProcessUserTimeLimit.QuadPart =
q * nNanosecondsPerMilliseco nd / 100;
}
q = GetDlgItemInt(hwnd, IDC_PERJOBUSERTIMELIMIT, &f, FALSE);
if (f)
{
joeli.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_JOB_TIME;
joeli.BasicLimitInformation.PerJobUserTimeLimit.QuadPart =
q * nNanosecondsPerMilliseco nd / 100;
}
s = GetDlgItemInt(hwnd, IDC_MINWORKINGSETSIZE, &f, FALSE);
if (f)
{
joeli.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_WORKINGSET;
joeli.BasicLimitInformation.MinimumWorkingSetSize = s * 1024 * 1024;
s = GetDlgItemInt(hwnd, IDC_MAXWORKINGSETSIZE, &f, FALSE);
if (f)
{
joeli.BasicLimitInformation.MaximumWorkingSetSize = s * 1024 * 1024;
}
else
{
joeli.BasicLimitInformation.LimitFlags &=~JOB_OBJECT_LIMIT_WORKINGSET;
chMB("Both minimum and maximum working set sizes must be set.\n"
"The working set limits will NOT be in effect.");
}
}
d = GetDlgItemInt(hwnd, IDC_ACTIVEPROCESSLIMIT, &f, FALSE);
if (f)
{
joeli.BasicLimitInformation.LimitFlags |=
JOB_OBJECT_LIMIT_ACTIVE_PROCESS;
joeli.BasicLimitInformation.ActiveProcessLimit = d;
}
s = GetDlgItemInt(hwnd, IDC_AFFINITYMASK, &f, FALSE);
if (f)
{
joeli.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_AFFINITY;
joeli.BasicLimitInformation.Affinity = s;
}
joeli.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_PRIORITY_CLASS;
switch (ComboBox_GetCurSel(GetDlgItem(hwnd, IDC_PRIORITYCLASS)))
{
case 0:
joeli.BasicLimitInformation.LimitFlags &=
~JOB_OBJECT_LIMIT_PRIORITY_CLASS;
break;
case 1:
joeli.BasicLimitInformation.PriorityClass =
IDLE_PRIORITY_CLASS;
break;
case 2:
joeli.BasicLimitInformation.PriorityClass =
BELOW_NORMAL_PRIORITY_CLASS;
break;
case 3:
joeli.BasicLimitInformation.PriorityClass =
NORMAL_PRIORITY_CLASS;
break;
case 4:
joeli.BasicLimitInformation.PriorityClass =
ABOVE_NORMAL_PRIORITY_CLASS;
break;
case 5:
joeli.BasicLimitInformation.PriorityClass =
HIGH_PRIORITY_CLASS;
break;
case 6:
joeli.BasicLimitInformation.PriorityClass =
REALTIME_PRIORITY_CLASS;
break;
}
int nSchedulingClass =
ComboBox_GetCurSel(GetDlgItem(hwnd, IDC_SCHEDULINGCLASS));
if (nSchedulingClass > 0)
{
joeli.BasicLimitInformation.LimitFlags |=
JOB_OBJECT_LIMIT_SCHEDULING_CLASS;
joeli.BasicLimitInformation.SchedulingClass = nSchedulingClass - 1;
}
s = GetDlgItemInt(hwnd, IDC_MAXCOMMITPERJOB, &f, FALSE);
if (f)
{
joeli.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_JOB_MEMORY;
joeli.JobMemoryLimit = s * 1024 * 1024;
}
s = GetDlgItemInt(hwnd, IDC_MAXCOMMITPERPROCESS, &f, FALSE);
if (f)
{
joeli.BasicLimitInformation.LimitFlags |=
JOB_OBJECT_LIMIT_PROCESS_MEMORY;
joeli.ProcessMemoryLimit = s * 1024 * 1024;
}
if (IsDlgButtonChecked(hwnd, IDC_CHILDPROCESSESCANBREAKAW AYFROMJOB))
joeli.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_BREAKAWAY_OK;
if (IsDlgButtonChecked(hwnd, IDC_CHILDPROCESSESDOBREAKAWA YFROMJOB))
joeli.BasicLimitInformation.LimitFlags |=
JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;
if (IsDlgButtonChecked(hwnd, IDC_TERMINATEPROCESSONEXCEPT IONS))
joeli.BasicLimitInformation.LimitFlags |=
JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION;
f = g_job.SetExtendedLimitInfo(&joeli,
((joeli.BasicLimitInformation.LimitFlags & JOB_OBJECT_LIMIT_JOB_TIME)
!= 0) ? FALSE :
IsDlgButtonChecked(hwnd, IDC_PRESERVEJOBTIMEWHENAPPLY INGLIMITS));
chASSERT(f);
// Set UI Restrictions
DWORD jobuir = JOB_OBJECT_UILIMIT_NONE; // A fancy zero (0)
if (IsDlgButtonChecked(hwnd, IDC_RESTRICTACCESSTOOUTSIDEU SEROBJECTS))
jobuir |= JOB_OBJECT_UILIMIT_HANDLES;
if (IsDlgButtonChecked(hwnd, IDC_RESTRICTREADINGCLIPBOARD ))
jobuir |= JOB_OBJECT_UILIMIT_READCLIPBOARD;
if (IsDlgButtonChecked(hwnd, IDC_RESTRICTWRITINGCLIPBOARD ))
jobuir |= JOB_OBJECT_UI, LIMIT_WRITECLIPBOARD;
if (IsDlgButtonChecked(hwnd, IDC_RESTRICTEXITWINDOW))
jobuir |= JOB_OBJECT_UILIMIT_EXITWINDOWS;
if (IsDlgButtonChecked(hwnd, IDC_RESTRICTCHANGINGSYSTEMPA RAMETERS))
jobuir |= JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS;
if (IsDlgButtonChecked(hwnd, IDC_RESTRICTDESKTOPS))
jobuir |= JOB_OBJECT_UILIMIT_DESKTOP;
if (IsDlgButtonChecked(hwnd, IDC_RESTRICTDISPLAYSETTINGS))
jobuir |= JOB_OBJECT_UILIMIT_DISPLAYSETTINGS;
if (IsDlgButtonChecked(hwnd, IDC_RESTRICTGLOBALATOMS))
jobuir |= JOB_OBJECT_UILIMIT_GLOBALATOMS;
chVERIFY(g_job.SetBasicUIRestrictions(jobuir));
}
///////////////////////////////////////////////////////////////////////////////
void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
case IDCANCEL:
// User is terminating our app, kill the job too.
KillTimer(hwnd, 1);
g_job.Terminate(0);
EndDialog(hwnd, id);
break;
case IDC_PERJOBUSERTIMELIMIT:
{
// The job time must be reset if setting a job time limit
BOOL f;
GetDlgItemInt(hwnd, IDC_PERJOBUSERTIMELIMIT, &f, FALSE);
EnableWindow(
GetDlgItem(hwnd, IDC_PRESERVEJOBTIMEWHENAPPLY INGLIMITS), !f);
}
break;
case IDC_APPLYLIMITS:
Dlg_ApplyLimits(hwnd);
PostQueuedCompletionStat us(g_hIOCP, 0, COMPKEY_STATUS, NULL);
break;
case IDC_TERMINATE:
g_job.Terminate(0);
PostQueuedCompletionStat us(g_hIOCP, 0, COMPKEY_STATUS, NULL);
break;
case IDC_SPAWNCMDINJOB:
{
// Spawn a command shell and place it in the job
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
TCHAR sz[] = TEXT("CMD");
CreateProcess(NULL, sz, NULL, NULL,
FALSE, CREATE_SUSPENDED, NULL, NULL, &si, π);
g_job.AssignProcess(pi.hProcess);
ResumeThread(pi.hThread);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
PostQueuedCompletionStat us(g_hIOCP, 0, COMPKEY_STATUS, NULL);
break;
case IDC_ASSIGNPROCESSTOJOB:
{
DWORD dwProcessId = GetDlgItemInt(hwnd, IDC_PROCESSID, NULL, FALSE);
HANDLE hProcess = OpenProcess(
PROCESS_SET_QUOTA | PROCESS_TERMINATE, FALSE, dwProcessId);
if (hProcess != NULL)
{
chVERIFY(g_job.AssignProcess(hProcess));
CloseHandle(hProcess);
}
else
chMB("Could not assign process to job.");
}
PostQueuedCompletionStat us(g_hIOCP, 0, COMPKEY_STATUS, NULL);
break;
}
}
///////////////////////////////////////////////////////////////////////////////
void WINAPI Dlg_OnTimer(HWND hwnd, UINT id)
{
PostQueuedCompletionStat us(g_hIOCP, 0, COMPKEY_STATUS, NULL);
}
///////////////////////////////////////////////////////////////////////////////
INT_PTR WINAPI Dlg_Proc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog);
chHANDLE_DLGMSG(hwnd, WM_TIMER, Dlg_OnTimer);
chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand);
}
return(FALSE);
}
///////////////////////////////////////////////////////////////////////////////
int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int)
{
// Create the completion port that receives job notifications
g_hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
// Create a thread that waits on the completion port
g_hThreadIOCP = chBEGINTHREADEX(NULL, 0, JobNotify, NULL, 0, NULL);
// Create the job object
g_job.Create(NULL, TEXT("JobLab"));
g_job.SetEndOfJobInfo(JOB_OBJECT_POST_AT_END_OF_JOB);
g_job.AssociateCompletionPort(g_hIOCP, COMPKEY_JOBOBJECT);
DialogBox(hinstExe, MAKEINTRESOURCE(IDD_JOBLAB), NULL, Dlg_Proc);
// Post a special key that tells the completion port thread to terminate
PostQueuedCompletionStat us(g_hIOCP, 0, COMPKEY_TERMINATE, NULL);
// Wait for the completion port thread to terminate
WaitForSingleObject(g_hThreadIOCP, INFINITE);
// Clean up everything properly
CloseHandle(g_hIOCP);
CloseHandle(g_hThreadIOCP);
// NOTE: The job is closed when the g_job's destructor is called.
return(0);
}
///////////////////////////////// End Of File /////////////////////////////////
#pragma once
///////////////////////////////////////////////////////////////////////////////
#include <malloc.h> // for _alloca
///////////////////////////////////////////////////////////////////////////////
class CJob
{
public:
CJob(HANDLE hJob = NULL);
~CJob();
operator HANDLE() const { return(m_hJob); }
// Functions to create/open a job object
BOOL Create(PSECURITY_ATTRIBUTES psa = NULL, PCTSTR pszName = NULL);
BOOL Open(PCTSTR pszName, DWORD dwDesiredAccess,
BOOL fInheritHandle = FALSE);
// Functions that manipulate a job object
BOOL AssignProcess(HANDLE hProcess);
BOOL Terminate(UINT uExitCode = 0);
// Functions that set limits/restrictions on the job
BOOL SetExtendedLimitInfo(PJOBOBJECT_EXTENDED_LIMIT_INFORMATION pjoeli,
BOOL fPreserveJobTime = FALSE);
BOOL SetBasicUIRestrictions(DWORD fdwLimits);
BOOL GrantUserHandleAccess(HANDLE hUserObj, BOOL fGrant = TRUE);
BOOL SetSecurityLimitInfo(PJOBOBJECT_SECURITY_LIMIT_INFORMATION pjosli);
// Functions that query job limits/restrictions
BOOL QueryExtendedLimitInfo(PJOBOBJECT_EXTENDED_LIMIT_INFORMATION pjoeli);
BOOL QueryBasicUIRestrictions (PDWORD pfdwRestrictions);
BOOL QuerySecurityLimitInfo(PJOBOBJECT_SECURITY_LIMIT_INFORMATION pjosli);
// Functions that query job status information
BOOL QueryBasicAccountingInfo (
PJOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION pjobai);
BOOL QueryBasicProcessIdList(DWORD dwMaxProcesses,
PDWORD pdwProcessIdList, PDWORD pdwProcessesReturned = NULL);
// Functions that set/query job event notifications
BOOL AssociateCompletionPort(HANDLE hIOCP, ULONG_PTR CompKey);
BOOL QueryAssociatedCompletio nPort(
PJOBOBJECT_ASSOCIATE_COMPLETION_PORT pjoacp);
BOOL SetEndOfJobInfo(
DWORD fdwEndOfJobInfo = JOB_OBJECT_TERMINATE_AT_END_OF_JOB);
BOOL QueryEndOfJobTimeInfo(PDWORD pfdwEndOfJobTimeInfo);
private:
HANDLE m_hJob;
};
///////////////////////////////////////////////////////////////////////////////
inline CJob::CJob(HANDLE hJob)
{
m_hJob = hJob;
}
///////////////////////////////////////////////////////////////////////////////
inline CJob::~CJob()
{
if (m_hJob != NULL)
CloseHandle(m_hJob);
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CJob::Create(PSECURITY_ATTRIBUTES psa, PCTSTR pszName)
{
m_hJob = CreateJobObject(psa, pszName);
return(m_hJob != NULL);
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CJob::Open(
PCTSTR pszName, DWORD dwDesiredAccess, BOOL fInheritHandle)
{
m_hJob = OpenJobObject(dwDesiredAccess, fInheritHandle, pszName);
return(m_hJob != NULL);
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CJob::AssignProcess(HANDLE hProcess)
{
return(AssignProcessToJobObject (m_hJob, hProcess));
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CJob::AssociateCompletionPort(HANDLE hIOCP, ULONG_PTR CompKey)
{
JOBOBJECT_ASSOCIATE_COMPLETION_PORT joacp = { (PVOID) CompKey, hIOCP };
return(SetInformationJobObject(m_hJob,
JobObjectAssociateComple tionPortInformation, &joacp, sizeof(joacp)));
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CJob::SetExtendedLimitInfo(
PJOBOBJECT_EXTENDED_LIMIT_INFORMATION pjoeli, BOOL fPreserveJobTime)
{
if (fPreserveJobTime)
pjoeli->BasicLimitInformation.LimitFlags |=
JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME;
// If we are to preserve the job's time information,
// the JOB_OBJECT_LIMIT_JOB_TIME flag must not be on
const DWORD fdwFlagTest =
(JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME | JOB_OBJECT_LIMIT_JOB_TIME);
if ((pjoeli->BasicLimitInformation.LimitFlags & fdwFlagTest)
== fdwFlagTest)
{
// These flags are mutually exclusive but both are on, error
DebugBreak();
}
return(SetInformationJobObject(m_hJob,
JobObjectExtendedLimitIn formation, pjoeli, sizeof(*pjoeli)));
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CJob::SetBasicUIRestrictions(DWORD fdwLimits)
{
JOBOBJECT_BASIC_UI_RESTRICTIONS jobuir = { fdwLimits };
return(SetInformationJobObject(m_hJob,
JobObjectBasicUIRestrict ions, &jobuir, sizeof(jobuir)));
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CJob::SetEndOfJobInfo(DWORD fdwEndOfJobInfo)
{
JOBOBJECT_END_OF_JOB_TIME_INFORMATION joeojti = { fdwEndOfJobInfo };
joeojti.EndOfJobTimeAction = fdwEndOfJobInfo;
return(SetInformationJobObject(m_hJob,
JobObjectEndOfJobTimeInf ormation, &joeojti, sizeof(joeojti)));
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CJob::SetSecurityLimitInfo(
PJOBOBJECT_SECURITY_LIMIT_INFORMATION pjosli)
{
return(SetInformationJobObject(m_hJob,
JobObjectSecurityLimitIn formation, pjosli, sizeof(*pjosli)));
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CJob::QueryAssociatedCompletio nPort(
PJOBOBJECT_ASSOCIATE_COMPLETION_PORT pjoacp)
{
return(QueryInformationJobObjec t(m_hJob,
JobObjectAssociateComple tionPortInformation, pjoacp, sizeof(*pjoacp),
NULL));
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CJob::QueryBasicAccountingInfo (
PJOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION pjobai)
{
return(QueryInformationJobObjec t(m_hJob,
JobObjectBasicAndIoAccou ntingInformation, pjobai, sizeof(*pjobai),
NULL));
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CJob::QueryExtendedLimitInfo(
PJOBOBJECT_EXTENDED_LIMIT_INFORMATION pjoeli)
{
return(QueryInformationJobObjec t(m_hJob, JobObjectExtendedLimitIn formation,
pjoeli, sizeof(*pjoeli), NULL));
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CJob::QueryBasicProcessIdList(DWORD dwMaxProcesses,
PDWORD pdwProcessIdList, PDWORD pdwProcessesReturned)
{
// Calculate the # of bytes necessary
DWORD cb = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST) +
(sizeof(DWORD) * (dwMaxProcesses - 1));
// Allocate those bytes from the stack
PJOBOBJECT_BASIC_PROCESS_ID_LIST pjobpil =
(PJOBOBJECT_BASIC_PROCESS_ID_LIST) _alloca(cb);
// Were those bytes allocated OK? If so, keep going
BOOL fOk = (pjobpil != NULL);
if (fOk)
{
pjobpil->NumberOfProcessIdsInList = dwMaxProcesses;
fOk = ::QueryInformationJobObjec t(m_hJob, JobObjectBasicProcessIdL ist,
pjobpil, cb, NULL);
if (fOk)
{
// We got the information, return it to the caller
if (pdwProcessesReturned != NULL)
*pdwProcessesReturned = pjobpil->NumberOfProcessIdsInList ;
CopyMemory(pdwProcessIdList, pjobpil->ProcessIdList,
sizeof(DWORD) * pjobpil->NumberOfProcessIdsInList );
}
}
return(fOk);
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CJob::QueryBasicUIRestrictions (PDWORD pfdwRestrictions)
{
JOBOBJECT_BASIC_UI_RESTRICTIONS jobuir;
BOOL fOk = QueryInformationJobObjec t(m_hJob, JobObjectBasicUIRestrict ions,
&jobuir, sizeof(jobuir), NULL);
if (fOk)
*pfdwRestrictions = jobuir.UIRestrictionsClass;
return(fOk);
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CJob::QueryEndOfJobTimeInfo(PDWORD pfdwEndOfJobTimeInfo)
{
JOBOBJECT_END_OF_JOB_TIME_INFORMATION joeojti;
BOOL fOk = QueryInformationJobObjec t(m_hJob, JobObjectBasicUIRestrict ions,
&joeojti, sizeof(joeojti), NULL);
if (fOk)
*pfdwEndOfJobTimeInfo = joeojti.EndOfJobTimeAction;
return(fOk);
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CJob::QuerySecurityLimitInfo(
PJOBOBJECT_SECURITY_LIMIT_INFORMATION pjosli)
{
return(QueryInformationJobObjec t(m_hJob, JobObjectSecurityLimitIn formation,
pjosli, sizeof(*pjosli), NULL));
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CJob::Terminate(UINT uExitCode)
{
return(TerminateJobObject(m_hJob, uExitCode));
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CJob::GrantUserHandleAccess(HANDLE hUserObj, BOOL fGrant)
{
return(UserHandleGrantAccess(hUserObj, m_hJob, fGrant));
}
//////////////////////////////// End of File //////////////////////////////////
//Microsoft Developer Studio generated resource script.
//
#include "Resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "afxres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (U.S.) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _WIN32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif //_WIN32
/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//
IDD_JOBLAB DIALOG DISCARDABLE 6, 18, 356, 297
STYLE DS_CENTER | WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Job Lab"
FONT 8, "MS Sans Serif"
BEGIN
GROUPBOX "Basic and Extended Limits",IDC_STATIC,4,4,348,112
LTEXT "Per-process user time limit (ms):",IDC_STATIC,8,18,98,8
EDITTEXT IDC_PERPROCESSUSERTIMELIMIT,108,16,58,12,ES_RIGHT |
ES_AUTOHSCROLL | ES_NUMBER
LTEXT "Per-job user time limit (ms):",IDC_STATIC,176,18,83,8
EDITTEXT IDC_PERJOBUSERTIMELIMIT,288,16,58,12,ES_RIGHT |
ES_AUTOHSCROLL | ES_NUMBER
LTEXT "Min working set size (MB):",IDC_STATIC,8,32,83,8
EDITTEXT IDC_MINWORKINGSETSIZE,108,30,58,12,ES_RIGHT |
ES_AUTOHSCROLL | ES_NUMBER
LTEXT "Max working set size (MB):",IDC_STATIC,176,32,85,8
EDITTEXT IDC_MAXWORKINGSETSIZE,288,30,58,12,ES_RIGHT |
ES_AUTOHSCROLL | ES_NUMBER
LTEXT "Active process limit:",IDC_STATIC,8,46,63,8
EDITTEXT IDC_ACTIVEPROCESSLIMIT,108,44,58,12,ES_RIGHT |
ES_AUTOHSCROLL | ES_NUMBER
LTEXT "Affinity mask (decimal):",IDC_STATIC,176,46,72,8
EDITTEXT IDC_AFFINITYMASK,288,44,58,12,ES_RIGHT | ES_AUTOHSCROLL |
ES_NUMBER
LTEXT "Priority class:",IDC_STATIC,8,60,42,8
COMBOBOX IDC_PRIORITYCLASS,108,58,58,73,CBS_DROPDOWNLIST |
WS_VSCROLL | WS_TABSTOP
LTEXT "Scheduling class:",IDC_STATIC,176,61,56,8
COMBOBOX IDC_SCHEDULINGCLASS,288,58,58,88,CBS_DROPDOWNLIST |
WS_VSCROLL | WS_TABSTOP
LTEXT "Max commit limit for job (MB):",IDC_STATIC,8,74,92,8
EDITTEXT IDC_MAXCOMMITPERJOB,108,72,58,12,ES_RIGHT |
ES_AUTOHSCROLL | ES_NUMBER
LTEXT "Max commit limit per process (MB):",IDC_STATIC,176,74,
109,8
EDITTEXT IDC_MAXCOMMITPERPROCESS,288,72,58,12,ES_RIGHT |
ES_AUTOHSCROLL | ES_NUMBER
CONTROL "Child processes can breakaway from the job",
IDC_CHILDPROCESSESCANBREAKAW AYFROMJOB,"Button",
BS_AUTOCHECKBOX | WS_TABSTOP,8,88,155,10
CONTROL "Child processes do breakaway from the job",
IDC_CHILDPROCESSESDOBREAKAWA YFROMJOB,"Button",
BS_AUTOCHECKBOX | WS_TABSTOP,176,88,151,10
CONTROL "Terminate processes on unhandled exceptions",
IDC_TERMINATEPROCESSONEXCEPT IONS,"Button",
BS_AUTOCHECKBOX | WS_TABSTOP,8,102,163,10
CONTROL "Preserve Job time when applying limits",
IDC_PRESERVEJOBTIMEWHENAPPLY INGLIMITS,"Button",
BS_AUTOCHECKBOX | WS_TABSTOP,176,102,136,10
GROUPBOX "Restrict UI access",IDC_STATIC,4,122,236,68
CONTROL "To outside USER objects",
IDC_RESTRICTACCESSTOOUTSIDEU SEROBJECTS,"Button",
BS_AUTOCHECKBOX | WS_TABSTOP,12,134,96,10
CONTROL "Reading from the clipboard",
IDC_RESTRICTREADINGCLIPBOARD ,"Button",BS_AUTOCHECKBOX |
WS_TABSTOP,12,148,101,10
CONTROL "Writing to the clipboard",IDC_RESTRICTWRITINGCLIPBOARD ,
"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,162,89,10
CONTROL "Exiting Windows",IDC_RESTRICTEXITWINDOW,"Button",
BS_AUTOCHECKBOX | WS_TABSTOP,12,176,68,10
CONTROL "Changing system parameters",
IDC_RESTRICTCHANGINGSYSTEMPA RAMETERS,"Button",
BS_AUTOCHECKBOX | WS_TABSTOP,120,134,106,10
CONTROL "To creating/switching desktops",IDC_RESTRICTDESKTOPS,
"Button",BS_AUTOCHECKBOX | WS_TABSTOP,120,148,115,10
CONTROL "To changing display settings",
IDC_RESTRICTDISPLAYSETTINGS,"Button",BS_AUTOCHECKBOX |
WS_TABSTOP,120,162,105,10
CONTROL "To global atoms",IDC_RESTRICTGLOBALATOMS,"Button",
BS_AUTOCHECKBOX | WS_TABSTOP,120,176,66,10
DEFPUSHBUTTON "Apply &limits",IDC_APPLYLIMITS,248,124,104,14
PUSHBUTTON "&Terminate processes",IDC_TERMINATE,248,142,104,14
PUSHBUTTON "&Spawn CMD in job",IDC_SPAWNCMDINJOB,248,160,104,14
EDITTEXT IDC_PROCESSID,248,178,48,14,ES_RIGHT | ES_AUTOHSCROLL |
ES_NUMBER
PUSHBUTTON "&Put PID in job",IDC_ASSIGNPROCESSTOJOB,302,178,50,14
LISTBOX IDC_STATUS,4,196,348,96,NOT LBS_NOTIFY |
LBS_NOINTEGRALHEIGHT | LBS_NOSEL | WS_VSCROLL |
WS_TABSTOP
END
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE DISCARDABLE
BEGIN
"Resource.h\0"
END
2 TEXTINCLUDE DISCARDABLE
BEGIN
"#include ""afxres.h""\r\n"
"\0"
END
3 TEXTINCLUDE DISCARDABLE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// DESIGNINFO
//
#ifdef APSTUDIO_INVOKED
GUIDELINES DESIGNINFO DISCARDABLE
BEGIN
IDD_JOBLAB, DIALOG
BEGIN
RIGHTMARGIN, 352
BOTTOMMARGIN, 153
END
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_JOBLAB ICON DISCARDABLE "joblab.ico"
#endif // English (U.S.) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED