Windows内核对象
- 作者: luckzj
- 发表时间: 2010年7月29日
- 本文链接: http://www.soft-bin.com/html/2010/07/29/windows_kernel_object.html
- copy right (c) http://soft-bin.com all right reserved.
- 转载请注明出处
Windows 内核对象的概念是一个Windows程序开发人员所必须掌握的基本概念,遗憾的是很多Windows开发人员对此并不是十分了解。这种情况对于经验丰富的C++程序员来说,相对还要好一些,但对于一些高级语言的使用者,例如C#的开发人员来说,由于高级语言方便的特性,更加导致开发者对底层的忽视。这也可以算是使用高级语言的弊端吧。
这篇文章仅对内核对象作一个大致的介绍,但其中涉及到的操作系统的其他方面的知识,则不作深入讨论。有关Windows编程更多的文章,请查看本站有关Windows编程的其他文章: http://soft-bin.com。
什么是内核对象
内核对象是由操作系统维护管理的一个内存块。进程通过使用使用内核提供的API函数来创建管理对象。常见的内核对象包括文件对象,事件对象,互斥对象,管道对象,进程对象等等。
进程在创建内核对象时,会得到一个内核对象的句柄(HANDLE)用以对内核对象进行操作。这个句柄实际上是一个整型值,表示该内核对象在进程句柄表中的所引值。
进程句柄表
每个进程在创建时,系统都会为它分配一个句柄表。该句柄表用于保存内核对象的句柄。句柄表中包含了该内核对象的内存块的指针,访问屏蔽标志等信息。而我们熟知的内核对象的句柄,则是内核对象在这张句柄表中的位置索引。
在创建一个内核对象时,系统会查找进程句柄表,找到一个空的表项,将内核对象信息填入其中,并将索引值以句柄的形式返回给调用者。
不同的进程会有各自不同的进程句柄表,因此我们如果我们将一个进程中的某个句柄拿到另外一个进程中就会变得完全没有意义,因为我们并不知道另外一个进程的句柄表的相应位置保存的是什么信息。有可能会是其他内核对象的信息,也有可能根本就没有信息。
内核对象计数
内核对象由系统管理。系统为每一个内核对象维护一个计数器。当我们创建一个内核对象时,计数器被初始化。当这个内核对象再次被某个进程所打开(打开的含义是指在进程句柄表中新建一个表项,同一个进程可以多次打开一个内核对象)时,计数器会自动加1。
打开内核对象的方式有很多,例如使用OpenXXX方法打开,又或者可以通过子进程继承父进程内核对象的方式打开,这些我将会在后面介绍。
我们可以调用CloseHandle来关闭一个内核对象句柄,每次我们调用这个函数时,内核对象的引用计数就会减1,当引用计数为0时,则系统会自动释放掉这个内核对象。
这里就存在一个问题,是不是如果我们不调用CloseHandle,就一定会造成系统的内存泄漏呢? 这个问题是不一定的,在进程退出时,系统会扫描进程句柄表,找到尚未关闭的句柄,为其调用CloseHandle以关闭对内核对象的引用。
创建一个内核对象
通常我们使用 CreateXXX 一类的函数来创建一个内核对象,例如:
HANDLE CreateThread(
PSECURITY_ATTRIBUTES pas,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE pfnStartAddr,
PVOID pvParam,
DWORD dwCreationFlags,
PDWORD pdwThreadId);
HANDLE CreateFile(
PCTSTR pszFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
PSECURITY_ATTRIBUTES psa,
DWORD dwCreationDistribution,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile);
HANDLE CreateFileMapping(
HANDLE hFile,
PSECURITY_ATTRIBUTES psa,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
PCTSTR pszName);
我们可以发现这些函数里都会有一个参数是 PSECURITY_ATTRIBUTES psa,这个结构是用来控制内核兑现的访问权限,其定义如下:
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES;
这个结构中lpSecurityDescriptor用于设置内核对象的访问权限,而bInheritHandle则用于指定此内核对象是否可被子进程所继承。
实际上我们判断一个对象是否是内核对象有两个比较简单的方法:
1: 内核对象通常通过句柄来进行控制,这个句柄的数据类型是 HANDLE
2: 内核对象在创建时,需要传入一个 SECURITY_ATTRIBUTES结构以控制其访问权限
在创建内核对象时,如果创建成功,则返回内核对象的句柄,但如果创建失败,则可能会返回0(NULL),也可能会返回-1(INVALID_HANDLE_VALUE),我们需要查阅MSDN,以确保对于返回值的检查不出现问题。
大多数情况下,错误返回会是NULL,而对于CreateFile,如果未能打开指定的文件,则会返回 INVALID_HANDLE_VALUE。
跨进程共享内核对象
内核对象属于内核所有,我们可以跨进程共享同一个内核对象。进程之间的同步操作,很多时候都会使用到跨进程共享内核对象。
跨进程共享内核对象的方式有很多,我将分别加以介绍。
父子进程之间通过继承共享内核对象
内核对象可以被子进程所继承,将前面讲到的 SECURITY_ATTRIBUTES 结构中的 bInheritHandle 设置为 TRUE,则这个内核对象可以被子进程所继承。
所谓的继承,实际上是指在创建子进程时,如果创建者指明需要子进程继承自身的句柄,则系统会扫描创建者进程的句柄表,将安全标志中标识为可以继承的句柄表项,拷贝到子进程句柄表中的对应位置,使得句柄,也就是句柄表索引值一致,而后将内核对象计数器加1。
子进程在继承了父进程的内核对象后,自身是不知道自己继承了什么句柄的,父进程需要使用其他方式将句柄值通知给子进程。这些方式可以包括使用命令行传入,使用环境变量或者是直接使用PostMessage发送消息给子进程等。
在有些时候,我们希望改变内核对象的可继承性,以使得不同的子进程可以继承不同的内核对象,在这个时候我们可以使用
SetHandleInformation(HANDLE hObj, DWORD dwMask, DWORD dwFlags)
来对内核对象属性进行设置,例如如果需要设置继承属性为不可继承,则作如下调用:
SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, 0)
通过命名对象共享内核对象
第二种共享内核对象的方式是通过给对象命名。很多内核对象是可以命名的,例如:
HANDLE CreateMutex(
PSECURITY_ATTRIBUTES psa,
BOOL bInitialOwner,
PCTSTR pszName);
HANDLE CreateEvent(
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
BOOL bInitialState,
PCTSTR pszName);
这些创建函数最后一个参数都是内核对象的名称,我们可以传入NULL以创建一个匿名内核对象。在创建一个命名对象时,系统首先会检查是否已经存在这个对象,如果不存在,则创建之,如果存在,则检查此对象与要创建的对象是否类型相同,不同则返回错误,相同则返回此对象的句柄,并将此对象的引用计数加1。
实际上我们可以不用使用CreateXX来打开命名对象,而是使用OpenXX来打开:
HANDLE OpenMutex(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);
HANDLE OpenEvent(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);
这些函数在内核对象不存在时,不会去创建它,而是会返回NULL。
前面我们说到内核对象的名字如果冲突,会引起程序的异常,这种情况在拥有终端服务器的机器上会好一些。服务程序的命名内核对象总是放在全局名字空间中,应用程序的命名内核对象将会放入Session的名字空间中。我们也可以强制指定名字空间,使用 Global\Name的形式指定对象进入全局名字空间,使用Local\Name 的形式指定对象进入局部名字空间。
复制对象句柄以共享内核对象
共享内核对象的最后一个方法是复制对象句柄。我们使用 DuplicateHandle来复制对象句柄,其原型如下:
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle,
HANDLE hSourceHandle,
HANDLE hTargetProcessHandle,
PHANDLE phTargetHandle,
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwOptions);
这个函数取出一个进程的句柄表中的项目,然后将其拷贝到另一个句柄表中。这里需要注意的是,句柄仅仅是句柄表的索引值,只与其所属进程相关,而在其所属进程之外,则完全没有意义。
