在Windows上创建进程是一件很容易的事,但是在管理上就不那么方便了,主要体现在下面几个方面:
1. 各个进程的地址空间是独立的,想要在进程间共享资源比较麻烦
2. 进程间可能相互依赖,在进程间需要进行同步时比较麻烦
3. 在服务器上可能会出现一个进程创建一大堆进程来共同为客户服务,这组进程在逻辑上应该属于同一组进程
为了方便的管理同组的进程,Windows上提供了一个进程池来管理这样一组进程,在VC中将这个进程池叫做作业对象。它主要用来限制池中内存的一些属性,比如占用内存数,占用CPU周期,进程间的优先级,同时提供了一个同时关闭池中所有进程的方法。下面来说明它的主要用法
作业对象的创建
调用函数CreateJobObject,可以来创建作业对象,该函数有两个参数,第一个参数是一个安全属性,第二个参数是一个对象名称。作业对象本身也是一个内核对象,所以它的使用与常规的内核对象相同,比如可以通过命名实现跨进程访问,可以通过对应的Open函数打开命名作业对象。
添加进程到作业对象
可以通过AssignProcessToJobObject ,该函数只有两个参数,第一个是对应的作业对象,第二个是对应的进程句柄
关闭作业对象中的进程
可以使用TerminateJobObject 函数来一次关闭作业对象中的所有进程,它相当于对作业对象中的每一个进程调用TerminateProcess,相对来说是一个比较粗暴的方式,在实际中应该劲量避免使用,应该自己设计一种更好的退出方式
控制作业对象中进程的相关属性
可以使用SetInformationJobObject函数设置作业对象中进程的相关属性,函数原型如下:
BOOL WINAPI SetInformationJobObject(
__in HANDLE hJob,
__in JOBOBJECTINFOCLASS JobObjectInfoClass,
__in LPVOID lpJobObjectInfo,
__in DWORD cbJobObjectInfoLength
);
第一个参数是一个作业对象的句柄,第二个是一系列的枚举值,用来限制其中进程的各种信息。第三个参数根据第二参数的不同,需要传入对应的结构体,第四个参数是对应结构体的长度。下面是各个枚举值以及它对应的结构体
枚举值 | 含义 | 对应的结构体 |
---|---|---|
JobObjectAssociateCompletionPortInformation | 设置各种作业对象事件的完成端口 | JOBOBJECT_ASSOCIATE_COMPLETION_PORT |
JobObjectBasicLimitInformation | 设置作业对象的基本信息(如:进程作业集大小,进程亲缘性,进程CPU时间限制值,同时活动的进程数量等) | JOBOBJECT_BASIC_LIMIT_INFORMATION |
JobObjectBasicUIRestrictions | 对作业中的进程UI进行基本限制(如:指定桌面,限制调用ExitWindows函数,限制剪切板读写操作等)一般在服务程序上这个很少使用 | JOBOBJECT_BASIC_UI_RESTRICTIONS |
JobObjectEndOfJobTimeInformation | 指定当作业时间限制到达时,系统采取什么动作(如:通知与作业对象绑定的完成端口一个超时事件等) | JOBOBJECT_END_OF_JOB_TIME_INFORMATION |
JobObjectExtendedLimitInformation | 作业进程的扩展限制信息(限制进程的内存使用量等) | JOBOBJECT_EXTENDED_LIMIT_INFORMATION |
JobObjectSecurityLimitInformation | 限制作业对象进程中的安全属性(如:关闭一些组的特权,关闭某些特权等)要求作业对象所属进程或线程要具备更改这些作业进程安全属性的权限 | JOBOBJECT_SECURITY_LIMIT_INFORMATION |
限制进程异常退出的行为
在Windows中,如果进程发生异常,那么它会寻找处理该异常的对应的异常处理模块,如果没有找到的话,它会弹出一个对话框,让用户选择,但是这样对服务程序来说很不友好,而且有的服务器是在远程没办法操作这个对话框,这个时候需要使用某种方法让其不弹出这个对话框。
在作业对象中的进程,我们可以使用SetInformationJobObject函数中的JobObjectExtendedLimitInformation枚举值,将结构体JOBOBJECT_EXTENDED_LIMIT_INFORMATION中的BasicLimitInformation.LimitFlags成员设置为JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION。这相当于强制每个进程调用SetErrorMode并指定SEM_NOGPFAULTERRORBOX标志
获取作业对象属性和统计信息
调用QueryInformationJobObject函数来获取作业对象属性和统计信息。该函数的使用方法与之前的SetInformationJobObject函数相同。
下面列举下它可选择枚举值:
枚举值 | 含义 | 对应的结构体 |
---|---|---|
JobObjectBasicAccountingInformation | 基本统计信息 | JOBOBJECT_BASIC_ACCOUNTING_INFORMATION |
JobObjectBasicAndIoAccountingInformation | 基本统计信息和IO统计信息 | JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION |
JobObjectBasicLimitInformation | 基本的限制信息 | JOBOBJECT_BASIC_LIMIT_INFORMATION |
JobObjectBasicProcessIdList | 获取作业进程ID列表 | JOBOBJECT_BASIC_PROCESS_ID_LIST |
JobObjectBasicUIRestrictions | 查询进程UI的限制信息 | JOBOBJECT_BASIC_UI_RESTRICTIONS |
JobObjectExtendedLimitInformation | 查询作业进程的扩展限制信息 | JOBOBJECT_EXTENDED_LIMIT_INFORMATION |
JobObjectSecurityLimitInformation | 查询作业对象进程中的安全属性 | JOBOBJECT_SECURITY_LIMIT_INFORMATION |
这些信息基本上与上面的设置限制信息是对应的。使用上也是类似的
作业对象与完成端口
设置作业对象的完成端口一般是使用SetInformationJobObject,并将第二个参数的枚举值指定为JobObjectAssociateCompletionPortInformation,这样就可以完成一个作业对象和完成端口的绑定。
当作业对象发生某些事件的时候可以向完成端口发送对应的事件,这个时候在完成端口的线程中调用GetQueuedCompletionStatus可以获取对应的事件,但是这个函数的使用与之前在文件操作中的使用略有不同,主要体现在它的各个返回参数的含义上。各个参数函数如下:
lpNumberOfBytes:返回一个事件的ID,它的事件如下:
事件 | 事件含义 |
---|---|
JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS | 进程异常退出 |
JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT | 同时活动的进程数达到设置的上限 |
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO | 作业对象中没有活动的进程了 |
JOB_OBJECT_MSG_END_OF_JOB_TIME | 作业对象的CPU周期耗尽 |
JOB_OBJECT_MSG_END_OF_PROCESS_TIME | 进程的CPU周期耗尽 |
JOB_OBJECT_MSG_EXIT_PROCESS | 进程正常退出 |
JOB_OBJECT_MSG_JOB_MEMORY_LIMIT | 作业对象消耗内存达到上限 |
JOB_OBJECT_MSG_NEW_PROCESS | 有新进程加入到作业对象中 |
JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT | 进程消耗内存数达到上限 |
lpCompletionKey: 返回触发这个事件的对象的句柄,我们将完成端口与作业对象绑定后,这个值自然是对应作业对象的句柄
lpOverlapped: 指定各个事件对应的详细信息,在于进程相关的事件中,它返回一个进程ID
既然知道了各个参数的含义,我们可以使用PostQueuedCompletionStatus函数在对应的位置填充相关的值,然后往完成端口上发送自定义事件。只需要将lpNumberOfBytes设置为我们自己的事件ID,然后在线程中处理即可
下面是作业对象操作的完整例子
#include "stdafx.h"
#include <Windows.h>
DWORD IOCPThread(PVOID lpParam); //完成端口线程
int GetAppPath(LPTSTR pAppName, size_t nBufferSize)
{
TCHAR szAppName[MAX_PATH] = _T("");
DWORD dwLen = ::GetModuleFileName(NULL, szAppName, MAX_PATH);
if(dwLen == 0)
{
return 0;
}
for(int i = dwLen; i > 0; i--)
{
if(szAppName[i] == _T('\\'))
{
szAppName[i + 1] = _T('\0');
break;
}
}
_tcscpy_s(pAppName, nBufferSize, szAppName);
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
//获取当前进程的路径
TCHAR szModulePath[MAX_PATH] = _T("");
GetAppPath(szModulePath, MAX_PATH);
//创建作业对象
HANDLE hJob = CreateJobObject(NULL, NULL);
if(hJob == INVALID_HANDLE_VALUE)
{
return 0;
}
//创建完成端口
HANDLE hIocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 1);
if(hIocp == INVALID_HANDLE_VALUE)
{
return 0;
}
//启动监视进程
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)IOCPThread, (PVOID)hIocp, 0, NULL);
//将作业对象与完成端口绑定
JOBOBJECT_ASSOCIATE_COMPLETION_PORT jacp = {0};
jacp.CompletionKey = hJob;
jacp.CompletionPort = hIocp;
SetInformationJobObject(hJob, JobObjectAssociateCompletionPortInformation, &jacp, sizeof(jacp));
//为作业对象设置限制条件
JOBOBJECT_BASIC_LIMIT_INFORMATION jbli = {0};
jbli.PerProcessUserTimeLimit.QuadPart = 20 * 1000 * 10i64; //限制执行的用户时间为20ms
jbli.MinimumWorkingSetSize = 4 * 1024;
jbli.MaximumWorkingSetSize = 256 * 1024; //限制最大内存为256k
jbli.LimitFlags = JOB_OBJECT_LIMIT_PROCESS_TIME | JOB_OBJECT_LIMIT_JOB_MEMORY;
SetInformationJobObject(hJob, JobObjectBasicLimitInformation, &jbli, sizeof(jbli));
//指定不显示异常对话框
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli = {0};
jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION;
SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &jeli, sizeof(jeli));
//创建新进程
_tcscat_s(szModulePath, MAX_PATH, _T("JobProcess.exe"));
STARTUPINFO si = {0};
PROCESS_INFORMATION pi = {0};
CreateProcess(szModulePath, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED | CREATE_BREAKAWAY_FROM_JOB, NULL, NULL, &si, &pi);
//将进程加入到作业对象中
AssignProcessToJobObject(hJob, pi.hProcess);
//运行进程
ResumeThread(pi.hThread);
//查询作业对象的运行情况,在这查询基本统计信息和IO信息
JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION jbaai = {0};
DWORD dwRetLen = 0;
QueryInformationJobObject(hJob, JobObjectBasicAndIoAccountingInformation, &jbaai, sizeof(jbaai), &dwRetLen);
//等待进程退出
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
//给完成端口线程发送退出命令
PostQueuedCompletionStatus(hIocp, 0, (ULONG_PTR)hJob, NULL);
//等待线程退出
WaitForSingleObject(hIocp, INFINITE);
CloseHandle(hIocp);
CloseHandle(hJob);
return 0;
}
DWORD IOCPThread(PVOID lpParam)
{
BOOL bLoop = TRUE;
HANDLE hIocp = (HANDLE)lpParam;
DWORD dwReasonId = 0;
HANDLE hJob = NULL;
OVERLAPPED *lpOverlapped = {0};
while (bLoop)
{
BOOL bSuccess = GetQueuedCompletionStatus(hIocp, &dwReasonId, (PULONG_PTR)&hJob, &lpOverlapped, INFINITE);
if(!bSuccess)
{
return 0;
}
switch (dwReasonId)
{
case JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS:
{
//进程异常退出
DWORD dwProcessId = (DWORD)lpOverlapped;
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwProcessId);
if(INVALID_HANDLE_VALUE != hProcess)
{
DWORD dwExit = 0;
GetExitCodeProcess(hProcess, &dwExit);
printf("进程[%08x]异常退出,退出码为[%04x]\n", dwProcessId, dwExit);
}
}
break;
case JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT:
{
printf("同时活动的进程数达到上限\n");
}
break;
case JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO:
{
printf("没有活动的进程了\n");
}
break;
case JOB_OBJECT_MSG_END_OF_JOB_TIME:
{
printf("作业对象CPU时间周期耗尽\n");
}
break;
case JOB_OBJECT_MSG_END_OF_PROCESS_TIME:
{
DWORD dwProcessID = (DWORD)lpOverlapped;
printf("进程[%04x]CPU时间周期耗尽\n", dwProcessID);
}
break;
case JOB_OBJECT_MSG_EXIT_PROCESS:
{
DWORD dwProcessId = (DWORD)lpOverlapped;
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwProcessId);
if(INVALID_HANDLE_VALUE != hProcess)
{
DWORD dwExit = 0;
GetExitCodeProcess(hProcess, &dwExit);
printf("进程[%08x]正常退出,退出码为[%04x]\n", dwProcessId, dwExit);
}
}
break;
case JOB_OBJECT_MSG_JOB_MEMORY_LIMIT:
{
printf("作业对象消耗内存数量达到上限\n");
}
break;
case JOB_OBJECT_MSG_NEW_PROCESS:
{
DWORD dwProcessID = (DWORD)lpOverlapped;
printf("进程[ID:%u]加入作业对象[h:0x%08X]\n",dwProcessID,hJob);
}
break;
case JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT:
{
DWORD dwProcessID = (DWORD)lpOverlapped;
printf("进程[%04x]消耗内存数量达到上限\n",dwProcessID);
}
break;
default:
bLoop = FALSE;
break;
}
}
}
在上面的例子中需要注意一点,在创建进程的时候我们给这个进程一个CREATE_BREAKAWAY_FROM_JOB标志,由于Windows在创建进程时,默认会将这个子进程丢到父进程所在进程池中,如果父进程属于某一个进程池,那么我们再将子进程放到其他进程池中,自然会导致失败,这个标志表示,新创建的子进程不属于任何一个进程池,这样在后面的操作才会成功