每个内核对象都只是一个内存块,它由操作系统内核分配,并只能由操作系统内核访问。这个内存块是一个数据结构,其成员维护着与对象相关的信息。少数成员(安全描述符和使用计数)是所有对象都有的,但其他大多数成员都是不同类型的对象特有的
由于内核对象的数据结构只能由操作系统内核访问,所以应用程序不能在内存中定位这些数据结构并直接更改其内容。正因为有这个限制,所以微软能自由地添加、删除或修改这些数据结构中的成员,同时不会干扰任何应用程序的正常运行
为了增强系统的可靠性,内核对象的句柄是与进程相关的。如果进程A将一个属于A进程的内核对象句柄传送给进程B,那么进程B中的线程用进程A传入的句柄发出调用时,就有可能失败;更糟糕的情况是,进程B会根据传入的句柄值在进程B的句柄表中引用另一个完全不同的内核对象,将导致无法预测的后果
内核对象的所有者是操作系统内核,而非进程
使用计数:
操作系统内核知道有多少个进程正在使用一个特定的内核对象,因为每个对象都包含一个使用计数(usage count)
使用计数是所有内核对象类型都有的一个数据成员
初次创建一个对象的时候,其使用计数被设为1。另一个进程获得对现有内核对象的访问后,使用计数就会递增。进程终止运行后,操作系统内核将自动递减此进程仍然打开的所有内核对象的使用计数
如果一旦对象的使用计数递减为0,操作系统就会销毁该对象
内核对象可以用一个安全描述符(security descriptor, SD)来保护。安全描述符描述了谁拥有对象;哪些组和用户被允许访问或使用此对象;那些组和用户被拒绝访问此对象
进程内核对象句柄表(后文简称句柄表):
一个进程在初始化时,系统将为它分配一个句柄表(handle table)。这个句柄表仅供内核对象使用,不适用于用户对象或GDI对象
句柄表是一个由数据结构组成的数组。每个结构都包含一个内核对象指针、一个访问掩码和可继承标志,如:
一个进程首次初始化的时候,其句柄表为空。当进程内的一个线程调用一个会创建内核对象的函数,内核将为这个对象分配并初始化一个内存块。然后,内核扫描进程的句柄表,查找一项空白的记录项,并对其进行初始化(指针成员会被设置成内核对象的数据结构的内部内存地址,访问掩码将被设置成拥有完全访问权限,继承标志也会被设置)
用于创建内核对象的任何函数都会返回一个与进程相关的句柄,这个句柄可由同一个进程中运行的所有线程使用。系统用索引来表示内核对象的信息保存在进程句柄表中的具体位置,句柄值除以四即是实际的索引值
调用一个函数时,如果它接受一个内核对象句柄作为参数,就必须把Create*函数返回的值传给它。在内部,这个函数会查找进程的句柄表,获得目标内核对象的地址,然后以一种恰当的方式来操纵对象的数据结构
关闭内核对象(CloseHandle):
该函数首先检查调用进程的句柄表,验证“传给函数的句柄值”标识的是“进程确实有权访问的一个对象”。如果句柄是有效的,系统将获得内核对象的数据结构的地址,并将结构中的“使用计数”成员递减。如果使用计数变为0,内核对象将被销毁,并从内存中除去
就在CloseHandle函数返回之前,它会清楚进程句柄表中对应的记录项——这个句柄现在对我们的进程来说是无效的,不要再试图用它。无论内核对象当前是否销毁,这个清除过程都会发生!一旦调用CloseHandle,我们的进程就不能访问那个内核对象;但是如果对象的使用计数还没递减至0,它就不会被销毁
调用CloseHandle后,应将保存句柄值的变量置NULL。如果不小心使用了未被置NULL的已被CloseHandle的值,将会发生两种情况:
由于此变量所引用的句柄表记录项已被清除,调用函数调用失败
创建一个新的内核对象时,Windows会在句柄表中查找空白记录。所以,未被置NULL的变量极有可能匹配到新创建的内核对象。函数调用时,一旦错误地用这个尚未置NULL的变量,就可能定位到一个错误类型的内核对象(这种情况会报错)。跟糟糕的是,可能会定位到一个类型(和已经关闭的内核对象)相同的内核对象(这种情况不会报错)。这种情况下,应用程序的状态将损坏,没有任何办法可以恢复
当进程终止运行,操作系统会确保此进程使用的所有资源(内核对象、资源、内存块等)都被释放,系统会确保进程不会留下任何东西。对于内核对象,操作系统执行以下操作:进程终止时,系统自动扫描该进程的句柄表。如果这个表中的任何有效记录项(进程终止前没有关闭的对象),操作系统会关闭这些对象的句柄。若某对象的使用计数递减为0,内核就会销毁对象
只有在进程之间有一个父——子关系的时候,才可以使用对象句柄继承
步骤:
当为CreateProcess函数的bInheritHandles传递TRUE时,操作创建新的子进程,但不允许子进程立即执行它的代码。系统会为子进程创建一个新的、空白的进程句柄表——就像它为任何一个新进程所有的那样。系统还会多做一件事:它会遍历父进程的句柄表,对它的每一个及录像进行检查。凡是包含一个有效的“可继承的句柄”的项,都会被完成地复制到子进程的句柄表。在子进程的句柄表中,复制项的位置与它在父进程句柄表中的位置是完全一样的。这意味着:在父进程和子进程中,对一个内核对象进行标识的句柄值是完全一样的
对象句柄的继承只会在生成子进程的时候发生。假如父进程后来又创建了新的内核对象,并同样将它们的句柄设为可继承的句柄。那么正在运行的子进程是不会继承这些新句柄的
对象句柄的继承还有一个非常奇怪的特征:子进程并不知道自己继承了任何句柄。为了使子进程得到它想要的一个内核对象的句柄值,最常用的方式是将句柄值作为命令行参数传递给子进程。可以这样做的原因是,父子进程对一个内核对象进行标识的句柄值是完全一样的
可以调用SetHandleInformation来改变内核对象句柄的继承标志,这样父进程就可以控制哪些子进程能继承内核对象句柄了
当父进程创建一个内核对象时,父进程必须向系统指出它希望这个对象的句柄是可以继承的。注意,只有句柄是可以继承的,对象本身是不能继承的。为了创建一个可继承的句柄,父进程必须分配并初始化一个SECURITY_ATTRIBUTES结构,将SECURITY_ATTRIBUTES.bInheritHandle设为TRUE,并将这个结构的地址传给具体的Create函数
创建子进程,调用CreateProcess来完成。如果参数bInheritHandles被设置为TRUE,子进程就会继承父进程的“可继承句柄”的值
所有进程的所有内核对象都共享同一个命名空间,即使它们的类型并不相同
进程A创建了一个名为“TTMutex”的互斥量内核对象,之后,进程B发生以下调用
HANDLE hMutexProcessB = CreateMutex(NULL, FALSE, TEXT("TTMutex"));
用于创建内核对象的函数Create*总是返回具有完全访问权限的句柄。如果想限制一个句柄的访问权限,可以使用Create*ex
在调用Create*之后,可以立即调用GetLastError,若返回ERROR_ALREADY_EXIST则表明仅仅打开了一个现有的对象,而并非创建了一个新的
微软没有提供任何专门的机制来保证我们创建独一无二的对象名
可以利用命名对象来防止运行一个应用程序的多个实例
当进程B调用CreateMutex时,系统首先会查看是否存在一个名为“TTMutex”的内核对象。由于已存在,所以内核接着检查对象的类型。由于试图创建一个互斥对象,而名为“TTMutex”的对象也是一个互斥对象,所以系统接着执行一次安全检查,验证调用者是否拥有对该对象的完全访问权限。如果答案是肯定的,系统就会在进程B的句柄表中查找一个空白记录项,并将其初始化为指向现有的内核对象。如果类型不匹配,或调用者被拒绝访问,CreateMutex就会失败(返回NULL)
进程B调用CreateMutex时,它会向函数传递安全属性信息和第二个参数,如果已经存在一个指定名称的对象,这些参数就会被忽略
终端服务命名空间:
HANDLE h = CreateEvent(NULL, FALSE, FALSE, TEXT("Global\\MyName"));
也可以显示把一个内核对象放入当前会话的命名空间,只要在名称前加上“Local\”即可:
HANDLE h = CreateEvent(NULL, FALSE, FALSE, TEXT("Local\\MyName"));
在正在运行终端服务的计算机中,有多个用于内核对象的命名空间。其中一个是全局命名空间,所有客户端都能访问的内核对象要放在这个命名空间中。这个命名空间主要由服务使用。此外,每个客户端会话(client session)都有一个自己的命名空间,这样不会相互干扰
服务的命名内核对象始终位于全局命名空间中。默认情况下,在终端服务中,应用程序自己的命名内核对象在会话的命名空间内。可以在其名称前加上“Global\”前缀来强制把一个命名对象放入全局命名空间:
BOOL DuplicateHandle(HANDLE hSourceProcessHandle, ///< 源进程进程内核对象句柄 HANDLE hSourceHandle, ///< 要复制的源内核对象句柄 HANDLE hTargetProcessHandle, ///< 目标进程进程内核对象句柄 LPHANDLE lpTargetHandle, ///< 用来接收得到的句柄值 DWORD dwDesiredAccess, ///< 访问掩码 BOOL bInheritHandle, ///< 继承标志 DWORD dwOptions); ///< 句柄表项,若为DUPLICATE_SAME_ACCESS,函数会忽略dwDesiredAccess;若为DUPLICATE_CLOSE_SOURCE,内核对象的使用计数不变
这个函数获得一个进程的句柄表中的一个记录项,然后在另一个进程的句柄表中创建这个记录项的一个副本
调用函数之后,目标进程不知道它现在能访问一个新的内核对象,必须使用进程间通信方法来通知目标进程(命令行和环境变量均行不通,因为进程早就存在,需考虑其他方法)
假设一个进程拥有对一个文件映射对象的读写权限,可以使用该函数为现有的对象创建一个新句柄,并确保这个新句柄只有只读权限,这样就可以保证新句柄不会有意外的写入操作,这是该函数常用的方法之一
函数原型
原文地址:http://my.oschina.net/simplefocus/blog/290703