您的位置:首页 > 编程语言 > C语言/C++

Windows Via C/C++ Part Ⅰ Chapter3: 内核对象(3)

2009-08-22 23:57 567 查看
跨进程共享内核对象

不同进程中的线程常常需要共享内核对象,比如:
(1) 同一计算机上的两个进程使用文件映射对象共享数据块;
(2) 在互联的计算机上的程序可以借助邮件槽或是命名管道互相发送/接收数据;
(3) 不同进程中的线程可以使用用互斥量、信号量以及事件同步其执行。
内核对象的句柄是进程间独立的,这给跨进程共享内核对象带来了一定的困难。在下面的讨论中,我们将展示三种不同的机制来共享内核对象:对象句柄继承、命名对象以及复制对象句柄。

1 对象句柄继承(Object Handle Inheritance)

  进程间存在父-子关系时,可以使用对象句柄继承,父进程在创建子进程时可以决定子进程是否有权访问父进程句柄表中的某些条目。
  当父进程创建内核对象时,它必须向系统表明该对象的句柄能否被继承。注意没有继承对象这一说法,Windows仅支持对象句柄继承,换句话说,被继承的只是对象的句柄而不是对象本身。要创建可继承的句柄,父进程必须创建SECURITY_ATTRIBUTES结构的变量,将其bInheritHandle设置为TRUE后传递给Create*函数族。下面的代码创建了一个互斥量对象,其句柄是可继承的:
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;   // Make the returned handle inheritable.

HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);

  让我们回过头看看进程句柄表中Flags域的内容。每个句柄表的条目在其Flags域中都有一位用来标志当前对象的句柄是否可继承。使用PSECURITY_ATTRIBUTES为NULL的Create*函数产生的对象句柄是不可继承的,Flags域中的相应位被置为0。如果用上面的代码产生可继承的句柄,该位则为1。表3-2摸拟了这一情形,句柄表中有两个有效条目,索引1的对象句柄不可以继承,而索引3的则可以:



父进程调用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);


  我们在下一章会详细介绍这个函数,现在让我们看看它的bInheriHandles参数。向bInheriHandles传递TRUE时,操作系统在子进程创建完成后并不会立刻运行子进程,而是为它创建一个空的句柄表,然后系统遍历父进程的句柄表,将其中可继承的(Flags相应位为1)条目原封不动的拷贝到子进程句柄表的相同索引处,“相同索引”是很重要的,它保证了被继承的句柄在父子进程中指向同一个内核对象。除了拷贝句柄表条目,内核会将该条目对应的内核对象的引用计数加1。表3-3模拟了上述动作完成后子进程的句柄表,索引3指向的条目和父进程句柄表中索引为3的条目完全一致:



  在第13章“Windows 内存架构”中我们会看到,内核对象存储在可以被系统中所有进程共享的地址空间。在32位系统中,这些地址是从0x8000 0000到0xffff ffff,在64位系统中则是0x0000 0400 0000 0000到0xffff ffff ffff ffff。在上表中,索引3条目的访问掩码和标志位与父进程中的对应条目也是一致的,这意味着如果子进程再创建子进程(原父进程的孙进程)并将CreateProcess的bInheritHandle设为TRUE的话,产生的新进程也会继承该对象的句柄、访问掩码和标志位,对象的引用计数也会加1。
  对象句柄继承只发生在子进程被创建时,假如父进程在创建子进程之后又创建了若干可继承的内核对象,已经运行的子进程是不会继承这些新的句柄的——这很自然,因为句柄表的拷贝只发生在子进程被创建时。
  对象句柄继承有一个很奇怪的特性:子进程并不知道它继承了任何句柄。只有子进程表明它希望在被创建时获得某内核对象的访问权限时,对象句柄继承才是有用的。通常,子进程和父进程都是由同一团队开发的,但是如果子进程将由另一团队编写时,应该将子进程的需求写进文档。
  显然,子进程获得它所希望得到的句柄值最常用的方法是在命令行(command-line)参数中传递该句柄。子进程的初始化代码解析命令行并抽取出句柄值。获得句柄之后,子进程就可以根据自己句柄表中的条目访问对应的内核对象。对象句柄继承起作用的唯一原因是在父子进程中对象的句柄值都是相同的,而且子进程句柄表中的对应条目是从父进程完整拷贝而来的。除了以命令行参数传递句柄值,还可以使用发送消息或是设置环境变量的方式。创建子进程后,父进程可以等子进程完成初始化(调用WaitForInputIdle函数,第9章)后向子进程中的窗口发送消息传递句柄值。使用环境变量时,父进程向其环境数据块中添加一个变量,变量名必须是子进程知道的,变量值是子进程要继承的句柄。然后子进程被创建时会继承父进程的变量数据块,这时子进程就可以调用GetEnvironmentVariable函数获得句柄值了。当子进程打算产生另一个子进程时该方法非常有效,因为变量数据块仍然会被新的子进程继承,这样新的子进程就可以通过同样的方法获得句柄值了。在 http://support.microsoft.com/kb/190351中详细讨论了这种情况。

改变句柄标志
  父进程在创建了子进程之后,可能不想把句柄表中可继承的句柄让以后创建的子进程继承,这就需要改变相应内核对象的标志位,将其设置为不可继承的,可以调用SetHandleInformation更改其标志位:
BOOL SetHandleInformation(
HANDLE hObject,
DWORD dwMask,
DWORD dwFlags);

  函数的第一次个参数是要改变的内核对象的句柄,dwMask是要改变的标志位,通常一个句柄可以关联两个标志位:

#define HANDLE_FLAG_INHERIT            0x00000001
#define HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002

  现在,可以用下面的方法关闭或打开对象句柄的可继承性:
/** 打开句柄的可继承性 */
SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
/** 关闭句柄的可继承性 */
SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, 0);


  HANDLE_FLAG_PROTECT_FROM_CLOSE用来设置该句柄是否能够关闭,比如:
SetHandleInformation(hObj, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE);
CloseHandle(hObj);   // Exception is raised


  在debug模式下,关闭HANDLE_FLAG_PROTECT_FROM_CLOSE为TRUE的句柄时,CloseHandle抛出异常,非debug模式CloseHandle返回FALSE。
如果你要获取内核对象的属性,可以调用GetHandleInformation函数:
BOOL GetHandleInformation(
HANDLE hObject,
PDWORD pdwFlags);


  函数将对象当前标志位的设置写入参数pdwFlags指向的数据中,比如可以使用下面的代码检查某句柄是否可继承:
DWORD dwFlags;
GetHandleInformation(hObj, &dwFlags);
BOOL fHandleIsInheritable = (0 != (dwFlags & HANDLE_FLAG_INHERIT));


2 命名对象
  跨进行共享内核对象的第二种方法是给要共享的对象命名,许多核心对象都支持对象命名,比如:
HANDLE CreateMutex(
PSECURITY_ATTRIBUTES psa,
BOOL bInitialOwner,
PCTSTR pszName);

HANDLE CreateEvent(
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
BOOL bInitialState,
PCTSTR pszName);

HANDLE CreateSemaphore(
PSECURITY_ATTRIBUTES psa,
LONG lInitialCount,
LONG lMaximumCount,
PCTSTR pszName);

HANDLE CreateWaitableTimer(
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
PCTSTR pszName);

HANDLE CreateFileMapping(
HANDLE hFile,
PSECURITY_ATTRIBUTES psa,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
PCTSTR pszName);

HANDLE CreateJobObject(
PSECURITY_ATTRIBUTES psa,
PCTSTR pszName);


  这些函数最后一个参数pszName都是一个字符串,当你向其传递NULL时,创建的内核对象是匿名对象,匿名对象可以使用对象句柄继承或复制对象句柄的方法共享。使用名称来共享对象时,必须用pszName指定要创建的对象名称。pszName指向一个以0结尾的字符串,其长度最大是MAX_PATH(260)。问题是微软并没有提供任何对象命名的建议,而所有类型的内核对象都处在同一命名空间下,比你要创建一个名为"NamedObj"的核心对象,你无法保证已存在的对象中没有名为"NamedObj"的。下面的代码展示了这一点,CreateSemaphore总是返回NULL,因为同名的mutex对象已经存在了:
HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("NamedObj"));
HANDLE hSem = CreateSemaphore(NULL, 1, 1, TEXT("NamedObj"));
DWORD dwErrorCode = GetLastError();

  检查deErrorCode,你会发现它的值是6(ERROR_INVALID_HANDLE),ERROR_INVALID_HANDLE并不能给我们提供更多的关于的失败的描述,但我们对此也无能为力。
  既然我们已经了解如何给对象命名,我们就可以使用这种方法来共享对象,假如进程A调用语句 HANDLE hMutexProcessA = CreateMutex(NULL,FALSE,TEXT("JeffMutex")) 创建了名为JeffMutex的互斥量对象,显然该语句创建的句柄是不可继承的。接着,某进程B可以用语句 HANDLE hMutexProcessB = CreateMutex(NULL, FALSE, TEXT("JeffMutex")) 得到进程A创建的名为JeffMutex互斥量的访问权,并返回一个句柄hMutexProcessB。
  当进程B执行上面的语句时,系统首先检查名为"JeffMutex"的内核对象是否存在,因为进程A已经创建了名为"JeffMutex"的对象,系统接着检查该对象的类型与进程B要求创建的内核对象类型是否一致,在这儿两者都是Mutex,然后系统检查进程B是否有权访问已经存在命为"JeffMutex"的同类型对象,如果检查成功,系统会在进程B的句柄表中创建一个新的条目并用该对象的相关属性初始化。假如类型或访问权限检查失败,CreateMutex方法失败并返回NULL。在这儿进程B的CreateMutex调用成功,并返回一个有效的句柄值,但如果我们检查GetLastError的值,会发现它是ERROR_ALREADY_EXISTS,这说明进程B并不是创建了一个对象,而是打开了一个已经存在的对象,在这种情况下如果进程B调用CreateMutex函数时传递了安全属性参数,这些参数会被忽略。
  还有一种通过命名对象共享内核对象的机制,我们可以调用Open*族函数而非Create*族函数来访问已经存在的命名对象并得到其句柄:
HANDLE OpenMutex(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

HANDLE OpenEvent(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

HANDLE OpenSemaphore(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

HANDLE OpenWaitableTimer(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

HANDLE OpenFileMapping(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

HANDLE OpenJobObject(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

  这些函数的最后一个参数pszName包含要访问的内核对象的名称。这些函数搜索所有内核对象共享的单一命名空间并进行匹配。如果没有找到拥有指定名称的对象,函数返回NULL并置错误码为ERROR_FILE_NOT_FOUND,如果找到的同名对象类型不匹配,函数返回NULL并置错误码为ERROR_INVALID_HANDLE,如果找到了同类型的同名对象,系统检查函数调用者是否有权执行dwDesiredAccess指定的操作,检查通过过,函数返回要访问对象的有效句柄、更新调用进程的句柄表并将对象的引用计数加1。如果指定了bInheritHandle参数为TRUE,那么返回的句柄是可继承的。
Create*和Open*函数的主要不同之处在于Open*函数只是打开已存在的对象,而Create*在指定的命名对象不存在时会创建它。
  在上文中我们提过,微软没有提供任何给对象命名的建议。如果用户试图运行两个程序,而每个程序都试图创建名为"MyObject"的内核对象时就会出问题。我建议开发者使用GUID作为内核对象的名称以防止类似的情况发生。
  命名对象常被用来防止加载程序的多个实例。在_tmain或_tWinMain中调用Create*函数创建一个命名内核对象,接着调用GetLastError检查错误码,如果返回ERROR_ALREADY_EXISTS,则说明已经有该程序的实例运行了。下面的代码展示了这一点:
int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine,
int nCmdShow) {
HANDLE h = CreateMutex(NULL, FALSE,
TEXT("{FA531CC1-0497-11d3-A180-00105A276C3E}"));
if (GetLastError() == ERROR_ALREADY_EXISTS) {
// There is already an instance of this application running.
// Close the object and immediately return.
CloseHandle(h);
return(0);
}

// This is the first instance of this application running.
...
// Before exiting, close the object.
CloseHandle(h);
return(0);
}

[终端服务命名空间(Terminal Services Namespace)和私有命名空间(Private Namespace)因为我没有接触过,现在还不理解,暂不翻译]

3 复制对象句柄
  跨进程共享内核对象的最后一种方法是使用DuplicateHandle复制对象句柄:
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle,
HANDLE hSourceHandle,
HANDLE hTargetProcessHandle,
PHANDLE phTargetHandle,
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwOptions);

  函数将一个进程句柄表的某一条目拷贝到另一进程的句柄表中,注意不包括索引。DuplicateHandle的参数比较多,但较为直观。通常DuplicateHandle函数使用涉及到三个进程。hSourceProcessHandle和hTargetProcessHandle指定了两个进程句柄,代表复制句柄表条目的源进程和目标进程,这两个参数必须是进程句柄。hSourceHandle可以是任何类型内核对象的句柄,它必须代表hSourceProcessHandle进程句柄表中的某个有效条目,函数将把hSourceHandle句柄代表的内核对象属性复制到hTargetProcessHandle的句柄表中,并在phTargetHandle中写入hTargetProcessHandle句柄表中新条目的句柄值。
  DuplicateHandle的最后三个参数指定了新的对象句柄使用的访问掩码和继承标志。dwOptions可以设为0或是DUPLICATE_SAME_ACCESS和DUPLICATE_CLOSE_SOURCE的组合,指定DUPLICATE_SAME_ACCESS表示新的句柄将使用与源句柄完全相同的访问掩码,此时dwDesiredAccess参数将被忽略,指定DUPLICATE_CLOSE_SOURCE会使源进程关闭源句柄。
  接下来我将举例说明确DuplicateHandle的工作方式。在这个例子中,进程S拥有某个内核对象的访问权,我们将把通过复制句柄的方式将其复制给进程T。进程C是调用DuplicateHandle的媒介进程。表3-4是进程C的句柄表,其中有代表内核对象进程S(索引1)和内核对象进程T(索引2)的条目:



表3-5是进程S的句柄表,只包含一个有效条目(索引2),代表某个内核对象:



表3-6是进程T在进程C调用DuplicateHandle之前的句柄表,其中只包含一个有效条目(索引2),索引1处为空:



现在,进程C调用语句:DuplicateHandle(4, 8, 8, &hObj, 0, TRUE, DUPLICATE_SAME_ACCESS); 其中句柄值是根据每个进程的句柄表中索引乘以4得到的。表3-7是该语句调用之后进程T的句柄表:



  可以看到进程S句柄表中的第2个条目已经复制到T句柄表的第1个条目中了,而hObj返回4,表示进程T句柄表中刚复制得到的条目的句柄值(除以4既得到句柄表中的索引为1)。由于传递了DUPLICATE_SAME_ACCESS参数,因此新条目和原条目的访问掩码是一样的,但由于我们同时指定了hInheritHandle为TRUE,新条目的句柄变为可继承的。
  现在我们面临和对象句柄继承类似的问题:进程T并不知道它的句柄表中新增了某个条目。因此调用DuplicateHandle的进程(此处是C)必须用某种方式通知T。显然用命令行参数的方式是不可能的,因为此时T已经被创建并在运行了。这时必须使用窗口消息或别的进程间通信机制。
  DuplicateHandle的使用可以非常灵活。在实践中,一般不会有三个进程参与DuplicateHandle的调用,通常只有两个进程。比如进程S拥有某一内核的访问权限,而进程T想通过S得到该对象的访问权,或是进程S想给予进程T访问权。这可以通过下面的代码完成:
// 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);

HANDLE hObjInProcessT;   // An uninitialized handle relative to Process T.

// 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);


  GetCurrentProcess总是返回当前进程对象的句柄——在这个例子中是进程S的句柄。DuplicateHandle返回后,hObjInProcessT的值将是复制到进程T中的句柄表条目所表示对象的句柄。进程S不应该尝试关闭该句柄,因为它并不代表S句柄表中的任何条目。
  在另一种情况下你也可以使用DuplicateHandle,比如进程拥有一个文件映射对象的读/写权限。在某个代码段,你可能会调用一个应该对文件映射对象只拥有读权限的函数。为了使代码更加健壮,我们可以调用DuplicateHandle产生一个和原对象关联的新句柄并将其访问掩码设置为只读,然后将该新句柄传递给待调用的函数,这样函数中的代码就不可能向文件映射对象中写入任何内容了。以下的代码展示了这一点:
int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE,
LPTSTR szCmdLine, int nCmdShow) {

// 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);
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: