第3章内核对象
准确地理解内核对象对于想要成为一名Wi n d o w s软件开发能手的人来说是至关重要的。本章就来说说内核对象。
什么是内核对象
每个内核对象只是内核分配的一个内存块,并且只能由该内核访问。该内存块是一种数据结构,它的成员负责维护该对象的各种信息。有些数据成员(如安全性描述符、使用计数等)在所有对象类型中是相同的,但大多数数据成员属于特定的对象类型。
由于内核对象的数据结构只能被内核访问,因此应用程序无法在内存中找到这些数据结构并直接改变它们的内容。
对内核对象的操作,Wi n d o w s提供了一组函数来对这些结构进行操作。这些内核对象始终都可以通过这些函数进行访问。当调用一个用于创建内核对象的函数时,该函数就返回一个用于标识该对象的句柄。该句柄可以被视为一个不透明值,你的进程中的任何线程都可以使用这个值。将这个句柄传递给Wi n d o w s的各个函数,这样,系统就能知道你想操作哪个内核对象。
内核对象由内核所拥有,而不是由进程所拥有。换句话说,如果你的进程调用了一个创建内核对象的函数,然后你的进程终止运行,那么内核对象不一定被撤消。在大多数情况下,对象将被撤消,但是如果另一个进程正在使用你的进程创建的内核对象,那么该内核知道,在另一个进程停止使用该对象前不要撤消该对象,必须记住的是,内核对象的存在时间可以比创建该对象的进程长。
安全性问题:内核对象能够得到安全描述符的保护。安全描述符用于描述谁创建了该对象,谁能够访问或使用该对象,谁无权访问该对象。安全描述符通常在编写服务器应用程序时使用,如果你编写客户机端的应用程序,那么可以忽略内核对象的这个特性。
根据原来的设计, Windows 98并不用作服务器端的操作系统。为此,M i c r o s o f t公司没有在Windows 98中配备安全特性。不过,如果你现在为Windows 98设计软件,在实现你的应用程序时仍然应该了解有关的安全问题,并且使用相应的访问信息,以确保它能在Windows 2000上正确地运行
若要确定一个对象是否属于内核对象,最容易的方法是观察创建该对象所用的函数。创建内核对象的所有函数几乎都有一个参数,你可以用来设定安全属性的信息。
当一个进程被初始化时,系统要为它分配一个句柄表。该句柄表只用于内核对象,不用于用户对象或G D I对象。句柄表只是个数据结构的数组。每个结构都包含一个指向内核对象的指针、一个访问屏蔽和一些标志。当进程初次被初始化时,它的句柄表是空的。然后,当进程中的线程调用创建内核对象的函数时,比如C r e a t e F i l e M a p p i n g,内核就为该对象分配一个内存块,并对它初始化。这时,内核对进程的句柄表进行扫描,找出一个空项。由于句柄表是空的,内核便找到索引1位置上的结构并对它进行初始化。该指针成员将被设置为内核对象的数据结构的内存地址,访问屏蔽设置为全部访问权,同时,各个标志也作了设置. 最后,无论怎样创建内核对象,都要向系统指明将通过调用C l o s e H a n d l e来结束对该对象的操作。
跨越进程边界共享内核对象
许多情况下,在不同进程中运行的线程需要共享内核对象。下面是为何需要共享的原因:
• 文件映射对象使你能够在同一台机器上运行的两个进程之间共享数据块。
• 邮箱和指定的管道使得应用程序能够在连网的不同机器上运行的进程之间发送数据块。
• 互斥对象、信标和事件使得不同进程中的线程能够同步它们的连续运行,这与一个应用程序在完成某项任务时需要将情况通知另一个应用程序的情况相同。
第一个方法:对象句柄的继承性
只有当进程具有父子关系时,才能使用对象句柄的继承性。
请记住,虽然内核对象句柄具有继承性,但是内核对象本身不具备继承性。
若要创建能继承的句柄,父进程必须指定一个S E C U R I T Y _ AT T R I B U T E S结构并对它进行初始化,然后将该结构的地址传递给特定的C r e a t e函数。
现在介绍存放在进程句柄表项目中的标志。
每个句柄表项目都有一个标志位,用来指明该句柄是否具有继承性。当创建一个内核对象时,如果传递N U L L作为P S E C U R I T Y _ AT T R I B U T E S的参数,那么返回的句柄是不能继承的,并且该标志位是0。如果将b I n h e r i t H a n d l e成员置为T R U E,那么该标志位将被置为1。
第二个方法:对象命名
共享跨越进程边界的内核对象的第二种方法是给对象命名。许多(虽然不是全部)内核对象都是可以命名的。
第三个方法:复制句柄
共享跨越进程边界的内核对象的最后一个方法是使用D u p l i c a t e H a n d l e函数:
第四章 进程
本章介绍系统如何管理所有正在运行的应用程序。
首先讲述什么是进程,以及系统如何创建进程内核对象,以便管理每个进程。
然后将说明如何使用相关的内核对象来对进程进行操作。
接着,要介绍进程的各种不同的属性,以及查询和修改这些属性所用的若干个函数。
还要讲述创建或生成系统中的辅助进程所用的函数。
最后,说明如何来结束进程的运行。
进程的概念
进程通常被定义为一个正在运行的程序的实例,它由两个部分组成:
• 一个是操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计信息的地方。
• 另一个是地址空间,它包含所有可执行模块或D L L模块的代码和数据。它还包含动态内存分配的空间。如线程堆栈和堆分配空间。
进程是不活泼的。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,该线程负责执行包含在进程的地址空间中的代码。实际上,单个进程可能包含若干个线程,所有这些线程都“同时”执行进程地址空间中的代码。为此,每个线程都有它自己的一组C P U寄存器和它自己的堆栈。
若要使所有这些线程都能运行,操作系统就要为每个线程安排一定的C P U时间。它通过以一种循环方式为线程提供时间片(称为量程),造成一种假象,仿佛所有线程都是同时运行的一样。
当创建一个进程时,系统会自动创建它的第一个线程,称为主线程。然后,该线程可以创建其他的线程,而这些线程又能创建更多的线程。
注意:Windows 98只能在单处理器计算机上运行。Windows2000以后版本则可以运行在多处理器上。实现真正的多线程。
应用程序类型进入点嵌入可执行文件的启动函数
需要A N S I字符和字符串的G U I应用程序Wi n M a i n Wi n M a i n C RT S t a r t u p
需要U n i c o d e字符和字符串的G U I应用程序wWinMain wWi n M a i n C RT S t a r t u p
需要A N S I字符和字符串的C U I应用程序m a i n m a i n C RT S t a r t u p
需要U n i c o d e字符和字符串的C U I应用程序w m a i n w m a i n C RT S t a r t u p
启动函数的功能归纳如下:
• 检索指向新进程的完整命令行的指针。
• 检索指向新进程的环境变量的指针。
• 对C / C + +运行期的全局变量进行初始化。如果包含了S t d L i b . h文件,代码就能访问这些变量。
• 对C运行期内存单元分配函数( m a l l o c和c a l l o c)和其他低层输入/输出例程使用的内存栈进行初始化。
• 为所有全局和静态C + +类对象调用构造函数。当所有这些初始化操作完成后, C / C + +启动函数就调用应用程序的进入点函数。
当进入点函数返回时,启动函数便调用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代码。
进程的实例句柄
加载到进程地址空间的每个可执行文件或D L L文件均被赋予一个独一无二的实例句柄。可执行文件的实例作为( w ) Wi n M a i n的第一个参数h i n s t E x e来传递。对于加载资源的函数调用来说,通常都需要该句柄的值。
二、创建一个进程:
可以用CreateProcess函数创建一个进程:
BOOL CreateProcess(
PCTSTR pszApplicationName,
PCSTR pszCommandLine,
PSECURITY_ATTRIBUTES psaProcess,
PSECURITY_ATTRIBUTES psaThread,
BOOL bInheritHandles,
DWORD fdwCreate,
PVOID pvEnvironment,
PCTSTR pszCurDir,
PSTARTUPINFO psiStartInfo,
PPROCESS_INFORMATION ppiProcInfo);
当一个线程调用C r e a t e P r o c e s s时,系统就会创建一个进程内核对象,其初始使用计数是1。该进程内核对象不是进程本身,而是操作系统管理进程时使用的一个较小的数据结构。可以将进程内核对象视为由进程的统计信息组成的一个较小的数据结构。然后,系统为新进程创建一个虚拟地址空间,并将可执行文件或任何必要的D L L文件的代码和数据加载到该进程的地址空间中。
然后,系统为新进程的主线程创建一个线程内核对象(其使用计数为1)。与进程内核对象一样,线程内核对象也是操作系统用来管理线程的小型数据结构。通过执行C / C + +运行期启动代码,该主线程便开始运行,它最终调用Wi n M a i n、w Wi n M a i n、m a i n或w m a i n函数。如果系统成功地创建了新进程和主线程,C r e a t e P r o c e s s便返回T R U E。
下面分别介绍C r e a t e P r o c e s s的各个参数。
1 pszApplicationName和p s z C o m m a n d L i n e
p s z A p p l i c a t i o n N a m e和p s z C o m m a n d L i n e参数分别用于设定新进程将要使用的可执行文件的名字和传递给新进程的命令行字符串。
可以将地址传递给p s z A p p l i c a t i o n N a m e参数中包含想运行的可执行文件的名字的字符串。请注意,必须设定文件的扩展名,系统将不会自动假设文件名有一个. e x e扩展名。
psaProcess、p s a T h r e a d和b i n h e r i t H a n d l e s
若要创建一个新进程,系统必须创建一个进程内核对象和一个线程内核对象(用于进程的主线程),由于这些都是内核对象,因此父进程可以得到机会将安全属性与这两个对象关联起来。可以使用p s a P r o c e s s和p s a T h r e a d参数分别设定进程对象和线程对象需要的安全性。可以为这些参数传递N U L L,在这种情况下,系统为这些对象赋予默认安全性描述符。也可以指定两个S E C U R I T Y _ AT T R I B U T E S结构,并对它们进行初始化,以便创建自己的安全性权限,并将它们赋予进程对象和线程对象。b I n h e r i t H a n d l e s传递内核对象句柄继承性.
fdwCreate
f d w C r e a t e参数用于标识标志,以便用于规定如何来创建新进程。如果将标志逐位用O R操作符组合起来的话,就可以设定多个标志。
•D E B U G _ P R O C E S S标志用于告诉系统,父进程想要调试子进程和子进程将来生成的任何进程。本标志还告诉系统,当任何子进程(被调试进程)中发生某些事件时,将情况通知父进程(这时是调试程序)。
• D E B U G _ O N LY _ T H I S _ P R O C E S S标志与D E B U G _ P R O C E S S标志相类似,差别在于,调试程序只被告知紧靠父进程的子进程中发生的特定事件。如果子进程生成了别的进程,那么将不通知调试程序在这些别的进程中发生的事件。
• C R E AT E _ S U S P E N D E D标志可导致新进程被创建,但是,它的主线程则被挂起。这使得父进程能够修改子进程的地址空间中的内存,改变子进程的主线程的优先级,或者在进程有机会执行任何代码之前将进程添加给一个作业。一旦父进程修改了子进程,父进程将允许子进程通过调用R e s u m e T h r e a d函数来执行代码(第7章将作详细介绍)。
• D E TA C H E D _ P R O C E S S标志用于阻止基于C U I的进程对它的父进程的控制台窗口的访问,并告诉系统将它的输出发送到新的控制台窗口。如果基于C U I的进程是由另一个基于C U I的进程创建的,那么按照默认设置,新进程将使用父进程的控制台窗口(当通过命令外壳程序来运行C编译器时,新控制台窗口并不创建,它的输出将被附加在现有控制台窗口的底部)。通过设定本标志,新进程将把它的输出发送到一个新控制台窗口。
• C R E AT E _ N E W _ C O N S O L E标志负责告诉系统,为新进程创建一个新控制台窗口。如果同时设定C R E AT E _ N E W _ C O N S O L E和D E TA C H E D _ P R O C E S S标志,就会产生一个错误。
• C R E AT E _ N O _ W I N D O W标志用于告诉系统不要为应用程序创建任何控制台窗口。可以使用本标志运行一个没有用户界面的控制台应用程序。
• C R E AT E _ N E W _ P R O C E S S _ G R O U P标志用于修改用户在按下C t r l + C或C t r l + B r e a k键时得到通知的进程列表。如果在用户按下其中的一个组合键时,你拥有若干个正在运行的C U I进程,那么系统将通知进程组中的所有进程说,用户想要终止当前的操作。当创建一个新的C U I进程时,如果设定本标志,可以创建一个新进程组。如果该进程组中的一个进程处于活动状态时用户按下C t r l + C或C t r l _ B r e a k键,那么系统只通知用户需要这个进程组中的进程。
• C R E AT E _ D E FA U LT _ E R R O R _ M O D E标志用于告诉系统,新进程不应该继承父进程使用的错误模式(参见本章前面部分中介绍的S e t E r r o r M o d e函数)。
• C R E AT E _ S E PA R AT E _ W O W _ V D M标志只能当你在Windows 2000上运行1 6位Wi n d o w s应用程序时使用。它告诉系统创建一个单独的D O S虚拟机(V D M),并且在该V D M中运行1 6位Wi n d o w s应用程序。按照默认设置,所有1 6位Wi n d o w s应用程序都在单个共享的V D M中运行。在单独的VDM 中运行应用程序的优点是,如果应用程序崩溃,它只会使单个V D M停止工作,而在别的V D M中运行的其他程序仍然可以继续正常运行。另外,在单独的V D M中运行的1 6位Wi n d o w s应用程序有它单独的输入队列。这意味着如果一个应用程序临时挂起,在各个V D M中的其他应用程序仍然可以继续接收输入信息。运行多个V D M的缺点是,每个V D M都要消耗大量的物理存储器。Windows 98在单个V D M中运行所有的1 6位Wi n d o w s应用程序,不能改变这种情况。
• C R E AT E _ S H A R E D _ W O W _ V D M标志只能当你在Windows 2000上运行1 6位Wi n d o w s应用程序时使用。按照默认设置,除非设定了C R E AT E _ S E PA R AT E _ W O W _ V D M标志,否则所有1 6位Wi n d o w s应用程序都必须在单个V D M中运行。但是,通过在注册表中将H K E Y _ L O C A L _ M A C H I N E / s y s t e m / C u r r e n t C o n t r o l S e t / C o n t r o l / W O W下的D e f a u l t S e p a r a t e V D M 设置为“ y e s ”,就可以改变该默认行为特性。这时, C R E AT E _ S H A R E D _W O W _ V D M标志就在系统的共享V D M中运行1 6位Wi n d o w s应用程序。
• C R E AT E _ U N I C O D E _ E N V I R O N M E N T标志用于告诉系统,子进程的环境块应该包含U n i c o d e字符。按照默认设置,进程的环境块包含的是A N S I字符串。
• C R E AT E _ F O R C E D O S标志用于强制系统运行嵌入1 6位O S / 2应用程序的M O S - D O S应用程序。
• C R E AT E _ B R E A K AWAY _ F R O M _ J O B标志用于使作业中的进程生成一个与作业相关联的新进程
pvEnvironment
p v E n v i r o n m e n t参数用于指向包含新进程将要使用的环境字符串的内存块。在大多数情况下,为该参数传递N U L L,使子进程能够继承它的父进程正在使用的一组环境字符串。也可以使用G e t E n v i r o n m e n t S t r i n g s函数:该函数用于获得调用进程正在使用的环境字符串数据块的地址。可以使用该函数返回的地址,作为C r e a t e P r o c e s s的p v E n v i r o n m e n t参数。如果为p v E n v i r o n m e n t参数传递N U L L,那么这正是C r e a t e P r o c e s s函数所做的操作。当不再需要该内存块时,应该调用F r e e E n v i r o n m e n t S t r i n g s函数将内存块释放:
pszCurDir
p s z C u r D i r参数允许父进程设置子进程的当前驱动器和目录。如果本参数是N U L L,则新进程的工作目录将与生成新进程的应用程序的目录相同。如果本参数不是N U L L,那么p s z C u r D i r必须指向包含需要的工作驱动器和工作目录的以0结尾的字符串。注意,必须设定路径中的驱动器名。
psiStartInfo
p s i S t a r t I n f o参数用于指向一个S TA RT U P I N F O结构:
ppiProcInfo
p p i P r o c I n f o参数用于指向你必须指定的P R O C E S S _ I N F O R M AT I O N结构。C r e a t e P r o c e s s在返回之前要对该结构的成员进行初始化。该结构的形式如下面所示:
typedef struct _PROCESS_INFORMATION{
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
}PROCESS_INFORMATION;
终止进程的运行
若要终止进程的运行,可以使用下面四种方法:
• 主线程的进入点函数返回(最好使用这个方法)。
• 进程中的一个线程调用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。
一旦进程终止运行(无论采用何种方法),系统将确保该进程不会将它的任何部分遗留下来。绝对没有办法知道该进程是否曾经运行过。进程一旦终止运行,它绝对不会留下任何蛛丝马迹。希望这是很清楚的。
第五章 作业
Microsoft Windoss 2000提供了一个新的作业内核对象,使你能够将进程组合在一起,并且创建一个“沙框”,以便限制进程能够进行的操作。最好将作业对象视为一个进程的容器。但是,创建包含单个进程的作业是有用的,因为这样一来,就可以对该进程加上通常情况下不能加的限制。
注意: Windows 98不支持作业的操作。
5.1 对作业进程的限制
进程创建后,通常需要设置一个沙框(设置一些限制),以便限制作业中的进程能够进行的操作。可以给一个作业加上若干不同类型的限制:
• 基本限制和扩展基本限制,用于防止作业中的进程垄断系统的资源。
• 基本的U I限制,用于防止作业中的进程改变用户界面。
• 安全性限制,用于防止作业中的进程访问保密资源(文件、注册表子关键字等)。
这里对四种限制的成员函数及参数的介绍省略了,可以自己细看书中的介绍。
5.2 将进程放入作业
需要注意的一点是,该函数只允许将尚未被赋予任何作业的进程赋予一个作业。一旦进程成为一个作业的组成部分,它就不能转到另一个作业,并且不能是无作业的进程。另外,当作为作业的一部分的进程生成另一个进程的时候,新进程将自动成为父作业的组成部分。
5.3 终止作业中所有进程的运行
当然,想对作业进行的最经常的操作是撤消作业中的所有进程。D e v e l o p e r S t u d i o没有配备任何便于使用的方法,来停止进程中的某个操作,因为它不知道哪个进程是由第一个进程生成的。
若要撤消作业中的进程,只需要调用下面的代码:
BOOL Terminatejobobject (
Handle Hjob
uInt uexitcode
)
这类似为作业中的每个进程调用Te r m i n a t e P r o c e s s函数,将它们的所有退出代码设置为u E x i t C o d e。
5.4 查询作业统计信息
Q u e r y I n f o r m a t i o n J o b O b j e c t函数用来获取对作业的当前限制信息。也可以使用它来获取关于作业的统计信息。例如,若要获取基本的统计信息,可以调用Q u e r y I n f o r m a t i o n J o b O b j e c t,为第二个参数传递J o b O b j e c t B a s i c A c c o u n t i n g I n f o r m a t i o n ,并传递J O B O B J E C T _ B A S I C _ A C C O U N T I N G _ I N F O R M AT I O N结构的地址:
除了查询这些基本统计信息外,可以进行一次函数调用,以同时查询基本统计信息和I / O统计信息。为此,必须为第二个参数传递J o b O b j e c t B a s i c A n d I o A c c o u n t i n g I n f o r m a t i o n ,并传递J O B O B J E C T _ B A S I C _ A N D _ I O _ A C C O U N T I N G _ I N F O R M AT I O N结构的地址
5.5 作业通知信息
现在,已经知道了关于作业对象的基本知识,剩下要介绍的内容是关于通知的问题。例如,是否想知道作业中的所有进程何时终止运行或者分配的全部C P U时间是否已经到期呢?也许想知道作业中何时生成新进程或者作业中的进程何时终止运行。如果不关心这些通知信息(而且许多应用程序也不关心这些信息),作业的操作非常容易。如果关心这些事件,那么还有一些工作要做。
如果关心的是分配的所有C P U时间是否已经到期,那么可以非常容易地得到这个通知信息。当作业中的进程尚未用完分配的C P U时间时,作业对象就得不到通知。一旦分配的所有C P U时间已经用完, Wi n d o w s就强制撤消作业中的所有进程,并将情况通知作业对象。通过调用Wa i t F o r S i n g l e O b j e c t (或类似的函数),可以很容易跟踪这个事件。有时,可以在晚些时候调用S e t I n f o r m a t i o n J o b O b j e c t函数,使作业对象恢复未通知状态,并为作业赋予更多的C P U时间。
当开始对作业进行操作时,我觉得当作业中没有任何进程运行时,应该将这个事件通知作业对象。毕竟当进程和线程停止运行时,进程和线程对象就会得到通知。因此,当作业停止运行时它也应该得到通知。这样,就能够很容易确定作业何时结束运行。但是, M i c r o s o f t选择在分配的C P U时间到期时才向作业发出通知,因为这显示了一个错误条件。由于许多作业启动时有一个父进程始终处于工作状态,直到它的所有子进程运行结束,因此只需要在父进程的句柄上等待,就可以了解整个作业何时运行结束。S t a r t R e s t r i c t e d P r o c e s s函数用于显示分配给作业的C P U时间何时到期,或者作业中的进程何时终止运行。
前面介绍了如何获得某些简单的通知信息,但是尚未说明如何获得更高级的通知信息,如进程创建/终止运行等。如果想要得到这些通知信息,必须将更多的基础结构放入应用程序。特别是,必须创建一个I / O完成端口内核对象,并将作业对象或多个作业对象与完成端口关联起来。然后,必须让一个或多个线程在完成端口上等待作业通知的到来,这样它们才能得到处理。
一旦创建了I / O完成端口,通过调用S e t I n f o r m a t i o n J o b O b j e c t函数,就可以将作业与该端口关联起来.
最后要说明的一点是,按照默认设置,作业对象是这样配置的:当分配给作业的C P U时间已经到期时,作业的所有进程均自动停止运行,而J O B _ O B J E C T _ M S G _ E N D _ O F _ J O B _ T I M E通知尚未发送。