5.1 作业对象
(1)什么是作业对象
①进程的父子关系只存在于创建的子进程的那一刻,Windows并不一直维护着这种父子关系,这使得管理进程并不是件容易的事。
②作业对象是用于将一组进程作为一个管理单元的内核对象,本质上可以理解为其实就是进程池对象,可将作业对象看作是进程的容器。
③作来对象可以用来限制一组进程的占用内存数量、占用CPU周期数、进程优先级等的一个“沙箱”。
④最终可以通过作业对象将该对象中的所有进程全部关闭(普通方法很难控制)
⑤通过结合一个完成端口对象并利用一个线程实时动态的监控作业对象的执行(如得到一些消卢,以便及时响应作业对象中的进程变化情况)
(2)作业对象的基本用法
①CreateJobObject (创建作业对象)
②IsProcessInJob (进程是否己经与某个作业对象关联)
③SetInformationJobObject (设置作业对象或进程的限制)
④AssignProcessToJobObject (将进程添加到作业中)
⑤QueryInformationJobObject (查询作业对象中施加的限制)
⑥TerminateJobObject ("杀死"作业中所有的进程)
⑦CloseHandle (关闭作业对象句柄,导致所有进程不能访问作业对象,但作业仍存在!)
⑧在需要时可以通过OpenJobObject方法打开一个指定名称的作业对象句柄。
【StartRestrictedProcess函数】
5.2 创建作业
(1)CreateJobObject(PSECURITY_ATTRIBUTES psa,PCTSTR pszName);
(2)IsProcessInJob——判断进程是否己与一个作业关联
①如果进程己关联到一个作业,就无法再将当前进程再从作业中移除,这点可以确保进程无法摆脱对它施加的影响。
②通过资源管理器启动的应用程序默认会自动同一个专用作业关联,此作业的名称使用了“PCA”字符串(Program Compatibility Assistant)为前缀。Windows之所以提供这个特性,为了检测兼容性问题,当启动一个老版本的应用程序时(即Vista以前的版本),就会触发兼容性助手这个进程来发出警告。
③为了摆脱“PCA”前缀作业的关联,有两种方法:一种是通过命令行而不是资源管理中启动一个进程,还有一种方法是在CreateProcess的dwCreationFlags参数加入一个CREATE_BREAKAWAY_FROM_JOB及在基本限制结构体中的LimitFlags成员加入JOB_OBJECT_LIMITBREAKWAY_OK的标志,即如果没有指定标志,将子进程将默认与父进程的作业关联。
(3)OpenJobObject函数——打开作业对象的句柄
5.3 对作业施加影响——设置作业对象的属性
(1)SetInformationJobObject函数
参数 |
描述 |
hJob |
作业对象句柄 |
JobObjectInformationClass |
限制的类型 ①JobObjectBasicLimitInformation:设置作业对象的基本信息(如进程作业集大小、进程亲缘性、进程CPU时间限制值、同时活动的进程数量等) ②JobObjectBasicUIRestrictions:对作业中的进程UI基本限制(如指定桌面,限制调用ExitWindows函数,限制剪切板读写操作等)。 ③JobObjectEndOfJobTimeInformation:指定当作业时间限制到达时,系统采取什么动作(如:通知与作业对象绑定的完成端口一个超时事件) ④JobObjectEntendedLimitInformation:作业进程的扩展限制信息(如限进作业中的进程不要弹出异常对话框(同时要指定结构体的LimitFlags为JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION,这相当于给每个进程调用SetErrorMode)、限制进程的内存使用量等) ⑤JobObjectSecurityLimitInformation:限制作业对象中的进程安全属性(如关闭一些组的特权、关闭某些特权等)。但这实现这个限制,作业对象所属进程或线程要具备更改这些作业进程完全属性的权限。 |
PVOID pJobObjectInformation |
指向一个结构体,包含具体的限制 |
cbJobObjectInformationSize |
上述结构体的大小 |
(2)参数pJobObjectInformation中常用到的结构体
①【基本限制】:JOBOBJECT_BASIC_LIMIT_INFORMATION结构体
(注意:如果也设置了扩展结构,则该结构要在扩展结构之后设置,见【JobIOCP程序】)
成员 |
描述 |
备注 |
PerProcessUserTimeLimit |
表示分配给每个进程的用户模式执行时间(即占用CPU实际的时间),单位100ns。 |
如果一个进程累计的执行时间超时过该限额,进程会被终止。可以在作业运行期定期改变该值 |
PerJobUserTimeLimit |
分配给作业的用户模式执行时间 |
超时作业会被终止 |
LimitFlags |
哪些限制对作业有效 |
具体看后面说明 |
MinimumWorkingSetSize MaximumWorkingSetSize |
限制作业对象里的进程的最小、最大工作集 |
工作集:进程虚拟空间中实际被映射到物理内存页面的那部分被称为工作集。 |
ActiveProcessLimit |
表示作业中可以同时运行的最大进程数量 |
超过限额创建的新进程将提示“配额不足”的错误。 |
Affinity |
表示能够运行的进程的CPU子集 |
单独的进程可以在此基础上进一步限制 |
PriorityClass |
表示作业中所有进程的优先级 |
单独进程不能SetPriorityClass或GetPriorityClass,这可能不能成功设置或获取到真实的进程优先级 |
SchedulingClass |
当多个作业具有相同优先级作业的调度(注意是作业,不是进程或线程) |
优先级(0-9,默认5),值越大,CPU时间越长 |
说明:
A、当想让该结构的成员生效,还需在LimitFlags中加入相应的标志位
B、每次设置JOB_OBJECT_LIMIT_JOB_TIME,作业会扣除己终止运行的进程的CPU时间统计信息,从而显示当前活动的进程使用了多少CPU时间。
C、改变作业的CPU关联性时,又不想重置CPU时间统计信息,可用JOB_OBJECT_LIMIT_AFFINITY | JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME,但必须取消JOB_OBJECT_LIMIT_JOB_TIME,因为这与JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME是互斥的。
②【扩展限制】:基本(JOBOBJECT_BASIC_LIMIT_INFORMATION)+内存限制
成员 |
描述 |
备注 |
IoInfo |
保留不用。IO计数器 |
|
ProcessMemoryLimit |
每个进程能使用的内存量 |
LimitFlags需含有: JOB_OBJECT_LIMIT_PROCESS_MEMORY |
JobMemoryLimit |
作业(所有进程)能使用的内存量 |
LimitFlags需含有: JOB_OBJECT_LIMIT_JOB_MEMORY |
PeakProcessMemoryUsed |
只读。单个进己使用的内存空间大小 |
|
PeakJobMemoryUsed |
只读。作业全部进程己使用的内存空间大小 |
③【基本UI限制】JOBOBJECT_BASIC_UI_RESTRICTIONS结构体中使用的标志位
标志位(值) |
说明 |
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 |
防止进程使用作业外部的进程创建的用户对象的句柄(如HWND) 注意: A、当把Spy++放到一个作业内部运行,加了这个标志,Spy++将看不到其他进程的窗口(如记事本的窗口),只能看到他自己的窗口。 B、这个限制是单向的,即作业外部进程可以看到作业内部进程创建的用户对象。 C、但有时需要作业内部的进程向外部进程的一个窗口发送消息,这里可以在作业外部调用UserHandleGrandAccess给作业内部的某个进程授权访问给定窗口的权限。但该函数不能在作业内部使用,这是为了防止进程自己给自己授权。 |
④【安全性限制】XP(不包括XP)之后的系统不再支持该限制,需要为每个进程单独指定安全设置。
5.4 将进程放入作业中
(1)AssignProcessToJobObject(hJob,hProcess);
① CreateProcess时须使用CREATE_SUSPENDED将主线程挂起,才能加入作业中。
②在加入作业之前可以先通过IsProcessInJob子进程是否己在其他作业对象中。
(2)让新进程独立出来
当作业中的进程生成另一个进程的时,新进程会自动成为父进程所属的作业。但可以通过下面两种方法改变这种特性:
①打开JOBOBJECT_BASIC_LIMIT_INFROMATION 的LimitFlags成员的JOB_OBJECT_BREAKAWAY_OK标志,告诉系统,新生成的进程可以在作业外部运行。同时使用CREATE_BREAKAWAY_FROM_JOB 标志调用CreateProcess创建新进程。
②打开JOBOBJECT_BASIC_LIMIT_INFROMATION 的LimitFlags成员的JOB_OBJECT_SILENT_BREAKAWAY_OK标志,告诉系统,新生成的进程可以在作业外部运行。但此后CreateProcess新进程时不必使用CREATE_BREAKAWAY_FROM_JOB 标志。
5.5 终止作业中的所有线程
(1)TerminateJobObject相当于在对作业对象中的所有进程调用一次TerminateProcess,这种方式很暴力,可能引起资源没被正确释放就直接退出(当然最后操作系统会帮忙清理)。
(2)实际中放入作业对象的进程往往也是自行开发的进程,完全可以用诸如等待Event对象的方式来优雅地退出。
(3)查询限制和统计信息:QueryInformationJobObject
①函数原型
参数 |
描述 |
hJob |
作业外部进程:通过该函数是传入hJob就可查询指定作业的限制。作业内部进程:调用时可传入NULL |
JobObjectInfoClass |
查看的限制或统计信息的类型 JobObjectBasicAccountingInformation:基本统计信息 JobObjectBasicAndIoAccountingInformation:基本+I/O统计信息 JobObjectBasicLimitInformation:基本限制信息 JobObjectBasicProcessIdList:获取当前作业中所有进程ID集 JobObjectBasicUIRestrictions:基本UI限制信息 JobObjectExtendedLimitInformation:扩展限制信息 JobObjectSecurityLimitInformation:安全限制信息 |
lpJobObjectInfo |
返回指向特定类型信息的结构体缓冲区的指针 |
cbJobObjectInfoLength |
该数据结构的大小(以字节为单位) |
lpReturnLength |
缓冲区中实际填入的字节数 |
②JOBOBJECT_BASIC_ACCOUNTING_INFORMATION结构体
成员 |
描述 |
TotalUserTime |
指出作业中进程己使用多少用户模式的CPU时间 |
TotalKernelTime |
指出作业中进程己使用多少内核模式的CPU时间 |
ThisPeriodTotalUserTime |
与TotalUserTime一样,不同的是,如果调用SetInformationJobObject来更改基本限额信息,同时没有使用JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME标志时,这个值总为0. |
ThisPeriodTotalKernelTime |
与ThisPeriodTotalUserTime一样,不同的是这个值显示的是内核CPU时间。 |
TotalPageFaultCount |
指出作业中进程产生的页面错误总数 |
TotalProcesses |
指出所有进程的总数(含己退出的进程) |
ActiveProcesses |
指出作业中当前进程总数 |
TotalTerminatedProcesses |
指出因己超过预定CPU时间限制而被“杀死”的进程数 |
③JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION结构体中的IO_COUNTERS成员
成员 |
描述 |
ReadOperationCount WriteOperationCount OtherOperationCount |
作业中进程己执行的读、写IO操作的次数及非读/写操作的次数 |
ReadTransferCount WriteTransferCount OtherTransferCount |
上述操作期间传输的字节总数 |
★GetProcessIoCounters函数可获得未放入作为业的那些进程的信息
GetProcessIoCounters(hProcess,pIoCounters);
④JOBOBJECT_BASIC_PROCESS_ID_LIST结构体
成员 |
描述 |
NumberOfAssignedProcesses |
作业中的进程数量(由我们指定一个估计的值) |
NumberOfProcessIdsInList |
函数调用以后,将ProcessIDList中实际的元素个数返回在这个变量中。 |
DWORD ProcessIdList[1] |
进程ID集 |
【StartStrictProcess程序】演示作业的使用及获取统计信息
#include <stdio.h> #include <windows.h> #include <tchar.h> #include <strsafe.h> #include <malloc.h> //for _alloca函数 #include <locale.h> //获取作业中当前的进程ID集 void EnumProcessIdsInJob(HANDLE hJob) { //先假设作业中的进程不会超过10个 const int MAX_PROCESS_IDS = 10; //计算结构体和ID集所需的空间大小 DWORD cb = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST) + (MAX_PROCESS_IDS - 1)*sizeof(DWORD); //分配内存,注意_alloca是在栈(而不是堆上分配的,所以不需要释放) PJOBOBJECT_BASIC_PROCESS_ID_LIST pjobpil = (PJOBOBJECT_BASIC_PROCESS_ID_LIST)_alloca(cb); //估计最多的进程数 pjobpil->NumberOfAssignedProcesses = MAX_PROCESS_IDS; //查询 QueryInformationJobObject(hJob, JobObjectBasicProcessIdList, pjobpil, cb, &cb); //显示进程ID集 for (DWORD x = 0; x < pjobpil->NumberOfProcessIdsInList; x++) { _tprintf(TEXT("process%d(ID=%d)\n"),x+1,pjobpil->ProcessIdList[x]); } } //利用作业对象对进程进程管理的演示 void StartRestrictedProcess() { //检查进程是否己经关联到一个作业对象中了 //如果己经关联,就没办法再换到另一个作业对象中去 BOOL bInJob = FALSE; IsProcessInJob(GetCurrentProcess(), NULL, &bInJob); if (bInJob) { printf("Process already in a job\n"); return; } //创建作业内核对象 HANDLE hjob = CreateJobObject(NULL, TEXT("MyRestrictedProcessJob")); //在作业中放入线程对象的一些限制规则 //首先,设置基本限制 JOBOBJECT_BASIC_LIMIT_INFORMATION jobli = { 0 }; //进程总是运行在“空闲”优先级 jobli.PriorityClass = IDLE_PRIORITY_CLASS; //该作业对象在用户模式下不能超过1秒的CPU时间(课本这里错了吧,应该1ms) //jobli.PerJobUserTimeLimit.QuadPart = 2000* 10000; //多少个tick(1tick=100ns) //jobli.PerProcessUserTimeLimit.QuadPart = 1*10000; //1ms jobli.PerJobUserTimeLimit.QuadPart = 1000*10000I64; //10000tick=10000*100ns=1ms //这里只增加了以上两个限制 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; //初始化为0 //进程不能访问用户对象(如窗口句柄) jobuir.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_HANDLES; //进程不能关闭系统 jobuir.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_EXITWINDOWS; SetInformationJobObject(hjob, JobObjectBasicUIRestrictions, &jobuir, sizeof(jobuir)); //创建子进程并加入作业对象中 // Note:进程被创建后,其线程要立刻挂起,在加入作业对象前不能执行 // 任何代码,这是作业对象的要求。否则,进程执行的那部分代码 // 可能就不受作业对象的限制。 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 /*| CREATE_BREAKAWAY_FROM_JOB8*/, NULL,NULL,&si,&pi); //将子进程加入到作业对象中 // 注意:当创建子进程时,子进程会自动加入到父进程所在的作业对象 AssignProcessToJobObject(hjob, pi.hProcess); //现在可以恢复子进程的主线程,开始执行代码 ResumeThread(pi.hThread); CloseHandle(pi.hThread); //枚举作业对象中的进程值ID EnumProcessIdsInJob(hjob); //等待子进程结束或作业对象所分配的CPU时间被用完 HANDLE h[2]; h[0] = pi.hProcess; h[01] = hjob; /*WaitForMultipleObjects 1. nCount,DWORD类型,用于指定句柄数组的数量 2. lphObjects,Pointer类型,用于指定句柄数组的内存地址 3. fWaitAll,Boolean类型,True表示函数等待所有指定句柄的Object有信号为止 4. dwTimeout,DWORD类型,用于指定等待的Timeout时间,单位毫秒,可以是INFINITE 返回值: 如果fWaitAll为TRUE,则返回值表明所有指定对象的状态信号 如果fWaitAll为FALSE,则返回值-WAIT_OBJECT_0 表示lphObjects数组相应的对象变成有信号 */ DWORD dw = WaitForMultipleObjects(2, h, FALSE, INFINITE); switch (dw - WAIT_OBJECT_0) { case 0: printf("The process has terminated...\n"); break; case 1: printf("All of the job's allotted CPU time was used...\n"); 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); CloseHandle(pi.hProcess); CloseHandle(hjob); _tprintf(szInfo); _tsystem(_T("PAUSE")); return; } int main() { _tsetlocale(LC_ALL, TEXT("chs")); StartRestrictedProcess(); return 0; }