进程通常被定义为一个正在运行的程序的实例,它由两个部分组成:
• 一个是操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计信息的地方。
• 另一个是地址空间,它包含所有可执行模块或 D L L 模块的代码和数据。它还包含动态内存分配的空间。如线程堆栈和堆分配空间。
进程是不活泼的。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,该线程负责执行包含在进程的地址空间中的代码。
当创建一个进程时,系统会自动创建它的第一个线程,称为主线程。然后,该线程可以创建其他的线程,而这些线程又能创建更多的线程。
进程的实例句柄
加载到进程地址空间的每个可执行文件或
D L L
文件均被赋予一个独一无二的实例句柄。可执行文件的实例作为
( w ) Wi n M a i n
的第一个参数
h i n s t E x e
来传递。对于加载资源的函数调用来说,通常都需要该句柄的值。例如,若要从可执行文件的映象来加载图标资源,需要调用下面这个函数:
HICON LoadIcon( HINSTANCE hinst, PCTSTR pszIcon);
L o a d I c o n
的第一个参数用于指明哪个文件(可执行文件或
D L L
文件)包含你想加载的资源。
注意
:
实际情况说明,
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
用于标识不同的东西。
G e t M o d u l e H a n d l e
函数返回可执行文件或
D L L
文件加载到进程的地址空间时所用的句柄
/
基地址:
HMODULE GetModuleHandle( PCTSTR pszModule);
当调用该函数时,你传递一个以
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
运行期的启动代码开始运行的时候,它要检索进程的命令行,跳过可执行文件的名字,并将指向命令行其余部分的指针传递给
Wi n M a i n
的
p s z C m d L i n e
参数。
进程的环境变量
每个进程都有一个与它相关的环境块。环境块是进程的地址空间中分配的一个内存块。每个环境块都包含一组字符串,其形式如下:
VarName1=VarValue1/0
VarName2=VarValue2/0
VarName3=VarValue3/0
...
VarNameX=VarValueX/0
/0
每个字符串的第一部分是环境变量的名字,后跟一个等号,等号后面是要赋予变量的值。
DWORD GetEnvironmentVariable(
PCTSTR pszName,
PTSTR pszValue,
DWORD cchValue);
当调用
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
。
BOOL SetEnvironmentVariable(
PCTSTR pszName,
PCTSTR pszValue);
该函数用于将
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
,便从环境块中删除该变量。
进程的亲缘性
一般来说,进程中的线程可以在主计算机中的任何一个
C P U
上执行。但是一个进程的线程可能被强制在可用
C P U
的子集上运行。这称为进程的亲缘性,
进程的错误模式
进程可以告诉系统如何处理每一种错误。方法是调用
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
参数是表
4 - 3
的任何标志按位用
O R
连接在一起的组合。
表4-3 fuError Mode 参数的标志
标志
|
说明
|
SEM_FAILCRITICALERRORS
|
系统不显示关键错误句柄消息框,并将错误返回给调用进程
|
SEM_NOGOFAULTERRORBOX
|
系统不显示一般保护故障消息框。本标志只应该由采用异常情况处理程序来处理一般保护(G P)故障的调试应用程序来设定
|
SEM_NOOPENFILEERRORBOX
|
当系统找不到文件时,它不显示消息框
|
SEM_NOALIGNMENTFAULTEXCEPT
|
系统自动排除内存没有对齐的故障,并使应用程序看不到这些故障。本标志对x 8 6处理器不起作用
|
进程的当前驱动器和目录
通过调用下面两个函数,线程能够获得和设置它的进程的当前驱动器和目录:
DWORD GetCurrentDirectory( DWORD cchCurDir, PTSTR pszCurDir); BOOL SetCurrentDirectory(PCTSTR pszCurDir);
CreateProcess
函数
可以用
C r e a t e P r o c e s s
函数创建一个进程:
BOOL CreateProcess(
PCTSTR pszApplicationName,
PTSTR pszCommandLine,
PSECURITY_ATTRIBUTES psaProcess,
PSECURITY_ATTRIBUTES psaThread,
BOOL bInheritHandles,
DWORD fdwCreate,
PVOID pvEnvironment,
PCTSTR pszCurDir,
PSTARTUPINFO psiStartInfo,
PPROCESS_INFORMATION ppiProcInfo);
当一个线程调用
CreateProcess
时,系统就会创建一个进程内核对象,其初始使用计数是
1
。
当第一个参数为
NULL
时
, C r e a t e P r o c e s s
也按下面的顺序搜索该可执行文件:
1)
包含调用进程的
. e x e
文件的目录。
2)
调用进程的当前目录。
3) Wi n d o w s
的系统目录。
4) Wi n d o w s
目录。
5) PAT H
环境变量中列出的目录。
当然,如果文件名包含全路径,系统将使用全路径来查看可执行文件,并且不再搜索这些目录。如果系统找到了可执行文件,那么它就创建一个新进程,并将可执行文件的代码和数据映射到新进程的地址空间中。然后系统将调用
C / C + +
运行期启动例程。正如前面我们讲过的那样,
C / C + +
运行期启动例程要查看进程的命令行,并将地址作为
( w ) Wi n M a i n
的
p s z C m d L i n 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
函数(应该避免使用这种方法)。
•
进程中的所有线程自行终止运行(这种情况几乎从未发生)。
主线程的进入点函数返回
始终都应该这样来设计应用程序,即只有当主线程的进入点函数返回时,它的进程才终止运行。这是保证所有线程资源能够得到正确清除的唯一办法。让主线程的进入点函数返回,可以确保下列操作的实现:
•
该线程创建的任何
C + +
对象将能使用它们的析构函数正确地撤消。
•
操作系统将能正确地释放该线程的堆栈使用的内存。
•
系统将进程的退出代码(在进程的内核对象中维护)设置为进入点函数的返回值。
•
系统将进程内核对象的返回值递减
1
。
ExitProcess
函数
当进程中的一个线程调用
E x i t P r o c e s s
函数时,进程便终止运行:
VOID ExitProcess(UINT fuExitCode);
当主线程的进入点函数(
WinMain
、
wWinMain
、
main
或
wmain
)返回时,它将返回给
C / C + +
运行期启动代码,它能正确地清除该进程使用的所有的
C
运行期资源。当
C
运行期资源被释放之后,
C
运行期启动代码就显式调用
E x i t P r o c e s s
,并将进入点函数返回的值传递给它。这解释了为什么只需要主线程的进入点函数返回,就能够终止整个进程的运行。请注意,进程中运行的任何其他线程都随着进程而一道终止运行。
注意,调用
E x i t P r o c e s s
或
E x i t T h r e a d
可使进程或线程在函数中就终止运行。就操作系统而言,这很好,进程或线程的所有操作系统资源都将被全部清除。但是,
C / C + +
应用程序应该避免调用这些函数,因为
C / C + +
运行期也许无法正确地清除。
TerminateProcess
函数
调用
Te r m i n a t e P r o c e s s
函数也能够终止进程的运行:
BOOL TerminateProcess(HANDLE hProcess, UINT fuExitCode);
该函数与
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
来终止另一个进程或它自己的进程的运行。
h P r o c e s s
参数用于标识要终止运行的进程的句柄。
只有当无法用另一种方法来迫使进程退出时,才应该使用
Te r m i n a t e P r o c e s s
。终止运行的进程绝对得不到关于它将终止运行的任何通知,因为应用程序无法正确地清除,并且不能避免自己被撤消(除非通过正常的安全机制)。
进程终止运行时出现的情况
当进程终止运行时,下列操作将启动运行:
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)
进程内核对象的状态变成收到通知的状态(关于传送通知的详细说明,参见第
9
章)。系
统中的其他线程可以挂起,直到进程终止运行。
5)
进程内核对象的使用计数递减
1
。
注意,进程的内核对象的寿命至少可以达到进程本身那么长,但是进程内核对象的寿命可能大大超过它的进程寿命。当进程终止运行时,系统能够自动确定它的内核对象的使用计数。如果使用计数降为
0
,那么没有其他进程拥有该对象打开的句柄,当进程被撤消时,对象也被撤消。
子进程
子进程
,
能够处理比线程更复杂的东西
,
也能够保持相对的独立
,
但是就会有进程间的数据共享
, Wi n d o w s
提供了若干种方法,以便在不同的进程中间传送数据,比如动态数据交换(
D D E
)、
O L E
、管道和邮箱等。共享数据最方便的方法之一是,使用内存映射文件
.
大多数情况下,应用程序将另一个进程作为独立的进程来启动。这意味着进程创建和开始运行后,父进程并不需要与新进程进行通信,也不需要在完成它的工作后父进程才能继续运行。这就是
E x p l o r e r
的运行方式。当
E x p l o r e r
为用户创建一个新进程后,它并不关心该进程是否继续运行,也不在乎用户是否终止它的运行。
若要放弃与子进程的所有联系,
E x p l o r e r
必须通过调用
C l o s e H a n d l e
来关闭它与新进程及它的主线程之间的句柄。下面的代码示例显示了如何创建新进程以及如何让它以独立方式来运行:
PROCESS_INFORMATION pi; //Spawn the child process. BOOL fSuccess = CreateProcess(..., π); 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); }