第4章 进 程

    本章介绍系统如何管理所有正在运行的应用程序。首先讲述什么是进程,以及系统如何创建进程内核对象,以便管理每个进程。然后将说明如何使用相关的内核对象来对进程进行操作。接着,要介绍进程的各种不同的属性,以及查询和修改这些属性所用的若干个函数。还要讲述创建或生成系统中的辅助进程所用的函数。当然,如果不深入说明如何来结束进程的运行,那么这样的介绍肯定是不完整的。现在就来介绍进程的有关内容。进程通常被定义为一个正在运行的程序的实例,它由两个部分组成:

• 一个是操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计

信息的地方。

• 另一个是地址空间,它包含所有可执行模块或 D L L模块的代码和数据。它还包含动态内存分配的空间。如线程堆栈和堆分配空间。

    进程是不活泼的。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,该线程负责执行包含在进程的地址空间中的代码。实际上,单个进程可能包含若干个线程,所有这些线程都“同时”执行进程地址空间中的代码。为此,每个线程都有它自己的一组C P U寄存器和它自己的堆栈。每个进程至少拥有一个线程,来执行进程的地址空间中的代码。如果没有线程来执行进程的地址空间中的代码,那么进程就没有存在的理由了,系统就将自动撤消该进程和它的地址空间。若要使所有这些线程都能运行,操作系统就要为每个线程安排一定的 C P U时间。它通过以一种循环方式为线程提供时间片(称为量程)
,造成一种假象,仿佛所有线程都是

同时运行的一样。图4 - 1显示了在单个C P U的计算机上是如何实现这种运行方式的。如果计算机拥有多个 C P U,那么操作系统就要使用复杂得多的算法来实现 C P U上线程负载的平衡。
    当创建一个进程时,系统会自动创建它的第一个线程,称为主线程。然后,该线程可以创

建其他的线程,而这些线程又能创建更多的线程。

    Wi n d o w s 2 0 0 0 Micorsoft Windows 2000能够在拥有多个C P U的计算机上运行。 例如,我用来撰写本书的计算机就包含两个处理器。Windows 2000可以在每个C P U上运行不同的线程,这样,多个线程就真的在同时运行了。Windows 2000的内核能够在这种类型的系统上进行所有线程的管理和调度。不必在代码中进行任何特定的设置就能利用多处理器提供的各种优点。

    Windows 98 Windows 98只能在单处理器计算机上运行。即使计算机配有多个处理器,Wi n d o w s每次只能安排一个线程运行,而其他的处理器则处于空闲状态。

4.1 编写第一个Wi n d o w s应用程序

Wi n d o w s支持两种类型的应用程序。一种是基于图形用户界面( G U I)的应用程序,另一种是基于控制台用户界面(C U I)的应用程序。基于G U I的应用程序有一个图形前端程序。它能创建窗口,拥有菜单,可以通过对话框与用户打交道,并可使用所有的标准“ Wi n d o w s”组件。Wi n d o w s配备的所有应用程序附件(如N o t e p a d、C a l c u l a t o r和Wo r d P a d) ,几乎都是基于G
U I的应用程序。基于控制台的应用程序属于文本操作的应用程序。它们通常不能用于创建窗口或处理消息,并且它们不需要图形用户界面。虽然基于 C U I的应用程序包含在屏幕上的窗口中,但是窗口只包含文本。命令外壳程序 C M D . E X E(用于Windows 2000)和COMMAND.COM (用于Windows 98)都是典型的基于C U I的应用程序。

    这两种类型的应用程序之间的界限是非常模糊的。可以创建用于显示对话框的 C U I应用程序。例如,命令外壳程序可能拥有一个特殊的命令,使它能够显示一个图形对话框,在这个对话框中,可以选定你要执行的命令,而不必记住该外壳程序支持的各个不同的命令。也可以创建一个基于G U I的应用程序,它能将文本字符串输出到一个控制台窗口。我常常创建用于建立控制台窗口的G U I应用程序,在这个窗口中,我可以查看应用程序执行时的调试信息。当然你也可以在应用程序中使用图形用户界面,而不是老式的字符界面,因为字符界面使用起来不太方便。

    当使用Microsoft Visual C++来创建应用程序时,这种集成式环境安装了许多不同的链接程序开关,这样,链接程序就可以将相应的子系统嵌入产生的可执行程序。用于 C U I应用程序的链接程序开关是 / S U B S Y S T E M : C O N D O L E,而用于 G U I应用程序的链接程序开关是S U B S Y S T E M : W I N D O W S。当用户运行一个应用程序时,操作系统的加载程序就会查看可执行图形程序的标题,并抓取该子系统的值。如果该值指明一个 C U
I应用程序,那么加载程序就会自动保证为该应用程序创建文本控制台窗口。

    如果该值指明这是个G U I应用程序,那么加载程序不创建控制台窗口,而只是加载应用程序。一旦应用程序启动运行,操作系统就不再考虑应用程序拥有什么类型的用户界面。Wi n d o w s应用程序必须拥有一个在应用程序启动运行时调用的进入点函数。可以使用的进入点函数有4个:



    操作系统实际上并不调用你编写的进入点函数。它调用的是 C / C + +运行期启动函数。该函数负责对C / C + +运行期库进行初始化,这样,就可以调用m a l l o c和f r e e之类的函数。它还能够确保已经声明的任何全局对象和静态C + +对象能够在代码执行以前正确地创建。下面说明源代码中可以实现哪个进入点以及何时使用该进入点(见表4 - 1 )。

    链接程序负责在它连接可执行文件时选择相应的 C / C + +运行期启动函数。如果设定了/ S U B S Y S T E M : W I N D O W S链接程序开关,那么该链接程序期望找到一个 Wi n M a i n或w Wi n m a i n函数。如果这两个函数都不存在,链接程序便返回一个“未转换的外部符号”的错误消息。否则,它可以分别选择Wi n M a i n C RT S t a r t u p函数或w Wi n M
a i n C RT S t a r t u p函数。

 同样,如果设定了/ S U B S Y S T E M : C O N S O L E链接程序开关,那么该链接程序便期望找到m a i n或w m a i n函数,并且可以分别选择 m a i n C RT S t a r t u p函数或w m a i n C RT S t a r t u p函数。
    同样,如果m a i n或w m a i n都不存在,那么链接程序返回一条“未转换外部符号”的消息。
    但是,人们很少知道这样一个情况,即可以从应用程序中全部删除 / S U B S Y S T E M链接程序开关。当这样做的时候,链接程序能够自动确定应用程序应该连接到哪个子系统。当进行链接时,链接程序要查看代码中存在 4个函数(Wi n M a i n、w Wi n M a i n、m a i n或w m a i n)中的哪一个。然后确定可执行程序应该是哪一个子系统,并且确定可执行程序中应该嵌入哪个 C / C + +启动函数。
    Wi n d o w s / Visual C++编程新手常犯的错误之一是,当创建新的应用程序时,不小心选择了错误的应用程序类型。例如,编程员可能创建一个新的 Wi n 3 2应用程序项目,但是创建了一个进入点函数m a i n。当创建应用程序时,编程员会看到一个链接程序错误消息,因为 w i n 3 2应用程序项目设置了/ S U B S Y S T E M : W I N D O W S链接程序开关,但是不存在Wi n M a i
n或w Wi n M a i n函数。这时,编程员可以有4个选择:

    • 将m a i n函数改为Wi n M a i n。通常这不是最佳的选择,因为编程员可能想要创建一个控制台应用程序。

    • 用Visual C++创建一个新的Win32 控制台应用程序,并将现有的源代码添加给新应用程序项目。这个选项冗长而乏味,因为它好像是从头开始创建应用程序,而且必须删除原始的应用程序文件。

    • 单击Project Settings对话框的 L i n k选项卡,将 / S U B S Y S T E M : W I N D O W S开关改为/ S U B S Y S T E M : C O N S O L E。这是解决问题的一种比较容易的方法,很少有人知道他们只需要进行这项操作就行了。

    • 单击Project Settings对话框的L i n k选项卡,然后全部删除/ S U B S Y S T E M : W I N D O W S开关。这是我喜欢选择的方法,因为它提供了最大的灵活性。现在,连接程序将根据源代码中实现的函数进行正确的操作。

    当用Visual C++的Developer Studio创建新Wi n 3 2应用程序或Wi n 3 2控制台应用程序项目时,我不知道为什么这没有成为默认设置。
    所有的C / C + +运行期启动函数的作用基本上都是相同的。它们的差别在于,它们究竟是处理A N S I字符串还是U n i c o d e字符串,以及它们在对C运行期库进行初始化后它们调用哪个进入点函数。Visual C++配有C运行期库的源代码。可以在CR t0.c文件中找到这4个启动函数的代码。现在将启动函数的功能归纳如下:
    • 检索指向新进程的完整命令行的指针。

    • 检索指向新进程的环境变量的指针。

    • 对C / C + +运行期的全局变量进行初始化。如果包含了 S t d L i b . h文件,代码就能访问这些变量。表4 - 1列出了这些变量。

    • 对C运行期内存单元分配函数(m a l l o c和c a l l o c)和其他低层输入/输出例程使用的内存栈进行初始化。

    • 为所有全局和静态C + +类对象调用构造函数。当所有这些初始化操作完成后,C / C + +启动函数就调用应用程序的进入点函数。如果编写了一个w Wi n M a i n函数,它将以下面的形式被调用:

    当进入点函数返回时,启动函数便调用 C运行期的e x i t函数,将返回值(n M a i n R e t Va l)传递给它。E x i t函数负责下面的操作:

    • 调用由_ o n e x i t函数的调用而注册的任何函数。

    • 为所有全局的和静态的C + +类对象调用析构函数。

    • 调用操作系统的E x i t P r o c e s s函数,将n M a i n R e t Va l传递给它。这使得该操作系统能够撤消进程并设置它的e x i t代码。
    表4 - 2显示了程序能够使用的C / C + +运行期全局变量。

4.1.1 进程的实例句柄

加载到进程地址空间的每个可执行文件或 D L L文件均被赋予一个独一无二的实例句柄。可

执行文件的实例作为( w ) Wi n M a i n的第一个参数h i n s t E x e来传递。对于加载资源的函数调用来说,

通常都需要该句柄的值。例如,若要从可执行文件的映象来加载图标资源,需要调用下面这个

函数:


    L o a d I c o n的第一个参数用于指明哪个文件(可执行文件或D L L文件)包含你想加载的资源。许多应用程序在全局变量中保存( w ) Wi n M a i n的h i n s t E x e参数,这样,它就很容易被所有可执行文件的代码访问。

    Platform SDK文档中说,有些函数需要H M O D U L E类型的一个参数。它的例子是下面所示

的G e t M o d u l e F i l e N a m e函数:

    注意 实际情况说明,H M O D U L E与H I N S TA N C E是完全相同的对象。如果函数的文档指明需要一个H M O D U L E,那么可以传递一个H I N S TA N C E,反过来,如果需要一个H I N S TA N C E,也可以传递一个H M O D U L E。之所以存在两个数据类型,原因是在1 6位Wi n d o w s中,H M O D U L E和H I N S TA N C E用于标识不同的东西。
    ( w ) Wi n M a i n的h i n s t E x e参数的实际值是系统将可执行文件的映象加载到进程的地址空间时使用的基本地址空间。例如,如果系统打开了可执行文件并且将它的内容加载到地址0 x 0 0 4 0 0 0 0 0中,那么( w ) Wi n M a i n的h i n s t E x e参数的值就是0 x 0 0 4 0 0 0 0 0。
    可执行文件的映像加载到的基地址是由链接程序决定的。不同的链接程序可以使用不同的默认基地址。Visual C++链接程序使用的默认基地址是0 x 0 0 4 0 0 0 0 0,因为这是在运行Wi n d o w s9 8时可执行文件的映象可以加载到的最低地址。可以改变应用程序加载到的基地址,方法是使用M i c r o s o f t的链接程序中的/ B A S E : a d d r e s s链接程序开关。

    如果你想在Wi n d o w s上加载的可执行文件的基地址小于0 x 0 0 4 0 0 0 0 0,那么Windows 98加载程序必须将可执行文件重新加载到另一个地址。这会增加加载应用程序所需的时间,不过,这样一来,至少该应用程序能够运行。如果开发的应用程序将要同时在 Windows 98和Wi n d o w s2 0 0 0上运行,应该确保应用程序的基地址是0 x 0 0 4 0 0 0 0 0或者大于这个地址。

    下面的G e t M o d u l e H a n d l e函数返回可执行文件或D L L文件加载到进程的地址空间时所用的句柄/基地址:
    
    当调用该函数时,你传递一个以 0结尾的字符串,用于设定加载到调用进程的地址空间的可执行文件或 D L L文件的名字。如果系统找到了指定的可执行文件或 D L L文件名,G e t M o d u l e H a n d l e便返回该可执行文件或D L L文件映象加载到的基地址。如果系统没有找到该文件,则返回 N U L L。也可以调用 G e t M o d u l e H a n d l e,为 p s z M o d u l e参数传递
N U L L,G e t M o d u l e H a n d l e返回调用的可执行文件的基地址。这正是 C运行期启动代码调用 ( w ) Wi n M a i n函数时该代码执行的操作。
    请记住G e t M o d u l e H a n d l e函数的两个重要特性。首先,它只查看调用进程的地址空间。如果调用进程不使用常用的对话框函数,那么调用G e t M o d u l e H a n d l e并为它传递“C o m D l g 3 2”后,就会返回 N U L L,尽管 C o m D l g 3 2 . d l l可能加载到了其他进程的地址空间。第二,调用G e t M o d u l e H a n d
l e并传递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中,并且保留了( w ) Wi n M a i n的一个参数,目的仅仅是为了能够容易地转用1 6位Wi n d o w s应用程序。决不应该在代码中引用该参数。由于这个原因,我总是像下面这样编写( w ) Wi n M a i n函数:



4.1.3 进程的命令行

当一个新进程创建时,它要传递一个命令行。该命令行几乎永远不会是空的,至少用于创建新进程的可执行文件的名字是命令行上的第一个标记。但是在后面介绍 C r e a t e P r o c e s s函数时我们将会看到,进程能够接收由单个字符组成的命令行,即字符串结尾处的零。当 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字符串。但是,如果将Wi n M a i n改为w Wi n M a i n,就能够访问进程的U n i c o d e版本命令行。

    应用程序可以按照它选择的方法来分析和转换命令行字符串。实际上可以写入 p s z C m d L i n e参数指向的内存缓存,但是在任何情况下都不应该写到缓存的外面去。我总是将它视为只读缓存。如果我想修改命令行,首先我要将命令行拷贝到应用程序的本地缓存中,然后再修改本地缓存。

    也可以获得一个指向进程的完整命令行的指针,方法是调用G e t C o m m a n d L i n e函数:

    PTSTR GetCommandLine();
    该函数返回一个指向包含完整命令行的缓存的指针,该命令行包括执行文件的完整路径名。

    许多应用程序常常拥有转换成它的各个标记的命令行。使用全局性 _ _ a rg c(或_ _ w a rg v)变量,应用程序就能访问命令行的各个组成部分。下面这个函数 C o m m a n d L i n e To A rg v W将U n i c o d e字符串分割成它的各个标记:
    PWSTR CommandLineToArgvW(PWSTR pszCmdLine, int * pNumArgs);
    正如该函数名的结尾处的 W所暗示的那样,该函数只存在于 U n i c o d e版本中(W是英文单词‘Wi d e’的缩写) 。第一个参数p s z C m d L i n e指向一个命令行字符串。这通常是较早时调用G e t C o m m a n d L i n e W而返回的值。P N u m A rg s参数是个整数地址,该整数被设置为命令行中的参数的数目。
    C o m m a n d L i n e To A rg v W将地址返回给一个U n i c o d e字符串指针的数组。C o m m a n e L i n e To A rg v W负责在内部分配内存。大多数应用程序不释放该内存,它们在进程运行终止时依靠操作系统来释放内存。这是完全可行的。但是如果想要自己来释放内存,正确的方法是像下面这样调用H e a p F r e e函数:

4.1.4 进程的环境变量

每个进程都有一个与它相关的环境块。环境块是进程的地址空间中分配的一个内存块。每

个环境块都包含一组字符串,其形式如下:


    每个字符串的第一部分是环境变量的名字,后跟一个等号,等号后面是要赋予变量的值。环境块中的所有字符串都必须按环境变量名的字母顺序进行排序。
    由于等号用于将变量名与变量的值分开,因此等号不能是变量名的一部分。另外,变量中的空格是有意义的。例如,如果声明下面两个变量,然后将 X Y Z的值与A B C的值进行比较,那么系统将报告称,这两个变量是不同的,因为紧靠着等号的前面或后面的任何空格均作为比较时的条件被考虑在内。
XYZ = Windows (Notice the space after the equal sign.)
ABC = Windows
例如,如果将下面两个字符串添加给环境块,后面带有空格的环境变量 X Y Z包含H o m e,而没有空格的环境变量X Y Z则包含Wo r k。
    XYZ = Home(Notice the sapce before the equal sign.)
    XYZ = Word
    最后,必须将一个0字符置于所有环境变量的结尾处,以表示环境块的结束。
    Wi n d o w s 9 8 若要为 Windows 98创建一组初始环境变量,必须修改系统的A u t o E x e c . b a t文件,将一系列S E T行放入该文件。每个S E T行都必须采用下面的形式:
    SET VarName = VarValue
    当重新引导系统时,A u t o E x e c . b a t文件的内容被分析,设置的任何环境变量均可供在Windows 98会话期间启动的任何进程使用。

    Windows 2000  当用户登录到Windows 2000中时,系统创建一个外壳进程并将一组环境字符串与它相关联。通过查看注册表中的两个关键字,系统可以获得一组初始环境字符串。

    第一个关键字包含一个适用于系统的所有环境变量的列表:

    第二个关键字包含适用于当前登录的用户的所有环境变量的列表:
    HKEY_CURRENT_USER\Envirment
    用户可以对这些项目进行增加、删除或修改,方法是选定控制面板的S y s t e m小应用程序,单击A d v a n c e d选项卡,再单击Environment Va r i a b l e s按钮,打开图4 - 2所示的对话框:


    只有拥有管理员权限的用户才能修改系统变量列表中的变量。

应用程序也可以使用各种注册表函数来修改这些注册表项目。但是,若要使这些

修改在所有应用程序中生效,用户必须退出系统,然后再次登录。有些应用程序,如

E x p l o r e r、 Task Manager和
Control Panel等 , 在 它 们 的 主 窗 口 收 到
W M _

S E T T I N G C H A N G E消息时,用新注册表项目来更新它们的环境块。例如,如果要更新

注册表项目,并且想让有关的应用程序更新它们的环境块,可以调用下面的代码:

SendMessage(HWND_BROADCAST ,WM_SETTINGCHANGE ,0 ,(LPARAM)TEXT(“Environment”));

通常,子进程可以继承一组与父进程相同的环境变量。但是,父进程能够控制子进程继承什么环境变量,后面介绍C r e a t e P r o c e s s函数时就会看到这个情况。所谓继承,指的是子进程获得它自己的父进程的环境块拷贝,子进程与父进程并不共享相同的环境块。这意味着子进程能够添加、删除或修改它的环境块中的变量,而这个变化在父进程的环境块中却得不到反映。

应用程序通常使用环境变量来使用户能够调整它的行为特性。用户创建一个环境变量并对它进行初始化。然后,当用户启动应用程序运行时,该应用程序要查看环境块,找出该变量。如果找到了变量,它就分析变量的值,调整自己的行为特性。

环境变量存在的问题是,用户难以设置或理解这些变量。用户必须正确地拼写变量的名字,而且必须知道变量值期望的准确句法。另一方面,大多数图形应用程序允许用户使用对话框来调整应用程序的行为特性。这种方法对用户来说更加友好。

如果仍然想要使用环境变量,那么有几个函数可供应用程序调用。使用 G e t E n v i r o n m e n tVa r i a b l e函数,就能够确定某个环境变量是否存在以及它的值:

DWORD GetEnvironmentVariableW(

_In_opt_ LPCWSTR lpName,

_Out_writes_to_opt_(nSize, return + 1) LPWSTR lpBuffer,

_In_ DWORD nSize

);

TCHAR tcEnviromentVar[MAX_PATH] = {0};

GetEnvironmentVariable(_TEXT("TEMP") ,tcEnviromentVar ,MAX_PATH);

当调用G e t E n v i r o n m e n t Va r i a b l e时,p s z N a m e指向需要的变量名,p
s z Va l u e指向用于存放变量值的缓存,c c h Va l u e用于指明缓存的大小(用字符数来表示)。该函数可以返回拷贝到缓存的字符数,如果在环境中找不到该变量名,也可以返回
0。

许多字符串包含了里面可取代的字符串。例如,我在注册表中的某个地方找到了下面的字符串:

%USERPROFILE%\My Documents

百分数符号之间的部分表示一个可取代的字符串。在这个例子中,环境变量的值

USERPROFILE应该被放入该字符串中。

由于这种类型的字符串替换是很常用的,因此Wi n d o w s提供了E x p a n d E n v i r o n m e n t S t r i n g s函数:

DWORD ExpandEnvironmentStringsW(

_In_ LPCWSTR lpSrc,

_Out_writes_to_opt_(nSize, return) LPWSTR lpDst,

_In_ DWORD nSize

);

TCHAR tcFullEnviromentVar[MAX_PATH] = {0};

ExpandEnvironmentStrings(_TEXT("%TEMP%\\A") ,tcFullEnviromentVar ,MAX_PATH);

当调用该函数时,p s z S r c参数是包含可替换的环境变量字符串的这个字符串的地址。p s z D s t参数是接收已展开字符串的缓存的地址,n
S i z e参数是该缓存的最大值(用字符数来表示)。

最后,可以使用S e t E n v i r o n m e n t Va r i a b l e函数来添加变量、删除变量或者修改变量的值:

BOOL SetEnvironmentVariableW(

_In_ LPCWSTR lpName,

_In_opt_ LPCWSTR lpValue

);

SetEnvironmentVariable(_TEXT("TTT") ,_TEXT("C:"));

该函数用于将p s z N a m e参数标识的变量设置为p s z Va l u e参数标识的值。如果带有指定名字的变量已经存在,S e t
E n v i r o n m e n t Va r i a b l e就修改该值。如果指定的变量不存在,便添加该变量,如果p s z Va l u e是N U L L,便从环境块中删除该变量。

应该始终使用这些函数来操作进程的环境块。前面讲过,环境块中的字符串必须按变量名的字母顺序来存放,这样, S e t E n v i r o n m e n t Va r i a b l e就会很容易地找到它们。
S e t E n v i r o n m e n tVa r i a b l e函数具有足够的智能,使环境变量保持有序排列。

4.1.5 进程的亲缘性

一般来说,进程中的线程可以在主计算机中的任何一个 C P U上执行。但是一个进程的线程可能被强制在可用C P U的子集上运行。这称为进程的亲缘性,将在第
7章详细介绍。子进程继承了父进程的亲缘性。

4.1.6 进程的错误模式

与每个进程相关联的是一组标志,用于告诉系统,进程对严重的错误应该如何作出反映,

这包括磁盘介质故障、未处理的异常情况、文件查找失败和数据没有对齐等。进程可以告诉系统如何处理每一种错误。方法是调用S e t E r r o r M o d e函数:

UINT  SetErrorMode(UINT fuErrorMode);

f u E r r o r M o d e参数是下表的任何标志按位用O R连接在一起的组合。

默认情况下,子进程继承父进程的错误模式标志。换句话说,如果一个进程的

S E M _ N O G P FA U LT E R R O R B O X标志已经打开,并且生成了一个子进程,该子进程也拥有这个打开的标志。但是,子进程并没有得到这一情况的通知,它可能尚未编写以便处理
G P故障的错误。如果G P故障发生在子进程的某个线程中,该子进程就会终止运行,而不通知用户。父进 程 可 以 防 止 子 进 程 继 承 它 的 错 误 模 式 , 方 法 是 在 调 用
C r e a t e P r o c e s s 时 设 定C R E AT E _ D E FA U LT _ E R R O R _ M O D E标志(本章后面部分的内容将要介绍C
r e a t e P r o c e s s函数) 。

4.1.7 进程的当前驱动器和目录

当不提供全路径名时,Wi n d o w s的各个函数就会在当前驱动器的当前目录中查找文件和目录。例如,如果进程中的一个线程调用
C r e a t e F i l e来打开一个文件(不设定全路径名) ,那么系统就会在当前驱动器和目录中查找该文件。

系统将在内部保持对进程的当前驱动器和目录的跟踪。
由于该信息是按每个进程来维护的,因此改变当前驱动器或目录的进程中的线程,就可以为该进程中的所有线程改变这些信息。

通过调用下面两个函数,线程能够获得和设置它的进程的当前驱动器和目录:

DWORD GetCurrentDirectoryW(

_In_ DWORD nBufferLength,

_Out_writes_to_opt_(nBufferLength, return + 1) LPWSTR lpBuffer

);

TCHAR tcLocalAppPath[MAX_PATH] = {0};

GetCurrentDirectory(MAX_PATH ,tcLocalAppPath);

BOOL SetCurrentDirectoryW(

_In_ LPCWSTR lpPathName

);

SetCurrentDirectory(_TEXT("G:\\inetpub"));

4.1.8 进程的当前目录

系统将对进程的当前驱动器和目录保持跟踪,但是它不跟踪每个驱动器的当前目录。不过,有些操作系统支持对多个驱动器的当前目录的处理。这种支持是通过进程的环境字符串来提供的。例如,进程能够拥有下面所示的两个环境变量:

=C:=C:\Utility\Bin

=D:=D:\Program FIles

这些变量表示驱动器C的进程的当前目录是\ U t i l i t y \ B i n,并且指明驱动器D的进程的当前目录是\Program
Files。

如果调用一个函数,传递一个驱动器全限定名,以表示一个驱动器不是当前驱动器,那么系统就会查看进程的环境块,找出与指定驱动器名相关的变量。如果该驱动器的变量存在,系统将该变量的值用作当前驱动器。如果该变量不存在,系统将假设指定驱动器的当前目录是它的根目录。

例如,如果进程的当前目录是 C : \ U t i l i t y | B i n,并且你调用C r e a t e F i l e来打开D
: R e a d M e . T x t,那么系统查看环境变量 = D。因为= D变量存在,因此系统试图从
D:\Program Files目录打开该R e a d M e . T x t文件。如果= D变量不存在,系统将试图从驱动器
D的根目录来打开
R e a d M e . T x t。Wi n d o w s的文件函数决不会添加或修改驱动器名的环境变量,它们只是读取这些变量。

注意 可以使用C运行期函数_ c h d i r,而不是使用Wi n
d o w s的S e t C u r r e n t D i r e c t o r y函数来变更当前目录。_ c h d i r函数从内部调用S
e t C u r r e n t D i r e c t o r y,但是_chdir
也能够添加或修改该环境变量,这样,不同驱动器的当前目录就可以保留。

如果父进程创建了一个它想传递给子进程的环境块,子进程的环境块不会自动继承父进程的当前目录。相反,子进程的当前目录将默认为每个驱动器的根目录。如果想要让子进程继承父进程的当前目录,该父进程必须创建这些驱动器名的环境变量。并在生成子进程前将它们添加给环境块。通过调用G e t F u l l P a t h N a m e,父进程可以获得它的当前目录:

TCHAR szCurDir[MAX_PATH] = {0};

GetFullPathName(_TEXT("C:") ,MAX_PATH ,szCurDir ,NULL);

记住,进程的环境变量必须始终按字母顺序来排序。因此驱动器名的环境变量通常必须置于环境块的开始处。

4.1.9 系统版本

应用程序常常需要确定用户运行的是哪个 Wi n d o w s版本。例如,通过调用安全性函数,应用程序就能利用它的安全特性。但是这些函数只有在Windows 2000上才能得到全面的实现。Windows
API拥有下面的G e t Ve r s i o n函数:

DWORD GetVersion();

该函数已经有相当长的历史了。最初它是为 1 6位Wi n d o w s设计的。它的作用很简单,在高位字中返回M S - D O S版本号,在低位字中返回Wi
n d o w s版本号。对于每个字来说,高位字节代表主要版本号,低位字节代表次要版本号。

但是,编写该代码的程序员犯了一个小小的错误,函数的编码结果使得 Wi n d o w s的版本号颠倒了,即主要版本号位于低位字节,而次要版本号位于高位字节。由于许多程序员已经开始使用该函数,M i c r o s o f t不得不保持函数的原样,并修改了文档,以说明这个错误。

由于围绕着 G e t Ve r s i o n函数存在着各种混乱,因此
M i c r o s o f t增加了一个新函数G e t Ve r s i o n E x :

OSVERSIONINFO osvi;

ZeroMemory(&osvi ,sizeof(OSVERSIONINFO));

osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);

GetVersionExW(&osvi);

O S V E R S I O N I N F O E X结构在Windows 2000中是个新结构。Wi n d o w s的其他版本使用较老的O
S V E R S I O N I N F O结构,它没有服务程序包、程序组屏蔽、产品类型和保留成员。

注意,对于系统的版本号中的每个成分来说,该结构拥有不同的成员。这样做的目的是,

程序员不必提取低位字、高位字、低位字节和高位字节,因此应用程序能够更加容易地对它们期望的版本号与主机系统的版本号进行比较。下表描述了O S V E R S I O N I N F O E X结构的成员。

为了使操作更加容易,Windows 2000提供了一个新的函数,即Ve r i f y Ve r s i o n I n f o,用于对主机系统的版本与你的应用程序需要的版本进行比较:

BOOL VerifyVersionInfoW(

_Inout_ LPOSVERSIONINFOEXW lpVersionInformation,

_In_    DWORD dwTypeMask,

_In_    DWORDLONG dwlConditionMask

);

若要使用该函数,必须指定一个O S V E R S I O N I N F O E X结构,将它的d w O S Ve r s i o n I n f o S i z e成员初始化为该结构的大小,然后对该结构中的其他成员(这些成员对你的应用程序来说很重要)进行初始化。当调用Ve
r i f y Ve r s i o n I n f o时,d w Ty p e M a s k参数用于指明该结构的哪些成员已经进行了初始化。
d w Ty p e M a s k参数是用
O R连接在一起的下列标志中的任何一个标志:V E R _ M I N O RV E R S I O N,V E R _ M
A J O RV E R S I O N,V E R _ B U I L D N U M B E R,V E R _ P L AT F O R M I D,VER_
SERV I C E PA C K M I N O R, V E R _ S E RV I C E PA C K M A J O R,
V E R _ S U I T E N A M E,VER_PRODUCT_ TYPE。最后一个参数d w l C o n
d i t i o n M a s k是个6 4位值,用于控制该函数如何将系统的版本信息与需要的信息进行比较。

d w l C o n d i t i o n M a s k描述了如何使用一组复杂的位组合进行的比较。若要创建需要的位组合,可以使用V E R _ S E T _ C O N D I T I O N宏:

VER_SET_CONDITION(

DWORD dwlConditionMask,

ULONG dwTypeBitMask,

ULONG dwConditionMask)

第一个参数d w l C o n d i t i o n M a s k用于标识一个变量,该变量的位是要操作的那些位。请注意,不必传递该变量的地址,因为
V E R _ S E T _ C O N D I T I O N是个宏,不是一个函数。d w Ty p e B i t M a s k参数用于指明想要比较的O
S V E R S I O N I N F O E X结构中的单个成员。若要比较多个成员,必须多次调用
V E R _ S E T _ C O N D I T I O N宏,每个成员都要调用一次。传递给Ve r i f y Ve r s i o n I n f o的d
w Ty p e M a s k参数(V E R _ M I N O RV E R S I O N,V E R _ B U I L D N U M B E R等)的标志与用于V
E R _ S E T _ C O N D I T I O N的d w Ty p e B i t M a s k参数的标志是相同的。

V E R _ S E T _ C O N D I T I O N的最后一个参数d w C o n d i t i o n M a s k用于指明想如何进行比较。它可以是下列值之一:V E R _ E Q U A
L,V E R _ G R E AT E R,V E R _ G R E AT E R _ E Q U A L,V
E R _ L E S S或V E R _ L E S S _ E Q U A L。请注意,当比较V E R _ P R O D U C T _ T Y P E信息时,可以使用这些值。例如,V
E R _ N T _ W O R K S TAT I O N小于V E R _ N T _ S E RV E R。但是对于V E R _ S U I T E N A M E信息来说,不能使用这些测试值。相反,必须使用
V E R _ A N D(所有程序组都必须安装)或
V E R _ O R(至少必须安装程序组产品中的一个产品) 。

当建立一组条件后,可以调用 Ve r i f y Ve r s i o n I n f o函数,如果调用成功(如果主机系统符合应用程序的所有要求) ,则返回非零值。如果Ve r i f y Ve r s i o n I n f o返回0,那么主机系统不符合要求,或者表示对该函数的调用不正确。通过调用
G e t L a s t E r r o r函数,就能确定该函数为什么返回0。如果G e t L a s t E r r
o r返回E R R O R _ O L D _ W I N _ V E R S I O N,那么对该函数的调用是正确的,但是系统没有满足要求。

下面是如何测试主机系统是否正是Windows 2000的一个例子:



Windows核心编程 第四章 进程(上)的更多相关文章

  1. Windows核心编程 第四章 进程(下)

    4.3 终止进程的运行 若要终止进程的运行,可以使用下面四种方法: • 主线程的进入点函数返回(最好使用这个方法) . • 进程中的一个线程调用E x i t P r o c e s s函数(应该避免 ...

  2. windows核心编程---第四章 进程

    上一章介绍了内核对象,这一节开始就要不断接触各种内核对象了.首先要给大家介绍的是进程内核对象.进程大家都不陌生,它是资源和分配的基本单位,而进程内核对象就是与进程相关联的一个数据结构.操作系统内核通过 ...

  3. Windows核心编程 第四章 进程(中)

    4.2 CreateProcess函数 可以用C r e a t e P r o c e s s函数创建一个进程: BOOL CreateProcessW( _In_opt_ LPCWSTR lpAp ...

  4. windows核心编程 第5章job lab示例程序 解决小技巧

    看到windows核心编程 第5章的最后一节,发现job lab例子程序不能在我的系统(win8下)正常运行,总是提示“进程在一个作业里”         用process explorer程序查看 ...

  5. windows核心编程 第8章201页旋转锁的代码在新版Visual Studio运行问题

    // 全局变量,用于指示共享的资源是否在使用 BOOL g_fResourceInUse = FALSE; void Func1() { //等待访问资源 while(InterlockedExcha ...

  6. Windows核心编程 第七章 线程的调度、优先级和亲缘性(上)

    第7章 线程的调度.优先级和亲缘性 抢占式操作系统必须使用某种算法来确定哪些线程应该在何时调度和运行多长时间.本章将要介绍Microsoft Windows 98和Windows 2000使用的一些算 ...

  7. [转]Windows Shell 编程 第四章 【来源 http://blog.csdn.net/wangqiulin123456/article/details/7987933】

    第四章 文件的本质 以前,所有文件和目录都有一个确定的属性集:时间,日期,尺寸,以及表示‘只读的’,‘隐藏的,‘存档的’,或‘系统的’状态标志.然而,Windos95(及后来的WindowsNT4.0 ...

  8. windows核心编程---第六章 线程的调度

    每个线程都有一个CONTEXT结构,保存在线程内核对象中.大约每隔20ms windows就会查看所有当前存在的线程内核对象.并在可调度的线程内核对象中选择一个,将其保存在CONTEXT结构的值载入c ...

  9. Windows核心编程 第七章 线程的调度、优先级和亲缘性(下)

    7.6 运用结构环境 现在应该懂得环境结构在线程调度中所起的重要作用了.环境结构使得系统能够记住线程的状态,这样,当下次线程拥有可以运行的C P U时,它就能够找到它上次中断运行的地方. 知道这样低层 ...

随机推荐

  1. IntelliJ-IDEA 打包代码报错

    一.问题由来 使用 IntelliJ-IDEA 打包项目一直以来都没问题,可是上周的时候,突然打包就报错了,并且Maven中的pom.xml文件确定是没有改过,打包的配置文件也没有修改过. 报错信息如 ...

  2. Python2021哔哩哔哩视频爬取

    一.找到想要爬取的视频,进入网页源代码 在网页源代码里面可以很容易的找到视频各种清晰度的源地址 二.对地址发送请求 如果对视频源地址发送get请求会返回403 通过按F12进入开发者工具分析 发现并不 ...

  3. Android R 新特性分析及适配指南

    Android R(Android 11 API 30)于2020年9月9日正式发布,随国内各终端厂商在售Android设备的版本更新升级,应用软件对Android R 版本的兼容适配已迫在眉睫. 对 ...

  4. BuaacodingT651 我知道你不知道圣诞节做什么 题解(逻辑)

    题目链接 我知道你不知道圣诞节做什么 解题思路 第一句话:x,y不都为质数. 第二句话:对于xy=t,存在唯一一种x+y使得x,y不都为质数. 第三句话:对于x+y=s,存在唯一一种t=xy使得对于任 ...

  5. 微信小程序应用开发-手动创建

    基础知识: index.wxml的代码为 Html,有很多标签,如等 index.wwss相当于css 即样式 index.js中有很多函数,可自定义 操作步骤: 删除app.json文件中page/ ...

  6. 【linux】驱动-4-LED芯片手册分析

    目录 前言 4. LED芯片手册分析 4.1 内存管理单元MMU 4.1.1 MMU的功能 4.1.2 TLB的作用 4.2 地址转换函数 4.2.1 ioremap函数 4.2.2 iounmap函 ...

  7. HTML总结篇

    一.HTML基本结构标签 <!DOCTYPE html> <html lang="en"> <head> <meta charset=&q ...

  8. APP或者前端通过识别用户代理详细信息和浏览器数据进行安全防御

    使用用户代理解析API 识别 访问您网站的浏览器,机器人,操作系统和设备 上手免费 阅读文档 解码用户代理 识别检测浏览器,操作系统,平台,设备类型以及其他30多个字段 多种浏览器,机器人,手机,平板 ...

  9. vue+quasar+electron+springboot+mysql撸一个TODO LIST 看板

    先看效果 写本项目的目的有几点: 学习下vue+electron桌面开发 学习下java和spring开发(本人一直使用PHP) 一直缺少一款能适合自己的TODO LIST软件,能有桌面端的 可直接打 ...

  10. 【Java并发】1. Java线程内存模型JMM及volatile相关知识

    Java招聘知识合集:https://www.cnblogs.com/spzmmd/tag/Java招聘知识合集/ 该系列用于汇集Java招聘需要的知识点 JMM 并发编程的三大特性:可见性(vola ...