Chapter03-跨进程共享内核对象

一、windows内核对象的基本概念
1.所谓内核对象,实质就是由系统内核在内存中分配的一个内存块,这个内存块只能由内核分配,也只能由内核访问。常见的内核对象如:文件对象,时间对象,文件映射对象,互斥量对象,进程对象,线程对象等。
2.内核对象所对应的内存块是一个数据结构,这个结构中有多个属性字段。安全描述符(security descriptor, SD)和使用计数为所有对象都有的。
&&.使用计数用来表示当前有多少个进程在使用该进程对象,当使用计数变为0时,系统便释放该内核对象。
&&.安全描述符用来描述该内核对象的安全权限。因为内核对象的重要性,所有创建内核对象的API都要包含一个指向SD的指针参数,用来确定其对应的安全权限设置。默认可以使用NULL。

3.内核对象由windowsAPI创建,创建成功后会返回一个唯一的句柄值(handle)。每个进程都维护着一张内核对象的句柄表,里面记录了该对象的内存地址,安全权限和继承标志。函数返回的handle值右移2位便是该对象在进程句柄表中的索引号。所以,进程创建的内核对象是与进程相关的,这当然是处于系统健壮性和安全性的考量。因而一个进程创建的内核对象句柄是不能直接传递给另一个进程使用的,二者对应的是不同的内核对象句柄表。


二、跨进程边界共享内核对象

有三种方法:内核对象句柄继承、为对象句柄命名、复制对象句柄。

1、对象句柄的继承性

  只有当进程具有父子关系时,才能使用对象句柄的继承性。

首先,当父进程创建内核对象时,必须向系统指明,它希望对象的句柄是个可继承的句柄。(虽然内核对象句柄具有继承性,但是内核对象本身不具备继承性。

  若要创建能继承的句柄,父进程必须指定一个SECURITY_ATTRIBUTES结构并对它进行初始化,然后将该结构的地址传递给特定的Create***函数。下面的代码用于创建一个互斥对象,并将一个可继承的句柄返回给它:

   
   
SECURITY_ATTRIBUTES sa; sa.nLength = sizeof (sa); sa.lpSecuntyDescriptor = NULL; sa.bInheritHandle = TRUE; HANDLE hMutex = CreateMutex( & sa, FALSE,NULL);

存放在进程句柄表项目中的标志。每个句柄表项目都有一个标志位,用来指明该句柄是否具有继承性。如果将bInheritHandle 成员置为TRUE ,那么该标志位将被置为1 。

包含两个有效项目的进程句柄表

索引

内核对象内存块的指针

访问屏蔽(标志位的DWORD)

标志(标志位的DWORD )

1

0 x F 0 0 0 0 0 0 0

0 x ? ? ? ? ? ? ? ?

0 x 0 0 0 0 0 0 0 0

2

0 x 0 0 0 0 0 0 0 0

(无)

(无)

3

0 x F 0 0 0 0 0 1 0

0 x ? ? ? ? ? ? ? ?

0 x 0 0 0 0 0 0 0 1


让父进程生成子进程

使用对象句柄继承性时要执行的下一个步骤是让父进程生成子进程。这要使用CreateProcess函数来完成:

复制代码
   
   
BOOLCreateProcess( PCTSTR pszApplicationName, PTSTR pszCommandLine, PSECURITY_ATTRIBUTES psaProcess, PSECURITY_ATTRIBUTES psaThread, BOOL bInheritHandles, DWORD fdwCreale, PVOIO pvEnvironment, PCTSTR pszCurDir, PSTARTUPINFO psiStartInfo, PPROCESS_INFORMATION ppiProcInfo);
复制代码

 

bInheritHandle参数传递TRUE ,那么子进程就可以继承父进程的可继承句柄值。当传递TRUE 时,操作系统就创建该新子进程,但是不允许子进程立即开始执行它的代码。当然,系统为子进程创建一个新的和空的句柄表,就像它为任何新进程创建句柄表那样。不过,由于将TRUE 传递给 了CreateProcess 的bInheritHandles 参数,因此系统要进行另一项操作,即它要遍历父进程的句柄表,对于它找到的包含有效的可继承句柄的每个项目,系统会将该项目准确地拷贝到子进程的句柄表中。该项目拷贝到子进程的句柄表中的位置将与父进程的句柄表中的位置完全相同。这个情况非常重要,因为它意味着在父进程与子进程中,标识内核对象所用的句柄值是相同的。

除了拷贝句柄表项目外,系统还要递增内核对象的使用计数,因为现在两个进程都使用该对象。如果要撤消内核对象,那么父进程和子进程必须调用 该对象上的CloseHandle函数,也可以终止进程的运行。子进程不必首先终止运行,但是父进程也不必首先终止运行。实际上,CreateProcess函数返回后,父进程可以立即关闭对象的句柄,而不影响子进程对该对象进行操作的能力。

子进程为了确定它期望的内核对象的句柄值,最常用的方法是将句柄值作为一个命令行参数传递给子进程,该子进程的初始化代码对命令行进行分析(通常通过调用s s c a n f 函数来进行分析),并取出句柄值。一旦子进程拥有该句柄值,它就具有对该对象的无限访问权。句柄继承权起作用的唯一原因是,父进程和子进程中的共享内核对象的句柄值是相同的,这就是为什么父进程能够将句柄值作为命令行参数来传递的原因。

当然,可以使用其他形式的进程间通信,将已继承的内核对象句柄值从父进程传送给子进程。方法之一是让父进程等待子进程完成初始化,然后,父进程可以将一条消息发送或展示在子进程中的一个线程创建的窗口中。

另一个方法是让父进程将一个环境变量添加给它的环境程序块。该变量的名字是子进程知道要查找的某种信息,而变量的值则是内核对象要继承的值。这样,当父进程生成子进程时,子进程就继承父进程的环境变量,并且能够非常容易地调用GetEnvironmentVariable函数,以获取被继承对象的句柄值。如果子进程要生成另一个子进程,那么使用这种方法是极好的,因为环境变量可以被再次继承。

 

改变句柄的标志

有时会遇到这样一种情况,父进程创建一个内核对象,以便检索可继承的句柄,然后生成两个子进程。父进程只想要一个子进程来继承内核对象的句柄。

换句话说,有时可能想要控制哪个子进程来继承内核对象的句柄。

若要改变内核对象句柄的继承标志,可以调用SetHandleInformation 函数:

   
   
BOOLSetHandleInformation( HANDLE hObject, DWORD dwMask, DWORD dwFlags);

第一个参数hObject 用于标识一个有效的句柄。

第二个参数dwMask 告诉该函数想要改变哪个或那几个标志。目前有两个标志与每个句柄相关联:

   #define HANDLE FLAG_INHERIT 0x00000001

   #define HANDLE FLAG PROTECT FROM CLOSE0x00000002

如果想同时改变该对象的两个标志,可以逐位用O R 将这些标志连接起来。

第三个参数是dwFlags,用于指明想将该标志设置成什么值。

 

例如,若要打开一个内核对象句柄的继承标志,请创建下面的代码:

   
   
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

 

如果一个线程试图关闭一个受保护的句柄,CloseHandle 就会产生一个异常条件。很少想要将句柄保护起来,使他人无法将它关闭。但是如果一个进程生成了子进程,而子进程又生成了孙进程,那么该标志可能有用。父进程可能希望孙进程继承赋予子进程的对象句柄。不过,子进程有可能在生成孙进程之前关闭该句柄。如果出现这种情况,父进程就无法与孙进程进行通信,因为孙进程没有继承该内核对象。通过将句柄标明为“受保护不能关闭”,那么孙进程就能继承该对象。

但是这种处理方法有一个问题。子进程可以调用下面的代码来关闭HANDLE_FLAG_PROTECT_FROM_CLOSE标志,然后关闭句柄。

   SetHandleInformation(hobj, HANDLEMFLAG_PROlECl_FROM_CLOSE,  0);

   CloseHandle(hobj);

 

GetHandleInformation函数:

   
   
BOOLGetHandleInformation( HANDLE hObj, PDWORD pdwFlags);

该函数返回pdwFlags指向的DWORD 中特定句柄的当前标志的设置值。

若要了解句柄是否是可继承的,请使用下面的代码:

   
   
DWORD dwFlags; GetHandleInformation(hObj, & dwFlags); BOOL fHandleIsInheritable = ( 0 != (dwFlags & HANDLE_FLAG_INHERIT));

2、命名对象

共享跨越进程边界的内核对象的第二种方法是给对象命名,注意有些内核对象是不可以命名的,但多数内核对象可以命名。

下面的所有函数都可以创建命名的内核对象:

复制代码
   
   
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 pszName); 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);
复制代码

 

使用命名对象要注意的问题

所有这些对象都共享单个名空间。因此下面的用法是错的。

   
   
HANDLE hMutex = CreateMutex(NULL. FALSE, "JeffObj"); HANDLE hSem = CreateSemaphore(NULL, 1, 1, "JeffObj"); DWORD dwErrorCode = GetLastError();
所以为了防止名字的冲突,建议创建一个GUID ,并将GUID的字符串表达式用作对象名。

如何用命名对象来共享对象

Process A 启动运行,并调用下面的函数:

    
    
HANDLE hMutexPronessA = CreateMutex(NULL, FALSE, "JeffMutex");

 

另一进程ProcessB(不一定是Process A 的子进程)启动运行时,执行下面的代码:

     
     
HANDLE hMutexProcessB = CreateMutex(NULL, FALSE, "JeffMutex");

当Process B调用CreateMutex时,系统首先要查看是否已经存在一个名字为“JeffMutex ”的内核对象。

由于确实存在一个带有该名字的对象,因此内核要检查对象的类型。由于试图创建一个互斥对象,而名字为“JeffMutex ”的对象也是个互斥对象,

因此系统会执行一次安全检查,以确定调用者是否拥有对该对象的完整的访问权。

如果拥有这种访问权,系统就在ProcessB的句柄表中找出一个空项目,并对该项目进行初始化,使该项目指向现有的内核对象。

如果该对象类型不匹配,或者调用者被拒绝访问,那么CreateMutex 将运行失败(返回NULL)。

 

当Process B 对CreateMutex的调用取得成功时,它并不实际创建一个互斥对象。相反,Process B 只是被赋予一个与进程相关的句柄值,用于标识内核中现有的互斥对象。当然,由于Process B 的句柄表中的一个新项目要引用该对象,互斥对象的使用计数就会递增。在Process A和Process B 同时关闭它们的对象句柄之前,该对象是不会被撤消的。请注意,这两个进程中的句柄值很可能是不同的值。这是可以的。Process A 将使用它的句柄值,而Process B 则使用它自己的句柄值来操作一个互斥内核对象。

  注意当你的多个内核对象拥有相同的名字时,有一个非常重要的细节必须知道。当Process B 调用CreateMutex 时,它将安全属性信息和第二个参数传递给该函数。如果已经存在带有指定名字的对象,那么这些参数将被忽略。

 

确定它是否确实创建了一个新内核对象,而不是打开了一个现有的对象

复制代码
     
     
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. }
复制代码

 

按名字共享对象的另一种方法

不调用Create*函数,而是调用Open*函数:

复制代码
     
     
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);
复制代码
最后一个参数pszName用于指明内核对象的名字。不能为该参数传递NULL ,必须传递以0 结尾的地址。这些函数要搜索内核对象的单个名空间,以便找出匹配的空间。如果不存在带有指定名字的内核对象,该函数返回NULL 。

如果存在带有指定名字的内核对象,并且它是相同类型的对象,那么系统就要查看是否允许执行所需的访问(通过dwDesiredAccess参数进行访问)。如果拥有该访问权,调用进程的句柄表就被更新,对象的使用计数被递增。如果为bInheritHandle 参数传递TRUE,那么返回的句柄将是可继承的。

 

调用Create *函数与调用Open*函数之间的主要差别

如果对象并不存在,那么Create*函数将创建该对象,而Open*函数则运行失败。

 

一个小应用

命名对象常常用来防止运行一个应用程序的多个实例。若要做到这一点,只需要调用main或WinMain函数中Create*函数,以便创建一个命名对象(创建的是什么对象则是无所谓的)。当Create*函数返回时,调用GetLastError函数。如果GetLastError 函数返回ERROR_ALREADY_EXISTS ,那么你的应用程序的另一个实例正在运行,新实例可以退出。下面是说明这种情况的部分代码:

复制代码
      
      
intWINAPI 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 thisapplication running. //Before exiting ,close the object. CloseHandle(h), return(0); }

3
复制对象句柄

共享跨越进程边界的内核对象的最后一个方法是使用

复制代码
         
         
BOOL DuplicateHandle( HANDLE hSourceProcessHandle, HANDLE hSourceHandle, HANDLE hTargetProcessHandle, PHANDLE phTargetHandle, DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwOptions);
复制代码

 

简单说来,该函数取出一个进程的句柄表中的项目,并将该项目拷贝到另一个进程的句柄表中。

DuplicateHandle 函数最普通的用法要涉及系统中运行的3个不同进程。

第一和第三个参数hSourceProcessHandle和hTargetProcessHandle是内核对象句柄。这些句柄本身必须与调用DuplicateHandle 函数的进程相关。此外,这两个参数必须标识进程的内核对象

第二个参数hSourceHandle是任何类型的内核对象的句柄。但是该句柄值与调用DuplicateHandle的进程并无关系。相反,该句柄必须与hSourceProcessHandle句柄标识的进程相关。

第四个参数phTagetHandle是HANDLE 变量的地址,它将接收获取源进程句柄信息拷贝的项目索引。返回的句柄值与hTargetProcessHandle标识的进程相关。

最后3 个参数用于指明该目标进程的内核对象句柄表项目中使用的访问屏蔽值和继承性标志。dwOptions参数可以是0 (零),也可以是下面两个标志的任何组合:DUPLICATE_SAME_ACCESS和DUPLICATE_CLOSE_SOURCE。

如果设定了DUPLICATE_SAME_ACCESS标志,则告诉DuplicateHandle 函数,你希望目标进程的句柄拥有与源进程句柄相同的访问屏蔽。使用该标志将使DuplicateHandle忽略它的dwDesiredAccess参数。

如果设定了DUPLICATE_CLOSE_SOURCE标志,则可以关闭源进程中的句柄。该标志使得一个进程能够很容易地将内核对象传递给另一个进程。当使用该标志时,内核对象的使用计数不会受到影响。

 

例子

下面用一个例子来说明DuplicateHandle函数是如何运行的。在这个例子中。

Process S 是目前可以访问某个内核对象的源进程,

Process T 是将要获取对该内核对象的访问权的目标进程。

Process C 是执行对DuplicateHandle 调用的催化进程。

 

Process C 的句柄表包含两个句柄值,即1 和2 。句柄值1 用于标识Process S 的进程内核对象,句柄值2 则用于标识Process T 的进程内核对象。

Process C 的句柄表

索引

内核对象内存块的指针

访问屏蔽(标志位的D W O R D )

标志(标志位的D W O R D )

1

0xF0000000(Process S 的内核对象)

0 x ? ? ? ? ? ? ? ?

0 x 0 0 0 0 0 0 0 0

2

0xF0000010(Process T 的内核对象)

0 x ? ? ? ? ? ? ? ?

0 x 0 0 0 0 0 0 0 0

 

Process S 的句柄表,它包含句柄值为2 的单个项目。该句柄可以标识任何类型的内核对象,就是说它不必是进程的内核对象。

 

Process S 的句柄表

索引

内核对象内存块的指针

访问屏蔽(标志位的D W O R D )

标志(标志位的D W O R D )

1

0 x 0 0 0 0 0 0 0 0

(无)

(无)

2

0 x F 0 0 0 0 0 2 0

0 x ? ? ? ? ? ? ? ?

0 x 0 0 0 0 0 0 0 0

 

Process C 调用DuplicateHandle 函数之前Process T 的句柄表包含的项目。如你所见,Process T 的句柄表只包含句柄值为2 的单个项目,句柄项目1 目前未用。

 

调用DuplicateHandle函数之前Process T 的句柄表

索引

内核对象内存块的指针

访问屏蔽(标志位的D W O R D )

标志(标志位的D W O R D )

1

0 x 0 0 0 0 0 0 0 0

(无)

(无)

2

0 x F 0 0 0 0 0 3 0

0 x ? ? ? ? ? ? ? ?

0 x 0 0 0 0 0 0 0 0

 

如果Process C 现在使用下面的代码来调用DuplicateHandle ,那么只有Process T 的句柄表改变更。

 

调用DuplicateHandle函数之后Process T 的句柄表

索引

内核对象内存块的指针

访问屏蔽(标志位的D W O R D )

标志(标志位的D W O R D )

1

0 x F 0 0 0 0 0 2 0

0 x ? ? ? ? ? ? ? ?

0 x 0 0 0 0 0 0 0 1

2

0 x F 0 0 0 0 0 3 0

0 x ? ? ? ? ? ? ? ?

0 x 0 0 0 0 0 0 0 0

 

Process S 的句柄表中的第二项已经被拷贝到Process T 的句柄表中的第一项(见粉红色)。

 

DuplicateHandle 也已经将值1 填入Process C 的hObj变量中。值1是Process T 的句柄表中的索引,新项目将被放入该索引。

 

由于DUPLICATE_SAME_ACCESS标志被传递给了DuplicateHandle,因此Process T 的句柄表中该句柄的访问屏蔽与Process S 的句柄表项目中的访问屏蔽是相同的。

另外,传递DUPLICATE_SAME_ACCESS标志将使DuplicateHandle忽略它的DesiredAccess参数。最后请注意,继承位标志已经被打开,因为给DuplicateHandle 的bInheritHandle参数传递的是TRUE。

 

显然,你永远不会像在这个例子中所做的那样,调用传递硬编码数字值的DuplicateHandle函数。这里使用硬编码数字,只是为了展示函数是如何运行的。在实际应用程序中,变量可能拥有各种不同的句柄值,可以传递该变量,作为函数的参数。

与继承性一样,DuplicateHandle函数存在的奇怪现象之一是,目标进程没有得到关于新内核对象现在可以访问它的通知。因此,Process C 必须以某种方式来通知Process T,它现在拥有对内核对象的访问权,并且必须使用某种形式的进程间通信方式,以便将h O b j 中的句柄值传递给Process T 。显然,使用命令行参数或者改变Process T 的环境变量是不行的,因为该进程已经启动运行。因此必须使用窗口消息或某种别的IPC机制。

 

上面是DuplicateHandle的最普通的用法。它是个非常灵活的函数。不过,它很少在涉及3 个不同进程的情况下被使用(因为Process C 不可能知道对象的句柄值正在被Proces s使用)。

通常,当只涉及两个进程时,才调用DuplicateHandle 函数。比如一个进程拥有对另一个进程想要访问的对象的访问权,或者一个进程想要将内核对象的访问权赋予另一个进程。

 

例如,Process S 拥有对一个内核对象的访问权,并且想要让Process T 能够访问该对象。若要做到这一点,可以像下面这样调用DuplicateHandle :

复制代码
         
         
//ALL of the following code is executed by Process S. //Createamutex object accessible by Process S. HANDLE hObjProcessS = CreateMutex(NULL, FALSE, NULL); //Open ahandle to Process T's kernel object. HANDLE hProcessT = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessIdT); //Anuninitilized handle relative to Process T. HANDLE hObjProcessT; //GiveProcess T accesss to our mutex object DuplicateHandle(GetCurrentProcess(), hObjProcessS, hProcessT, &hObjProcessT, 0, FALSE, DUPLICATE_SAME_ACCESS); //Usesome IPC mechanism to get the handle //valuein hOnjProcess S into Process T //We nolonger need to communicate with Process T. CloseHandle(hProcessT); //WhenProcess S no longer needs to Use the mutex, //itshould close it. CloseHandle(hObjProcessS);
复制代码

 

在这个例子中,对GetCurrentProcess 的调用将返回一个伪句柄,该句柄总是用来标识调用端的进程Process S 。一旦DuplicateHandle 返回,hObjProcessT 就是与Process T 相关的句柄,它所标识的对象与引用Process S 中的代码时hObjProcess S 的句柄标识的对象相同。

Process S 决不应该执行下面的代码:

//ProcessS should never attempt to close the duplicated handle

CloseHandle(hObjProcessT);

如果Process S 要执行该代码,那么对代码的调用可能失败,也可能不会失败。如果Process S 恰好拥有对内核对象的访问权,其句柄值与hObjProcess T 的值相同,那么调用就会成功。该代码的调用将会关闭某个对象,这样Process S 就不再拥有对它的访问权,这当然会导致应用程序产生不希望有的行为特性。

 

下面是使用DuplicateHandle函数的另一种方法

假设一个进程拥有对一个文件映射对象的读和写访问权。在某个位置上,一个函数被调用,它通过读取文件映射对象来访问它。为了使应用程序更加健壮,可以使用DuplicateHandle 为现有的对象创建一个新句柄,并确保这个新句柄拥有对该对象的只读访问权。然后将只读句柄传递给该函数,这样,该函数中的代码就永远不会偶然对该文件映射对象执行写入操作。下面这个代码说明了这个例子:

 

复制代码
         
         
intWINAPI WinMain(HINSTANCE hinstExe, HINSTANCE, PSTR pszCmdLine, int nCmdShow) { //Create a file-mapping object; //the handle has read/write access. HANDLE hFileMapRW = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGF_READWRITE, 0, 10240, NULL); //Create anotherhandle to the file-mappingobject; //the handle has read-only access. HANDLE hFileMapRO; DuplicateHandle(GetCurrentProcess(),hFileMapRW, GetCurrentProcess(), &hFileMdpRO, FILE_MAP_READ, FALSE, 0); //Call the function that should only read //from the 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 //doesnot access the file mapping anymore CloseHandle(hFileMapRW); }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值