共享内存在 Windows 中是用 FileMapping 实现的。我们可以用 CreateFileMapping 创建一个内存文件映射对象, CreateFileMapping 这个 API 将创建一个内核对象,用于映射文件到内存。这里,我们并不需要一个实际的文件,所以,就不需要调用 CreateFile 创建一个文件, hFile 这个参数可以填写 INVALID_HANDLE_VALUE 。但是,文件长度是需要填的。Windows 支持长达 64bit 的文件,但是这里,我们的需求一定不会超过 4G , dwMaximumSizeHigh 一定是 0 ,长度填在 dwMaximumSizeLow 即可。然后调用 MapViewOfFile 映射到当前进程的虚拟地址上即可。一旦用完共享内存,再调用 UnmapViewOfFile 回收内存地址空间。
Windows 把 CreateFileMapping 和 MapViewOfFile 两个 API 分开做是有它的道理的。这是因为允许映射一个超过 4G 的文件,而地址空间最大只有 4G (实际上,一般用户的程序只能用到 2G) , MapViewOfFile 就可以指定文件的 Offset 而只映射一部分。
在 CreateFileMapping 的最后一个参数 pszName 填写一个名字,那么别的进程就可以用这个名字去调用 OpenFileMapping 来打开这个 FileMapping 对象,在新的进程内作映射。 不过,通过约定字符串的方法似乎不太优雅。
一个优雅的方法是,用 DuplicateHandle 在新进程中复制一份 FileMapping 对象出来,然后想办法把 Handle 通知新进程,比如用消息的方式传递过去。
如果需要共享内存的两个进程是父子关系,那么我们可以不用消息传递的方式来通知 FileMapping 的 Handle 。父进程可以用继承 Handle 的方式直接把 FileMapping 的 Handle 传递到子进程中。当然,在 CreateFileMapping 时就应该设置可以被继承的属性。
大约是这样:
SECURITY_ATTRIBUTES sa;
sa.nLength=sizeof(sa);
sa.lpSecurityDescriptor=NULL;
sa.bInheritHandle=TRUE;
handle=CreateFileMapping(INVALID_HANDLE_VALUE,&sa,PAGE_READWRITE,0,size,NULL);
这样,在 CreateProcess 的时候,如果 bInheritHandles 参数为 TRUE ,所有有可被继承属性的内核对象都会被复制到子进程中。
注:内核对象的继承就是在 CreateProcess 创建子进程,但是子进程的主线程尚未活动之前,内核扫描当前进程中所有内核对象,检查出有可继承属性的那些,再用 DuplicateHandle 复制一份到子进程。由于是内核对象,在内核中实质只有一份,所有只是引用记数加一,父进程和子进程对同一内核对象的 Handle 一定是相同的。
复制内核对象的过程是由 CreateProcess 内部完成的,我们可以放心的把对象 Handle (和子进程相同) 通过命令行传递给子进程。或者,用环境变量传递也可以。
值得注意的是,子进程用完了这个 FileMapping 对象后一样需要 CloseHandle 减去引用计数。
备注:
CreateProcess 调用时,pszCommandLine 不能直接填上一个不可修改的字符串。例如:
CreateProcess("test.exe","test argument",...);
这样就是错误的,因为 "test argument" 会被编译器编译放到不可修改的数据段中。正确的方法是:
char cmdline[]="test argument";
CreateProcess("test.exe",cmdline,...);
这样,命令行的字符串就被放在堆栈上,是可以被读写的。
CreateProcess 的倒数第二个参数需要填写一个 STARTUPINFOW 结构,这个结构很复杂,通常填起来很麻烦。我们可以复制一份父进程的结构,再酌情修改。方法是:
STARTUPINFO si={sizeof(si)};
PROCESS_INFORMATION pi;
GetStartupInfo(&si);
CreateProcess(...,&si,& pi);
这里, STARTUPINFO 结构的第一个长度信息通常应该填上,保证 GetStartupInfo(&si); 的正确执行。
下面是另一个人写的
通过windows api我们可以很容易的在内存中创建这么一个区域并轻易的读取它。这里需要用到两个关键的api函数:CreateFileMapping和MapViewOfFile,前者在内存中创建一个内存映象区,它需要一些参数包括这块区域的读写权限,唯一区域名称以及数据大小(这些参数将在下面代码的注释中给予说明,更多的参数说明请参看win sdk的帮助文件或查看msdn);后者返回一个指向这块区域的指针以便在程序中调用。下面我们将通过一个简单的例子来说明,这个例子由两个工程组成,一个通过一个按纽负责向内存映象区写入一个长度为11个字符的字符串(‘how are you‘,通过一个edit读入,如果edit中的字符数大于了11个程序将可能出错),另一个则通过一个按纽读出这个字符串,写入一个edit中。下面是部分的代码(为了节省篇幅,加上程序很简单,有些不会影想你阅读的代码没有贴出):
我们首先在第一个工程(窗口取名为SetValue)的单元的接口部分声明一个指向即将创建的内存区域的句柄和一个用来向这个区域写数据的指针变量:
var
……
hFileMapping:HWND;//指向内存区域的句柄
SetString:pchar;//这里应该使用标准的windows字符串而非delphi特有的string类型
这样我们可以在按纽的单击事件中编写相应的代码:
procedure TForm1.Button1Click(Sender: TObject);
begin
hFileMapping:=CreateFileMapping($FFFFFFFF,nil,Page_ReadWrite,0,11,'MapString');
//$FFFFFFFF表示使用虚拟文件,注意两个工程中应该使用同一个唯一的名称MapString
if hFileMapping=0 then
raise Exception.Create('Error creating map file!');
SetString:=MapViewOfFile(hFileMapping,File_Map_Write,0,0,11);
strcopy(SetString,pchar(Edit1.Text));
//注意这里应该使用拷贝函数而不是直接赋值 SetString:=pchar(Edit1.Text)
end;
当然,我们还可以在这个主工程中编写一个释放内存映象的过程:
procedure TForm1.Button2Click(Sender: TObject);
begin
UnMapViewOfFile(SetString);
if hFileMapping<>0 then
closehandle(hFileMapping);
end;
接下来是第2个工程的按纽单击事件,它和第一个工程几乎没有什么不同,只是我们修改了内存映象的读取权限为只读而以:
var
……
GetString:pchar;
hMapFileMaping:HWND;
procedure TForm1.Button1Click(Sender: TObject);
begin
hMapFileMaping:=CreateFileMapping($FFFFFFFF,nil,Page_ReadOnly,0,11,'MapString');
//再次注意一定要使用唯一的名称(MapString)和第一个工程保持一致
if hMapFileMaping=0 then
raise Exception.Create('Error');
GetString:=MapViewOfFile(hMapFileMaping,File_Map_Read,0,0,11);
Edit1.Text:=String(GetString);
end;