【windows核心编程】 第五章 作业
Windows核心编程第五章 作业
1、 作业可以看成是进程的容器,可以有多个进程运行在作业中,便于把这组进程看作一个整体来处理,可以对作业(中的进程)进行限制,即限额。
作业中的进程不能脱离作业,也就不能从一个作业转移到另一个作业。
把进程加入作业的过程是:创建进程,创建标志一定要有CREATE_SUSPEND,即创建后挂起新进程的主线程,把新进程放入作业后调用ResumeThread(新进程.主线程)启动该新进程。 对于新进程中又生成的子进程来说,默认也是在作业中的,但是可以修改一些标志让新进程的子进程不运行在作业中。
作业相当于一个【沙箱】,可以把一组进程组合在一起来限制进程能做什么,一个作业可以只包含一个进程。
2、 一些函数
BOOL IsProcessInJob( //hProcess标识的进程是否在某个作业中
HANDLE hProcess, //进程句柄
HANDLE hJob, //作业句柄,如果为NULL则是验证是否在任意一个作业中
PBOOL pbInJob //_out, TRUE为在某个作业中, FALSE为不在某个作业中
);
//创建一个作业内核对象
HANDLE WINAPI CreateJobObject(
__in_opt LPSECURITY_ATTRIBUTES lpJobAttributes, //安全属性
__in_opt LPCTSTR lpName //名字,最大限制为MAX_PATH个字符,名字是大小写敏感的,如果为NULL则该内核对象是无名的,如果名字为一个已经存在的比如事件对象、信号量、互斥对象、可等待定时器或者文件映射对象等,函数会失败,GetLastError返回ERROR_INVALID_HANDLE。 这个名字可以在一个私有的命名空间中,也可以在”Global\”或”Local\”为前缀的的命名空间中。
);
//把一个hProcess标识的进程放入hJob标志的作业中,需要注意的是CreateProcess创建进程时应该传入CREATE_SUSPEND标志。
BOOL WINAPI AssignProcessToJobObject(
__in HANDLE hJob, //作业内核对象句柄
__in HANDLE hProcess //进程内核对象句柄
);
hJob为用CreateJobObject创建的或者用OpenJobObject打开的作业内核对象句柄。
这个句柄必须有JOB_OBJECT_ASSIGN_PROCESS访问权限。
hProcess标识的进程不能在别的作业中,否则会以ERROR_ACCESS_DENIED错误失败。这个句柄必须有PROCESS_SET_QUOTA和PROCESS_TERMINATE访问权限。
函数成功返回非零,函数失败返回零。
默认情况下,在windows vista中通过windows资源管理器来启动一个应用程序时,进程会自动同一个专用的作业关联,此作业的名称使用了”PCA”字符串前缀。如果已经为应用程序定义了一个清单(manifest),windows资源管理器就不会将我们的进程同带”PCA”前缀的作业关联,它会假定我们已经解决了任何可能出现的问题,但是,在需要调试调试应用程序时,如果调试器是从windows资源管理器启动的,即使我们的应用程序有一个清单(mainifest),它也会从调试器继承带”PCA”前缀的作业,一个简单的解决方案是从命令行而不是从windows资源管理器中启动调试器,在这种情况下,我们的进程就不会与作业关联。
3、 对作业中的进程施加限制
BOOL WINAPI SetInformationJobObject(
__in HANDLE hJob, //作业句柄
__in JOBOBJECTINFOCLASS JobObjectInfoClass, //限制种类
__in LPVOID lpJobObjectInfo, //限制种类对应的数据结构
__in DWORD cbJobObjectInfoLength //第三个数据结构参数的大小
);
限制类型
1 基本限额
JobObjectBasicLimitInformation—JOBOBJECT_BASIC_LIMIT_INFORMATION
2 扩展后的基本限额
JobObjectExtendedLimitInformation—JOBOBJECT_EXTENDED_LIMIT_INFORMATION
3 基本的UI限额
JobObjectBasicUIRestrictions—JOBOBJECT_BASIC_UI_RESTRICTIONS
4 安全限额
JobObjectSecurityLimitInformation—JOBOBJECT_SECURITY_LIMIT_INFORMATION
①基本限额
typedef struct _JOBOBJECT_BASIC_LIMIT_INFORMATION {
LARGE_INTEGER PerProcessUserTimeLimit;//分配给每个进程的最大用户模式时间,时间间隔为100ns,LimitFlags成员要指定JOB_OBJECT_LIMIT_PROCESS_TIME标志
LARGE_INTEGER PerJobUserTimeLimit;//分配给作业对象的最大用户模式时间,时间间隔为100ns,默认情况下,在达到该时间限额时系统将自动终止所有进程的运行。可以在作业运行时定期改变这个值,LimitFlags指定JOB_OBJECT_LIMIT_JOB_TIME
DWORD LimitFlags;//指定哪些标志让哪些成员起效
SIZE_T MinimumWorkingSetSize;//
SIZE_T MaximumWorkingSetSize;//每个进程的最小工作集和最大工作集,LimitFlags指定JOB_OBJECT_LIMIT_WORKINGSET
DWORD ActiveProcessLimit;//指定作业中能并发运行的进程的最大数量,超过此限额的任何尝试都会导致新进程终止并报告配额不足的错误,LimitFlags指定JOB_OBJECT_LIMIT_ACTIVE_PROCESS
ULONG_PTR Affinity;//指定能够运行进程的CPU子集,单独的进程可以进一步对此设置,LimitFlags指定JOB_OBJECT_LIMIT_AFFINITY
DWORD PriorityClass;//指定关联的所有进程的优先级类,LimitFlags指定JOB_OBJECT_LIMIT_PRIORITY_CLASS
DWORD SchedulingClass;//做作业中的线程指定一个相对时间量差,值在0~9之间,LimitFlags指定JOB_OBJECT_LIMIT_SCHEDULING_CLASS
} JOBOBJECT_BASIC_LIMIT_INFORMATION, *PJOBOBJECT_BASIC_LIMIT_INFORMATION;
对于LimitFlags来说,需要注意一个标志如下:
JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION, 这个限额会导致系统关闭与作业关联的每一个进程的【未处理的异常】对话框,为此系统会为作业中的每个进程调换用SetErrorMode函数,并想它传递SEM_NOGPFAULTERRORBOX标志,作业中的一个进程在引发一个未处理的异常后,会立即终止运行,不会显示任何用户界面。
② 扩展基本限额
typedef struct _JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
IO_COUNTERS IoInfo; //保留
SIZE_T ProcessMemoryLimit;//如果JOBOBJECT_BASIC_LIMIT_INFORMATION 结构的LimitFlags成员指定了JOB_OBJECT_LIMIT_PROCESS_MEMORY,那么这个值为进程虚拟内存的大小
SIZE_T JobMemoryLimit; //如果JOBOBJECT_BASIC_LIMIT_INFORMATION 结构的LimitFlags成员指定了JOB_OBJECT_LIMIT_JOB_MEMORY,那么这个值为作业虚拟内存的大小
SIZE_T PeakProcessMemoryUsed; //只读
SIZE_T PeakJobMemoryUsed; //只读
} JOBOBJECT_EXTENDED_LIMIT_INFORMATION, *PJOBOBJECT_EXTENDED_LIMIT_INFORMATION;
③ UI限额
typedef struct _JOBOBJECT_BASIC_UI_RESTRICTIONS {
DWORD UIRestrictionsClass;
} JOBOBJECT_BASIC_UI_RESTRICTIONS, *PJOBOBJECT_BASIC_UI_RESTRICTIONS;
这个结构只有一个成员,
值 |
含义 |
JOB_OBJECT_UILIMIT_DESKTOP 0x00000040 |
阻止使用 CreateDesktop and SwitchDesktop 函数来创建或切换桌面 |
JOB_OBJECT_UILIMIT_DISPLAYSETTINGS 0x00000010 |
阻止进程通过ChangeDisplaySettings 函数来更改显示设置 |
JOB_OBJECT_UILIMIT_EXITWINDOWS 0x00000080 |
阻止进程通过 ExitWindows or ExitWindowsEx函数来注销、关机、重启或切断电源等. |
JOB_OBJECT_UILIMIT_GLOBALATOMS 0x00000020 |
为作业指定其专有的全局原子表,并限定作业中的进程只能访问此作业的表 |
JOB_OBJECT_UILIMIT_HANDLES 0x00000001 |
组织作业中的进程使用该作业外的进程创建的用户对象(比如在一个作业中创建进程spy++,该进程就不能使用作业外的进程创建的用户对象,比如窗口句柄等。 这个限制是单向的,该作业外部的进程还是可以使用该作业内部的进程创建的用户对象的) |
JOB_OBJECT_UILIMIT_READCLIPBOARD 0x00000002 |
组织进程读取剪贴板内容 |
JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS 0x00000008 |
阻止进程通过SystemParametersInfo 函数更改系统参数 |
JOB_OBJECT_UILIMIT_WRITECLIPBOARD 0x00000004 |
阻止进程写剪贴板 |
有时候我们需要作业内的进程和作业外的进程进行通信,而又有对作业的UI限额,因此可以使用下面的函数来为作业的进程授权/拒绝其访问某个作业外部的进程创建的用户对象。
BOOL UserHandleGrantAccess(
HANDLE hUserObj, //某个需要授权/拒绝访问的用户对象
HANDLE hJob, //作业句柄
BOOL bGrant //TRUE为授权, FALSE为拒绝
);
NOTE:该函数不允许作业内部的进程调用,这样可防止自己授权自己访问某作业外部进程创建的用户对象。
④ 安全限额
typedef struct _JOBOBJECT_SECURITY_LIMIT_INFORMATION {
DWORD SecurityLimitFlags;//安全限制标志(windows核心编程第五版 130页)
HANDLE JobToken;//作业中所有进程使用的访问令牌
PTOKEN_GROUPS SidsToDisable;//禁止对哪些SID进行访问检查
PTOKEN_PRIVILEGES PrivilegesToDelete;//从访问令牌中删除哪些特权
PTOKEN_GROUPS RestrictedSids;//一组只能拒绝的SID
} JOBOBJECT_SECURITY_LIMIT_INFORMATION, *PJOBOBJECT_SECURITY_LIMIT_INFORMATION;
///
void CwindowsCoreDlg::OnBnClickedButton2()
{
// TODO: 在此添加控件通知处理程序代码;
SECURITY_ATTRIBUTES ar;
ar.bInheritHandle = FALSE;
ar.lpSecurityDescriptor = NULL;
ar.nLength = sizeof(SECURITY_ATTRIBUTES);
HANDLE hJob = CreateJobObject(&ar, _T("firstJob"));
if (NULL != hJob)
{
DWORD dwErr = GetLastError();
if (ERROR_ALREADY_EXISTS == dwErr)
{
AfxMessageBox(_T("已经存在一个同名作业"));
}
else
{
AfxMessageBox(_T("作业创建成功"));
}
TCHAR chAppName[] = _T("D:\\Program Files\\Tencent\\QQ\\QQProtect\\Bin\\QQProtect.exe");
TCHAR chAppName2[] = _T("D:\\Program Files\\EditPlus 3\\EditPlus.exe");
STARTUPINFO startInfo1 = {sizeof(STARTUPINFO)};
PROCESS_INFORMATION procesinfo1;
BOOL bRet1 = CreateProcess(
NULL,
chAppName,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED | CREATE_BREAKAWAY_FROM_JOB,
NULL,
NULL,
&startInfo1,
&procesinfo1
);
STARTUPINFO startInfo2 = {sizeof(STARTUPINFO)};
PROCESS_INFORMATION procesinfo2 ;
BOOL bRet2 = CreateProcess(
NULL,
chAppName2,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED | CREATE_BREAKAWAY_FROM_JOB,
NULL,
NULL,
&startInfo2,
&procesinfo2
);
if (bRet1 != FALSE)
{
AssignProcessToJobObject(hJob, procesinfo1.hProcess);
ResumeThread(procesinfo1.hThread);
CloseHandle(procesinfo1.hProcess);
CloseHandle(procesinfo1.hThread);
}
if (bRet2 != FALSE)
{
AssignProcessToJobObject(hJob, procesinfo2.hProcess);
ResumeThread(procesinfo2.hThread);
CloseHandle(procesinfo2.hProcess);
CloseHandle(procesinfo2.hThread);
}
CloseHandle(hJob);
}
else
{
AfxMessageBox(_T("创建作业失败"));
}
}
///
void CwindowsCoreDlg::OnBnClickedButton3()
{
// TODO: 在此添加控件通知处理程序代码;
const int MAX_PROCESS_IDS = 10;
DWORD cb = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST) +
(MAX_PROCESS_IDS - 1) * sizeof(DWORD);
PJOBOBJECT_BASIC_PROCESS_ID_LIST pjobpil = (PJOBOBJECT_BASIC_PROCESS_ID_LIST)(_alloca(cb)); //_alloca在栈上动态份分配
pjobpil->NumberOfAssignedProcesses = MAX_PROCESS_IDS;
pjobpil->NumberOfProcessIdsInList = MAX_PROCESS_IDS;
QueryInformationJobObject(m_hJob, JobObjectBasicProcessIdList, pjobpil, cb, &cb);
CString strIDs;
for (int i = 0; i < pjobpil->NumberOfProcessIdsInList; ++ i)
{
CString strTemp;
strTemp.Format(_T("%d"), pjobpil->ProcessIdList[i]);
strTemp += CString(_T(", "));
strIDs += strTemp;
}
AfxMessageBox(strIDs);
}
4、 其他
查询作业中的限额信息
BOOL WINAPI QueryInformationJobObject(
__in_opt HANDLE hJob, //作业内核对象句柄
__in JOBOBJECTINFOCLASS JobObjectInfoClass,//限额种类
__out LPVOID lpJobObjectInfo,//限额种类结构体
__in DWORD cbJobObjectInfoLength,//结构体大小
__out_opt LPDWORD lpReturnLength//指出缓冲区中填充了多少字节,如果不关心传NULL即可
);
作业内的进程调用此函数时为hJob参数传入NULL即可或得本进程所在的作业被施加了哪些限额。
NOTE:
1 当一个进程成为作业中的进程时,就不能脱离作业或者成为其他作业中的进程。
2 一个作业中的进程的子进程默认也会成为该作业中的进程,但是有两种方法可以去除这种默认的情况:
① 基本限额JOBOBJECT_BASIC_LIMIT_INFORMATIO的LimitFlags成员打开JOB_OBJECT_LIMIT_BREAKAWAY_OK标志,并且在调用CreateProcess时为fdwCreate参数打开CREATE_BREAKAWAY_FROM_JOB,两者必须同时打开或同时不打开。
② 打开JOBOBJECT_BASIC_LIMIT_INFORMATION的LimitFlags成员的JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK标志即可,对于CreateProcess则不用传入其他标志。 这个标志会让作业中进程的子进程强制脱离作业。
5、 终止作业中的所有进程
BOOL TerminateJobObject(
HANDLE hJob, //作业内核对象句柄
UINT uExitCode //退出代码
);
查询作业统计信息
为QueryInformationJobObject函数传入种类obObjectBasicAccountingInformation和一个JOBOBJECT_BASIC_ACCOUNTING_INFORMATION结构即可
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统计信息
为QueryInformationJobObject传入一个种类JobObjectBasicAndIoAccountingInformation和一个JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATON结构即可
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, *PIO_COUNTERS;
这个结构指出已由作业中的进程执行过的读操作、写操作以及读/写操作的次数(以及这些操作期间传输的字节总数),对于不属于任何作业的进程,可以用如下函数:
BOOL GetProcessIoCounters(
HANDLE hProcess, //进程标识
PIO_COUNTGERS pIoCounters //IO_COUNTETRS结构指针
)
6、 作业通知
Microsoft选择在已分配的CPU时间到期时才讲作业的状态变成已触发,因为那意味着一个错误条件(error condition),在许多作业中,都会有一个父进程一直在运行,直至其所有子进程全部结束。 所以我们可以等待父进程的句柄,借此得知整个作业何时结束, 可以如下:
HANDLE h[2];
H[0] = processInfo.hProcess;
H[1] = hJob;
DWORD dw = WaitForMulitObjects(2, h, FALSE, INFINITE);
Switch(dw – WAIT_OBJECT_0)
{
Case 0:
//进程结束
Break;
Case 1:
//作业中所有的分配的CPU时间都用完
Break;
}
如果想要得到一些更具体的通知,比如进程创建/终止运行,要获得这些通知,必须在自己当程序中创建一个I/O完成端口(completion port)对象,并将我们的作业对象与完成端口关联。 然后必须有一个或者多个线程等待作业对象通知到达完成端口,以便进行处理。
创建了IO完成端口后,调用SetInformationJobObject将它与一个作业关联起来:
JOBOBJECT_ASSOCIATE_COMPLETION_PORT joacp;
joacp.CompletionKey = 1; //可以唯一标识这个作业的任意值
joacp.CompletionPort = hICOP; //完成端口内核对象句柄
SetInformationJobObject(hJob, JobObjecgtAssociateCompletionPortInformation, &joacp, sizeof(joacp));
执行上述代码后,系统将监视作业,只要有事件发生,就会把他们投递到对应的IO完成端口,同时可以调用QueryInformationJobObject来获取完成键(completion key)和完成端口句柄(completion port)。
子线程通过调用GetQueuedCompletionStatus来监视完成端口。
BOOL GetQueuedCompletionStatus(
HANDLE hIOCP, //IO完成端口句柄
PDWORD pNumBytesTransferred, //表示事件种类,见后面
PULONG_PTR pCompletionKey, //完成键(completion key)
POVERLAPPED *pOverlapped, //根据事件不同表示不同的值
DWORD dwMilliseconds //用于指定调用者线程等待完成端口的时间
);
作业事件通知:
消息标识 |
描述(什么事件,什么时候向完成端口投递此通知) |
JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS |
一个进程由于未处理的异常而终止时, lpOverlapped表示退出进程的ID |
JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT |
试图超过作业中的活动进程数时, lpOverlapped 为NULL |
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO |
作业中没有进程在运行时, lpOverlapped 为 NULL. |
JOB_OBJECT_MSG_END_OF_JOB_TIME 关于其说明见表格下方 |
作业分配的CPU时间到期时,但其中的进程不会自动终止(默认情况下会自动终止),我么可以允许进程继续执行,也可以设置一个新的时间限额 The value of lpOverlapped 为 NULL. |
JOB_OBJECT_MSG_END_OF_PROCESS_TIME |
进程分配的CPU时间到期时,并且进程将终止运行。 lpOverlapped 是这个进程ID |
JOB_OBJECT_MSG_EXIT_PROCESS |
某个进程终止时, lpOverlapped 是退出的进程ID |
JOB_OBJECT_MSG_JOB_MEMORY_LIMIT |
进程调拨的存储超过作业的限额时, lpOverlapped是这个进程ID,当进程没有报告他的ID时系统不会投递这个消息通知 |
JOB_OBJECT_MSG_NEW_PROCESS |
一个作业添加了一个新的进程时, lpOverlapped是新的进程ID |
JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT |
当进程试图调拨的存储超过进程的限额时, lpOverlapped 是此进程ID,当进程没有报告他的ID时系统不会投递这个通知 |
NOTE:
在作业的基本限额中我们知道当为作业分配的用户模式时间超时时作业中的进程会自动终止,此时不会投递JOB_OBJECT_MSG_END_OF_JOB_TIME这个通知(上面表格中) ,如果我们期望的是当为作业分配的用户模式时间超时时投递JOB_OBJECT_MSG_END_OF_JOB_TIME这个通知并且作业中的进程继续运行,那么我们必须执行以下类似代码:
JOBOBJECT_END_OF_JOB_TIME_INFORMATION joeojti;
Joeojti.EndOfJobTimeAction = JOB_OBJECT_POST_AT_END_OF_JOB; //对于EndOfJobTimeAction,其默认值为JOB_OBJECT_TERMINATE_AT_END_OF_JOB,即当作业分配时间用完时作业中的进程都终止运行。
//告诉作业对象,当作业的分配时间用完时投递JOB_OBJECT_MSG_END_OF_JOB_TIME这个通知给完成端口
SetInformationJobObject(hJob, JobObjectEndOfJobTimeInformation,
&joeojti, sizeof(joeojti));