读Windows核心编程 - 4

        进程分为两部分,一个是操作系统用来管理进程的内核对象,一个是地址空间。进程是不活泼的,活泼的是线程,每个线程都有它自己的一组CPU寄存器和它自己的堆栈。

        Windows支持两种应用程序,一种是GUI(基于图形用户界面),还有一种CUI(基于控制台用户界面)。Windows应用程序必须有一个在应用程序启动运行时的进入点函数。一共有4个:WinWain、wWinMain、main、wmain。操作系统实际上不直接调用我们编写的进入点函数。它调用的是C/C++运行期启动函数,其对应的函数也有四个:WinMainCRTStartup、wWinMainCRTStartup、mainCRTStartup、wmainCRTStartup。当我们用Visual C++创建一个应该程序项目时,Visual C++会根据创建的程序的类型指定链接程序开关,如果是GUI,链接程序的开关是/SUBSYSTEM:WINDOWS,如果是CUI,链接程序的开关是/SUBSYSTEM:CONSOLE。一个新手常见的错误是,创建了一个win32的应用程序,但是创建了一个进入点函数mian,这样因为在链接程序中的开关已经设定为/SUBSYSTEM:WINDOW,会产生一个链接错误。最好的办法是把链接程序开关删除,这样程序就可以自动的确定应该程序应该链接到哪个子系统。

C/C++运行期启动函数功能归纳如下:

1. 检索指向新进程的完整命令行的指针

2. 检索指向新进程的环境变量指针

3. 对C/C++运行期的全局变量进行初始化,比如:_pgmptr - 正在运行的程序的全路径和名字

4. 对所有全局和静态C++对象调用构造函数

当进入点返回后,启动函数便调用C运行时的exit函数。Exit函数负责下面的操作:

1. 调用由_onexit函数的调用而注册的任何函数

2. 为所有的全局和静态C++对象调用析构函数

3. 调用系统ExitProcess函数

        在我们调用LoadIcon这样的函数时,通常需要指定一个HINSTANCE(HMODULE)参数来指明哪个文件(可执行文件还是DLL文件)包含你想加载的资源。这个参数就是WinMain中的hinstExe参数,该参数实际值是系统将可执行文件加载到进程地址空间时使用的基本地址空间,而这个基地址是由链接程序决定的。调用GetModuleHandle函数可以返回可执行文件或DLL文件加载到进程的地址空间时所用的句柄/基地址,参数传一个以0结尾的字符串。如果传递NULL就得到可执行文件的基地址,这正是C运行期函数调用WinMain时执行的操作。这个函数注意两点:1. 它只可以获得本进程的地址空间。2. 如果传递NULL,在DLL中调用也返回可执行文件的基地址。

        每个进程都有一个与它相关的环境块。环境块是进程地址空间中分配的一个内存块。形式像这个样子:VarName=VarVaule,注意:等于号两边的空格都被考虑在内。如果通过注册表修改了环境变量,需要再次登录才能有效。如果想通过注册表修改,并让有关应用程序更新他们的环境块,可以调用如下代码:

SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (LPARAM) TEXT("Environment"));

子进程可以继承一组与父进程相同的环境变量。但是,父进程能够控制子进程继承什么样的环境变量。这里指的继承是复制,也就是子进程之后对环境变量的修改影响不到父进程。通过GetEnvironmentVariable函数可以确定某个环境变量是否存在以及它的值。SetEnvironmentVariable函数则用于添加、删除、修改环境变量。

        通过SetErrorMode函数可以设置进行的错误模式,该模式是指当进程遇到严重错误时应该如何作出反映,这些错误包括磁盘介质故障,未处理的异常,文件查找失败等。通常子进程继承父进程的错误模式标志,也可以在CreateProcess函数中传递CREATE_DEFAULT_ERROR_MODE防止子进程继承。

        如果用CreateFile来打开一个文件(不设定全路径),那么系统就会在当前驱动器当前目录中查找该文件。系统总是在内部保持对进程的当前驱动器和目录的跟踪。这些信息是由进程维护的。通过GetCurrentDirectory和SetCurrentDiretory可以获得和设置进程的当前驱动器和目录。有些操作系统支持多个驱动器的当前目录的处理,例如,进程拥有下面所示的两个环境变量: =C:=C:/Utility/Bin        =D:=D:/Program Files, 现在的当前目录是C:/Utility/Bin,并且你调用CreateFile来打开D:ReadMe.txt,那么系统查看环境变量=D。因为=D存在,因此系统试图从D:/Program File目录打开该文件。如果不存在,系统就试图从驱动器D的根目录打开该文件。子进程的环境块不会自己继承父进程的当前目录。如果想要子进程继承父进程的当前目录,该父进程必须创建这些驱动器名的环境变量。调用GetFullPathName,父进程可以获得它的当前目录。如:GetFullPathName(TEXT("C:"), MAX_PATH, szCurDir, NULL);

        Windows提供了三个函数可以确定操作系统版本相关的函数:GetVersion,GetVersionEx,VerifyVersionInfo.具体使用方法参看核心编程page56.

CreateProcess:原型如下:

BOOL CreateProcess(
  LPCTSTR lpApplicationName,
  LPTSTR lpCommandLine,
  LPSECURITY_ATTRIBUTES lpProcessAttributes,
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  BOOL bInheritHandles,
  DWORD dwCreationFlags,
  LPVOID lpEnvironment,
  LPCTSTR lpCurrentDirectory,
  LPSTARTUPINFO lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);

 1. lpApplicationName和lpCommandLine

        这两个参数可执行文件和命令行字符串。注意到一点,lpCommandLine的原型是LPTSTR,这意味着CreateProcess希望你传递一个非常量的字符串地址。如果传进去一个TEXT(“NOTEPAD”)就会出现违规访问的问题。但是如果传进去一个ANSI字符串,就不会出现这个问题,因为系统已经制作了一个命令行字符串的临时拷贝。如果lpApplication不是NULL(大多数情况是NULL),那么可以将包含想运行的可执行文件的字符串地址传递给lpApplicationName参数。这里必须提供文件的扩展名,系统不会自动假设有一个exe扩展名。如果没用提供全路径,那么系统只在当前目录查找,如果找不到则运行失败。如果lpApplicationName是NULL,那么需要为lpCommandLine提供一个完整的命令行,系统假定字符串中的第一个标记是可执行文件的名字,如果没有指定扩展名,系统假定扩展名为exe。如果在lpCommandLine中没有指定全路径,那么系统依次在以下目录中查找:包含调用进程的.exe文件的目录-->调用进程的当前目录-->Windows系统目录-->Windows目录-->PATH环境变量中列出的目录。

2. lpProcessAttributes、lpThreadAttributes和bInheritHandles

        可以使用lpProcessAttributes和lpThreadAttributes参数分别设定进程对象和线程对象的安全性。如果传递NULL则赋予默认安全性描述符。否则需要传递一个SECURITY_ATTRIBUTES的数组来创建自己的安全性权限。调用CreateProcess将在父进程的句柄表中新增两项,进程对象和线程对象,而SECURITY_ATTRIBUTES结构体中的bInheritHandles成员则确定了该对象的可继承性。比如:Process A中调用了CreateProcess创建进程B,并设定lpProcessAttributes.bInheritHandles = TRUE, lpThreadAttributes.bInheritHandles = FALSE。现在A又调用CreateProcess创建进程C,如果bInheritHandles设置为FALSE,那么两个内核对象都得不到继承,如果bInheritHandles设置为TRUE,那么进程B对象得到继承,也就是复制到了进程C的句柄表中。而线程B对象得不到继承。与此同时,进程A的句柄表中有多了进程C和线程C两项。

3. dwCreationFlags

        这个参数用于规定如何创建进程,可以用OR操作符将多个标志组合起来。比如,CREATE_SUSPENDED表示新创建的主线程挂起。类似的参数有很多,可以参考核心编程page63。另外dwCreationFlags参数还可以设定进程的优先级类。

4. lpEnviroment

        lpEnviroment参数用于指向包含新进程要使用的环境字符串的内存块。如果传递NULL,子进程将继承它的父进程正在使用的一组环境字符串。将GetEnvironmentStrings()的返回值传递给这个参数效果跟传递NULL一样,该函数获得调用进程正在使用的环境变量字符串数据块的地址。当不再需要该内存块时,应该调用FreeEnvironmentStrings函数将内存块释放。

5. lpCurrentDirectory

        这个参数允许父进程设置子进程的当前驱动器和目录。如果本参数为NULL,则新进程的工作目录将与生成新进程的应用程序的目录相同。如果不传递NULL,那么必须指向包含需要的工作驱动器和工作目录的以0结尾的字符串。

6. lpStartupInfo

        这是一个结构体,里面有很多成员,一般情况下只需要把该结构体全部赋为0,把该结构体的大小赋给其中的一个成员就可以了。这个结构体中有些成员只有在子应用程序创建一个重叠窗口时才有意义,而另一些成员则只有在子应用程序执行基于CUI的输入输出时才有意义。具体每个成员的意义可参看核心编程page65。最后,应用程序可以调用GetStartupInfo以便获得由父进程初始化的STARTUPINFO结构的拷贝。子进程可以查看该结构,并根据该结构的成员的值来改变它的行为特征。虽然在windows文档中没有明确的说明,但是在调用这个函数之前必须像下面这样对该结构的cb成员进程初始化:STARTUPINFO si = {sizeof(si)}; GetStartupInfo(&si);

7. lpProcessInformation

        结构体PROCESS_INFORMATION一共包含四个成员,进程和线程的句柄以及ID。在创建进程的时候,系统为每个对象赋予一个初始引用计数1。然后,在CreateProcess返回之前,该函数打开进程对象和线程对象,并将每个对象的与进程相关的句柄放入PROCESS_INFORMATION结构中。当CreateProcess在内部打开这些对象时,每个对象的引用计数变为2。这意味着如果要使系统释放进程对象,除了该进程必须终于外,父进程必须调用CloseHandle。线程也同样。进程ID和线程ID都是独一无二的标志符,其他内核对象不能使用相同的ID,另外进程和线程也不能使用相同的ID。如果应用程序使用ID来跟踪线程或者进程,必须注意一点,就是这些ID可以重复使用,意思是说,如果一个ID为122的进程对象被释放,那么当另外一个进程创建起来时,122就可以被赋给这个进程。所以最好不要用ID,应该定义一个持久性更好的机制,比如内核对象和窗口句柄等。

终止进程的运行:

若要终止进程的运行,可以使用下面四种方法:

1. 主线程的进入点函数返回(最好的方法)

        这种方法是保证所有线程资源能够得到正确清楚的唯一办法:

        1. 该线程创建的任何C++对象将能使用他们的析构函数正确的撤销。

        2. 操作系统能将正确地释放该线程的堆栈使用的内存。

        3. 系统将进程的退出代码(在进程的内核对象中维护)设置为进入点函数的返回值。

        4. 系统将进程的内核对象的引用计数递减1.

2. 进程中的一个线程调用ExitProcess(应该避免使用)

        正常情况下,当主线程的进入点函数返回时,它将返回给C/C++运行期启动代码,它能正确地清楚该清楚使用的所有的C运行时资源。当C运行期资源被释放以后,C运行期启动代码就显示调用ExitProcess,并将进入点函数的值传递给它。注意,调用ExitProcess或ExitThread可使进程或线程在函数中就终止运行。就操作提供而言,就很好,进程或线程的所有操作系统资源都将被全部清楚。但是,C/C++应用程序应该避免使用这些函数,因为C/C++运行期也许无法正确地清楚。这里有个问题不是很明白,操作系统不负责C/C++资源的清理?进程结束了,所有的内存都被收回,还有什么资源得不到释放的么???

3. 另一个进程中的线程调用TerminateProcess(应该避免使用)

        与ExitProcess不同的是,这个函数可以终止另一个进程或它自己进程的运行。虽然进程确定没有机会执行自己的清楚操作,但是操作系统可以在进程之后进行全面的清除,使得所有操作系统资源都不会保留下来。这意味着进程使用的所有的内存都被释放,所有打开的文件全部关闭,所有内核对象的引用计数均被递减,同时所有的用户对象和GDI对象均被撤销。注意:TerminateProcess是个异步函数,因此无法保证进程被终止。如果想要确切了解进程是否已经终止运行,必须调用WaitForSingleObject或者类似的函数,并传递进程的句柄。

4. 进程中的所有线程自行终止运行(几乎不会发生)

当进程终止时出现的情况:

1. 进程中的所有线程被终止

2. 进程指定的用户对象和GDI对象均被释放。所有内核对象均被关闭(递减...)。

3. 进程的退出代码将从STILL_ACTIVE改为传递给ExitProcess或TerminateProcess的代码。

4. 进程内核对象的状态变为收到通知状态(详见第9章)。

5. 进程内核对象的引用计数减1,如果降为0,内核对象被撤销。

        进程内核对象的生命期比进程长,进程内核对象维护关于进程的统计信息。即使进程已经终止运行,该信息也是有用的。例如,你可能想要知道进程需要多少CPU时间,或者,你想通过调用GetExitCodeProcess来获得目前已经撤销的进程的退出代码。如果调用GetExitCodeProcess的进程还未终止,得到的退出代码为STILL_ACTIVE,否则便返回进程的退出代码值。

子进程:

PROCESS_INFORMATION pi;
DWORD dwExitCode;
BOOL fSuccess 
= CreateProcess(..., &pi);
if(fSuccess)
{
        CloseHandle(pi.hThread);
        WaitForSingleObject(pi.hProcess, INFINITE);
        GetExitCodeProcess(pi.hProcess, 
&dwExitCode);
        CloseHandle(pi.hProcess);
}

CloseHandle(pi.hThread):当CreateProcess返回后立即关闭了子进程的主线程的内核对象。原因是如果子进程的主线程又产生了一个线程而自己终止运行,那么系统就可以从内存中释放子进程的主线程对象。

WaitForSingleObject(pi.hProcess, INFINITE):父进程挂起,直到子进程终止运行。当WaitForSingleObject返回时,就可以调用GetExitCodeProcess获得子进程的退出代码。

        如果要创建一个独立运行的子进程,在CreateProcess之后直接关掉进程和线程的内核对象就可。这就是Explorer的运行方式。当Explorer为用户创建一个新进程后,它并不关心该进程是否继续运行,也不在乎用户是否终止它的运行。

发布了25 篇原创文章 · 获赞 0 · 访问量 8万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章