什么是”内核对象”
作为 Windows 软件开发人员,你经常都要创建、打开和处理内核对象。系统会创建和处理 几种类型的内核对象,比如访问令牌对象、事件(event)对象、文件对象、文件映射 对象、I/O 完成端口对象、作业对象、mailslot 对象、mutex 对象、pipe 对象、进程对象、semaphore 对象、线程对象、waitable timer 对象以及thread pool worker factory 对象等等.
每个内核对象都只是一个内存块,它由内核分配,并 只能由内核访问。这个内存块是一个数据结构,其成员维护着与对象相关的信息。内核对象的数据结构只能由内核访问,所以应用程序不能在内存中定位这些数据结构并 直接更改其内容, 所以Microsoft提供了一组API函数. 这些API会返回一个HANDLE.这个HANDLE可以传给其他的API. 来修改HANDLE所承载的数据结构的数据.
为了增强操作系统的可靠性,这些句柄值是与进程相关的。所以,如果将句柄值传给另一个 进程中的线程(通过某种进程间通信方式),那么另一个进程用你的进程的句柄值来发出调 用时,就可能失败(Dos时代只能运行一个进程. 但是Windows时代, 每个进程都有4G的独立地址空间. 所以你这个进程的虚拟地址数据传到另一个进程那并没有用);甚至更糟,它们会在你的进程句柄表的同一个索引位置处,创建到一个完全不同的内核对象的引用.
每个内核对象都有一个引用计数.初次创建一个对象的时候,其使用计数被设为 1。另一个进程获得对现有内核对象的访问后,使用计数就会递 增。进程终止运行后,内核将自动递减此进程仍然打开的所有内核对象的使用计数。一个对 象的使用计数变成 0,内核就会销毁该对象。这样可以保证系统中不存在没有被任何 进程引用的内核对象。而系统也提供了一个这样的API:BOOL CloseHandle(HANDLE hObject); 它会减少内核对象的1点引用计数.
共享内核对象
那么如果有时候需要在不同进程中运行的线程需要共享内核对象那该怎么办呢?有三种方法.
1. 使用对象句柄继承
只有在进程之间有一个父----子关系的时候,才可以使用对象句柄继承。在这种情况下,父进 程有一个或多个内核对象句柄可以使用,而且父进程决定生成一个子进程,并允许子进程访 问父进程的内核对象。为了使这种继承生效,父进程必须执行几个步骤。首先为了创建一个可继承的句柄,那么父进程必须分配并初始化一个 SECURITY_ATTRIBUTES 结 构,并将这个结构的地址传给具体的Create函数. 下面举个例子:
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL; // 默认安全描述符
sa.bInheritHandle = TRUE; // 使返回的句柄成为可继承的句柄
HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);
为了使用对象句柄继承,下一步是由父进程生成子进程。这是通过 CreateProcess 函数来完 成的,如下所示:
BOOL CreateProcess(
PCTSTR pszApplicationName,
PTSTR pszCommandLine,
PSECURITY_ATTRIBUTES psaProcess,
PSECURITY_ATTRIBUTES psaThread,
BOOL bInheritHandles,
DWORD dwCreationFlags,
PVOID pvEnvironment,
PCTSTR pszCurrentDirectory,
LPSTARTUPINFO pStartupInfo,
PPROCESS_INFORMATION pProcessInformation);
如果向bInheritHandles这个参数传递 TRUE,子进程就会继承父进程的“可继承的句柄”的值。然后操作系统会创建新的子进程,但不允许子进程立即执行它的代码。当然,系统会
为子进程创建一个新的、空白的进程句柄表——就像它为任何一个新进程所做的那样。但是,
由于你向 CreateProcess 函数的 bInheritHandles 参数传递了 TRUE,所以系统还会多做一
件事情:它会遍历父进程的句柄表,对它的每一个记录项进行检查。凡是包含一个有效的“可
继承的句柄”的项,都会被完整地拷贝到子进程的句柄表。在子进程的句柄表中,拷贝项的
位置与它在父进程句柄表中的位置是完全一样的。这是非常重要的一个设计,因为它意味着:
在父进程和子进程中,对一个内核对象进行标识的句柄值是完全一样的, 访问掩码和标志也都一样. 除了拷贝句柄表的记录项,系统还会递增内核对象的使用计数,因为两个进程现在都在使用 这个对象。一个内核对象要想被销毁,父进程和子进程要么都对这个对象调用 CloseHandle, 要么都终止运行。子进程不一定先终止——但父进程也不一定。事实上,父进程可以在 CreateProcess 函数返回之后立即关闭它的内核对象句柄,子进程照样可以操纵这个对象。
当然,也可以使用其他进程间通信技术将继承的内核对象句柄值从父进程传入子进程。使用其他进程间通信技术将继承的内核对象句柄值从父进程传入子进程。一个
技术是让父进程等待子进程完成初始化(利用第9章讨论的 WaitForInputIdle 函数);然后, 父进程可以将一条消息 send 或 post 到由子进程中的一个线程创建的一个窗口。
当然, 也可以让父进程向其环境块添加一个环境变量。变量的名称应该是子进程知道去查找 的一个名称,而变量的值应该是准备被子进程继承的那个内核对象的句柄值。然后,当父进程生成子进程的时候,这个子进程会继承父进程的环境变量,所以能轻松调用 GetEnvironmentVariable 来获得这个继承到的内核对象的句柄值。如果子进程还要生成另一个子进程,这种方式就非常不错,因为环境变量是可以反复继承的。
有时可能遇到这样一种情况:父进程创建了一个内核对象,得到了一个可继承的句柄,然后
生成了两个子进程。但是,父进程只希望其中的一个子进程继承内核对象句柄。换言之,你 有时可能想控制哪些子进程能继承内核对象句柄。可以调用 SetHandleInformation 函数来 改变内核对象句柄的继承标志。如下所示:
BOOL SetHandleInformation(
HANDLE hObject, // 要改变标志的内核对象句柄
DWORD dwMask, // 要改成什么标志
DWORD dwFlags);
要打开继承标志可以这样写:
SetHandleInformation(hHandle, HANDLE_FLAG_INHERIT,HANDLE_FLAG_INHERIT);
关闭则是这样写:
SetHandleInformation( hObj , HANDLE_FLAG_INHERIT , 0);
而要得到句柄的相关信息. 可以用这个API:
BOOL GetHandleInformation(HANDLEhObject, PDWORD pdwFlags);
如果要判断是否可以继承: 可以这样写
DWORD dwFlags;
GetHandleInformation(hObj, &dwFlags);
BOOL bIsInheritable = (0 != (dwFlags & HANDLE_FLAG_INHERIT));
2.为对象命名来共享内核对象
许多内核对象创建的时候都是可以命名的. 比如互斥体(Mutex)、事件(event)、信号
(semaphore)、内存映射文件(FileMapping)、作业(JobObject). 一般内核对象的名字的赋值位置位于创建内核对象API的最后一处.
每个内核对象的名称都是唯一的. 如果创建了一个内核对象和之前的同名. 那么将会创建失败并且返回NULL. GetLastError()将会返回6(ERROR_INVALID_HANDLE).
当进程A创建了进程B(进程B不一定是进程A的子进程)后,进程A中有一个名为”ProcessAMutex”的互斥体对象. 进程B则可以通过CreateMutex(NULL, FALSE, TEXT(“ProcessAMutex”));来获得对象. 如果进程B中没有”ProcessAMutex”这样的互斥体对象, 进程B也将会新建一个. 并将互斥体对象的引用计数加一. 必须ProcessA和ProcessB都CloseHandle才能销毁这个内核对象.
倘若采用OpenMutex(MUTEXT_ALL_ACCESS,FALSE, T(“ProcessAMutex”))获得内核对象. 如果失败OpenMutext将会返回NULL.
3.跨进程边界共享对象
为了跨越进程边界来共享内核对象, 也可以使用DuplicateHandle函数:
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle,
HANDLE hSourceHandle,
HANDLE hTargetProcessHandle,
PHANDLE phTargetHandle,
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwOptions);
现在假设ProcessS能访问一个内核对象, 并希望ProcessT也能访问这个对象, 那么可以这样调用:
/ All of the following code is executed by Process S.
// Create a mutex object accessible by Process S.
HANDLE hObjInProcessS = CreateMutex(NULL, FALSE, NULL);
// Get a handle to Process T's kernel object.
HANDLE hProcessT = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessIdT);
// An uninitialized handle relative to Process T.
HANDLE hObjInProcessT;
// Give Process T access to our mutex object.
DuplicateHandle(GetCurrentProcess(), hObjInProcessS, hProcessT,
&hObjInProcessT, 0, FALSE, DUPLICATE_SAME_ACCESS);
// Use some IPC mechanism to get the handle value of hObjInProcessS into Process T.
...
// We no longer need to communicate with Process T.
CloseHandle(hProcessT);
...
// When Process S no longer needs to use the mutex, it should close it.
CloseHandle(hObjInProcessS);
还可以通过另一种方式来使用 DuplicateHandle :假设一个进程拥有对一个文件映射对象的 读写权限。在程序中的某个位置,我们调用了一个函数,并希望它对文件映射对象进行只读 访问。为了使你的应用程序变得更健壮,可以使用 DuplicateHandle 为现有的对象创建一个 新句柄,并确保这个新句柄只设置了只读权限。然后,把这个只读句柄传给函数。采取这种 方式,函数中的代码绝对不会对文件映射对象执行意外的写入操作。以下代码对此进行了演示:
// Create a file-mapping object; the handle has read/write access.
HANDLE hFileMapRW = CreateFileMapping(INVALID_HANDLE_VALUE,
NULL, PAGE_READWRITE, 0, 10240, NULL);
// Create another handle to the file-mapping object;
// the handle has read-only access.
HANDLE hFileMapRO;
DuplicateHandle(GetCurrentProcess(), hFileMapRW, GetCurrentProcess(),
&hFileMapRO, FILE_MAP_READ, FALSE, 0);
// Call the function that should only read from the file mapping.
ReadFromTheFileMapping(hFileMapRO);
// Close the read-only file-mapping object.
CloseHandle(hFileMapRO);
// We can still read/write the file-mapping object using hFileMapRW.
...
// When the main code doesn't access the file mapping anymore,
// close it.
CloseHandle(hFileMapRW);