第03章 内 核 对 象

        在介绍Windows API的时候,首先要讲述内核对象以及它们的句柄。本章将要介绍一些比较抽象的概念,在此并不讨论某个特定内核对象的特性,相反只是介绍适用于所有内核对象的特性。
        首先介绍一个比较具体的问题,准确地理解内核对象对于想要成为一名 Wi n d o w s软件开发能手的人来说是至关重要的。内核对象可以供系统和应用程序使用来管理各种各样的资源,比如进程、线程和文件等本章讲述的概念也会出现在本书的其他各章之中。但是,在你开始使用实际的函数来操作内核对象之前,是无法深刻理解本章讲述的部分内容的。因此当阅读本书
的其他章节时,可能需要经常回过来参考本章的内容

 

3.1 什么是内核对象

        作为一个Wi n d o w s软件开发人员,你经常需要创建、打开和操作各种内核对象。系统要创建和操作若干类型的内核对象,比如存取符号对象、事件对象、文件对象、文件映射对象、I / O完成端口对象、作业对象、信箱对象、互斥对象、管道对象、进程对象、信标对象、线程对象和等待计时器对象等。这些对象都是通过调用函数来创建的。例如, C r e a t e F i l e M a p p i n g函数可使系统能够创建一个文件映射对象。每个内核对象只是内核分配的一个内存块,并且只能由该内核访问。该内存块是一种数据结构,它的成员负责维护该对象的各种信息。有些数据成员(如安全性描述符、使用计数等)在所有对象类型中是相同的,但大多数数据成员属于特定的对象类型。例如,进程对象有一个进程 I D、一个基本优先级和一个退出代码,而文件对象则拥有一个字节位移、一个共享模式和一个打开模式
        由于内核对象的数据结构只能被内核访问,因此应用程序无法在内存中找到这些数据结构并直接改变它们的内容。 M i c r o s o f t规定了这个限制条件,目的是为了确保内核对象结构保持状态的一致。这个限制也使M i c r o s o f t能够在不破坏任何应用程序的情况下在这些结构中添加、删除和修改数据成员。
        如果我们不能直接改变这些数据结构,那么我们的应用程序如何才能操作这些内核对象呢?解决办法是, Wi n d o w s提供了一组函数,以便用定义得很好的方法来对这些结构进行操作。这些内核对象始终都可以通过这些函数进行访问。当调用一个用于创建内核对象的函数时,该函数就返回一个用于标识该对象的句柄。该句柄可以被视为一个不透明值,你的进程中的任何线程都可以使用这个值。将这个句柄传递给 Wi n d o w s的各个函数,这样,系统就能知道你想操作哪个内核对象。本章后面还要详细讲述这些句柄的特性。
        为了使操作系统变得更加健壮,这些句柄值是与进程密切相关的。因此,如果将该句柄值传递给另一个进程中的一个线程(使用某种形式的进程间的通信)那么这另一个进程使用你的进程的句柄值所作的调用就会失败。在 3 . 3节“跨越进程边界共享内核对象”中,将要介绍 3种机制,使多个进程能够成功地共享单个内核对象


3.1.1 内核对象的使用计数

        内核对象由内核所拥有,而不是由进程所拥有。换句话说,如果你的进程调用了一个创建内核对象的函数,然后你的进程终止运行,那么内核对象不一定被撤消。在大多数情况下,对象将被撤消,但是如果另一个进程正在使用你的进程创建的内核对象,那么该内核知道,在另一个进程停止使用该对象前不要撤消该对象,必须记住的是,内核对象的存在时间可以比创建该对象的进程长。
        内核知道有多少进程正在使用某个内核对象,因为每个对象包含一个使用计数。使用计数是所有内核对象类型常用的数据成员之一。当一个对象刚刚创建时,它的使用计数被置为 1。然后,当另一个进程访问一个现有的内核对象时,使用计数就递增 1。当进程终止运行时,内核就自动确定该进程仍然打开的所有内核对象的使用计数。如果内核对象的使用计数降为 0,内核就撤消该对象。这样可以确保在没有进程引用该对象时系统中不保留任何内核对象。


3.1.2 安全性


        内核对象能够得到安全描述符的保护。安全描述符用于描述谁创建了该对象,谁能够访问或使用该对象,谁无权访问该对象。安全描述符通常在编写服务器应用程序时使用,如果你编写客户机端的应用程序,那么可以忽略内核对象的这个特性。                 Windows 98 根据原来的设计, Windows 98并不用作服务器端的操作系统。为此,M i c r o s o f t公司没有在Windows 98中配备安全特性。不过,如果你现在为 Windows 98设计软件,在实现你的应用程序时仍然应该了解有关的安全问题,并且使用相应的访问信息,以确保它能在Windows 2000上正确地运行用于创建内核对象的函数几乎都有一个指向 S E C U R I T Y _ AT T R I B U T E S结构的指针作为其参数,下面显示了C r e a t e F i l e M a p p i n g函数的指针:

HANDLE CreateFileMapping(
  HANDLE hFile,                       //物理文件句柄
  LPSECURITY_ATTRIBUTES lpAttributes, //安全设置
  DWORD flProtect,                    //保护设置
  DWORD dwMaximumSizeHigh,            //高位文件大小
  DWORD dwMaximumSizeLow,             //低位文件大小
  LPCTSTR lpName                      //共享内存名称
);

1) 物理文件句柄
   任何可以获得的物理文件句柄, 如果你需要创建一个物理文件无关的内存映射也无妨, 将它设置成为 0xFFFFFFFF(INVALID_HANDLE_VALUE)就可以了.

   如果需要和物理文件关联, 要确保你的物理文件创建的时候的访问模式和"保护设置"匹配, 比如: 物理文件只读, 内存映射需要读写就会发生错误. 推荐你的物理文件使用独占方式创建.

   如果使用 INVALID_HANDLE_VALUE, 也需要设置需要申请的内存空间的大小, 无论物理文件句柄参数是否有效, 这样 CreateFileMapping 就可以创建一个和物理文件大小无关的内存空间给你, 甚至超过实际文件大小, 如果你的物理文件有效, 而大小参数为0, 则返回给你的是一个和物理文件大小一样的内存空间地址范围.  返回给你的文件映射地址空间是可以通过复制, 集成或者命名得到, 初始内容为0.

2) 保护设置
   就是安全设置, 不过一般设置NULL就可以了, 使用默认的安全配置. 在win2k下如果需要进行限制, 这是针对那些将内存文件映射共享给整个网络上面的应用进程使用是, 可以考虑进行限制.

3) 高位文件大小
   弟兄们, 我想目前我们的机器都是32位的东东, 不可能得到超过32位进程所能寻址的私有32位地址空间, 一般还是设置0吧, 我没有也不想尝试将它设置超过0的情况.
4) 低位文件大小
   这个还是可以进行设置的, 不过为了让其他共享用户知道你申请的文件映射的相关信息, 我使用的时候是在获得的地址空间头部添加一个结构化描述信息, 记录内存映射的大小, 名称等, 这样实际申请的空间就比输入的增加了一个头信息结构大小了, 我认为这样类似BSTR的方式应该是比较合理的.

5) 共享内存名称
   这个就是我今天测试的时候碰壁的祸根, 因为为了对于内存进行互斥访问, 我设置了一个互斥句柄, 而名称我选择和命名共享内存同名, 之下就是因为他们使用共同的namespace导致了错误, 呵呵.

7) 调用CreateFileMapping的时候GetLastError的对应错误
   ERROR_FILE_INVALID     如果企图创建一个零长度的文件映射, 应有此报
   ERROR_INVALID_HANDLE   如果发现你的命名内存空间和现有的内存映射, 互斥量, 信号量, 临界区同名就麻烦了
   ERROR_ALREADY_EXISTS   表示内存空间命名已经存在

8) 相关服务或者平台的命名保留
   Terminal Services: 
   命名可以包含 "Global\" 或者 "Local\" 前缀在全局或者会话名空间初级文件映射. 其他部分可以包含任何除了(\)以外的字符, 可以参考 Kernel Object Name Spaces.

   Windows 2000 or later: 
   如果 Terminal Services 没有运行 "Global\" 和 "Local\" 前缀的特殊含义就被忽略了

        大多数应用程序只是为该参数传递 N U L L,这样就可以创建带有默认安全性的内核对象。默认安全性意味着对象的管理小组的任何成员和对象的创建者都拥有对该对象的全部访问权,而其他所有人均无权访问该对象。但是,可以指定一个 S E C U R I T Y _ AT T R I B U T E S结构,对它进行初始化,并为该参数传递该结构的地址。 S E C U R I T Y _ AT T R I B U T E S结构类似下面的样子:

typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength; / /结构体的大小,可用SIZEOF取得
LPVOID lpSecurityDescriptor; / /安全描述符
BOOL bInheritHandle ;/ /安全描述的对象能否被新创建的进程继承
} SECURITY_ATTRIBUTES,* PSECURITY_ATTRIBUTES;

         尽管该结构称为S E C U R I T Y _ AT T R I B U T E S,但是它包含的与安全性有关的成员实际上只有一个,即l p S e c u r i t y D e s c r i p t o r。如果你想要限制人们对你创建的内核对象的访问,必须创建一个安全性描述符,然后像下面这样对 S E C U R I T Y _ AT T R I B U T E S结构进行初始化:

    HANDLE hMapFile;
    unsigned char *pBuf;
    SECURITY_ATTRIBUTES SecAttr;
    SECURITY_DESCRIPTOR SecDesc;

    SecAttr.nLength = sizeof(SecAttr);// Used for versioning
    SecAttr.bInheritHandle = FALSE;   // Discussed later
    SecAttr.lpSecurityDescriptor = &SecDesc;// Address of an initialized SD

    InitializeSecurityDescriptor(&SecDesc, SECURITY_DESCRIPTOR_REVISION);
    SetSecurityDescriptorDacl(&SecDesc, TRUE, 0, FALSE);


    hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE, &SecAttr, PAGE_READWRITE, 0, BUF_SIZE, "MyfileMapping");
    if(hMapFile == NULL)
    {
        return GetLastError();
    }

        由于b I n h e r i t H a n d l e这个成员与安全性毫无关系,因此准备推迟到本章后面部分继承性一节中再介绍b I n h e r i t H a n d l e这个成员。
        当你想要获得对相应的一个内核对象的访问权(而不是创建一个新对象)时,必须设定要对该对象执行什么操作。例如,如果想要访问一个现有的文件映射内核对象,以便读取它的数据,那么应该调用下面这个O p e n f i l e M a p p i n g函数:

 HANDLE hFileMapping =  OpenFileMapping(FILE_MAP_READ,FALSE,
                        TEXT("MyFileMapping"));

        通过将F I L E _ M A P _ R E A D作为第一个参数传递给 O p e n F i l e M a p p i n g,指明打算在获得对该文件映象的访问权后读取该文件, O p e n F i l e M a p p i n g函数在返回一个有效的句柄值之前,首先执行一次安全检查。如果(已登录用户)被允许访问现有的文件映射内核对象, O p e n F i l eM a p p i n g就返回一个有效的句柄。但是,如果被拒绝访问该对象, O p e n F i l e M a p p i n g将返回N U L L,而调用G e t L a s t E r r o r函数则返回5(亲身经历,改过相关的问题eg:copyfile(E R R O R _ A C C E S S _ D E N I E D),同样,大多数应用程序并不使用该安全性,因此将不进一步讨论这个问题。
        Windows 98 虽然许多应用程序不需要考虑安全性问题,但是 Wi n d o w s的许多函数要求传递必要的安全访问信息。为Windows 98设计的若干应用程序在Windows 2000上无法正确地运行,因为在实现这些应用程序时没有对安全问题给于足够的考虑。
        例如,假设一个应用程序在开始运行时要从注册表的子关键字中读取一些数据。为了正确地进行这项操作,你的代码应该调用 R e g O p e n K e y E x,传递K E Y _ Q U E RY _VA L U E,以便获得必要的访问权。(这个也是见过的!!注册表问题
        但是,许多应用程序原先是为 Windows 98开发的,当时没有考虑到运行 Wi n d o w s2 0 0 0的需要。由于Windows 98没有解决注册表的安全问题,因此软件开发人员常常要调用R e g O p e n K e y E x函数,传递K E Y _ A l l _ A C C E S S,作为必要的访问权。开发人员这样做的原因是,它是一种比较简单的解决方案,意味着开发人员不必考虑究竟需要什么访问权。问题是注册表的子关键字可以被用户读取,但是不能写入。(仔细阅读,你会有所发现!
         因此,当该应用程序现在放在 Windows 2000上运行时,用K E Y _ A L L _ A C C E S S调用R e g O p e n K e y E x就会失败,而且,没有相应的错误检查方法,应用程序的运行就会产生不可预料的结果。
         如果开发人员想到安全问题,把 K E Y _ A L L _ A C C E S S改为K E Y _ Q U E RY _ VA L U E ,则该产品可适用于两种操作系统平台。
         开发人员的最大错误之一就是忽略安全访问标志。使用正确的标志会使最初为Windows 98 设计的应用程序更易Windows 2000 转换。
         除了内核对象外,你的应用程序也可以使用其他类型的对象,如菜单、窗口、鼠标光标、刷子和字体等。这些对象属于用户对象或图形设备接口(G D I)对象,而不是内核对象。当初次着手为Wi n d o w s编程时,如果想要将用户对象或 G D I对象与内核对象区分开来,你一定会感到不知所措。比如,图标究竟是用户对象还是内核对象呢?若要确定一个对象是否属于内核对
象,最容易的方法是观察创建该对象所用的函数。创建内核对象的所有函数几乎都有一个参数,你可以用来设定安全属性的信息,这与前面讲到的 C r e a t e F i l e M a p p i n g函数是相同的。(用户对象和内核对象的区别
         用于创建用户对象或G D I对象的函数都没有P S E C U R I T Y _ AT T R I B U T E S参数。例如,让我们来看一看下面这个C r e a t e I c o n函数:

HICON CreateIcon(
HINSTANCE     hInstance,
int     nWidth,
int     nHeight,
BYTE    nPlanes,
BYTE    nBitsPixel,
CONST BYTE *lpANDbits,
CONST BYTE *lpXORbits,
)
[说明]
创建一个图标
[参数表]
hInstance ------ Long,准备拥有图标的应用程序的实例的句柄。可用GetWindowWord函数获得拥有一个窗体或控件的一个实例的句柄
nWidth --------- Long,图标图象的宽度。可用GetSystemMetrics函数判断一个特定设备的正确编号。VGA的编号是32
nHeight -------- Long,图标图象的高度。可用GetSystemMetrics函数判断一个特定设备的正确编号。VGA的编号是32
nPlanes -------- Byte,lpXORbits数据数组中的色层数量
nBitsPixel ----- Byte,lpXORbits数据数组中每像素的位数
lpANDbits ------ Byte,指向AND位图数据的指针
lpXORbits ------ Byte,指向XOR位图数据的指针
[返回值]
Long,执行成功返回图标的句柄,零表示失败。会设置GetLastError
[其它]
一旦不再需要,注意用DestroyIcon函数释放鼠标指针占用的内存及资源

3.2 进程的内核对象句柄表


        当一个进程被初始化时,系统要为它分配一个句柄表该句柄表只用于内核对象,不用于用户对象或G D I对象。句柄表的详细结构和管理方法并没有具体的资料说明。通常我并不介绍操作系统中没有文档资料的那些部分。不过,在这种情况下,我会进行例外处理,因为,作为一个称职的Wi n d o w s程序员,必须懂得如何管理进程的句柄表。由于这些信息没有文档资料,因此不能保证所有的详细信息都正确无误,同时,在Windows 2000、 Windows 98和Windows CE中,它们的实现方法是不同的。为此,请认真阅读下面介绍的内容以加深理解,在此不学习系统是如何进行操作的。
        表3 - 1显示了进程的句柄表的样子。可以看到,它只是个数据结构的数组。每个结构都包含一个指向内核对象的指针、一个访问屏蔽和一些标志。
                                                                                      表3-1 进程的句柄结构                           

索 引内核对象内存块 的指针访问屏蔽(标志位的D W O R D)标 志(标志位的D W O R D)
10 x ? ? ? ? ? ? ? ?0 x ? ? ? ? ? ? ? ?0 x ? ? ? ? ? ? ? ?
20 x ? ? ? ? ? ? ? ?0 x ? ? ? ? ? ? ? ?0 x ? ? ? ? ? ? ? ?

 

3.2.1 创建内核对象


        当进程初次被初始化时,它的句柄表是空的。然后,当进程中的线程调用创建内核对象的函数时,比如 C r e a t e F i l e M a p p i n g,内核就为该对象分配一个内存块,并对它初始化。这时,内核对进程的句柄表进行扫描,找出一个空项。由于表 3 - 1中的句柄表是空的,内核便找到索引 1位置上的结构并对它进行初始化。该指针成员将被设置为内核对象的数据结构的内存地址,访问屏蔽设置为全部访问权,同时,各个标志也作了设置(关于标志,将在本章后面部分的继承性一节中介绍)。
        下面列出了用于创建内核对象的一些函数(但这决不是个完整的列表):

1. 函数原型:


    HANDLE WINAPI CreateThread(
      _In_opt_  LPSECURITY_ATTRIBUTES  lpThreadAttributes,   
      _In_      SIZE_T                 dwStackSize,
      _In_      LPTHREAD_START_ROUTINE lpStartAddress,
      _In_opt_  LPVOID                 lpParameter,
      _In_      DWORD                  dwCreationFlags,
      _Out_opt_ LPDWORD                lpThreadId
    );
2. 参数说明:

第一个参数 lpThreadAttributes 表示线程内核对象的安全属性,一般传入NULL表示使用默认设置。

第二个参数 dwStackSize 表示线程栈空间大小。传入0表示使用默认大小(1MB)。

第三个参数 lpStartAddress 表示新线程所执行的线程函数地址,多个线程可以使用同一个函数地址。

第四个参数 lpParameter 是传给线程函数的参数。

第五个参数 dwCreationFlags 指定额外的标志来控制线程的创建,为0表示线程创建之后立即就可以进行调度,如果为CREATE_SUSPENDED则表示线程创建后暂停运行,这样它就无法调度,直到调用ResumeThread()。

第六个参数 lpThreadId 将返回线程的ID号,传入NULL表示不需要返回该线程ID号。

3.返回值

线程创建成功返回新线程的句柄,失败返回NULL


HANDLE CreateFileMapping(
  HANDLE hFile,                       //物理文件句柄
  LPSECURITY_ATTRIBUTES lpAttributes, //安全设置
  DWORD flProtect,                    //保护设置
  DWORD dwMaximumSizeHigh,            //高位文件大小
  DWORD dwMaximumSizeLow,             //低位文件大小
  LPCTSTR lpName                      //共享内存名称
);


1、首先来看看如何使用信号量

    信号量Semaphore常用有三个函数,使用很方便。下面是这几个函数的原型和使用说明。

CreateSemaphore

函数功能:创建信号量

函数原型:

HANDLE CreateSemaphore(

  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,

  LONG lInitialCount,

  LONG lMaximumCount,

  LPCTSTR lpName

);

函数说明:

第一个参数表示安全控制,一般直接传入NULL。

第二个参数表示初始资源数量。

第三个参数表示最大并发数量。

第四个参数表示信号量的名称,传入NULL表示匿名信号量。



OpenSemaphore

函数功能:打开信号量

函数原型:

HANDLE OpenSemaphore(

  DWORD dwDesiredAccess,

  BOOL bInheritHandle,

  LPCTSTR lpName

);

函数说明:

第一个参数表示访问权限,对一般传入SEMAPHORE_ALL_ACCESS。详细解释可以查看MSDN文档。

第二个参数表示信号量句柄继承性,一般传入TRUE即可。

第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个信号量。


ReleaseSemaphore

函数功能:递增信号量的当前资源计数

函数原型:

BOOL ReleaseSemaphore(

  HANDLE hSemaphore,

  LONG lReleaseCount, 

  LPLONG lpPreviousCount

);

函数说明:

第一个参数是信号量的句柄。

第二个参数表示增加个数,必须大于0且不超过最大资源数量。

第三个参数可以用来传出先前的资源计数,设为NULL表示不需要传出。

       注意:当前资源数量大于0,表示信号量处于触发,等于0表示资源已经耗尽故信号量处于末触发。在对信号量调用等待函数时,等待函数会检查信号量的当前资源计数,如果大于0(即信号量处于触发状态),减1后返回让调用线程继续执行。一个线程可以多次调用等待函数来减小信号量。


最后一个 信号量的清理与销毁

由于信号量是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。


2、在经典多线程问题中设置一个信号量和一个关键段。用信号量处理主线程与子线程的同步,用关键段来处理各子线程间的互斥。详见代码:

#include <stdio.h>
#include <process.h>
#include <windows.h>
long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
//信号量与关键段
HANDLE            g_hThreadParameter;
CRITICAL_SECTION  g_csThreadCode;
int main()
{
	printf("     经典线程同步 信号量Semaphore\n");
	printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
 
	//初始化信号量和关键段
	g_hThreadParameter = CreateSemaphore(NULL, 0, 1, NULL);//当前0个资源,最大允许1个同时访问
	InitializeCriticalSection(&g_csThreadCode);
 
	HANDLE  handle[THREAD_NUM];	
	g_nNum = 0;
	int i = 0;
	while (i < THREAD_NUM) 
	{
		handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
		WaitForSingleObject(g_hThreadParameter, INFINITE);//等待信号量>0
		++i;
	}
	WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
	
	//销毁信号量和关键段
	DeleteCriticalSection(&g_csThreadCode);
	CloseHandle(g_hThreadParameter);
	for (i = 0; i < THREAD_NUM; i++)
		CloseHandle(handle[i]);
	return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
	int nThreadNum = *(int *)pPM;
	ReleaseSemaphore(g_hThreadParameter, 1, NULL);//信号量++
	Sleep(50);//some work should to do
	EnterCriticalSection(&g_csThreadCode);
	++g_nNum;
	Sleep(0);//some work should to do
	printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);
	LeaveCriticalSection(&g_csThreadCode);
	return 0;
}

        用于创建内核对象的所有函数均返回与进程相关的句柄,这些句柄可以被在相同进程中运行的任何或所有线程成功地加以使用。该句柄值实际上是放入进程的句柄表中的索引,它用于标识内核对象的信息存放的位置。 因此当调试一个应用程序并且观察内核对象句柄的实际值时,会看到一些较小的值,如1, 2等。请记住,句柄的含义并没有记入文档资料,并且可能随时变
更。实际上在Windows 2000中,返回的值用于标识放入进程的句柄表的该对象的字节数,而不是索引号本身。
        每当调用一个将内核对象句柄接受为参数的函数时,就要传递由一个 C r e a t e * &函数返回的值。从内部来说,该函数要查看进程的句柄表,以获取要生成的内核对象的地址,然后按定义得很好的方式来生成该对象的数据结构。
        如 果 传 递 了 一 个 无 效 索 引 ( 句 柄 ) ,该函数便返回失败,而 G e t L a s t E r r o r 则返回 6(E R R O R _ I N VA L I D _ H A N D L E)。由于句柄值实际上是放入进程句柄表的索引,因此这些句柄是与进程相关的,并且不能由其他进程成功地使用。
       如果调用一个函数以便创建内核对象,但是调用失败了,那么返回的句柄值通常是 0(N U L L)。发生这种情况是因为系统的内存非常短缺,或者遇到了安全方面的问题。不过有少数函数在运行失败时返回的句柄值是-1(I N VA L I D _ H A N D L E _ VA L U E)。例如,如果C r e a t e F i l e未能打开指定的文件,那么它将返回I N VA L I D _ H A N D L E _ VA L U E,而不是返回N U L L。当查看创建内核对象的函数返回值时,必须格外小心。特别要注意的是,只有当调用C r e a t e F i l e函数时,
才能将该值与I N VA L I D _ H A N D L E _ VA L U E进行比较。下面的代码是不正确的:

HANDLE hMutex = CreateMutex(...);
if(hMutex == INVALID_HANDLE_VALUE)
{
    // we will nerver excute this code because 
    // CreateMutex return NULL if it fail.
}

        同样,下面的代码也不正确:

HANDLE hFile = CreateFile(...);
if(hFile == NULL)
{
    // we will nerver excute this code because 
    // CreateFile return INVALID_HANDLE_VALUE(-1) if it fail.
}

3.2.2 关闭内核对象


        无论怎样创建内核对象,都要向系统指明将通过调用 C l o s e H a n d l e来结束对该对象的操作:该函数首先检查调用进程的句柄表,以确保传递给它的索引(句柄)用于标识一个进程实际上无权访问的对象。如果该索引是有效的,那么系统就可以获得内核对象的数据结构的地址,并可确定该结构中的使用计数的数据成员。如果使用计数是0,该内核便从内存中撤消该内核对象。
        如果将一个无效句柄传递给 C l o s e H a n d l e,将会出现两种情况之一。如果进程运行正常,C l o s e H a n d l e返回FA L S E,而G e t L a s t E r r o r则返回E R R O R _ I N VA L I D _ H A N D L E。如果进程正在排除错误,系统将通知调试程序,以便能排除它的错误。
        在C l o s e H a n d l e返回之前,它会清除进程的句柄表中的项目,该句柄现在对你的进程已经无效,不应该试图使用它。无论内核对象是否已经撤消,都会发生清除操作。当调用C l o s e H a n d l e函数之后,将不再拥有对内核对象的访问权,不过,如果该对象的使用计数没有递减为0,那么该对象尚未被撤消。这没有问题,它只是意味着一个或多个其他进程正在使用
该对象。当其他进程停止使用该对象时(通过调用 C l o s e H a n d l e),该对象将被撤消。
         假如忘记调用 C l o s e H a n d l e函数,那么会不会出现内存泄漏呢?答案是可能的,但是也不一定。在进程运行时,进程有可能泄漏资源(如内核对象)。但是,当进程终止运行时,操作系统能够确保该进程使用的任何资源或全部资源均被释放,这是有保证的。对于内核对象来说,系统将执行下列操作:当进程终止运行时,系统会自动扫描进程的句柄表。如果该表拥有任何
无效项目(即在终止进程运行前没有关闭的对象),系统将关闭这些对象句柄。如果这些对象中的任何对象的使用计数降为0,那么内核便撤消该对象。
         因此,应用程序在运行时有可能泄漏内核对象,但是当进程终止运行时,系统将能确保所有内容均被正确地清除。另外,这个情况适用于所有对象、资源和内存块,也就是说,当进程终止运行时,系统将保证进程不会留下任何对象。
 

3.3 跨越进程边界共享内核对象

        许多情况下,在不同进程中运行的线程需要共享内核对象。下面是为何需要共享的原因:
        • 文件映射对象使你能够在同一台机器上运行的两个进程之间共享数据块。
        • 邮箱和指定的管道使得应用程序能够在连网的不同机器上运行的进程之间发送数据块。
        • 互斥对象、信标和事件使得不同进程中的线程能够同步它们的连续运行,这与一个应用程序在完成某项任务时需要将情况          通知另一个应用程序的情况相同。
       由于内核对象句柄与进程相关,因此这些任务的执行情况是不同的。不过, M i c r o s o f t公司有若干很好的理由将句柄设计成与进程相关的句柄。最重要的理由是要实现它的健壮性。如果内核对象句柄是系统范围的值,那么一个进程就能很容易获得另一个进程使用的对象的句柄,从而对该进程造成很大的破坏。另一个理由是安全性。内核对象是受安全性保护的,进程在试图操作一个对象之前,首先必须申请获得操作该对象的许可权。对象的创建人只需要拒绝向用户赋予许可权,就能防止未经授权的用户接触该对象。
        在下面的各节中,将要介绍允许进程共享内核对象的 3个不同的机制。

3.3.1 对象句柄的继承性

        只有当进程具有父子关系时,才能使用对象句柄的继承性。在这种情况下,父进程可以使用一个或多个内核对象句柄,并且该父进程可以决定生成一个子进程,为子进程赋予对父进程的内核对象的访问权。若要使这种类型的继承性能够实现,父进程必须执行若干个操作步骤。
        首先,当父进程创建内核对象时,必须向系统指明,它希望对象的句柄是个可继承的句柄。请记住,虽然内核对象句柄具有继承性,但是内核对象本身不具备继承性。
        若要创建能继承的句柄,父进程必须指定一个 S E C U R I T Y _ AT T R I B U T E S结构并对它进行
初始化,然后将该结构的地址传递给特定的 C r e a t e函数。下面的代码用于创建一个互斥对象,
并将一个可继承的句柄返回给它:

    SECURITY_ATTRIBUTES sa;
    SecAttr.nLength = sizeof(sa); 
    SecAttr.lpSecurityDescriptor = NULL;
    SecAttr.bInheritHandle = TRUE;  // Make the return handle inheritable.
    HANDLE hMutex = CreateMutex(&sa,FALSE,NULL);

        该代码对一个S E C U R I T Y _ AT T R I B U T E S结构进行初始化,指明该对象应该使用默认安全性(在Windows 98中该安全性被忽略)来创建,并且返回的句柄应该是可继承的。
        Windows 98 尽管Windows 98不拥有完整的对安全性的支持,但是它却支持继承性,因此, Windows 98能够正确地使用b I n h e r i t H a n d l 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。
                                                              表3 - 2显示了一个进程的句柄表                                                 

索 引内核对象内存块 的指针访问屏蔽(标志位的D W O R D)标 志(标志位的D W O R D)
10 x F 0 0 0 0 0 0 00 x ? ? ? ? ? ? ? ?0 x ? ? ? ? ? ? ? ?
20 x 0 0 0 0 0 0 0 0(无)(无)
30 x ? ? ? ? ? ? ? ?0 x ? ? ? ? ? ? ? ?0 x ? ? ? ? ? ? ? ?

        表3 - 2表示该进程拥有对两个内核对象(句柄 1和3)的访问权。句柄1是不可继承的,而句柄3是可继承的。
        使用对象句柄继承性时要执行的下一个步骤是让父进程生成子进程。这要使用 C r e a t eP r o c e s s函数来完成
 

函数说明:

CreateProcess是Windows下用于创建进程的API函数,用来创建一个新的进程和它的主线程,这个新进程运行指定的可执行文件。

函数原型:

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 

);

参数定义:


lpApplicationName
指向一个NULL结尾的、用来指定可执行模块的字符串。
这个字符串可以是可执行模块的绝对路径,也可以是相对路径,在后一种情况下,函数使用当前驱动器和目录建立可执行模块的路径。
这个参数可以被设为NULL,在这种情况下,可执行模块的名字必须处于 lpCommandLine 参数最前面并由空格符与后面的字符分开。
lpCommandLine
指向一个以NULL结尾的字符串,该字符串指定要执行的命令行。
这个参数可以为空,那么函数将使用lpApplicationName参数指定的字符串当做要运行的程序的命令行。
如果lpApplicationName和lpCommandLine参数都不为空,那么lpApplicationName参数指定将要被运行的模块,lpCommandLine参数指定将被运行的模块的命令行。新运行的进程可以使用GetCommandLine函数获得整个命令行。C语言程序可以使用argc和argv参数。
lpProcessAttributes
指向一个SECURITY_ATTRIBUTES结构体,这个结构体决定是否返回的句柄可以被子进程继承。如果lpProcessAttributes参数为空(NULL),那么句柄不能被继承。
在Windows NT中:SECURITY_ATTRIBUTES结构的lpSecurityDescriptor成员指定了新进程的安全描述符,如果参数为空,新进程使用默认的安全描述符。
lpThreadAttributes
同lpProcessAttribute,不过这个参数决定的是线程是否被继承.通常置为NULL.
bInheritHandles
指示新进程是否从调用进程处继承了句柄。
如果参数的值为真,调用进程中的每一个可继承的打开句柄都将被子进程继承。被继承的句柄与原进程拥有完全相同的值和访问权限。
dwCreationFlags
指定附加的、用来控制优先类和进程的创建的标志。以下的创建标志可以以除下面列出的方式外的任何方式组合后指定。
⑴值:CREATE_DEFAULT_ERROR_MODE
含义:新的进程不继承调用进程的错误模式。CreateProcess函数赋予新进程当前的默认错误模式作为替代。应用程序可以调用SetErrorMode函数设置当前的默认错误模式。
这个标志对于那些运行在没有硬件错误环境下的多线程外壳程序是十分有用的。
对于CreateProcess函数,默认的行为是为新进程继承调用者的错误模式。设置这个标志以改变默认的处理方式。
⑵值:CREATE_NEW_CONSOLE
含义:新的进程将使用一个新的控制台,而不是继承父进程的控制台。这个标志不能与DETACHED_PROCESS标志一起使用。
⑶值:CREATE_NEW_PROCESS_GROUP
含义:新进程将是一个进程树的根进程。进程树中的全部进程都是根进程的子进程。新进程树的用户标识符与这个进程的标识符是相同的,由lpProcessInformation参数返回。进程树经常使用GenerateConsoleCtrlEvent函数允许发送CTRL+C或CTRL+BREAK信号到一组控制台进程。
⑷值:CREATE_SEPARATE_WOW_VDM
如果被设置,新进程将会在一个私有的虚拟DOS机(VDM)中运行。另外,默认情况下所有的16位Windows应用程序都会在同一个共享的VDM中以线程的方式运行。单独运行一个16位程序的优点是一个应用程序的崩溃只会结束这一个VDM的运行;其他那些在不同VDM中运行的程序会继续正常的运行。同样的,在不同VDM中运行的16位Windows应用程序拥有不同的输入队列,这意味着如果一个程序暂时失去响应,在独立的VDM中的应用程序能够继续获得输入。
⑸值:CREATE_SHARED_WOW_VDM
如果WIN.INI中的Windows段的DefaultSeparateVDM选项被设置为真,这个标识使得CreateProcess函数越过这个选项并在共享的虚拟DOS机中运行新进程。
⑹值:CREATE_SUSPENDED
含义:新进程的主线程会以暂停的状态被创建,直到调用ResumeThread函数被调用时才运行。
⑺值:CREATE_UNICODE_ENVIRONMENT
含义:如果被设置,由lpEnvironment参数指定的环境块使用Unicode字符,如果为空,环境块使用ANSI字符。
⑻值:DEBUG_PROCESS
含义:如果这个标志被设置,调用进程将被当做一个调试程序,并且新进程会被当做被调试的进程。系统把被调试程序发生的所有调试事件通知给调试器。
如果你使用这个标志创建进程,只有调用进程(调用CreateProcess函数的进程)可以调用WaitForDebugEvent函数。
⑼值:DEBUG_ONLY_THIS_PROCESS
含义:如果此标志没有被设置且调用进程正在被调试,新进程将成为调试调用进程的调试器的另一个调试对象。如果调用进程没有被调试,有关调试的行为就不会产生。
⑽值:DETACHED_PROCESS
含义:对于控制台进程,新进程没有访问父进程控制台的权限。新进程可以通过AllocConsole函数自己创建一个新的控制台。这个标志不可以与CREATE_NEW_CONSOLE标志一起使用。
〔11〕值:CREATE_NO_WINDOW
含义:系统不为新进程创建CUI窗口,使用该标志可以创建不含窗口的CUI程序。
dwCreationFlags参数
还用来控制新进程的优先类,优先类用来决定此进程的线程调度的优先级。如果下面的优先级类标志都没有被指定,那么默认的优先类是NORMAL_PRIORITY_CLASS,除非被创建的进程是IDLE_PRIORITY_CLASS。在这种情况下子进程的默认优先类是IDLE_PRIORITY_CLASS。
可以选择下面的标志中的一个:
优先级:HIGH_PRIORITY_CLASS
含义:指示这个进程将执行时间临界的任务,所以它必须被立即运行以保证正确。这个优先级的程序优先于正常优先级或空闲优先级的程序。一个例子是Windows任务列表,为了保证当用户调用时可以立刻响应,放弃了对系统负荷的考虑。确保在使用高优先级时应该足够谨慎,因为一个高优先级的CPU关联应用程序可以占用几乎全部的CPU可用时间。
优先级:IDLE_PRIORITY_CLASS
含义:指示这个进程的线程只有在系统空闲时才会运行并且可以被任何高优先级的任务打断。例如屏幕保护程序。空闲优先级会被子进程继承。
优先级:NORMAL_PRIORITY_CLASS
含义:指示这个进程没有特殊的任务调度要求。
优先级:REALTIME_PRIORITY_CLASS
含义:指示这个进程拥有可用的最高优先级。一个拥有实时优先级的进程的线程可以打断所有其他进程线程的执行,包括正在执行重要任务的系统进程。例如,一个执行时间稍长一点的实时进程可能导致磁盘缓存不足或鼠标反映迟钝。
lpEnvironment
指向一个新进程的环境块。如果此参数为空,新进程使用调用进程的环境。
一个环境块存在于一个由以NULL结尾的字符串组成的块中,这个块也是以NULL结尾的。每个字符串都是name=value的形式。
因为相等标志被当做分隔符,所以它不能被环境变量当做变量名。
与其使用应用程序提供的环境块,不如直接把这个参数设为空,系统驱动器上的当前目录信息不会被自动传递给新创建的进程。对于这个情况的探讨和如何处理,请参见注释一节。
环境块可以包含Unicode或ANSI字符。如果lpEnvironment指向的环境块包含Unicode字符,那么dwCreationFlags字段的CREATE_UNICODE_ENⅥRONMENT标志将被设置。如果块包含ANSI字符,该标志将被清空。
请注意一个ANSI环境块是由两个零字节结束的:一个是字符串的结尾,另一个用来结束这个快。一个Unicode环境块是由四个零字节结束的:两个代表字符串结束,另两个用来结束块。
lpCurrentDirectory
指向一个以NULL结尾的字符串,这个字符串用来指定子进程的工作路径。这个字符串必须是一个包含驱动器名的绝对路径。如果这个参数为空,新进程将使用与调用进程相同的驱动器和目录。这个选项是一个需要启动应用程序并指定它们的驱动器和工作目录的外壳程序的主要条件。
lpStartupInfo
指向一个用于决定新进程的主窗体如何显示的STARTUPINFO结构体。
lpProcessInformation
指向一个用来接收新进程的识别信息的PROCESS_INFORMATION结构体。

        下一章将详细介绍这个函数的用法,不过现在我想要让你注意 b I n h e r i t H a n d l e这个参数。一般来说,当生成一个进程时,将为该参数传递 FA L S E。该值告诉系统,不希望子进程继承父进程的句柄表中的可继承句柄。
        但是,如果为该参数传递 T R U E,那么子进程就可以继承父进程的可继承句柄值。当传递T R U E时,操作系统就创建该新子进程,但是不允许子进程立即开始执行它的代码。当然,系统为子进程创建一个新的和空的句柄表,就像它为任何新进程创建句柄表那样。不过,由于将T R U E传递给了C r e a t e P r o c e s s的b I n h e r i t H a n d l e s参数,因此系统要进行另一项操作,即它要遍历父进程的句柄表,对于它找到的包含有效的可继承句柄的每个项目,系统会将该项目准确地拷贝到子进程的句柄表中。该项目拷贝到子进程的句柄表中的位置将与父进程的句柄表中的位置完全相同。这个情况非常重要,因为它意味着在父进程与子进程中,标识内核对象所用的句柄值是相同的。
        除了拷贝句柄表项目外,系统还要递增内核对象的使用计数,因为现在两个进程都使用该对象。如果要撤消内核对象,那么父进程和子进程必须调用该对象上的 C l o s e H a n d l e函数,也可以终止进程的运行。子进程不必首先终止运行,但是父进程也不必首先终止运行。实际上,C r e a t e P r o c e s s函数返回后,父进程可以立即关闭对象的句柄,而不影响子进程对该对象进行操作的能力。
       表3 - 3显示了子进程被允许运行前该进程的句柄表。可以看到,项目 1和项目2尚未初始化,因此是个无效句柄,子进程是无法使用的。但是,项目 3确实标识了一个内核对象。实际上,它标识的内核对象的地址是0 x F 0 0 0 0 0 1 0,这与父进程的句柄表中的对象地址相同。访问屏蔽与父进程中的屏蔽相同,两者的标志也相同。这意味着如果该子进程要生成它自己的子进程(即父进程的孙进程),该孙进程也将继承与该内核对象句柄相同的句柄值、同样的访问权和相同的标志,同时,对象的使用计数再次被递增。
                                          表3-3 继承父进程的可继承句柄后的子进程句柄表

索 引内核对象内存块的指针

              访问屏蔽

(标志位的D W O R D)

                标 志

(标志位的D W O R D)

10 x 0 0 0 0 0 0 0 0(无)(无)
20 x 0 0 0 0 0 0 0 0(无)(无)
30 x F 0 0 0 0 0 1 0 0 x ? ? ? ? ? ? ? ?0 x 0 0 0 0 0 0 0 1

        应该知道,对象句柄的继承性只有在生成子进程的时候才能使用。如果父进程准备创建带有可继承句柄的新内核对象,那么已经在运行的子进程将无法继承这些新句柄。
        对象句柄的继承性有一个非常奇怪的特征,那就是当使用它时,子进程不知道它已经继承了任何句柄。只有在另一个进程生成子进程时记录了这样一个情况,即它希望被赋予对内核对象的访问权时,才能使用内核对象柄的继承权。通常,父应用程序和子应用程序都是由同一个公司编写的,但是,如果另一个公司记录了子应用程序期望的对象,那么该公司也能够编写子应用程序。
        子进程为了确定它期望的内核对象的句柄值,最常用的方法是将句柄值作为一个命令行参数传递给子进程,该子进程的初始化代码对命令行进行分析(通常通过调用 s s c a n f函数来进行分析),并取出句柄值。一旦子进程拥有该句柄值,它就具有对该对象的无限访问权。请注意,句柄继承权起作用的唯一原因是,父进程和子进程中的共享内核对象的句柄值是相同的,这就是为什么父进程能够将句柄值作为命令行参数来传递的原因。
        当然,可以使用其他形式的进程间通信,将已继承的内核对象句柄值从父进程传送给子进程。方法之一是让父进程等待子进程完成初始化(使用第 9章介绍的Wa i t F o r I n p u I d l e函数),然后,父进程可以将一条消息发送或展示在子进程中的一个线程创建的窗口中。
        另一个方法是让父进程将一个环境变量添加给它的环境程序块。该变量的名字是子进程知道要查找的某种信息,而变量的值则是内核对象要继承的值。这样,当父进程生成子进程时,子进程就继承父进程的环境变量,并且能够非常容易地调用 G e t E n v i r o n m e n t Va r i a b l e函数,以获取被继承对象的句柄值。如果子进程要生成另一个子进程,那么使用这种方法是极好的,因为环境变量可以被再次继承。

3.3.2 改变句柄的标志

        有时会遇到这样一种情况,父进程创建一个内核对象,以便检索可继承的句柄,然后生成两个子进程。父进程只想要一个子进程来继承内核对象的句柄。换句话说,有时可能想要控制哪个子进程来继承内核对象的句柄若要改变内核对象句柄的继承标志,可以调用S e t H a n d l e I n f o r m a t i o n函数:

控制哪些子进程能继承内核对象句柄,可调用SetHandleInformation函数改变内核对象句柄的继承标志。
SetHandleInformation设置。
BOOL SetHandleInformation(
HANDLE hObject, 
DWORD dwMask, 
DWORD dwFlags);
改变句柄的标志,目前可改变的标志有两种
#define HANDLE_FLAG_INHERIT   0x00000001  // 继承标志
#define HANDLE_FLAG_PROJECT_FROM_CLOSE   0x00000001 // 保护不允许关闭句柄标志
例如
// 设置句柄值可继承:
SetHandleInformation(hObject, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
// 设置句柄不可继承:
SetHandleInformation(hObject, HANDLE_FLAG_INHERIT, 0);
// 设置句柄值不可关闭,受保护:
SetHandleInformation(hObject, HANDLE_FLAG_PROJECT_FROM_CLOSE, HANDLE_FLAG_PROJECT_FROM_CLOSE);
// 设置句柄值可关闭,不受保护:
SetHandleInformation(hObject, HANDLE_FLAG_PROJECT_FROM_CLOSE, 0);
 
1、关于设置句柄为不可继承的用法,在创建一个回路的socket时使用,如果可以子进程可以继承的话,会出现多个回路的socket,通讯会受影响
 
2、关于设置句柄不可以关闭,受保护时,父进程创建子进程,子进程再创建孙进程时,子进程把句柄关闭,导致孙进程无法继续使用,这时候要保护

        可以看到,该函数拥有 3个参数。第一个参数 h O b j e c t用于标识一个有效的句柄。第二个参数d w M a s k告诉该函数想要改变哪个或那几个标志。目前有两个标志与每个句柄相关联:

#define HANDLE_FLAG_INHERIT   0x00000001  // 继承标志
#define HANDLE_FLAG_PROTECT_FROM_CLOSE   0x00000002 

       如果想同时改变该对象的两个标志,可以逐位用OR将这些标志连接起来S e t H a n d l eI n f o r m a t i o n函数的第三个参数是 d w F l a g s,用于指明想将该标志设置成什么值。例如,若要打开一个内核对象句柄的继承标志,请创建下面的代码:

例如,要打开一个内核对象句柄的继承标志,可以像下面这样写:
SetHandleInformation( 
hObj, 
HANDLE_FLAG_INHERIT ,
HANDLE_FLAG_INHERIT 
);

      若要关闭该标志,请创建下面的代码:

要关闭这个标志,可以像下面这样写:
SetHandleInformation( 
hObj , 
HANDLE_FLAG_INHERIT ,
0
);

       H A N D L E _ F L A G _ P R O T E C T _ F R O M _ C L O S E标志用于告诉系统,该句柄不应该被关闭

// 设置句柄值不可关闭,受保护:
SetHandleInformation(
hObject, 
HANDLE_FLAG_PROJECT_FROM_CLOSE, 
HANDLE_FLAG_PROJECT_FROM_CLOSE
);
CloseHandle(hObject);//Except is raised 异常

        如果一个线程试图关闭一个受保护的句柄, C l o s e H a n d l e就会产生一个异常条件。很少想要将句柄保护起来,使他人无法将它关闭。但是如果一个进程生成了子进程,而子进程又生成了孙进程,那么该标志可能有用。父进程可能希望孙进程继承赋予子进程的对象句柄。不过,子进程有可能在生成孙进程之前关闭该句柄。如果出现这种情况,父进程就无法与孙进程进行通信,因为孙进程没有继承该内核对象。通过将句柄标明为“受保护不能关闭”,那么孙进程就能继承该对象。
        但是这种处理方法有一个问题。子进程可以调用下面的代码来关闭 H A N D L E _ F L A G _P R O T E C T _ F R O M _ C L O S E标志,然后关闭句柄。       

       父进程可能打赌说,子进程将不执行该代码。当然,父进程也可能打赌说,子进程将生成孙进程。因此这种打赌没有太大的风险。
        为了完整地说明问题,也要讲一下 G e t H a n d l e I n f o r m a t i o n函数的情况:

BOOL GetHandleInformation(
HANDLE hObkect, 
PDWORD pdwFlags
);

        该函数返回p d w F l a g s指向的D W O R D中特定句柄的当前标志的设置值。若要了解句柄是否是可继承的,请使用下面的代码:

DWORD dwFlags;
GetHandleInformation(hObj,&dwFlags);
BOOL fHandleIsInheritable = (0 != (dwFlags & HANDLE_FLAG_INHERIT));


3.3.3 命名对象

        共享跨越进程边界的内核对象的第二种方法是给对象命名。许多(虽然不是全部)内核对象都是可以命名的。例如,下面的所有函数都可以创建命名的内核对象:

HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, // 指向安全属性的指针
BOOL bInitialOwner, // 初始化互斥对象的所有者
LPCTSTR lpName // 指向互斥对象名的指针
);

HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性
BOOL bManualReset, // 复位方式
BOOL bInitialState, // 初始状态
LPCTSTR lpName // 对象名称
);

HANDLE WINAPI CreateSemaphore( 
  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, 
  LONG lInitialCount, 
  LONG lMaximumCount, 
  LPCTSTR lpName 
);
第一个参数:安全属性,如果为NULL则是默认安全属性 
第二个参数:信号量的初始值,要>=0且<=第三个参数 
第三个参数:信号量的最大值 
第四个参数:信号量的名称 
返回值:指向信号量的句柄,如果创建的信号量和已有的信号量重名,那么返回已经存在的信号量句柄

// 创建一个可等待的计时器对象。
HANDLE CreateWaitableTimer(
 PSecurityAttributes lpTimerAttributes,  {安全}
 BOOL bManualReset,   {True: 可调度多个线程; False: 只调度一个线程}
 PCTSTR  lpTimerName{名称}
);

HANDLE CreateFileMapping(
  HANDLE hFile,                       //物理文件句柄
  LPSECURITY_ATTRIBUTES lpAttributes, //安全设置
  DWORD flProtect,                    //保护设置
  DWORD dwMaximumSizeHigh,            //高位文件大小
  DWORD dwMaximumSizeLow,             //低位文件大小
  LPCTSTR lpName                      //共享内存名称
);


HANDLE CreateJobObject(
LPSECURITY_ATTRIBUTES psa,
PCTSTR  lpName,
)



        所有这些函数都有一个共同的最后参数 p s z N a m e。当为该参数传递 N U L L时,就向系统指明了想创建一个未命名的(匿名)内核对象。当创建一个未命名的对象时,可以通过使用继承性(如上一节介绍的那样)或 D u p l i c a t e H a n d l e (下一节将要介绍)共享跨越进程的对象。若要按名字共享对象,必须为对象赋予一个名字。
        如果没有为p s z N a m e参数传递M U L L,应该传递一个以0结尾的字符串名字的地址。该名字的长度最多可以达到M A X _ PAT H(定义为2 6 0)个字符。但是, M i c r o s o f t没有提供为内核对象赋予名字的指导原则。例如,如果试图创建一个称为“J e ff O b j”的对象,那么不能保证系统中不存在一个名字为“J e ff O b j”的对象。更为糟糕的是,所有这些对象都共享单个名空间。由于这个原因,对下面这个C r e a t e S e m a p h o r e函数的调用将总是返回N U L L:
        如果在执行上面的代码后观察 d w E r r o r c o d e的值,会看到返回的代码是 6(E R R O R _I N VA L I D _ H A N D L E )。这个错误代码没有很强的表意性,但是你又能够做什么呢?既然已知道如何给对象命名,那么让我们来看一看如何用这种方法来共享对象。比如说,Process A启动运行,并调用下面的函数:调用该函数能创建一个新的互斥内核对象,为它赋予名字“J e ff M u t e x”。请注意, P r o c e s sA的句柄h M u t e x P r o c e s s A不是一个可继承的句柄,并且当你只是命名对象时,它不必是个可继承的句柄。
         过些时候,某个进程会生成 Process B。 Process B不一定是Process A的子进程。它可能是E x p l o r e r或其他任何应用程序生成的进程。 Process B不必是Process A的子进程这一事实正是使用命名对象而不是继承性的优越性。当 Process B启动运行时,它执行下面的代码:
        当Process B调用C r e a t e M u t e x时,系统首先要查看是否已经存在一个名字为“J e ff M u t e x”的内核对象。由于确实存在一个带有该名字的对象,因此内核要检查对象的类型。由于试图创建一个互斥对象,而名字为“J e ff M u t e x”的对象也是个互斥对象,因此系统会执行一次安全检查,以确定调用者是否拥有对该对象的完整的访问权。如果拥有这种访问权,系统就在Process B的句柄表中找出一个空项目,并对该项目进行初始化,使该项目指向现有的内核对象。
如果该对象类型不匹配,或者调用者被拒绝访问,那么 C r e a t e M u t e x将运行失败(返回N U L L)。
当Process B对C r e a t e M u t e x的调用取得成功时,它并不实际创建一个互斥对象。相反,
Process B只是被赋予一个与进程相关的句柄值,用于标识内核中现有的互斥对象。当然,由于
Process B 的句柄表中的一个新项目要引用该对象,互斥对象的使用计数就会递增。在 Process A
和Process B同时关闭它们的对象句柄之前,该对象是不会被撤消的。请注意,这两个进程中的
句柄值很可能是不同的值。这是可以的。 Process A将使用它的句柄值,而Process B 则使用它自
己的句柄值来操作一个互斥内核对象。
注意 当你的多个内核对象拥有相同的名字时,有一个非常重要的细节必须知道。
当Process B调用C r e a t e M u t e x时,它将安全属性信息和第二个参数传递给该函数。如
果已经存在带有指定名字的对象,那么这些参数将被忽略。应用程序能够确定它是否
确实创建了一个新内核对象,而不是打开了一个现有的对象。方法是在调用 C r e a t e *函
数后立即调用G e t L a s t E r r o r:

按名字共享对象的另一种方法是,进程不调用 C r e a t e *函数,而是调用下面显示的 O p e n *函
数中的某一个:

        注意,所有这些函数都拥有相同的原型。最后一个参数 p s z N a m e用于指明内核对象的名字。不能为该参数传递 N U L L,必须传递以0结尾的地址。这些函数要搜索内核对象的单个名空间,以便找出匹配的空间。如果不存在带有指定名字的内核对象,该函数返回 N U L L, G e t L a s t E r r o r返回2(E R R O R _ F I L E _ N O T _ F O U N D)。但是,如果存在带有指定名字的内核对象,并且它是相同类型的对象,那么系统就要查看是否允许执行所需的访问(通过 d w D e s i r e d A c c e s s参数进行访问)。如果拥有该访问权,调用进程的句柄表就被更新,对象的使用计数被递增。如果为b I n h e r i t H a n d l e参数传递T R U E,那么返回的句柄将是可继承的。

        调用C r e a t e *函数与调用O p e n *函数之间的主要差别是,如果对象并不存在,那么 C r e a t e *函数将创建该对象,而O p e n *函数则运行失败。
        如前所述, M i c r o s o f t没有提供创建唯一对象名的指导原则。换句话说,如果用户试图运行来自不同公司的两个程序,而每个程序都试图创建一个称为“M y O b j e c t”的对象,那么这就是个问题。为了保证对象的唯一性,建议创建一个G U I D,并将G U I D的字符串表达式用作对象名。命名对象常常用来防止运行一个应用程序的多个实例。若要做到这一点,只需要调用 m a i n或Wi n M a i n函数中C r e a t e *函数,以便创建一个命名对象(创建的是什么对象则是无所谓的)。当C r e a t e * 函 数 返 回 时 , 调 用 G e t L a s t E r r o r 函 数 。 如 果 G e t L a s t E r r o r 函 数 返 回E R R O R _ A L R E A D Y _ E X I S T S,那么你的应用程序的另一个实例正在运行,新实例可以退出。下面是说明这种情况的部分代码:


3.3.4 终端服务器的名字空间

        注意,终端服务器能够稍稍改变上面所说的情况。终端服务器拥有内核对象的多个名字空间。如果存在一个可供内核对象使用的全局名字空间,就意味着它可以供所有的客户程序会话访问。该名字空间主要供服务程序使用。此外,每个客户程序会话都有它自己的名字空间。它能防止运行相同应用程序的两个或多个会话之间出现互相干扰的情况,也就是说一个会话无法访问另一个会话的对象,尽管该对象拥有相同的名字。在没有终端服务器的机器上,服务程序和应用程序拥有上面所说的相同的内核对象名字空间,而在拥有终端服务器的机器上,
却不是这样。
        服务程序的名字空间对象总是放在全局名字空间中。按照默认设置,在终端服务器中,应用程序的命名内核对象将放入会话的名字空间中。但是,如果像下面这样将“G l o b a l \”置于对象名的前面,就可以使命名对象进入全局名字空间:       

       也可以显式说明想让内核对象进入会话的名字空间,方法是将“L o c a l \”置于对象名的前面:
       M i c r o s o f t将G l o b a l和L o c a l视为保留关键字,除非要强制使用特定的名字空间,否则不应该使用这两个关键字。 M i c r o s o f t还将S e s s i o n视为保留关键字,虽然目前它没有任何意义。请注意,所有这些保留关键字是区分大小写字母的。如果主机不运行终端服务器,这些关键字将被忽略。


3.3.5 复制对象句柄

        共享跨越进程边界的内核对象的最后一个方法是使用 D u p l i c a t e H a n d l e函数:

        简单说来,该函数取出一个进程的句柄表中的项目,并将该项目拷贝到另一个进程的句柄表中。 D u p l i c a t e H a n d l e函数配若干个参数,但是实际上它是非常简单的。 D u p l i c a t e H a n d l e函数最普通的用法要涉及系统中运行的 3个不同进程。
        当调用 D u p l i c a t e H a n d l e函数时,第一和第三个参数 h S o u r c e P r o c e s s H a n d l e和h Ta rg e tP r o c e s s H a n d l e是内核对象句柄。这些句柄本身必须与调用 D u p l i c a t e H a n d l e函数的进程相关此
外,这两个参数必须标识进程的内核对象。如果将句柄传递给任何其他类型的内核对象,那么该函数运行就会失败。第 4章将详细介绍进程的内核对象,而现在只需要知道,每当系统中启动一个新进程时都会创建一个进程内核对象。
        第二个参数h S o u r c e H a n d l e是任何类型的内核对象的句柄。但是该句柄值与调用 D u p l i c a t eH a n d l e的进程并无关系。相反,该句柄必须与 h S o u r c e P r o c e s s H a n d l e句柄标识的进程相关。第四个数p h Ta rg e t H a n d l e是H A N D L E变量的地址,它将接收获取源进程句柄信息拷贝的项目索引。返回的句柄值与h Ta rg e t P r o c e s s H a n d l e标识的进程相关。
        D u p l i c a t e H a n d l e的最后3个参数用于指明该目标进程的内核对象句柄表项目中使用的访问屏蔽值和继承性标志。 d w O p t i o n s参数可以是 0(零),也可以是下面两个标志的任何组合:D U P L I C AT E _ S A M E _ A C C E S S和D U P L I C AT E _ C L O S E _ S O U R C E。

       如果设定了D U P L I C AT E _ S A M E _ A C C E S S标志,则告诉D u p l i c a t e H a n d l e函数,你希望目标进程的句柄拥有与源进程句柄相同的访问屏蔽。使用该标志将使 D u p l i c a t e H a n d l e忽略它的d w D e s i r e d A c c e s s参数。
       如果设定了D U P L I C AT E _ C L O S E _ S O U R C E标志,则可以关闭源进程中的句柄。该标志使得一个进程能够很容易地将内核对象传递给另一个进程。当使用该标志时,内核对象的使用计数不会受到影响。
       下面用一个例子来说明D u p l i c a t e H a n d l e函数是如何运行的。在这个例子中, Process S是目前可以访问某个内核对象的源进程, Process T是将要获取对该内核对象的访问权的目标进程。
        Process C 是执行对D u p l i c a t e H a n d l e调用的催化进程。Process C的句柄表(表3 - 4)包含两个句柄值,即1和2。句柄值1用于标识Process S的进程内核对象,句柄值2则用于标识Process T 的进程内核对象。
                                                             表3-4 Process C的句柄表
索 引                        内核对象内存块                            访问屏蔽                                               标 志
                                          的指针                     (标志位的D W O R D)             (标志位的D W O R D)
1                               0 x F 0 0 0 0 0 0 0                  0 x ? ? ? ? ? ? ? ?                         0 x 0 0 0 0 0 0 0 0
                           (Process S 的内核对象)
2                               0 x F 0 0 0 0 0 1 0                 0 x ? ? ? ? ? ? ? ?                          0 x 0 0 0 0 0 0 0 0
                           (Process T 的内核对象)
        表3 - 5是Process S的句柄表,它包含句柄值为 2的单个项目。该句柄可以标识任何类型的内核对象,就是说它不必是进程的内核对象。

                                                              表3-5 Process S的句柄表


索 引                    内核对象内存块                                访问屏蔽                                              标 志
                                    的指针                              (标志位的D W O R D)         (标志位的D W O R D)
1                        0 x 0 0 0 0 0 0 0 0                                       (无)                                               (无)
2                        0 x F 0 0 0 0 0 2 0                           0 x ? ? ? ? ? ? ? ?                       0 x 0 0 0 0 0 0 0 0
                              (任何内核对象)
          表3 - 6显示了Process C调用D u p l i c a t e H a n d l e函数之前Process T的句柄表包含的项目。如你所见, Process T的句柄表只包含句柄值为2的单个项目,句柄项目1目前未用。
                                      表3-6 调用D u p l i c a t e H a n d l e函数之前Process T 的句柄表
索 引 内核对象内存块 访问屏蔽 标 志
的指针 (标志位的D W O R D) (标志位的D W O R D)
1 0 x 0 0 0 0 0 0 0 0 (无) (无)
2 0 x F 0 0 0 0 0 3 0 0 x ? ? ? ? ? ? ? ? 0 x 0 0 0 0 0 0 0 0
(任何内核对象)
         如果Process C现在使用下面的代码来调用D u p l i c a t e H a n d l e,那么只有Process T的句柄表改变更,如表3 - 7所示。
                                        表3-7 调用D u p l i c a t e H a n d l e函数之后Process T的句柄表
索 引 内核对象内存块 访问屏蔽 标 志
的指针 (标志位的D W O R D) (标志位的D W O R D)
1 0 x F 0 0 0 0 0 0 2 0 0 x ? ? ? ? ? ? ? ? 0 x 0 0 0 0 0 0 0 1
2 0 x F 0 0 0 0 0 0 3 0 0 x ? ? ? ? ? ? ? ? 0 x 0 0 0 0 0 0 0 0
(任何内核对象)
        Process S的句柄表中的第二项已经被拷贝到 Process T的句柄表中的第一项。 D u p l i c a t eH a n d l e也已经将值1填入Process C的h O b j变量中。值1是Process T的句柄表中的索引,新项目将被放入该索引。
        由于D U P L I C AT E _ S A M E _ A C C E S S标志被传递给了D u p l i c a t e H a n d l e,因此Process T的句柄表中该句柄的访问屏蔽与 Process S 的句柄表项目中的访问屏蔽是相同的。另外,传递D U P L I C AT E _ S A M E _ A C C E S S标志将使D u p l i c a t e H a n d l e忽略它的D e s i r e d A c c e s s参数。最后请注意,继承位标志已经被打开,因为给 D u p l i c a t e H a n d l e的b I n h e r i t H a n d l e参数传递的是T R U E。
        显然,你永远不会像在这个例子中所做的那样,调用传递硬编码数字值的 D u p l i c a t e H a n d l e函数。这里使用硬编码数字,只是为了展示函数是如何运行的。在实际应用程序中,变量可能拥有各种不同的句柄值,可以传递该变量,作为函数的参数。
        与继承性一样, D u p l i c a t e H a n d l e函数存在的奇怪现象之一是,目标进程没有得到关于新内核对象现在可以访问它的通知。因此, Process C必须以某种方式来通知 Process T,它现在拥有对内核对象的访问权,并且必须使用某种形式的进程间通信方式,以便将 h O b j中的句柄值传递给Process T。显然,使用命令行参数或者改变Process T的环境变量是不行的,因为该进程已经启动运行。因此必须使用窗口消息或某种别的 I P C机制。
        上面是D u p l i c a t e H a n d l e的最普通的用法。如你所见,它是个非常灵活的函数。不过,它很少在涉及3个不同进程的情况下被使用(因为 Process C不可能知道对象的句柄值正在被 P r o c e s sS使用)。通常,当只涉及两个进程时,才调用 D u p l i c a t e H a n d l e函数。比如一个进程拥有对另一个进程想要访问的对象的访问权,或者一个进程想要将内核对象的访问权赋予另一个进程。例如, Process S拥有对一个内核对象的访问权,并且想要让 Process T能够访问该对象。若要做到这一点,可以像下面这样调用D u p l i c a t e H a n d l e :
        在这个例子中,对G e t C u r r e n t P r o c e s s的调用将返回一个伪句柄,该句柄总是用来标识调用端的进程Process S。一旦D u p l i c a t e H a n d l e返回, h O b j P r o c e s s T就是与Process T相关的句柄,它
所标识的对象与引用Process S中的代码时h O b j P r o c e s s S的句柄标识的对象相同。 Process S决不
应该执行下面的代码:
       如果Process S要执行该代码,那么对代码的调用可能失败,也可能不会失败。如果 P r o c e s sS恰好拥有对内核对象的访问权,其句柄值与 h O b j P r o c e s s T的值相同,那么调用就会成功。该代码的调用将会关闭某个对象,这样 Process S就不再拥有对它的访问权,这当然会导致应用程序产生不希望有的行为特性。
        下面是使用D u p l i c a t e H a n d l e函数的另一种方法。假设一个进程拥有对一个文件映射对象的读和写访问权。在某个位置上,一个函数被调用,它通过读取文件映射对象来访问它。为了使应用程序更加健壮,可以使用 D u p l i c a t e H a n d l e为现有的对象创建一个新句柄,并确保这个新句柄拥有对该对象的只读访问权。然后将只读句柄传递给该函数,这样,该函数中的代码就永远不会偶然对该文件映射对象执行写入操作。下面这个代码说明了这个例子:
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值