Windows高级编程之进程

进程通常被定义为一个正在运行的程序的实例,它由两个部分组成:
• 一个是操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计信息的地方。
• 另一个是地址空间,它包含所有可执行模块或 D L L模块的代码和数据。它还包含动态内存分配的空间。如线程堆栈和堆分配空间。
进程是不活泼的。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,该线程负责执行包含在进程的地址空间中的代码。实际上,单个进程可能包含若干个线程,所有这些线程都“同时”执行进程地址空间中的代码。为此,每个线程都有它自己的一组C P U寄存器和它自己的堆栈。每个进程至少拥有一个线程,来执行进程的地址空间中的代码。如果没有线程来执行进程的地址空间中的代码,那么进程就没有存在的理由了,系统就将自动撤消该进程和它的地址空间。
当创建一个进程时,系统会自动创建它的第一个线程,称为主线程。然后,该线程可以创建其他的线程,而这些线程又能创建更多的线程。
Windows应用程序必须拥有一个在应用程序启动运行时调用的进入点函数。一般有如下几个:WinMain/wWinMain/main/wmain。
启动函数的功能归纳如下:
• 检索指向新进程的完整命令行的指针。
• 检索指向新进程的环境变量的指针。
• 对C/C++运行期的全局变量进行初始化。如果包含了StdLib.h文件,代码就能访问这些变量。
• 对C运行期内存单元分配函数(malloc和calloc)和其他低层输入/输出例程使用的内存栈进行初始化。
• 为所有全局和静态C++类对象调用构造函数。

当进入点函数返回时,启动函数便调用C运行期的exit函数,将返回值(nMainRetVal)传递给它。Exit函数负责下面的操作:
• 调用由_onexit函数的调用而注册的任何函数。
• 为所有全局的和静态的C + +类对象调用析构函数。
• 调用操作系统的ExitProcess函数,将nMainRetVal传递给它。这使得该操作系统能够撤消进程并设置它的exit代码。

4.1.1 进程的实例句柄
加载到进程地址空间的每个可执行文件或D L L文件均被赋予一个独一无二的实例句柄。可执行文件的实例作为(w)WinMain的第一个参数hinstExe来传递。对于加载资源的函数调用来说,通常都需要该句柄的值。
Platform SDK文档中说,有些函数需要HMODULE类型的一个参数。实际情况说明,HMODULE与HINSTANCE是完全相同的对象。
(w)WinMain的hinstExe参数的实际值是系统将可执行文件的映象加载到进程的地址空间时使用的基本地址空间。可执行文件的映像加载到的基地址是由链接程序决定的。不同的链接程序可以使用不同的默认基地址。Visual C++链接程序使用的默认基地址是0x00400000,因为这是在运行Windows 98时可执行文件的映象可以加载到的最低地址。可以改变应用程序加载到的基地址,方法是使用Microsoft的链接程序中的/BASE:address链接程序开关。
GetModuleHandle函数返回可执行文件或D L L文件加载到进程的地址空间时所用的句柄/基地址。当调用该函数时,你传递一个以 0结尾的字符串,用于设定加载到调用进程的地址空间的
可执行文件或 D L L文件的名字。如果系统找到了指定pszModule参数传递 N U L L,GetModuleHandle返回调用的可执行文件的基地址。这正是C运行期启动代码调用(w)WinMain函数时该代码执行的操作。
GetModuleHandle函数的两个重要特性:首先,它只查看调用进程的地址空间;第二,调用GetModuleHandle并传递N U L L值,就会返回进程的地址空间中可执行文件的基地址。因此,即使通过包含在D L L中的代码来调用(N U L L) ,返回的值也是可执行文件的基地址,而不是D L L文件的基地址。
4.1.2 进程的前一个实例句柄
C / C + +运行期启动代码总是将N U L L传递给( w ) Wi n M a i n的h i n s t E x e P r e v参数。目的仅仅是为了能够容易地转用1 6位Wi n d o w s应用程序。决不应该在代码中引用该参数。
4.1.3 进程的命令行
当一个新进程创建时,它要传递一个命令行。该命令行几乎永远不会是空的,至少用于创建新进程的可执行文件的名字是命令行上的第一个标记。当 C运行期的启动代码开始运行的时候,它要检索进程的命令行,跳过可执行文件的名字,并将指向命令行其余部分的指针传递给Wi n M a i n的p s z C m d L i n e参数。p s z C m d L i n e参数总是指向一个A N S I字符串。
获得一个指向进程的完整命令行的指针,方法是调用G e t C o m m a n d L i n e函数:
PTSTR GetCommandLine()
该函数返回一个指向包含完整命令行的缓存的指针,该命令行包括执行文件的完整路径名。
4.1.4 进程的环境变量
每个进程都有一个与它相关的环境块。环境块是进程的地址空间中分配的一个内存块。每个环境块都包含一组字符串,其形式如下:
VarName1=VarValue1/0
/0
每个字符串的第一部分是环境变量的名字,后跟一个等号,等号后面是要赋予变量的值。环境块中的所有字符串都必须按环境变量名的字母顺序进行排序。变量中的空格是有意义的。必须将一个0字符置于所有环境变量的结尾处,以表示环境块的结束。
通常,子进程可以继承一组与父进程相同的环境变量。但是,父进程能够控制子进程继承什么环境变量。所谓继承,指的是子进程获得它自己的父进程的环境块拷贝,子进程与父进程并不共享相同的环境块。这意味着子进程能够添加、删除或修改它的环境块中的变量,而这个变化在父进程的环境块中却得不到反映。
应用程序通常使用环境变量来使用户能够调整它的行为特性。用户创建一个环境变量并对它进行初始化。然后,当用户启动应用程序运行时,该应用程序要查看环境块,找出该变量。如果找到了变量,它就分析变量的值,调整自己的行为特性。
使用 G e t E n v i r o n m e n tVa r i a b l e函数,就能够确定某个环境变量是否存在以及它的值:
E x p a n d E n v i r o n m e n t S t r i n g s函数:
使用S e t E n v i r o n m e n t Va r i a b l e函数来添加变量、删除变量或者修改变量的值:
4.1.5 进程的亲缘性
进程中的线程可以在主计算机中的任何一个 C P U上执行。但是一个进程的线程可能被强制在可用C P U的子集上运行。这称为进程的亲缘性,
4.1.7 进程的当前驱动器和目录
当不提供全路径名时,Wi n d o w s的各个函数就会在当前驱动器的当前目录中查找文件和目录。
系统将在内部保持对进程的当前驱动器和目录的跟踪。 由于该信息是按每个进程来维护的,因此改变当前驱动器或目录的进程中的线程,就可以为该进程中的所有线程改变这些信息。
线程能够GetCurrentDirectory获得和SetCurrentDirectory设置它的进程的当前驱动器和目录:
4.1.8 进程的当前目录
通过调用G e t F u l l P a t h N a m e,父进程可以获得它的当前目录:
4.1.9 系统版本
DWORD GetVersion()
BOOL GetVersionEx(POSVERSIONINFO pVersionInformation);
BOOL VerifyVersionInfo(POSVERSIONINFOEX pVersionInformation,DWORD dwTypeMask,DWORDLONG dwlConditionMask);
使用VER_SET_CONDITION宏VER_SET_CONDITION(DWORDLONG dwlConditionMask,ULONG dwTypeBitMask,ULONG dwConditionMask)

/*测试当前Windows 2000*/

4.2 CreateProcess函数

 当一个线程调用CreateProcess时,系统就会创建一个进程内核对象,其初始使用计数是 1。该进程内核对象不是进程本身,而是操作系统管理进程时使用的一个较小的数据结构。可以将进程内核对象视为由进程的统计信息组成的一个较小的数据结构。然后,系统为新进程创建一个虚拟地址空间,并将可执行文件或任何必要的D L L文件的代码和数据加载到该进程的地址空间中。
系统为新进程的主线程创建一个线程内核对象(其使用计数为 1) 。与进程内核对象一样,线程内核对象也是操作系统用来管理线程的小型数据结构。通过执行 C / C + +运行期启动代码,该主线程便开始运行,它最终调用WinMain、wWinMain、main或wmain函数。如果系统成功地创建了新进程和主线程,CreateProcess便返回TRUE。
4.2.1 pszApplicationName和pszCommandLine
pszApplicationName和pszCommandLine参数分别用于设定新进程将要使用的可执行文件的名字和传递给新进程的命令行字符串。
pszCommandLine参数的原型是PTSTR。这意味着CreateProcess期望你将传递一个非常量字符串的地址。因为如果命令行字符串不包含在文件映象的只读部分中,就会出现违规访问的问题。解决这个问题的最好办法是在调用CreateProcess之前将常量字符串拷贝到临时缓存中。
    当pszApplicationName填充NULL时,CreateProcess分析pszCommandLine字符串时,它将查看字符串中的第一个标记,并假设该标记是想运行的可执行文件的名字。如果可执行文件的文件名没有扩展名,便假设它的扩展名为.exe。CreateProcess也按下面的顺序搜索该可执行文件:
    1) 包含调用进程的.exe文件的目录。
    2) 调用进程的当前目录。
    3) Windows的系统目录。
    4) Windows目录。
    5) PATH环境变量中列出的目录。
    如果pszApplicationName不传递N U L L,可以将地址传递给pszApplicationName参数中包含想运行的可执行文件的名字的字符串。必须设定文件的扩展名,系统将不会自动假设文件名有一个.exe扩展名。CreateProcess假设该文件位于当前目录中,除非文件名前面有一个路径。如果在当前目录中找不到该文件,CreateProcess将不会在任何其他目录中查找该文件,它运行失败了。
    4.2.2 psaProcess/psaThread/binheritHandles
   若要创建一个新进程,系统必须创建一个进程内核对象和一个线程内核对象(用于进程的主线程) ,由于这些都是内核对象,因此父进程可以得到机会将安全属性与这两个对象关联起来。可以使用psaProcess和psaThread参数分别设定进程对象和线程对象需要的安全性。为NULL时,系统为这些对象赋予默认安全性描述符。也可以指定两个SECURITY_ATTRIBUTES结构,并对它们进行初始化,以便创建自己的安全性权限,并将它们赋予进程对象和线程对象。
    4.2.3 fdwCreate
   fdwCreate参数用于标识标志,以便用于规定如何来创建新进程。
    4.2.4 pvEnvironment
   pvEnvironment参数用于指向包含新进程将要使用的环境字符串的内存块。在大多数情况下,为该参数传递NULL,使子进程能够继承它的父进程正在使用的一组环境字符串。
   可以使用GetEnvironmentStrings和FreeEnvironmentStrings获得和释放环境字符串数据块。
    4.2.5 pszCurDir
    pszCurDir参数允许父进程设置子进程的当前驱动器和目录。如果本参数是 N U L L,则新进程的工作目录将与生成新进程的应用程序的目录相同。如果本参数不是 N U L L,那么pszCurDir必须指向包含需要的工作驱动器和工作目录的以 0结尾的字符串。注意,必须设定路径中的驱动器名。
    4.2.6 psiStartInfo
    psiStartInfo参数用于指向一个STARTUPINFO结构:
     当Wi n d o w s创建新进程时,它将使用该结构的有关成员。大多数应用程序将要求生成的应用程序仅仅使用默认值。至少应该将该结构中的所有成员初始化为零,然后将 c b成员设置为该结构的大小:
    STARTUPINFO si = {sizeof(si)};
    CreateProcess(...,&si,...);
    4.2.7 ppiProcInfo
   ppiProcInfo参数用于指向你必须指定的PROCESS_INFORMATION结构。CreateProcess在返回之前要对该结构的成员进行初始化。
    创建新进程可使系统建立一个进程内核对象和一个线程内核对象。在创建进程的时候,系统为每个对象赋予一个初始使用计数值1。然后,在CreateProcess返回之前,该函数
打开进程对象和线程对象,并将每个对象的与进程相关的句柄放入PROCESS_INFORMATION结构的hProcess和hThread成员中。当CreateProcess在内部打开这些对象时,每个对象的使用计
数就变为2。
    意味着在系统能够释放进程对象前,该进程必须终止运行(将使用计数递减为 1) ,并且父进程必须调用C l o s e H a n d l e(再将使用计数递减1,使之变为0) 。同样,若要释放线程对象,该线程必须终止运行,父进程必须关闭线程对象的句柄。
    必须关闭子进程和它的主线程的句柄,以避免在应用程序运行时泄漏资源。关闭句柄只是告诉系统,你对进程或线程的统计数据不感兴趣。进程或线程将继续运行,直到它自己终止运行。
    4.3 终止进程的运行
   若要终止进程的运行,可以使用下面四种方法:
   • 主线程的进入点函数返回(最好使用这个方法) 。
   • 进程中的一个线程调用ExitProcess函数(应该避免使用这种方法) 。
   • 另一个进程中的线程调用TerminateProcess函数(应该避免使用这种方法) 。
   • 进程中的所有线程自行终止运行(这种情况几乎从未发生) 。
   4.3.1 主线程的进入点函数返回
   始终都应该这样来设计应用程序,即只有当主线程的进入点函数返回时,它的进程才终止运行。这是保证所有线程资源能够得到正确清除的唯一办法。
让主线程的进入点函数返回,可以确保下列操作的实现:
   • 该线程创建的任何C + +对象将能使用它们的析构函数正确地撤消。
   • 操作系统将能正确地释放该线程的堆栈使用的内存。
   • 系统将进程的退出代码(在进程的内核对象中维护)设置为进入点函数的返回值。
   • 系统将进程内核对象的返回值递减1。
   4.3.2 ExitProcess函数
   当进程中的一个线程调用ExitProcess函数时,进程便终止运行
   VOID ExitProcess(UNIT fuExitCode);
   该函数用于终止进程的运行,并将进程的退出代码设置为fuExitCode。ExitProcess函数并不返回任何值,因为进程已经终止运行。如果在调用ExitProcess之后又增加了什么代码,那么该代码将永远不会运行。
   当主线程的进入点函数WinMain返回时,它将返回给C/C++运行期启动代码,它能正确地清除该进程使用的所有的C运行期资源。当C运行期资源被
释放之后,C运行期启动代码就显式调用ExitProcess,并将进入点函数返回的值传递给它。这解释了为什么只需要主线程的进入点函数返回,就能够终止整个进程的运行。请注意,进程中运行的任何其他线程都随着进程而一道终止运行。
   决不应该显式调用ExitProcess函数。
   4.3.3 TerminateProcess函数
   调用TerminateProcess函数也能够终止进程的运行:
   BOOL TerminateProcess(HANDLE hProcess,UINT fuExitCode);
   该函数与ExitProcess有一个很大的差别,那就是任何线程都可以调用TerminateProcess来终止另一个进程或它自己的进程的运行。 hProcess参数用于标识要终止运行的进程的句柄。当进程终止运行时,它的退出代码将成为你作为fuExitCode参数来传递的值。
   4.3.4 进程终止运行时出现的情况
  当进程终止运行时,下列操作将启动运行:
  1) 进程中剩余的所有线程全部终止运行。
  2) 进程指定的所有用户对象和G D I对象均被释放,所有内核对象均被关闭(如果没有其他进程打开它们的句柄,那么这些内核对象将被撤消。但是,如果其他进程打开了它们的句柄,内核对象将不会撤消) 。
  3) 进程的退出代码将从S T I L L _ A C T I V E改为传递给E x i t P r o c e s s或Te r m i n a t e P r o c e s s的代码。
  4) 进程内核对象的状态变成收到通知的状态。系统中的其他线程可以挂起,直到进程终止运行。
  5) 进程内核对象的使用计数递减1。
  4.4 子进程
  如果想创建新进程,让它进行一些操作,并且等待结果,可以使用类似下面的代码:
  PROCESS_INFORMATION pi;
  DWORD dwExitCode;
  //Spawn the child process
  BOOL fSuccess = CreateProcess(...,&pi);
  if(fSuccess)
  {
      //Close the thread handle as soon as it is no longer needed
      CloseHandle(pi.hThread);
      //Suspend our execution until the child has terminated
      WaitForSingleObject(pi.hProcess,INFINITE);
      //The Child Process terminated;get its exit code
      GetExitCodeProcess(pi.hProcess,&dwExitCode);
      //Close the process handle as soon as it is no longer needed
      CloseHandle(pi.hProcess);
  }

  运行独立的子进程
  大多数情况下,应用程序将另一个进程作为独立的进程来启动。这意味着进程创建和开始运行后,父进程并不需要与新进程进行通信,也不需要在完成它的工作后父进程才能继续运行。这就是Explorer的运行方式。当Explorer为用户创建一个新进程后,它并不关心该进程是否继续运行,也不在乎用户是否终止它的运行。
  若要放弃与子进程的所有联系,Explorer必须通过调用CloseHandle来关闭它与新进程及它的主线程之间的句柄。
  PROCESS_INFORMATION pi;
  //Spawn the child process
  BOOL fSuccess = CreateProcess(...,&pi);
  if(fSuccess)
  {
     //Allow the system to destroy the process & thread kernel objects as soon as the child process terminates
     CloseHandle(pi.hThread);
     CloseHandle(pi.hProcess);
  }
 
  

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【原 书 名】 Advanced Windows (3rd Ed) 【原出版社】 Microsoft Press 【作 者】(美)Jeffrey Richter 【译 者】 王书洪[同译者作品] 刘光明 【内容简介】 本书以详尽资料和大量的范例深入讨论了有关32位Windows编程高级问题,包括进程和线程的管理、Win 32的内存管理、消息处理、动态链接库、文件系统和设备输入输出、结构化异常处理、Unicode等,并重点讨论了Windows 95和Windows NT在实现上的不同之处,其中包含了大量的编程技巧。 本书可供高等院校计算机专业的师生和广大的计算机编程人员使用。 【人物简介】 Jeffrey RichterJeffrey Richter是一位在全球享有盛誉的技术作家,尤其在Windows/.NET领域有着杰出的贡献。他的第一本Windows著作Windows 3: A Developer's Guide大获好评,从而声名远扬。之后,他又推出了经典著作《Windows 高级编程指南》和《Windows核心编程》。如今这两本书早已成为Windows程序设计领域的颠峰之作,培育了几代软件开发设计人员。他的每一本新作问世,我们都有理由相信这是一本巨著,我们想要的一切尽在其中。Jeffery 是Wintellect公司的创始人之一,也是MSDN杂志.NET专栏的特邀编辑。现在他正领导开发该公司的.NET程序设计课程,向大众推广.NET技术。因为他自1999年开始就参与了微软.NET框架开发组的咨询工作,与这些一线人员一起经历了.NET的孕育与诞生,所以他对.NET思想的领悟、对.NET的细节熟稔,是其他任何作家难以企及的。他是.NET著作领域中当之无愧的一面旗帜。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值