什么是进程?进程就是一个正在运行的程序的实例,由两部分组成:
- 内核对象。操作系统用内核对象对进程进行管理,内核对象是操作系统保存进程统计信息的地方。
- 地址空间。其中包含所有可执行文件或DLL模块的代码和数据,以及一些其他的数据,提供线程运行的环境。
进程比较”懒惰“,它不做任何事情,所有的事情都交给线程在它的上下文中运行。一个进程可以拥有多个线程,多个线程公用一个进程的上下文环境来运行进程地址空间中所包含的代码。
当系统创建一个进程的时候,会自动为其创建一个线程,称为”主线程“,然后由这个线程再去创建其他的线程。
1 创建一个进程
创建一个进程用CreteProcess()函数来创建,函数如下:
BOOL
WINAPI
CreateProcessW(
__in_opt LPCWSTR lpApplicationName,
__inout_opt LPWSTR lpCommandLine,
__in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes,
__in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in BOOL bInheritHandles,
__in DWORD dwCreationFlags,
__in_opt LPVOID lpEnvironment,
__in_opt LPCWSTR lpCurrentDirectory,
__in LPSTARTUPINFOW lpStartupInfo,
__out LPPROCESS_INFORMATION lpProcessInformation
);
参数说明:
- lpApplicationName,
指定新进程要使用的可执行文件的名称。
- lpCommandLine,
要传给新进程的命令行字符串。
- lpProcessAttributes,
进程安全属性,通常设为NULL。
- lpThreadAttributes,
线程安全属性,通常设为NULL。
- bInheritHandles,
是否可继承,当设为true时,父进程的上下文环境可由子进程继承。
- dwCreationFlags,
标识影响进程创建方式的标志。一般设为0.
- lpEnvironment,
指向一块内存,其中包含新进程要使用的环境字符串,默认为NULL,此时子进程将继承父进程使用的一组环境字符串。
- lpCurrentDirectory,
允许父进程设置子进程的当前驱动器和目录。默认为NULL,此时新进程的工作目录与生产新进程的应用程序是一样的。如果不为NULL,则lpCurrentDirectory必须指向一个用0为终止符的字符串,其中包含所要的工作驱动器和目录。必须在路径中指定一个驱动器号。
- lpStartupInfo,
指向一个STARTUPINFO结构或STARTUPINFOEX结构,默认为
- lpProcessInformation
指向一个PROCESS_INFORMATION结构,CreteProcess()函数在返回前会初始化这个结构的成员。
2 终止进程
进程可以通过以下4种方式终止:
- 主线程的入口点函数返回(通常情况下)
- 进程中的一个线程调用ExitProcess()函数(不推荐)。
- 另一个进程中的线程调用TerminateProcess()(不推荐).
- 进程中的所有线程都“自然死亡”(几乎从来不会发生)
2.1 主线程的入口点函数返回
让主线程的入口点函数返回,可以保证以下操作被执行:
- 该线程创建的任何C++对象都将由这些对象的析构函数正确销毁
- 操作系统将正确释放线程栈使用的内存
- 系统将进程的退出代码设为入口点函数的返回值
- 系统递减进程内核对象的使用计数
当主线程的入口点函数(WinMain, wWinMain, main 或 wmain)返回时,会返回到C/C++运行库启动代码,后者将正确清理进程使用的全部C运行时资源。释放了C运行时资源之后,再显式的调用ExitProcess,
并将入口点函数返回值传给它,从而终止整个进程。进程中运行的其他任何线程都会随进程一起终止。
2.2 ExitProcess函数
VOID ExitProcess(UINT fuExitCode);
该函数会终止进程,并将进程的退出代码设为fuExitCode。ExitProcess没有返回值,因为这个时候进程已经结束了,在ExitProcess之后的代码,再也不会执行了。Windows PlatForm SDK文档中说:一个进程在
其所有线程结束后才会终止。不过,C/C++运行库采用一个不同的策略,即不管进程中是否还有其他线程在运行,只要应用程序的主线程从它的入口点函数返回,C/C++运行库就会调用ExitProcess来终止进程。但是,
如果入口点函数调用的是ExitThread,而不是调用ExitProcess或者入口点函数直接返回,应用程序的主线程将停止执行,但是只要进程只要海油其他线程正在运行,进程就不会终止。不管是调用ExitProcess还是ExitThread都不会再返回当前函数调用。这样做在操作系统层面,进程或线程的操作系统资源会被正确清理。但是在C/C++运行库中有的资源可能不会正确被清理。下面是一个简单的例子:
1 #include <Windows.h>
2 #include <stdio.h>
3
4 class CObj
5 {
6 public:
7 CObj(){printf("CObj Constructor \n");}
8 ~CObj(){printf("CObj Destructor \n");}
9 };
10
11 CObj g_GlobalObj;
12
13 int main()
14 {
15 CObj localObj;
16 ExitProcess(0);
17 }
执行上述代码,输出结果如下:
CObj Constructor
CObj Destructor
这段代码构造了两个对象,一个是全局对象,一个是局部对象,但是由于直接调用了ExitProcess,造成进程当场死亡,C/C++运行库没有任何机会去清理它的堆栈。
2.3 TerminateProcess 函数
调用TerminatProcess函数也可以终止一个进程:
BOOL TerminatProcess(HANDLE hProcess, UINT fuExitCode)
TerminatProcess也是用来终止一个进程的,但是与上一节中我们提到的ExitProcess的区别在于:ExitProcess只能终止本进程,而TerminatProcess可以由任何线程调用来终止另外一个进程或者它自己的进程。hProcess就是我们所要终止的进程的句柄,退出代码的值就是fuExitCode。一般只有在无法通过其他方法终止一个进程的时候,才会调用TerminatProcess函数。被终止的进程不会得到任何被终止的通知便挂掉了,所以它是没有机会去做清理工作的。不过不用担心,虽然进程没有机会去执行自己的清理工作,但是操作系统会在进程终止后进行彻底的清理工作。进程在终止后不会泄露任何东西。
TerminatProcess函数是异步的,在函数返回的时候,系统并不保证进程已经被强制终止。所以为了确定进程是否终止,我们需要调用WaitForSingleObject函数。
2.4 当进程中的所有线程终止时
如果一个进程中所有的线程都终止了,操作系统就会认为没有任何理由再去保持进程的地址空间,所以就会终止整个进程。而进程的退出代码会被设为最后一个终止的那个线程的退出代码。
2.5 总结
当一个进程终止时,操作系统会做一下操作。
- 终止进程中遗留的任何线程
- 释放进程分配的所有用户对象和GDI对象,关闭所有内核对象,即使内核对象的计数减1。
- 进程的退出代码从STILL_ACTIVE变为传给ExitProcess或TerminateProcess函数的代码。
- 进程内核对象的状态变成已触发状态。
- 进程内核对象的使用计数减1。
当一个进程终止的时候,如果系统中还有另外一个进程打开了这个进程的内核对象的句柄,那么进程内核对象的使用计数就不会为0。
3 进程相关变量和函数
3.1 获取进程实例句柄
3.1.1 GetModuleHandle
HMODULE WINAPI GetModuleHandle( LPCWSTR lpModuleName)
lpModuleName是模块的名称,要传递一个以0为终止符的字符串,指定已在主调进程的地址空间中加载的一个可执行文件或DLL文件的名称,即“**.dll”或者“**.exe”。如果系统找到指定的可执行文件或DLL文件名称,则会返回可执行文件/DLL文件映像加载到的基地址,即实例句柄。如果没有扩展名,则默认为“dll”。 如果模块名称通过路径来指定,则路径中必须使用"\",而不是"/". 执行时。该函数通过名称(大小写不敏感)来查看调用进程已映射的模块,返回符合的模块句柄。如果lpModuleName参数设置为NULL,则直接返回主调进程的可执行文件的基地址。成功,则返回句柄,失败,返回NULL。错误信息:GetLastError()
GetModuleHandle函数不会增加所指定模块的引用数,不管调用该函数几次,只要调用一次FreeLibrary函数,该模块就从进程中卸载了。在多线程中,模块句柄在不同线程中不总是有效的。如:当在一个线程中调用了该函数获取了某一模块的句柄,但在使用该句柄之前,另一个线程把该句柄 Free了,并重新获取了其他模块的句柄。这个时候第一个线程再去使用这个句柄变量,就不再是之前它打算操作的那个模块了,而是第二个线程修改后的模块句柄了。
3.1.2 GetModuleHandleEx
要知道一个可执行文件或DLL文件被加载到进程地址空间的什么位置,可以使用GetModuleHandleEX函数来返回一个句柄/基地址。
BOOL WINAPI GetModuleHandleEx( DWORD dwFlags, LPCTSTR lpModuleName, HMODULE* phModule);
参数dwFlags指定获取模块句柄的标志:
- 如果是0,则当调用该函数时,模块的引用计数自动增加,调用者在使用完模块句柄后,必须调用一次FreeLibrary
- 如果是GET_MODULE_HANDLE_EX_FLAG_PIN,则模块一直映射在调用该函数的进程中,直到该进程结束,不管调用多少次FreeLibrary
- 如果是GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,则同GetModuleHandle相同,不增加引用计数
- 如果是GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,则lpModuleName是模块中的一个地址
参数lpModuleName将当前函数的地址作为参数。
3.1.3 __ImageBase
__ImageBase是一个链接器定义的伪变量,表明可执行文件被映射到应用程序内存中的位置。
下面是使用以上三种方法来获取当前进程实例句柄的例子:
#include <Windows.h>
#include <stdio.h>
extern "C" const IMAGE_DOS_HEADER __ImageBase;
void DumpModule()
{
HMODULE hModue = NULL;
hModue =GetModuleHandle(NULL);
printf("GetMOduleHandle(NULL) == ox%x\r\n",hModue);
printf("_ImageBase == ox%x\r\n",(HINSTANCE)&__ImageBase);
hModue = NULL;
GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
(PCTSTR)DumpModule,&hModue);
printf("GetMOduleHandleEX(NULL) == ox%x\r\n",(HINSTANCE)&__ImageBase);
return;
}
int main()
{
DumpModule();
system("pause");
return 0;
}
运行结果如下: