4.2 CreateProcess函数
(1)函数原型
参数 |
描述 |
PCTSTR pszApplicationName |
新进程要使用的可执行文件的名字 |
PTSTR pszCommandLine |
要传递给新进程的命令行字符串,注意PTSTR说明该字符 串应该是可读可写的。 |
PSECURITY_ATTRIBUTES psaProcess |
进程安全描述符 |
PSECURITY_ATTRIBUTES psaThread |
线程安全描述符 |
BOOL hInheritHandles |
是否从父进程继承可继承句柄 |
DWORD fdwCreate |
创建标志 |
PVOID pvEnviroment |
新进程要使用的环境块。NULL时表示继承父进程的。还可用GetEnvironmentStrings获取主调进程的,用完后FreeEnvironmentStrings释放。 |
PCTSTR pszCurrDir |
设置进程的当前目录。为NULL时与父进程当前工作目录一样。不为NULL时,指定的路径中包含一个驱动器号。 |
PSTARTUPINFO psiStartInfo |
startup信息,父进程可以指定与其子进程的主窗口有关的属性。 |
PPROCESS_INFORMATION ppiProcInfo |
进程信息 |
返回值 |
BOOL型 |
(2)注意进程内核对象不是进程本身,只是一个小型的数据结构,由操作系统用来管理这个进程的。可以理解为进程的一些统计信息构成的一个数据结构。只能当系统为新进程分配虚拟地址空间并加载可执行的文件和数据到内存里,才能称得上是一个进程(即进程和进程内核对象是两块内存,进程销毁时不会自动把内核对象销毁)。同理,线程内核对象也不是线程本身,而是一个小型数据结构,以便于操作系统管理线程。
(3)CreateProcess函数内部执行过程的一些说明
①该函数会创建一个进程内核对象和主线程内核对象,他们的使用计数将分别被初始化为1。
②当ppiProcInfo不为NULL时,函数内部,还会同时将这两种对象返回给父进程使用,所以计数各加1,即分别变为2,并填充ppiProcInfo结构。因此为了正确释放内核对象,父、子进程都要调用CloseHandle来关闭子进程及其主线程对象。(注意函数内部会先初始化新进程句柄表,并根据 hInheritHandles参数复制父进程中可继承对象句柄到新进程句柄表中,然后再分别填加子进程内核对象及其主线程内核对象到父进程句柄表中,这顺序很重要,可参考潘爱民《Windows内核原理与实现P138~146》)。
③可见新进程句柄表中并没有记录新进程本身及其主线程的句柄值。如果在新进程中要使用这两个对象,要通过GetCurrentProcess或GetCurrentThread获得,但因并不是从进程本身的句柄表中获取的,所以会出现与一般句柄值(索引)格式不一样的值,难道这就是伪句柄的来历?同时,伪句柄与真实句柄不一样,它使用简单,不用关闭,不会造成内存泄漏,因为当进程结束时,操作系统会自行调用CloseHandle来关闭这些对象。顺便说明一下,这两个函数获取的句柄值是个固定值是0xFFFFFFFF(进程)和0xFFFFFFFE(线程))。
4.2.1 pszApplication和pszCommandLine参数
(1)pszCommandLine:
①在CreateProcess内部会修改参数的值,所以不能传入常量字符串
错误调用 |
正确调用 |
CreateProcess(NULL,TEXT("NOTEPAD"),…) 其中的TEXT("NOTEPAD")分配在常量区,不可更改,会出现访问违规,错误!但在Vista版本下的ANSI版本不会出错。 |
TCHAR szCmdLine[]=TEXT("NOTEPAD");//数据区 CreateProcess(NULL,szCmdLine,…) //正确 |
②CreateProcess会解析pszCommandLine字符串,串中的第一个标志为可执行文件的名称。如果没有扩展名,则默认是.exe,如果不提供完整路径,则搜索顺序:
A、主调用进.exe文件所在目录
B、主调进程当前的工作目录(即GetCurrentDirectory得到的目录)
C、Windows系统目录,即GetSystemDirectory返回的System32子文件夹
D、Windows目录
E、PATH环境变量中列出的目录
③C/C++调用运行时启动函数,则pszCommandLine除第1个标志之后,剩余的字符串传递给(w)WinMain的pszCmdLine参数。
(2)psaApplicationName参数
①如果要指定该参数时,则必须指定文件扩展名(如.exe),同时如果没有完整路径,会假定是在当前工作目录中查找,该函数不会在任何其他目录中查找。
②当指定了该参数以后,CreateProcess也会将pszCommandLine作为新进程的命令行传递给它。
//确保路径是可读/写的内存区块
TCHAR szPath[] = TEXT("WORDPAD abc.txt"); //其中第1个标志WORDPAD在这可以是任意的字符
//创建子进程
CreateProcess(TEXT("C:\WINDOWS\SYSTEM32\NOTEPAD.EXE"),szPath,...);
4.2.2 psaProcess、psaThread和bInheritHandles参数
(1)设置进程线程内核对象的安全描述符,如果为NULL,则会指定默认的SA
(2)可以在psaProcess和psaThread中设置这两个内核对象在子进程中是否可以被孙进程继承。
(3)即使可以通过psaProcess和psaThread限制子进程句柄的访问权限,但父进程对子进程句柄仍然有完全的访问权,这些权限只对子进程及其子进程有效。
(4)bInheritHandles指明子进程是否继承父进程句柄表中的可继承句柄
【Inherit.cpp程序】——用来演示内核对象继承的特点(半成品程序)
/**************************************************************** Module name :Inherit.cpp Notices:Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre ****************************************************************/ #include <windows.h> #include <tchar.h> #include <strsafe.h> int WINAPI _tWinMain(HINSTANCE hInstanceExe, HINSTANCE, PTSTR pszCmdLine, int nCmdShow) { TCHAR szPath[MAX_PATH]; //为创建子进程准备一个STARTUPINFO结构 STARTUPINFO si = { sizeof(si) }; SECURITY_ATTRIBUTES saProcess, saThread; PROCESS_INFORMATION piProcessB, piProcessC; //从进程A创建子进程B saProcess.nLength = sizeof(saProcess); saProcess.lpSecurityDescriptor = NULL; saProcess.bInheritHandle = TRUE;//设置B进程内核对象句柄为可继承 saThread.nLength = sizeof(saThread); saThread.lpSecurityDescriptor = NULL; saThread.bInheritHandle = FALSE; //进程B的主线程设置为不可继承 //创建子进程B StringCchCopy(szPath, _countof(szPath), TEXT("ProcessB")); CreateProcess(NULL, szPath, &saProcess, &saThread, FALSE, 0, NULL, NULL, &si, &piProcessB); //pi结构体包含两个与进程A相关联的句柄值, //hProcess为进程B内核对象句柄,并且该句柄是可继承的。 //hThread为进程B的主线程内核对象句柄,且该句柄是不可继承的。 //创建子进程C时,将指定psaProcess和psaThread都为NULL。即默认C进程对象和线程对象句柄是 //不可继承的。 //因bInheritHandles参数设为TRUE,则进程C将继承进程B的主线程句柄 StringCchCopy(szPath, _countof(szPath), TEXT("ProcessC")); CreateProcess(NULL, szPath, NULL, NULL, TRUE, 0, NULL, NULL, &si, &piProcessC); return 0; }
4.2.3 fdwCreate参数——可以是以下多个标志的组合
标志 |
描述 |
DEBUG_PROCESS |
父进程可以调试子进程(及子进程生成的所有进程),在任何一个子进程(被调试程序)中发生的特定事件,要通知父进程(即调试器) |
DEBUG_ONLY_THIS_PROCESS |
类似DEBUG_PROCESS,但只发生在关系最近的子进程中的所有特定事件,父进程才会得到通知。如果子进程再生成新的进程,那么新进程发生的特定事件,调试器是不会得到通知的。 |
CREATE_SUSPENDED |
创建新进程并挂起其主线程,这样父进程就可以修改子进程的地址空间或其主线程的优先级等。修改完后调用ResumeThread恢复子进程 |
DETACHED_PROCESS |
阻止CUI进程访问其父进程的控制台窗口,并告诉系统将它该CUI进程的输出发送到一个新的控制台窗口。(默认下,一个子CUI进程会使用父进程的控制台窗口),但必须AllocConsole来创建它自己的控制台。 |
CREATE_NEW_CONSOLE |
新进程创建一个新的控制台。(注意DETACHED_PROCESS与CREATE_NEW_CONSOLE不能同时使用!) |
CREATE_NO_WINDOW |
不要为应用程序创建任何控制台窗口。 |
CREATE_NEW_PROCESS_GROUP |
对CUI程序而言的。用于为新进程组。在同一组中的所有进程,当按下Ctrl+C中断当前操作时,系统会向这个组的进程发出通知。 |
CREATE_DEFAULT_ERROR_MODE |
表明新进程不会继承父进程所用的错误模式 |
CREATE_SEPRATE_WOM_VDM CREATE_SHARED_WOW_VDM |
只运行16位Windows应用程序。默认下这种程序都在单独一个VDM中运行。但指定为CREATE_SHARED_WOW_VDM标志后,会在一个单独的VDM中运行。 |
CREATE_UNICODE_ENVIRONMENT |
进程默认的环境块是ANSI字符串,指定该标志后,将使用UNICODE字符串 |
CREATE_FORCEDOS |
强制运行嵌入在16位OS/2应用程序中的MS_DOS应用程序 |
CREATE_BREAKAWAY_FROM_JOB |
允许一个作业中的进程生成一个和作业无关的进程 |
EXTENDED_STARTUPINFO_PRESENT |
向psiStartInfo参数的是STARTUPINFOEX结构 |
4.2.4 psiStartInfo参数——STARTUPINFO或STARTUPINFOEX结构体
字段 |
窗口或 控制台 |
描述 |
cb |
两者 |
sizeof(STARTUPINFO)或sizeof(STARTUPINFOEX) |
pReserved |
两者 |
保留,必须为NULL |
PSTR pDesktop |
两者 |
表明是哪个桌面上的启动应用程序,如果桌面己经存在,则新进程会与指定桌面关也联。如果不存在,则会为新进程创建一个桌面。指定为NULL,表示与当前桌面关联。 |
pTitle |
控制台 |
控制台程序的窗口标题,为NULL时会将执行文件的名称作为其标题 |
dwX,dwY |
两者 |
指定应用程序在屏幕上的X和Y坐标(单位:像素)。当子过程用CW_USEDEFAULT作为CreateWindow时,会应用这两个值。 |
dwXSize,dwYSize |
两者 |
指定窗口宽度和高度(单位:像素)。当子过程用CW_USEDEFAULT作为CreateWindow参数时,会传递这两个值 |
dwXCountChars、 dwYCountChars |
控制台 |
指定子进程控制台窗口的宽度和高度(用字符数表示) |
dwFillAttribute |
控制台 |
指定子进程的控制台窗口所用的文本和背景色 |
dwFlags |
两者 |
STARTF_USESIZE:使用dwXSize和dwYSize成员 STARTF_USESHOWWINDOW:使用wShowWindow成员 STARTF_USEPOSITION:使用dwX和dwY成员 STARTF_USECOUNTCHARS:使用dwXCountChars和dwYCountChars STARTF_USEFILLATTRIBUTE:使用dwFillAttribute成员 STARTF_USESTDHANDLES:使用hStdInput、hStdOutput、hStdError STARTF_RUNFULLSCREEN:控制下台应用程序以全屏模式启动 |
wShowWindows |
窗口 |
指定应用程序的主窗口如何显示。 ①第1次调用ShowWindow时,将使用该值,而不是ShowWindow的nCmdShow参数。后续的ShowWindow调用将使用nCmdShow参数(当然,如果指定为SW_SHOWDEFAULT时,仍会使用wShowWindows)。 ②在Windows资源管理器启动应用程序时,Windows将先根据应用程序启动快捷方式(右键→属性→运行方式)中指定的方式准备一个STARTUPIFO结构,并调用CreateProcess来创建子进程。 |
cbReserved2 |
两者 |
保留,必须为0 |
pReserved2 |
两者 |
,必须为NULL |
hStdInput、 hStdOutput hStdError |
控制台 |
控制台输入、输出缓冲区句柄。这些字段用于重定向子进程输入/输出。 hStdInput标识一个键盘缓冲区 hStdOutput和hStdError标识一个控制台窗口的缓冲区。 |
PPROC_THREAD_ATTRIBUTE_LIST pAttributeList; |
窗口 |
额外的“属性”参数,只有STARTUPINFOEX结构才有这字段。 列表中的每一个项都是键/值,而当中的键要么具有PROC_THREAD_ATTRIBUTE_HANDLE_LIST属性,要么具有PROC_THRED_ATTRIBUTE_PRAENT_PROCESS属性,他们分别用来说明父进程中哪些句柄要被继承或改变子进程的父进程为指定的进程而不是当前调用CreateProcess的进程。 |
(1)使用前该结构体的内容需先清零,否则垃圾内容可能影响CreateProcess的行为。
(2)由于pAttributeList属性列表是不透明的,正确的使用方法如下:(半成品程序)
#include <stdio.h> #include <windows.h> int main() { SIZE_T cbAttributeListSize = 0; //获得属性列表所需内存块的大小 BOOL bRet = InitializeProcThreadAttributeList(NULL, 1, 0, &cbAttributeListSize); //为属性列表分配内存 PPROC_THREAD_ATTRIBUTE_LIST pAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST) HeapAlloc(GetProcessHeap(), 0, cbAttributeListSize); //由于属性列表是不透明的,要第二次调用下列函数来初始化属性列表 bRet = InitializeProcThreadAttributeList(pAttributeList, 1, 0, &cbAttributeListSize); //初始化好了,可以增加、删除、修改键值对了 //改变新进程的父进程 UpdateProcThreadAttribute(pAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, 新的父进程句柄, sizeof(HANDLE), NULL, 0); //指定可继承的句柄 UpdateProcThreadAttribute(pAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, 指向一个允许子进程访问、可继承句柄的数组, sizeof(HANDLE)*句柄数, NULL, 0); //创建新的子进程 STARTUPINFOEX esi = { sizeof(STARTUPINFOEX) }; esi.lpAttributeList = pAttributeList; //下列必须指定EXTENDED_STARTUPINFO_PRESENT bRet = CreateProcess(..., EXTENDED_STARTUPINFO_PRESENT, ..., &esi.StartupInfo, ...); //不再需要参数的时候,要释放内存 DeleteProcThreadAttributeList(pAttributeList); return 0; }
4.2.5 ppiProcInfo参数——PROCESS_INFORMATION结构体
字段 |
描述 |
备注 |
hProcess |
新进程内核对象句柄(供父进程使用) |
①CreateProcess会创建这两个内核对象,使用计数分别初始化为1。但函数内部会使用完全访问权限来“打开”这两个对象,所以使用计数变为2,然后并将各自与主进程相关的句柄值填入这两个成员。同时将进程内核对象返回给主调进程。 ②要释放这两个对象时,必须在主进程分别CloseHandle这两个对象,同时子进程要别CloseHandle这两个对象。 |
hThread |
新进程的主线程对象句柄(供父进程使用) |
|
dwProcessID |
进程编号(系统级别) |
注意:ID会被重用,即如果一个进程(或线程)被释放,新进程(线程)产生时,会重用这个ID。所以保存ID下来以供后继使用是很不安全的,切记ID会被系统立即重用。如果要利用ID跟踪进程或线程,可以使用GetCurrentProcessID来获取当前进程ID或GetCurrentThreadID来获取当前线程ID。 |
dwThreadID |
主线程ID(系统级别) |
注意:
(1)GetProcessID、GetThreadID——获得与指定句柄对应的进程(线程)ID
(2)GetProcessIdOfThread——根据线程句柄获得所在进程ID
(3)父进程只有在生成子进程的一瞬间才存在父—子关系。到子进程开始执行代码的那一刻,这种关系就不存在了。ToolHelp函数允许通过PROCESSENTRY32结构(内部有一个th32ParentProcessID)查询父进程,但请记住,由于ID会被立即重用,所以这个ID也是不可靠的。
4.3 终止进程——4种方式
第1种 |
主线程入口点函数返回(强烈推荐) |
第2种 |
进程中的一个线程调用ExitProcess函数(要避免这种方式) |
第3种 |
另一进程或自己调用TerminateProcess函数(要避免这种方式) |
第4种 |
进程中所有线程都“自然死亡”(这种情况几乎不会发生) |
4.3.1 主线程的入口函数返回——推荐方式(会正确释放内存)
4.3.2 ExitProcess函数
(1)C/C++运行库中的启运函数在调用主线程入口函数(如WinMain)返回后,WinMainCRTStartup将正确清理进程使用的全部C运行时资源,并显示调用ExitProcess,并将入口函数的返回值作为ExitProcesss参数,作为进程的代码退出。在如果WinMain中调用的是ExitThread,应用程序主线程将停止,但只要有其他线程正在运行,进程就不会终止。
(2)调用ExitProcess或ExitThread将导致进程或线程直接终止,再也不会返回当前函数调用。因此,有些清理工作可能不会被正确执行。(如课本P101页的例子)
4.3.3 TerminateProcess函数
(1)可能终止另一进程或自己进程,参数hProcess为要被中止的进程)
(2)该方法只有在无法通在其他方法退出进程时,才使用。但这种情况下,一些保存工作都无法执行。
(3)操作系统会在进程终止之后彻底清理,所以进程终止后不会造成任何内存泄漏。
4.3.4 当进程中的所有线程终止时
因为没有所有的线程都中止了,保存进程就没有意义,所以会自动终止进程。
4.3.5 当进程终止运行时
(1)进程终止时会依次执行以下的清理操作
①终止进程中遗留的任何线程。
②释放进程分配的所有用户对象和GDI对象,关闭所有内核对象(如果其他程序打开情况下)。
③进程的退出代码从STILL_ACTIVE变为传给ExitProcess或TerminateProcess函数的代码
④进程内核对象的状态变为己触发状态。
⑤进程内核对象的使用计数减1(注意:这时进程销毁了,但内核对象可能还在!)
(2)GetExitCodeProcess是从内核对象的数据结构中提取进程的退出代码标志的。所以不管进程有没有终止,任何时候都可以调用该函数。获取的退出代码为STILL_ACTIVE表示进程还没终止。如果己经终止,会返回实际的退出代码。但该函数仍然不能判断进程内核对象是否被释放了。
(3)CloseHandle会使内核对象使用计数递减1,但它只是告诉操作系统我们己经对进程的统计数据不感兴趣,关闭了以后,就不要再使用该内核对象了,那很不安全,因为我们不知什么时候该对象可能被释放。
4.4 子进程
(1)父子进程之间有相互协作关系的情况
PROCESS_INFORMATION pi; DWORD dwExitCode; //创建子进程 BOOL fSuccess = CreateProcess(…,&pi); if(fSuccess) { //当不再使有用子线程对象时,关闭它 CloseHandle(pi.hThread); //父进程挂起自己,直到子进程完成任务,结束返回 WaitForSingleObject(pi.hProcess,INFINITE); //挂起自己,等待hProcess变为有信号 //获取子进程的退出代码 GetExitCodeProcess(pi.hProcess,&dwExitCode);//用于判断子进程是否是正常退出的 //关闭子进程 CloseHandle(pi.hProcess); //注意子进程的使用计数为2,在子进程退出后,其内部会将计数减1. //在父进程也要CloseHandle一次。才能正确释放该内核对象。 }
(2)运行独立的子进程——父子进程不需要进程通信的
PROCESS_INFORMATION pi; //创建子进程 BOOL fSuccess = CreateProcess(…,&pi); if(fSuccess) { //当子进程结束时,允许销毁子进程内核对象及其主线程对象(因为使用计数分别为2) CloseHandle(pi.hThread); //注意子进程(或线程)的使用计数为2,在子进程(线程)退出后, CloseHandle(pi.hProcess);//其内部会将计数减1. 在父进程也要分别CloseHandle一次 }
【CreateProcess程序】
//父进程程序
#include <windows.h> #include <tchar.h> #include <strsafe.h> #define GRS_USEPRINTF() TCHAR pBuf[1024] = {} #define GRS_PRINTF(...) StringCchPrintf(pBuf, 1024, __VA_ARGS__); WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), pBuf, lstrlen(pBuf), NULL, NULL); #define GRS_USEPRINTFA() CHAR pBufA[1024] = {} #define GRS_PRINTFA(...) StringCchPrintfA(pBufA, 1024, __VA_ARGS__); WriteConsoleA(GetStdHandle(STD_OUTPUT_HANDLE), pBufA, lstrlenA(pBufA), NULL, NULL); #define GRS_ALLOC(sz) HeapAlloc(GetProcessHeap(),0,sz) #define GRS_CALLOC(sz) HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sz) #define GRS_SAFEFREE(p) if(NULL != p){HeapFree(GetProcessHeap(),0,p);p=NULL;} //获取应用程序所在的路径 void GetAppPath(PTSTR pszPath) { DWORD dwLen = 0; //获取当前进程已加载模块的文件的完整路径 dwLen = GetModuleFileName(NULL, pszPath, MAX_PATH); //NULL表示获取当前exe全路径 if (dwLen == 0) return; for (DWORD i = dwLen; i > 0; i--) { if ('\'==pszPath[i]) { pszPath[i + 1] = '