进程概述
创建进程函数详解
进程的创建
进程的终止
进程总体执行流程
进程线程优先级
(本章节中例子都是用 VS2005 编译调试的)
进程定义:
通常被定义为一个正在运行的程序实例,是一个程序在其自身的地址空间中的一次执行活动
程序相关描述:
进程组成:
地址空间:
系统赋予每个进程独立的虚拟的地址空间,每个进程都有自己的私有的地址空间,各自的线程都可以访问各自进程地址空间中的数据,但是一般情况下各个进程的线程是无法直接访问其他进程的地址空间的数据
注意:
VC 的编译链接器
集成开发环境会设置各种链接器开关,使链接器将子系统的正确类型嵌入最终可执行文件.
对应程序类型和相应的入口函数
应用程序类型 入口函数 嵌入可执行文件的启动函数
处理ANSI字符和字符串的GUI应用程序 _tWinMain(WinMain) WinMainCRTStartup
处理Unicode字符和字符串的GUI应用程序 _tWinMain(wWinMain) wWinMainCRTStartup
处理ANSI字符和字符串的GUI应用程序 _tmain(main) mainCRTStartup
处理Unicode字符和字符串的GUI应用程序 _tmain(wmain) wmainCRTStartup
实例句柄(HINSTANCE)
加载到进程地址空间的每一可执行文件(EXE)或动态链接库(DLL)文件都被赋予了一个独一无二的实例句柄(HINSTANCE).(w)WinMain 的 hInstance 参数的实际值是一个内存基址,系统将可执行文件的映像加载到进程空间的这个位置
为了知道一个可执行文件(EXE)或动态链接库(DLL)被加载到进程地址空间的什么位置,可以使用 GetModuleHandle 函数来返回一个句柄/基址
还需注意的是事实上 HMOBULE 和 HINSTANCE 完全是一回事,如果某个文档指出需要一个HMODULE 参数,我们可以传入一个 HINSTANCE
进程标识(PID)
创建一个进程内核对象时,系统会为此对象分配一个独一无二的标识符.系统中没有别的进程内核对象会有相同的 ID 编号,这同样适用于线程内核对象,创建一个线程内核对象时,此对象会被分配一个独一无二的,系统级别的ID编号,进程 ID 和线程 ID 分享同一个号码池,这意味着线程和进程不可能有相同的 ID,此外,一个对象分配到的 ID 绝不可能是0.进程和线程ID会被系统立即重用(假定在创建一个进程之后,系统初始化可一个进程对象,并将 ID 值 124 分配给他,如果在建立一个新的进程对象,系统不会将同一个 ID 编号分配给它,但是,如果一个进程对象以及释放,系统可以将 124 分配给下一个创建的进程对象).
获得进程(或线程)标识的函数
系统确实会记住每个进程父进程的 ID,但由于 ID 会立即被重用,所以等我们获得父进程的 ID 的时候,那个 ID 可能已经是系统运行的一个完全不同的进程.要保证一个进程或线程 ID 不被重用,唯一的办法就是保证进程或线程对象不被销毁,对应子进程,除非父进程复制了自己的进程或线程对象句柄,并允许子进程继承这些句柄,否则无法确保父进程的进程ID或线程ID的有效性.
获取当前进程句柄
获取当前进程句柄 GetCurrentProcess (这个函数都返回的是一个伪句柄.它不会在主调进程的句柄表中新建句柄.而且调用这个函数,不会影响进程内核对象的使用计数器.如果调用 CloseHandle 函数关闭一个伪句柄,CloseHandle 只是简单地忽略此调用,并返回 FALSE,将伪句柄转换为真正的句柄: DuplicateHandle)
函数原型
BOOL CreateProcess( LPCTSTR lpApplicationName, // name of executable module LPTSTR lpCommandLine, // command line string LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD BOOL bInheritHandles, // handle inheritance option DWORD dwCreationFlags, // creation flags LPVOID lpEnvironment, // new environment block LPCTSTR lpCurrentDirectory, // current directory name LPSTARTUPINFO lpStartupInfo, // startup information LPPROCESS_INFORMATION lpProcessInformation // process information );
参数说明:
指向一个NULL结尾的、用来指定可执行模块的字符串(必须指定文件的扩展名,系统不会自动假设文件有一个 .exe 的扩展名).这个字符串可以使可执行模块的绝对路径,也可以是相对路径,在后一种情况下,函数使用当前驱动器和目录建立可执行模块的路径.
这个参数可以被设为NULL,在这种情况下,可执行模块的名字必须处于 lpcommandline 参数的最前面并由空格符与后面的字符分开.这个被指定的模块可以是一个win32应用程序.
如果适当的子系统在当前计算机上可用的话,它也可以是其他类型的模块(如ms-dos 或 os/2).在windows nt中.
如果可执行模块是一个16位的应用程序,那么这个参数应该被设置为NULL并且因该在lpcommandline参数中指定可执行模块的名称.16位的应用程序是以dos虚拟机或win32上的windows(wow) 为进程的方式运行.
指向一个NULL结尾的、用来指定要运行的命令行.这个参数可以为空,那么函数将使用参数指定的字符串当作要运行的程序的命令行.
如果lpapplicationname和lpcommandline参数都不为空,那么lpapplicationname参数指定将要被运行的模块,lpcommandline参数指定将被运行的模块的命令行.新运行的进程可以使用getcommandline函数获得整个命令行.c语言程序可以使用argc和argv参数.
如果lpapplicationname参数为空,那么这个字符串中的第一个被空格分隔的要素指定可执行模块名.
如果文件名不包含扩展名,那么.exe将被假定为默认的扩展名.
如果文件名以一个点(.)结尾且没有扩展名,或文件名中包含路径,.exe将不会被加到后面.
如果文件名中不包含路径,windows将按照如下顺序寻找这个可执行文件:
如果被创建的进程是一个以ms-dos或16位windows为基础的应用程序,lpcommandline参数应该是一个以可执行文件的文件名作为第一个要素的绝对路径,因为这样做可以使32位windows程序工作的很好,这样设置lpcommandline参数是最强壮的.
指向一个security_attributes结构体,这个结构体决定是否返回的句柄可以被子进程继承.
如果lpprocessattributes参数为空(NULL),那么句柄不能被继承.在windows nt中:security_attributes结构的lpsecuritydescriptor成员指定了新进程的安全描述符.
如果参数为空,新进程使用默认的安全描述符.在windows95中:security_attributes结构的lpsecuritydescriptor成员被忽略.
指向一个security_attributes结构体,这个结构体决定是否返回的句柄可以被子进程继承.
如果lpthreadattributes参数为空(NULL),那么句柄不能被继承.在windows nt中,security_attributes结构的lpsecuritydescriptor成员指定了主线程的安全描述符.
如果参数为空,主线程使用默认的安全描述符.在windows95中:security_attributes结构的lpsecuritydescriptor成员被忽略.
指示新进程是否从调用进程处继承了句柄.
如果参数的值为真,调用进程中的每一个可继承的打开句柄都将被子进程继承.被继承的句柄与原进程拥有完全相同的值和访问权限.
指定附加的、用来控制优先类和进程的创建的标志.以下的创建标志可以以除下面列出的方式外的任何方式组合后指定.
进程创建标志
进程优先级
dwcreationflags参数还用来控制新进程的优先类,优先类用来决定此进程的线程调度的优先级.如果下面的优先级类标志都没有被指定,那么默认的优先类是normal_priority_class,除非被创建的进程是idle_priority_class.在这种情况下子进程的默认优先类是idle_priority_class.可以下面的标志中的一个:
指向一个新进程的环境块.
如果此参数为空,新进程使用调用进程的环境.一个环境块存在于一个由以NULL结尾的字符串组成的块中,这个块也是以NULL结尾的.每个字符串都是name=value的形式.因为相等标志被当作分隔符,所以它不能被环境变量当作变量名.与其使用应用程序提供的环境块,不如直接把这个参数设为空,系统驱动器上的当前目录信息不会被自动传递给新创建的进程.对于这个情况的探讨和如何处理,请参见注释一节.环境块可以包含unicode或ansi字符.
如果lpenvironment指向的环境块包含unicode字符,那么dwcreationflags字段的create_unicode_environment标志将被设置.
如果块包含ansi字符,该标志将被清空.请注意一个ansi环境块是由两个零字节结束的:一个是字符串的结尾,另一个用来结束这个快.一个unicode环境块石油四个零字节结束的:两个代表字符串结束,另两个用来结束块.
指向一个以NULL结尾的字符串,这个字符串用来指定子进程的工作路径.这个字符串必须是一个包含驱动器名的绝对路径.如果这个参数为空,新进程将使用与调用进程相同的驱动器和目录.这个选项是一个需要启动启动应用程序并指定它们的驱动器和工作目录的外壳程序的主要条件.
指向一个用于决定新进程的主窗体如何显示的startupinfo结构体.
//用于指定新进程的主窗口如何显示,成员参见msdn typedef struct _STARTUPINFO { DWORD cb; LPTSTR lpReserved; LPTSTR lpDesktop; LPTSTR lpTitle; DWORD dwX; DWORD dwY; DWORD dwXSize; DWORD dwYSize; DWORD dwXCountChars; DWORD dwYCountChars; DWORD dwFillAttribute; DWORD dwFlags; WORD wShowWindow; WORD cbReserved2; LPBYTE lpReserved2; HANDLE hStdInput; HANDLE hStdOutput; HANDLE hStdError; } STARTUPINFO, *LPSTARTUPINFO;
指向一个用来接收新进程的识别信息的process_information结构体.
typedef struct _PROCESS_INFORMATION { HANDLE hProcess; //进程句柄 HANDLE hThread; //主线程句柄 DWORD dwProcessId; //进程标识id DWORD dwThreadId; //主线程标识id } PROCESS_INFORMATION;
返回值:
说明:
createprocess函数用来运行一个新程序.winexec和loadmodule函数依旧可用,但是它们同样通过调用createprocess函数实现.
另外createprocess函数除了创建一个进程,还创建一个线程对象.这个线程将连同一个已初始化了的堆栈一起被创建,堆栈的大小由可执行文件的文件头中的描述决定.线程由文件头处开始执行.
新进程和新线程的句柄被以全局访问权限创建.对于这两个句柄中的任一个,如果没有安全描述符,那么这个句柄就可以在任何需要句柄类型作为参数的函数中被使用.当提供安全描述符时,在接下来的时候当句柄被使用时,总是会先进行访问权限的检查,如果访问权限检查拒绝访问,请求的进程将不能使用这个句柄访问这个进程.
这个进程会被分配给一个32位的进程标识符.直到进程中止这个标识符都是有效的.它可以被用来标识这个进程,或在openprocess函数中被指定以打开这个进程的句柄.进程中被初始化了的线程一样会被分配一个32位的线程标识符.这个标识符直到县城中止都是有效的且可以用来在系统中唯一标识这个线程.这些标识符在process_information结构体中返回.
当在lpapplicationname或lpcommandline参数中指定应用程序名时,应用程序名中是否包含扩展名都不会影响运行,只有一种情况例外:一个以.com为扩展名的ms-dos程序或windows程序必须包含.com扩展名.
调用进程可以通过waitforinputidle函数来等待新进程完成它的初始化并等待用户输入.这对于父进程和子进程之间的同步是极其有用的,因为createprocess函数不会等待新进程完成它的初始化工作.举例来说,在试图与新进程关联的窗口之前,进程应该先调用waitforinputidle.
首选的结束一个进程的方式是调用exitprocess函数,因为这个函数通知这个进程的所有动态链接库(dlls)程序已进入结束状态.其他的结束进程的方法不会通知关联的动态链接库.注意当一个进程调用exitprocess时,这个进程的其他县城没有机会运行其他任何代码(包括关联动态链接库的终止代码).
exitprocess, exitthread, createthread, createremotethread,当一个进程启动时(调用了createprocess的结果)是在进程中序列化进行的.在一段地址空间中,同一时间内这些事件中只有一个可以发生.这意味着下面的限制将保留:
当进程中最后一个线程终止时,下列的事件发生:
假设当前在c盘上的目录是/msvc/mfc且有一个环境变量叫做c:,它的值是c:/msvc/mfc,就像前面lpenvironment中提到过的那样,这样的系统驱动器上的目录信息在createprocess函数的lpenvironment参数不为空时不会被自动传递到新进程里.一个应用程序必须手动地把当前目录信息传递到新的进程中.为了这样做,应用程序必须直接创建环境字符串,并把它们按字母顺序排列(因为windows nt和windows 95使用一种简略的环境变量),并把它们放进lpenvironment中指定的环境块中.类似的,他们要找到环境块的开头,又要重复一次前面提到的环境块的排序.
一种获得驱动器x的当前目录变量的方法是调用getfullpathname("x:",..).这避免了一个应用程序必须去扫描环境块.如果返回的绝对路径是x:/,就不需要把这个值当作一个环境数据去传递了,因为根目录是驱动器x上的新进程的默认当前目录.
由createprocess函数返回的句柄对于进程对象具有process_all_access的访问权限.
由lpcurrentdirectory参数指定的当前目录室子进程对象的当前目录.lpcommandline参数指定的第二个项目是父进程的当前目录.
对于windows nt,当一个进程在指定了create_new_process_group的情况下被创建时,一个对于setconsolectrlhandler(NULL,true)的调用被用在新的进程上,这意味着对新进程来说ctrl+c是无效的.这使得上层的外科程序可以自己处理ctrl+c信息并有选择的把这些信号传递给子进程.ctrl+break依旧有效,并可被用来中断进程/进程树的执行.
附加说明
CreateProcess 时,系统将会创建一个进程内核对象,其初始化使用计数器为1,经常内核对象不是进程本身,而是操作系统用来管理进程的一个小型数据结构(可以把进程内核对象想象成有进程统计信息构成的一个小心的数据结构).然后系统为新进程创建一个虚拟地址空间.并将可执行文件(和所有必有的动态链接库(DLL))的代码以及数据加载到进程的地址空间中.
CreateProcess 在进程完全初始化好之前就返回 TRUE.这意味着操作系统加载程序尚未尝试定位所有必要的动态链接库(DLL).如果有一个动态链接库(DLL)找不到或者不能正确初始化.进程就会终止.因为 CreateProcess 返回 TRUE.所以父进程不会注意到子进程的任何初始化问题
创建过程:
具体流程图
注意:
在创建一个进程时,系统会为该进程建立一个警察内核对象和一个线程内核对象,而该内核对象有有一个计数器,系统会为这两个对象赋予初始的计数为 1,在 CreateProcess 函数返回之前,它将打开创建的进程对象和线程对象,并将每个对象与进程和线程相关的句柄放在其最后一个参数 PROCESS_INFORMATION 结构体变量的对应成员中.当 CreateProcess 函数在其内部打开这些对象时,每个对象的使用计数就变为2,如果在父进程中不需要使用子进程的这两个句柄则可以调用 CloseHandle 函数关闭它们(关闭一个进程或线程的句柄.是不会强迫系统"杀死"此进程或线程的.关闭句柄只是告诉系统我们队进程或线程的统计数据不再感兴趣了.进程或线程会继续运行直到自行终止),系统会将子进程的进程内核对象和线程对象的计数器减1,当子进程终止运行时,系统会将这些使用计数器减 1,这时子进程的进程内核对象和线程内核对象都为 0,这两个内核对象就能够被释放了,所以在编程中,当不需要这些内核对象时,总应该调用 CloseHandle 函数关闭它们
代码样例:
定义必要结构体变量:
//指定新进程的主界面出现的样式 STARTUPINFO sui; //用于接收创建新进程后新进程的一些信息 PROCESS_INFORMATION pi;
初始化 startupinfo:
ZeroMemory(&sui,sizeof(STARTUPINFO));
创建进程:
//打开 vim 编辑器 CreateProcess("c:\\program files\\vim\\vim73\\gvim.exe",NULL,NULL,NULL, true,0,NULL,NULL,&sui,&pi);
进程创建后的清理操作:
//关闭进程句柄 CloseHandle(pi.hProcess); //关闭主线程句柄 CloseHandle(pi.hThread);
程序源码:
#include<windows.h> #include<cstdlib> using namespace std; void main() { STARTUPINFO sui; PROCESS_INFORMATION pi; ZeroMemory(&sui,sizeof(STARTUPINFO)); if(!CreateProcess("c:\\program files\\vim\\vim73\\gvim.exe",NULL,NULL,NULL, true,0,NULL,NULL,&sui,&pi)) { MessageBox(NULL,"创建子进程失败!","警告",MB_OK); return; } else { CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } system("pause"); }
运行结果:
终止进程的 4 中方式
ExitProcess / ExitProcess 说明:
调用 TerminateProcess 或 ExitProcess 会导致进程或线程直接终止运行,C/C++ 应用程序应该避免调用这些函数,因为C/C++ 运行库也许不能正确执行清理工作
调获取进程退出代码
用 GetExitCodeProcess 来获得已经终止的一个进程退出代码
设置进程响应严重错误
每个进程都关联了一组标志,这些标志的作用是让系统知道进程如何响应严重错误(磁盘介质错误,未处理异常,文件查找错误,数据对其错误等),进程可以调用 SetErrorMode 函数来告诉系统如何处理这些错误.默认情况下子进程是继承父进程的错误模式的标志
代码样例
用 ExitProcess / ExitProcess 退出进程样例
#include <windows.h> #include <iostream> #include <cstdlib> using namespace std; class TEST{ public: ~TEST(){cout<<"this is TEST destructor!"<<endl;} }; void main() { TEST test; //通过 TerminateProcess 杀死进程 //DWORD nID; //HANDLE hPro; //获得进程 ID,后通过进程 ID 获得进程句柄 //nID = GetCurrentProcessId(); //hPro = OpenProcess(PROCESS_ALL_ACCESS,FALSE,nID); //TerminateProcess(hPro,0); //通过 ExitProcess 杀死进程 ExitProcess(0); //下面这句话永远无法执行 system("pasue"); }
参考资料
执行流程(下图是看Windows核心编程的个人理解,若发现者错误若发现恳请提出)
执行流程相关解释
启动函数用途简单总结:(即上图的C/C++运行库启动代码)
主函数返回以后,启动函数将调用 C 运行库函数 exit,向其传递返回值(nMainRetVal).exit 函数执行以下任务
一个进程终止时,系统会依次执行以下操作:(即为上图的进程清理工作)