许多情况下,在不同进程中运行的线程需要共享内核对象。下面是为何需要共享的原因: • 文件映射对象使你能够在同一台机器上运行的两个进程之间共享数据块。 • 邮箱和指定的管道使得应用程序能够在连网的不同机器上运行的进程之间发送数据块。 • 互斥对象、信标和事件使得不同进程中的线程能够同步它们的连续运行,这与一个应用程序在完成某项任务时需要将情况通知另一个应用程序的情况相同。
由于内核对象句柄与进程相关,因此这些任务的执行情况是不同的。不过,M i c r o s o f t 公司有若干很好的理由将句柄设计成与进程相关的句 柄。最重要的理由是要实现它的健壮性。如果内核对象句柄是系统范围的值,那么一个进程就能很容易获得另一个进程使用的对象的句柄,从而对该 进程造成很大的破坏。另一个理由是安全性。内核对象是受安全性保护的,进程在试图操作一个对象之前,首先必须申请获得操作该对象的许可权。 对象的创建人只需要拒绝向用户赋予许可权,就能防止未经授权的用户接触该对象。
在下面的各节中,将要介绍允许进程共享内核对象的3 个不同的机制。
3.3.1 对象句柄的继承性
3.3.2 改变句柄的标志
3.3.3 命名对象
共享跨越进程边界的内核对象的第二种方法是给对象命名。许多(虽然不是全部)内核对象都是可以命名的。例如,下面的所有函数都可以创建命名的内核对象:
HANDLE CreateMutex( PSLCURITY_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 pszNarne); HANDLE CreateWaitableTimer( PSLCURITY_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);
所有这些函数都有一个共同的最后参数p s z N a m e 。当为该参数传递N U L L 时,就向系统指明了想创建一个未命名的(匿名)内核对象。当创建一个未命名的对象时,可以通过使用继承性(如上一节介绍的那样)或D u p l i c a t e H a n d l e (下一节将要介绍)共享跨越进程的对象。若要按名字共享对象,必须为对象赋予一个名字。
如果没有为p s z N a m e 参数传递M U L L ,应该传递一个以0 结尾的字符串名字的地址。该名字的长度最多可以达到M A X _ PAT H (定义为2 6 0 )个字符。但是,M i c r o s o f t 没有提供为内核对象赋予名字的指导原则。例如,如果试图创建一个称为“J e ff O b j ”的对象,那么不能保证系统中不存在一个名字为“J e ff O b j ”的对象。更为糟糕的是,所有这些对象都共享单个名空间。由于这个原因,对下面这个C r e a t e S e m a p h o r e 函数的调用将总是返回N U L L :
HANDLE hMutex = CreateMutex(NULL. FALSE, "JeffObj"); HANDLE hSem = CreateSemaphore(NULL, 1, 1, "JeffObj"); DWORD dwErrorCode = GetLastError();
如果在执行上面的代码后观察d w E r r o r c o d e 的值,会看到返回的代码是6 (E R R O R _I N VA L I D _ H A N D L E )。这个错误代码没有很强的表意性,但是你又能够做什么呢?
既然已知道如何给对象命名,那么让我们来看一看如何用这种方法来共享对象。比如说,Process A 启动运行,并调用下面的函数:
HANDLE hMutexPronessA = CreateMutex(NULL, FALSE, "JeffMutex");
调用该函数能创建一个新的互斥内核对象,为它赋予名字“J e ff M u t e x ”。请注意,P r o c e s sA 的句柄h M u t e x P r o c e s s A 不是一个可继承的句柄,并且当你只是命名对象时,它不必是个可继承的句柄。
过些时候,某个进程会生成Process B 。Process B 不一定是Process A 的子进程。它可能是E x p l o r e r 或其他任何应用程序生成的进程。Process B 不必是Process A 的子进程这一事实正是使用命名对象而不是继承性的优越性。当Process B 启动运行时,它执行下面的代码:
HANDLE hMutexProcessB = CreateMutex(NULL, FALSE, "JeffMutex");
当Process B 调用C r e a t e M u t e x 时,系统首先要查看是否已经存在一个名字为“J e ff M u t e x ”的内核对象。由于确实存在一个带有该名字的对象,因此内核要检查对象的类型。由于试图创建一个互斥对象,而名字为“J e ff M u t e x ”的对象也是个互斥对象,因此系统会执行一次安全检查,以确定调用者是否拥有对该对象的完整的访问权。如果拥有这种访问权,系统就在Process B 的句柄表中找出一个空项目,并对该项目进行初始化,使该项目指向现有的内核对象。如果该对象类型不匹配,或者调用者被拒绝访问,那么C r e a t e M u t e x 将运行失败(返回N U L L )。当Process B 对C r e a t e M u t e x 的调用取得成功时,它并不实际创建一个互斥对象。相反,Process B 只是被赋予一个与进程相关的句柄值,用于标识内核中现有的互斥对象。当然,由于Process B 的句柄表中的一个新项目要引用该对象,互斥对象的使用计数就会递增。在Process A和Process B 同时关闭它们的对象句柄之前,该对象是不会被撤消的。请注意,这两个进程中的句柄值很可能是不同的值。这是可以的。Process A 将使用它的句柄值,而Process B 则使用它自己的句柄值来操作一个互斥内核对象。
注意当你的多个内核对象拥有相同的名字时,有一个非常重要的细节必须知道。当Process B 调用C r e a t e M u t e x 时,它将安全属性信息和第二个参数传递给该函数。如果已经存在带有指定名字的对象,那么这些参数将被忽略。应用程序能够确定它是否确实创建了一个新内核对象,而不是打开了一个现有的对象。方法是在调用C r e a t e *函数后立即调用G e t L a s t E r r o r :
HANDLE hMutex = CreateMutex(&sa, FALSE, "JeffObj"); if (GetLastError() == ERROR_ALREADY_EXISTS) { //Opened a handle to an existing object. //sa.lpSecurityDescriptor and the second parameter //(FALSE) are ignored } else { //Created a brand new object. //sa.lpSecurityDescriptor and the second parameter //(FALSE) are used to construct the object. }
按名字共享对象的另一种方法是,进程不调用C r e a t e *函数,而是调用下面显示的O p e n *函数中的某一个:
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 Openjob0bject( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName);
注意,所有这些函数都拥有相同的原型。最后一个参数p s z N a m e 用于指明内核对象的名字。不能为该参数传递N U L L ,必须传递以0 结尾的地址。这些函数要搜索内核对象的单个名空间,以便找出匹配的空间。如果不存在带有指定名字的内核对象,该函数返回N U L L ,G e t L a s t E r r o r返回2 (E R R O R _ F I L E _ N O T _ F O U N D )。但是,如果存在带有指定名字的内核对象,并且它是相同类型的对象,那么系统就要查看是否允许执行所需的访问(通过d w D e s i r e d A c c e s s 参数进行访问)。如果拥有该访问权,调用进程的句柄表就被更新,对象的使用计数被递增。如果为b I n h e r i t H a n d l e 参数传递T R U E ,那么返回的句柄将是可继承的。
调用C r e a t e *函数与调用O p e n *函数之间的主要差别是,如果对象并不存在,那么C r e a t e *函数将创建该对象,而O p e n *函数则运行失败。
如前所述,M i c r o s o f t 没有提供创建唯一对象名的指导原则。换句话说,如果用户试图运行来自不同公司的两个程序,而每个程序都试图创建一个称为“M y O b j e c t ”的对象,那么这就是个问题。为了保证对象的唯一性,建议创建一个G U I D ,并将G U I D 的字符串表达式用作对象名。命名对象常常用来防止运行一个应用程序的多个实例。若要做到这一点,只需要调用m a i n 或Wi n M a i n 函数中C r e a t e *函数,以便创建一个命名对象(创建的是什么对象则是无所谓的)。当C r e a t e *函数返回时,调用G e t L a s t E r r o r 函数。如果G e t L a s t E r r o r 函数返回E R R O R _ A L R E A D Y _ E X I S T S ,那么你的应用程序的另一个实例正在运行,新实例可以退出。下面是说明这种情况的部分代码:
int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE, PSTR pszCmdLine, int nCmdShow) { HANDLE h = CreateMutex(NULL, FALSE, "{FA531CC1-0497-11d3-A180-00105A276C3E}"); lf (GetLastError() == ERROR_ALREADY_EXISTS) { //There is already an instance //of the application running return(0), } //This is the first instance of this application running. //Before exiting ,close the object. CloseHandle(h), return(0); }
3.3.4 终端服务器的名字空间
3.3.5 复制对象句柄
共享跨越进程边界的内核对象的最后一个方法是使用D u p l i c a t e H a n d l e 函数: