内核对象就是在操作系统内核中进行资源分配和管理的一种数据结构。应用程序是无法在其管理的内存中找到这些资源并改变的。也就是说内核对象不是属于某个进程的,而是属于操作系统的。 内核对内核对象的维护:
一个内核对象可能同时被多个进程调用,操作系统内核为了维护内核对象,引入引用计数机制,当一个进程创建内核对象时,其引用计数为1,其他进程引用时,引用计数+1, 到进程调用CloseHandle时,引用计数-1, 当引用计数为0时,内核会销毁该内核对象资源。
内核对象的创建:
虽然内核对象是属于操作系统的,到操作系统为我们提供的一些API, 我们可以在应用程序中调用系统给我们创建内核对象的API, 来让操作系统在内核中给我们创建此内核对象。(在32位系统中,内核对象的内容被保存在0x80000000至0xFFFFFFFF的这个内核地址空间中)
例如:
CreateProcess//创建进程内核对象
CreateEvent//创建Event事件内核对象
int socket(int domain, int type, int protocol) //创建socket套接字内核对象
这些对象的具体数据结构,用户是不知道的,API只给用户返回一个int型的 句柄(文件描述符)。
进程句柄表
进程句柄表(Handle Table)
进程句柄表(Handle Table)是进程维护其使用的内核对象索引的表。当一个进程被初始化时,系统要为他分配一个句柄表
其数据结构由:索引值、该索引对应对象指针等组成
当通过系统API创建内核对象后,会返回一个句柄(索引),就是该内核对象在句柄表中的索引(注意因句柄表可能会分层,所以该句柄最后两位(共32位)表示该对象在句柄表中所在的层数,因此如果要得到实际的索引值, 必将该句柄值右移2位。
句柄
首先让我们来了解一下什么是内核对象。内核对象通过API来创建,每个内核对象是一个数据结构,它对应一块内存,由操作系统内核分配,并且只能由操作系统内核访问。在此数据结构中少数成员如安全描述符和使用计数是所有对象都有的,但其他大多数成员都是不同类型的对象特有的。内核对象的数据结构只能由操作系统提供的API访问,应用程序在内存中不能访问。调用创建内核对象的函数后,该函数会返回一个句柄,它标识了所创建的对象。它可以由进程的任何线程使用。在32位系统中,句柄是一个32位值。64位系统中则是64位值。
很多人对句柄到底是什么东西很疑惑。有人说是指针有人说是索引。其实句柄仅仅是独立于每个进程的句柄表的一个索引。在每个进程中都存在一个句柄表,列出了所有本进程内可以使用的句柄
。它只是一个有数据结构组成的数组,每个结构都包含一个指向内核对象的指针、访问掩码、继承标识等,而句柄仅仅是句柄表数组的下标。由于每个进程都存在句柄表,因此句柄是独立于进程的,虽然将一个进程的句柄传给另一个进程不一定会失败,但是它引用的是另一个进程完全不同的内核对象。后面的跨进程边界共享内核对象将介绍如何跨界成共享内核对象。
内核对象的所有者是操作系统内核,而非进程。也就是说多个进程可以共享一个内核对象。内核对象数据结构内有一个使用计数成员,它是所有对象都有的一个成员,标识该内核对象被引用的次数。刚创建时使用计数被初始化为1,如果有另一个进程获得对此内核对象的访问后,使用计数就会递增。一个使用此内核对象的进程终止后或是对此内核对象调用CloseHandle,操作系统内核会自动递减内核对象的使用计数。一旦计数变为0,操作系统内核就会销毁对象。
安全描述符用以描述内核对象的安全性。它描述了内核对象的拥有者,那组用户可以访问此对象,那组用户无访问权限。安全描述符对应一个数据结构:SECURITY_ATTRIBUTES结构,几乎内核对象在创建时都需要传入此结构,但是大部分情况下都是传入NULL,表示使用默认的安全性。
除了使用内核对象,应用程序还需要使用其他类型的对象,如菜单、窗口、鼠标光标等,这些属于用户对象或GDI对象,而非内核对象。要判断一个对象是不是内核对象,最简单的方法就是查看创建这个对象的函数,几乎所有的创建内核对象的函数都需要指定安全属性信息的参数,而用于创建用户对象的函数都不需要使用安全描述符。
一个进程在刚被创建时,它的句柄表是空的。当进程内的一个线程调用创建内核对象的函数时,内核将为这个对象分配并初始化一个内存块,然后扫描进程的句柄表,查找一个空白的记录项,并对其进行初始化。指针成员将会被初始化为内核对象的地址,继承标志也会被设置。
用于创建内核对象的函数都会返回一个与进程相关的句柄,此句柄可由属于该进程的所有线程引用。调用一个函数,如果它需要一个内核对象句柄的参数,就必须为它传递一个句柄。在内部,这个函数会查找进程的句柄表,获得目标内核对象的地址然后对此数据结构进行操作。如果我们直接使用其他进程的的句柄,那么实际引用的只是那个进程句柄表中位于同一索引的内核对象,它们仅仅是索引值相同而已。创建内核对象的函数在失败时会返回NULL。但有时也有的函数会返回-1,如CreateFile,它返回的是INVALID_HANDLE_VALUE而不是NULL。失败的原因可能是内存不足或是没有权限。这在检查内核对象是否创建成功时要特别注意。
当进程不再使用某内核对象时应该调用CloseHandle来向系统表明我们已经结束使用此对象。在内部该函数会扫描进程的句柄表,如果句柄是有效的,系统就获得此内核对象的数据结构的地址,并将此结构的使用计数成员递减1。如果使用计数变为0,句柄表对应的记录项将会被清除,内核对象将被销毁,所占内存将会被释放。此后再在此进程内使用此句柄将会发生未知错误。因为调用CloseHandle后此内核对象不知是否已经被销毁,如果没有销毁那么此次对此句柄的使用将没有问题。如果此内核对象已被销毁,且句柄表对应项已经被其他项占据,此时操作的将是另一个内核对象,可能发生无法预知的错误。因此在调用CloseHandle后要最好将原来的变量赋值为NULL。
即使在进程结束了对内核对象的访问后,没有调用CloseHandle,进程终止时,操作系统也会确保进程所使用的所有资源都被释放。系统自动扫描进程句柄表,将所有内核对象的使用计数都减1。同样如存在使用计数为0的内核对象,它就会被释放。但是这毕竟不是个好习惯,如果我们开发的程序是长时间运行的程序,由于没有主动调用CloseHandle,进程已经不再使用的内核对象仍然得不到释放,越往后运行系统所占内存就越大。这跟内存泄露很类似,自己开辟的堆空间不再使用时要自己主动释放。对于内核对象这也同样适用。在任务管理器的Handle列可以查看每个进程占用的内核对象数。