应师父要求,做一些关于进程的调研,师父给的任务是:子进程读取父进程信息。我却认为事情并不简单。正巧前几天说要写windows api专栏,那就开始了,先跟大家分享一首ZAYN的iT's YoU,配合音乐会更佳哦。
进程可以说是计算机最伟大的设计,它配合计算机硬件将程序给人一种持续运行的感觉,其实后台的实际运行较为复杂。进程包含:一个内核对象,操作系统用其管理进程。一个地址空间,包含所有可执行文件(.EXE)与.DLL的代码和数据,还包含动态内存分配的,如线程堆栈和堆的分配。
进程要做事情还是需要线程帮忙,当系统创建一个进程时同时创建了它的主线程(primary thread),然后这个线程在创建更多的线程,后者再创建更多的线程。如果进程内没有线程要执行的地址空间中的代码,系统会销毁进程及其地址空间。对于所有要运行的线程,操作系统轮流为每个线程分配时间。
以上属于老生常谈的内容,以做铺垫。
CreateProcess函数
函数API的参数:
BOOL CreateProcess(
LPCSTR lpApplicationName, // 要执行的模块的名称,
LPSTR lpCommandLine, // 要执行的命令行。
LPSECURITY_ATTRIBUTES lpProcessAttributes, // 确定是否可以由子进程继承新进程对象的返回句柄。
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 确定子进程是否可以继承新线程对象的返回句柄。
BOOL bInheritHandles, // 指示新进程是否从调用进程处继承了句柄。
DWORD dwCreationFlags, // 控制优先级类和创建进程的标志。
LPVOID lpEnvironment, // 指向新进程的环境块的指针。
LPCSTR lpCurrentDirectory, // 进程当前目录的完整路径。
LPSTARTUPINFO lpStartupInfo, // 决定新进程主窗体如何显示的结构体。
LPPROCESS_INFORMATION lpProcessInformation // 接收新进程的识别信息的结构体。
);
在我们启动这个函数后,操作系统创建一个进程内核对象,初始计数为1,进程内核为进程统计信息构成的一个小型数据结构。最后将可执行文件(以及必要的DLL)的代码以及数据加载到进程的地址空间。
系统为新的进程船舰线程内核对象,主线程执行C/C++运行时的启动历程,由链接器设为应用程序入口,最终会调用WinMain、wWinMain、main、wmain函数,成功创建两者后返回TRUE。
我们来看参数lpApplicationName与lpCommandLine,lpApplicationName里传入可执行文件的路径,其优先级低于lpCommandLine,为兼容Windows的POSIX系统,POSXI指定了最初的接口调用标准。也就是说,我们传入lpApplicationName后再设置lpCommandLine后,lpApplicationName就不管用了。而lpCommandLine传入的是命令。
lpProcessAttributes指向一个SECURITY_ATTRIBUTES结构体,这个结构体决定是否返回的句柄可以被子进程继承。如果lpProcessAttributes参数为空(NULL),那么句柄不能被继承。
lpThreadAttributes同lpProcessAttributes原理一致,不过这个参数决定的是线程是否被继承,通常置为NULL。
bInheritHandles指示新进程是否从调用进程处继承了句柄。
fdwCreate参数比较有意思:它可以由多个标志位组成,用OR连接起来。
1、Debug_Process标志位:任何一个子进程发生特定事件后通知父进程。
2、Debug_Only_this_Process:类似1中功能,区别是最亲近的子进程才上报信息给父进程。
3、Create_Suspended:创建新进程的同时挂起其主线程。
4、Detached_Process标志阻止子进程访问其父进程的控制应用台,告知系统将其发送至新的控制应用台。
。。。。。。。。。。。。。。
此外,fdwCreate允许我们指定一个优先级类,书上说没必要,系统会分配好优先级。不过我还是贴出来这个优先级的图表吧。
lpEnvironment参数指向一块内存,包含新进程要使用的环境字符串,大多时候传入NULL,子进程将会继承父进程。
pszCurDir参数允许父进程设置子进程的当前驱动和目录,参数位NULL则子进程与父进程一样的。
psiStartInfo参数指向STARTUPINFO或STARTUPINFOEX变量的提针。其指向的数据结构异常炫酷,这里不深究了,留点头发吧。
typedef struct _STARTUPINFO {
DWORD cb;
PSTR lpReserved;
PSTR lpDesktop;
PSTR lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD dwFillAttribute;
DWORD dwFlags;
WORD wShowWindow;
WORD cbReserved2;
PBYTE lpReserved2;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
} STARTUPINFO, *LPSTARTUPINFO;
typedef struct _STARTUPINFOEX {
STARTUPINFO StartupInfo;
struct _PROC_THREAD_ATTRIBUTE_LIST *lpAttributeList;
} STARTUPINFOEX, *LPSTARTUPINFOEX;
感兴趣的去搜索STARTUPINFO和STARTUPINFOEX结构的成员。
通常情况下使用默认值,Windows建立新进程时会使用STARTUPINFO(EX)的成员变量,大多数状况下可使用这些变量的默认值,此时你应该该将其cb域设置为结构的大小,并将其他域清0,以下:
STARTUPINFO si = { sizeof(si) };
CreateProcess(..., &si, ...);
lpProcessInformation参数指向一个结构,结构如下:
typedef struct _PROCESS_INFORMATION{
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessID;
DWORD dwThreadId;
} PROCESS_INFORMATION
这块是我们为数不多的需要传参的地方,因此着重解释以下。
前面介绍过了创建新进程操作系统会创建进程内核对象和主线程内核对象,系统分别给他们分配数值1,再CreateProcess返回前,会打开进程对象和线程对象,并将各自与进程相关联的句柄放入上面数据结构中的hProcess与hThread中。当打开这些对象时,实用计数变为2。
如果想释放这些进程对象,进程必须终止,使用对象计数减为1,并且父进程必须调用ClodeHandle(使他再减1变为0)。线程与之类似,并且父进程必须关闭到线程对象的句柄。
我们再编写进程线程相关代码时,一定记得关闭父进程到子进程的句柄,关闭句柄只是对进程或线程的数据不感兴趣了,不看了,并不会影响子进程或线程,它们会继续执行,直至自行终止。
CreateProcess返回前会将ID填充到Process_Information结构的dwProcessId与dwThreadId中,但是直接使用这些ID会面临问题:即我们使用ID追踪进程线程时可能其已经被释放,这样就定位不到我们想要的。可以使用GetCurrentProcessId获得当前进程ID,GetCurrentThreadId获得当前线程ID。此外,通过GetProcessId与GetThreadId来获得与指定句柄对应的一个线程的ID,最后根据线程句柄调用GetprocessIdOfThread来获得其所在进程的ID。
这又一点非常重要的东西:我们想知道子进程的父进程,需要知道只有子进程生成的一瞬间才能确定父子关系,到子进程执行代码前那一刻就不存在父子关系了。ToolHelp函数允许进程通过Processentry32结构查询其父进程,在此结构中有个th32ParentProcessID成员,MSDN文档称,这个成员能返回父进程ID。
其结构如下:
PROCESSENTRY32 结构如下:
typedef struct tagPROCESSENTRY32 {
DWORD dwSize; // 结构大小;
DWORD cntUsage; // 此进程的引用计数;
DWORD th32ProcessID; // 进程ID;
DWORD th32DefaultHeapID; // 进程默认堆ID;
DWORD th32ModuleID; // 进程模块ID;
DWORD cntThreads; // 此进程开启的线程计数;
DWORD th32ParentProcessID;// 父进程ID;
LONG pcPriClassBase; // 线程优先权;
DWORD dwFlags; // 保留;
char szExeFile[MAX_PATH]; // 进程全名;
} PROCESSENTRY32;
用例:
#include <stdio.h>
#include <windows.h>
#include <TlHelp32.h>
int main()
{
// 为进程的所有线程拍个快照
HANDLE hSnapshort = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshort == INVALID_HANDLE_VALUE)
{
printf(“CreateToolhelp32Snapshot调用失败!\n”);
return -1;
}
}
指利用ID寻找父进程会有问题,就是父进程被消失,ID会重新分配。如需保证一个进程的ID不被重用,需保证其进程、线程不被销毁。为此,在创建这些进程线程后不关闭其句柄即可,登到不再使用ID时再CloseHandle关闭。
下面会根据以上做几个demo测验以下。FIghting!!!
下期传送门:
OpenProcess | Process32First | GetExitCodeProcess - WINDOWS API 第二弹 进程相关_Thomas_Lbw的博客-CSDN博客