《Windows核心编程》第3章 内核对象

Windows 系统创建和处理的内核对象有:访问令牌对象、事件对象、文件对象、文件映射对象、I/O完成端口对象、作业对象、邮件槽对象、互斥对象、管道对象、进程对象、信号量对象、线程对象、可等待的计时器对象以及线程池工厂对象等等。利用Sysinternals提供的免费工具WinObj可以查看一个包含所有内核对象类型的列表。

每个内核对象都只是一个内存块,它由操作系统内核分配,并只能由操作系统内核访问。该内存块是一个数据结构,其成员维护着与对象相关的信息。少数成员(如安全描述符和使用计数等)是所有对象共有的,但大多数成员都是不同类型对象特有的。

在调用一个创建内核对象的函授后,函授会返回一个句柄,它标识了所创建的对象。在32位Windows进程中,句柄是一个32位值;在64位Windows进程中,则是一个64位值。

内核对象的所有者是操作系统,而不是进程。我们在进程中通过函数调用来创建的内核对象,当进程终止运行,该内核对象大多数情况下会被销毁,除非另一个进程正在使用该内核对象。简单的说,内核对象的生命期可能长于创建它的进程。

操作系统通过内核对象数据成员中的“使用计数”来管理内核对象的生命周期。当内核对象创建时,其使用计数设为1。其他进程获得该内核对象的访问后,使用计数会递增。进程终止后,操作系统将自动递减此进程仍然打开的所有内核对象的使用计数。当内核对象的使用计数变为0时,操作系统就会销毁该内核对象。
内核对象的安全描述符指明了谁(通常是对象的创建者)拥有对象、哪些组和用户被允许访问或使用此对象、哪些组和用户被拒绝访问此对象。

Windows中有些对象属于用户对象或GDI对象(如菜单、窗口、鼠标光标、画刷和字体),而非内核对象。判断某个对象是不是内核对象,最简单的方式是查看创建该对象函数的所有参数中有无指定安全属性信息的参数。

进程在初始化时,操作系统为它分配一个句柄表。这个句柄表仅给内核对象使用,不适用于用户对象或GDI对象。

通过调用CloseHandle向系统表明我们已经结束使用该内核对象。该函数内部首先检查主调进程的句柄表,验证该句柄值是否对于该进程有效。如果有效,系统将该获得内核对象的数据结构地址,并将结构中的“使用计数”递减。如果使用计数变为0,内核对象被销毁,并从内存中去除。一旦调用 CloseHandle,我们的进程就不能访问这个内核对象了。通常比较好的做法是在调用CloseHandle后,并将标识内核对象的句柄变量设置为 NULL。

在应用程序运行时,它有可能会泄露内核对象,但当进程终止运行,操作系统能保证一切都能被正确清除。这适用于所有内核对象、资源(包含GDI对象)以及内存块。

要在应用程序运行期间检测内核对象是否泄露,可以利用Windows任务管理器。需要选择“查看|选择列”菜单,然后在弹出“选择进程页列”对话框中勾选上“句柄数”。如果发现某个应用程序的句柄数持续增长,接下来我们就可以使用Sysinternals提供的免费工具Process Explore来确定哪些内核对象没有关闭。

利用文件映射对象,可以在同一台机器上运行的两个不同进程之间共享数据块;借助邮件槽和命名管道,在网络中的不同计算机上运行的进程可以相互发送数据块;互斥量、信号量和事件允许不同进程中的线程同步执行。

可以利用三种不同的机制来允许进程共享内核对象:使用对象句柄继承、为对象命名、复制对象句柄。

为了使用对象句柄继承,父进程要执行以下步骤:
1)父进程创建的内核对象具有可以被继承的属性(通过给安全属性结构中变量bInheritHandle赋值TRUE);
2)父进程创建生成子进程(通过调用函数CreateProcess完成,并将其参数bInheritHandles赋值TRUE,表明子进程能够继承父进程的“可继承的句柄”);

对象句柄的继承只会发生在生成子进程的时候。如果父进程后来又创建了新的内核对象,并将标识它们的句柄设为可继承的句柄,那么正在运行的子进程是不会继承这些新句柄的。

内核对象的内容保存在内核地址空间中--系统上运行的所有进程都共享这个空间。对于32位系统,这是从0x80000000到0xFFFFFFF之间的内存空间。对于64位系统,则是从0x00000400’00000000到0xFFFFFFFF’FFFFFFFF之间的内存空间。

为了使子进程得到它想要的内核对象的句柄值的几种方式:
1)最常见的方式是在父进程创建子进程时,将句柄值作为命令行参数传给子进程;
2)父进程等待子进程完成初始化,然后父进程将一条消息发送或发布到子进程中的一个线程创建的一个窗口;
3)父进程向其环境块添加一个环境变量,变量名称是子进程知道的。而变量的值则是准备被子进程继承的那个内核对象的句柄值。然后利用子进程会继承父进程的环境变量的特性,子进程能够通过调用GetEnvironmentVariable来获得继承到的内核对象的句柄值。(由于环境变量可以反复继承,所以这种方式对于如果子进程还要生成另一个子进程就非常合适!)

可以调用SetHandleInformation函数来改变内核对象句柄的继承标志:
BOOL SetHandleInformation(HANDLE hObject, DWORD dwMask, DWORD dwFlags);
相反,通过调用GetHandleInformation函数来返回指定句柄的当前标志:
BOOL GetHandleInformation(HANDLE hObject, PDWORD pdwFlags);

微软没有提供专门的机制来保证为内核对象指定的名称是唯一的!所以保证内核对象名称唯一性的只能是程序员自己!

通过命名的对象方式来共享内核对象,进程之间不必满足存在父子进程关系的条件。

用于创建内核对象的函数总是返回具有完全访问权限的句柄。如果想限制一个句柄的访问权限,可以使用这些函数的扩展版本(名称带Ex后缀),它们接受一个额外的DWORD dwDesiredAccess参数。

调用Create*函数和调用Open*函数的主要区别是:如果内核对象不存在,Create*函数会创建它。而Open*函数不同,如果对象不存在,它只是简单的以调用失败而结束。

可以利用命名对象来防止运行一个应用程序的多个实例。例如可以在应用程序初始化的地方,调用Create*函数来创建一个命名对象,然后再调用 GetLastError。如果GetLastError返回ERROR_ALREADY_EXISTS,则表明应用程序的另一个实例正在运行,新的实例可以退出。

一个服务的命名内核对象始终位于全局命名空间内。默认情况下,在终端服务中,应用程序自己的命名内核对象在会话的命名空间内。我们可以通过在其名称前面加上“Global/”前缀,来强制把一个命名对象放入全局命名空间。也可以显示指出我们把一个内核对象放入当前会话的命名空间,只需在名称前加上 “Local/”的前缀。

复制对象句柄可以通过调用函数DuplicateHandle,这个函数获得一个进程的句柄表中的记录项,然后在另一个进程的句柄表中创建这个记录项的一个副本。该函数最常见的一种用法可能涉及系统中同时运行的三个不同进程。举例说明该函数的工作方式:
进程S是源进程,它拥有对一个内核对象的访问权;进程T是目标进程,它将获得对这个内核对象访问权。进程C则是一个催化剂进程,它执行对DuplicateHandle的调用。
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle,// 拥有源句柄的那个进程的句柄。如源句柄从属于当前进程,则使用GetCurrentProcess
    HANDLE hSourceHandle,         // 指定对象的现有句柄
    HANDLE hTargetProcessHandle,// 即将拥有新对象句柄的一个进程的句柄。如源句柄从属于当前进程,则使用GetCurrentProcess
    LPHANDLE lpTargetHandle,    // 指定用于装载新句柄的一个长整型变量
    DWORD dwDesiredAccess,
    BOOL bInheritHandle,
    DWORD dwOptions
    );

实际中涉及三个不同进程时,复制对象句柄是很少使用的。通常只涉及两个进程的时候才会调用DuplicateHandle函数。如以下场景:进程S能访问一个内核对象,并希望进程T也能访问这个对象,可以像如下方式使用DuplicateHandle函数:
    /*** 在进程S中执行以下的代码 ***/
    // 创建一个进程S能访问的互斥对象
    HANDLE hObjInProcessS = CreateMutex(NULL, FALSE, NULL);
    // 得到进程T的进程句柄,dwProcessIdT是进程T的进程Id
    HANDLE hProcessT = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessIdT);
    // 相对进程T的未被初始化的句柄
    HANDLE hObjInProcessT;
    // 使进程T能够访问互斥对象
    DuplicateHandle(GetCurrentProcess(), hObjInProcessS, hProcessT,
                    &hObjInProcessT, 0, FALSE, DUPLICATE_SAME_ACCESS);
    // 利用进程间通讯机制,在进程T中得到互斥对象句柄值
    ......
    // 当不需要与进程T通讯时,关闭得到的进程T句柄
    CloseHandle(hProcessT);
    // 当进程S不再需要使用互斥对象时,也关闭之
    CloseHandle(hObjInProcessS);


调用GetCurrentProcess()返回的是一个伪句柄,该句柄始终标识主调进程(即进程S)。当函数DuplicateHandle返回后,hObjInProcessT就是一个相对于进程T的句柄,它标识的对象就是进程S中hObjInProcessS所标识的对象。注意在进程S中绝不要执行这句代码:CloseHandle(hObjInProcessT);// Do not execute in process S

你可能感兴趣的:(数据结构,编程,windows,null,Access,通讯)