进程通信专题

5、文件映射

1、课程目标

  1. 理解文件映射的原理和优势
  2. 掌握CreateFileMapping和MapViewOfFile的使用
  3. 实现基于文件映射的进程间通信
  4. 学会使用命名文件映射对象共享数据

2、名词解释

术语 解释
文件映射 将文件内容映射到进程虚拟地址空间的技术
内存映射文件 通过内存访问方式读写文件的机制
页面文件 系统虚拟内存交换文件pagefile.sys
命名映射对象 可跨进程共享的有名称的文件映射对象
视图 映射到进程地址空间的文件区域

3、使用工具

工具 用途
Visual Studio 编译调试程序
Process Explorer 查看进程句柄和映射
VMMap 分析进程虚拟内存布局

4、技术原理

4.1、文件映射机制

文件映射原理:
┌─────────────────────────────────────────────────┐
│                   物理内存                        │
│  ┌─────────────────────────────────────────┐    │
│  │         共享内存页面                      │    │
│  └─────────────────────────────────────────┘    │
│        ↑                    ↑                   │
│        │                    │                   │
│  ┌─────┴─────┐        ┌────┴─────┐            │
│  │ 进程A视图  │        │ 进程B视图 │            │
│  └───────────┘        └──────────┘            │
│                                                 │
│  CreateFileMapping() → 创建/打开映射对象         │
│  MapViewOfFile()     → 映射到进程地址空间        │
│  UnmapViewOfFile()   → 解除映射                 │
│  CloseHandle()       → 关闭映射对象              │
└─────────────────────────────────────────────────┘

4.2、文件映射与普通IPC对比

特性 文件映射 管道 邮槽
速度 最快(直接内存访问) 中等 较慢
数据持久性 可选(可基于文件)
通信方式 双向 单向/双向 单向
随机访问 支持 顺序 顺序

5、代码实现

5.1、示例1:基础文件映射 - 写入端

// FileMapWriter.c - 文件映射写入端
#include <windows.h>
#include <stdio.h>

#define SHARED_MEM_NAME "Global\\MySharedMemory"
#define SHARED_MEM_SIZE 4096

typedef struct _SHARED_DATA {
    DWORD dwMagic;          // 魔数验证
    DWORD dwDataSize;       // 数据大小
    DWORD dwSequence;       // 序列号
    CHAR  szData[4000];     // 实际数据
} SHARED_DATA, *PSHARED_DATA;

int main() {
    HANDLE hMapFile = NULL;
    PSHARED_DATA pSharedData = NULL;
    
    // 创建文件映射对象(基于页面文件)
    hMapFile = CreateFileMappingA(
        INVALID_HANDLE_VALUE,   // 使用页面文件
        NULL,                    // 默认安全属性
        PAGE_READWRITE,          // 可读写
        0,                       // 高32位大小
        SHARED_MEM_SIZE,         // 低32位大小
        SHARED_MEM_NAME          // 映射对象名称
    );
    
    if (hMapFile == NULL) {
        printf("CreateFileMapping failed: %d\n", GetLastError());
        return 1;
    }
    
    // 将文件映射到进程地址空间
    pSharedData = (PSHARED_DATA)MapViewOfFile(
        hMapFile,               // 映射对象句柄
        FILE_MAP_ALL_ACCESS,    // 完全访问权限
        0, 0,                   // 偏移量
        SHARED_MEM_SIZE         // 映射大小
    );
    
    if (pSharedData == NULL) {
        printf("MapViewOfFile failed: %d\n", GetLastError());
        CloseHandle(hMapFile);
        return 1;
    }
    
    printf("[Writer] Shared memory created at: 0x%p\n", pSharedData);
    printf("[Writer] Press Enter to write data...\n");
    getchar();
    
    // 写入共享数据
    pSharedData->dwMagic = 0xDEADBEEF;
    pSharedData->dwDataSize = strlen("Hello from Writer!") + 1;
    pSharedData->dwSequence = 1;
    strcpy(pSharedData->szData, "Hello from Writer!");
    
    printf("[Writer] Data written. Sequence: %d\n", pSharedData->dwSequence);
    printf("[Writer] Press Enter to update data...\n");
    getchar();
    
    // 更新数据
    pSharedData->dwSequence = 2;
    strcpy(pSharedData->szData, "Updated message from Writer!");
    pSharedData->dwDataSize = strlen(pSharedData->szData) + 1;
    
    printf("[Writer] Data updated. Sequence: %d\n", pSharedData->dwSequence);
    printf("[Writer] Press Enter to exit...\n");
    getchar();
    
    // 清理资源
    UnmapViewOfFile(pSharedData);
    CloseHandle(hMapFile);
    
    return 0;
}

5.2、示例2:基础文件映射 - 读取端

// FileMapReader.c - 文件映射读取端
#include <windows.h>
#include <stdio.h>

#define SHARED_MEM_NAME "Global\\MySharedMemory"
#define SHARED_MEM_SIZE 4096

typedef struct _SHARED_DATA {
    DWORD dwMagic;
    DWORD dwDataSize;
    DWORD dwSequence;
    CHAR  szData[4000];
} SHARED_DATA, *PSHARED_DATA;

int main() {
    HANDLE hMapFile = NULL;
    PSHARED_DATA pSharedData = NULL;
    DWORD lastSequence = 0;
    
    // 打开已存在的文件映射对象
    hMapFile = OpenFileMappingA(
        FILE_MAP_READ,          // 只读访问
        FALSE,                  // 不继承句柄
        SHARED_MEM_NAME         // 映射对象名称
    );
    
    if (hMapFile == NULL) {
        printf("OpenFileMapping failed: %d\n", GetLastError());
        printf("Please start Writer first.\n");
        return 1;
    }
    
    // 映射视图
    pSharedData = (PSHARED_DATA)MapViewOfFile(
        hMapFile,
        FILE_MAP_READ,          // 只读映射
        0, 0,
        SHARED_MEM_SIZE
    );
    
    if (pSharedData == NULL) {
        printf("MapViewOfFile failed: %d\n", GetLastError());
        CloseHandle(hMapFile);
        return 1;
    }
    
    printf("[Reader] Mapped to shared memory at: 0x%p\n", pSharedData);
    
    // 持续读取数据
    while (1) {
        // 检查魔数
        if (pSharedData->dwMagic != 0xDEADBEEF) {
            printf("[Reader] Waiting for valid data...\n");
            Sleep(1000);
            continue;
        }
        
        // 检查是否有新数据
        if (pSharedData->dwSequence != lastSequence) {
            lastSequence = pSharedData->dwSequence;
            printf("[Reader] New data (Seq=%d): %s\n", 
                   pSharedData->dwSequence, 
                   pSharedData->szData);
        }
        
        Sleep(500);
        
        // 按ESC退出
        if (GetAsyncKeyState(VK_ESCAPE) & 0x8000)
            break;
    }
    
    UnmapViewOfFile(pSharedData);
    CloseHandle(hMapFile);
    
    return 0;
}

5.3、示例3:带同步的共享内存通信

// SyncSharedMem.h - 共享头文件
#ifndef SYNC_SHARED_MEM_H
#define SYNC_SHARED_MEM_H

#define SHM_NAME        "Local\\SyncSharedMem"
#define MUTEX_NAME      "Local\\SyncSharedMemMutex"
#define EVENT_WRITE     "Local\\SyncSharedMemEventWrite"
#define EVENT_READ      "Local\\SyncSharedMemEventRead"
#define SHM_SIZE        8192

typedef struct _COMMAND_PACKET {
    DWORD dwCommand;        // 命令类型
    DWORD dwParam1;         // 参数1
    DWORD dwParam2;         // 参数2
    DWORD dwDataLen;        // 数据长度
    BYTE  bData[8000];      // 数据缓冲区
} COMMAND_PACKET, *PCOMMAND_PACKET;

// 命令定义
#define CMD_PING        1
#define CMD_EXECUTE     2
#define CMD_DOWNLOAD    3
#define CMD_UPLOAD      4
#define CMD_EXIT        0xFF

#endif
// SyncServer.c - 同步共享内存服务端
#include <windows.h>
#include <stdio.h>
#include "SyncSharedMem.h"

int main() {
    HANDLE hMapFile = NULL;
    HANDLE hMutex = NULL;
    HANDLE hEventWrite = NULL;
    HANDLE hEventRead = NULL;
    PCOMMAND_PACKET pPacket = NULL;
    BOOL bRunning = TRUE;
    
    // 创建同步对象
    hMutex = CreateMutexA(NULL, FALSE, MUTEX_NAME);
    hEventWrite = CreateEventA(NULL, FALSE, FALSE, EVENT_WRITE);
    hEventRead = CreateEventA(NULL, FALSE, FALSE, EVENT_READ);
    
    // 创建共享内存
    hMapFile = CreateFileMappingA(
        INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE,
        0, SHM_SIZE, SHM_NAME);
    
    if (!hMapFile || !hMutex || !hEventWrite || !hEventRead) {
        printf("Failed to create resources: %d\n", GetLastError());
        return 1;
    }
    
    pPacket = (PCOMMAND_PACKET)MapViewOfFile(
        hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, SHM_SIZE);
    
    printf("[Server] Ready and waiting for commands...\n");
    
    while (bRunning) {
        // 等待客户端写入命令
        DWORD dwWait = WaitForSingleObject(hEventWrite, 5000);
        
        if (dwWait == WAIT_TIMEOUT)
            continue;
        
        // 获取互斥锁
        WaitForSingleObject(hMutex, INFINITE);
        
        // 处理命令
        switch (pPacket->dwCommand) {
            case CMD_PING:
                printf("[Server] Received PING\n");
                strcpy((char*)pPacket->bData, "PONG");
                pPacket->dwDataLen = 5;
                break;
                
            case CMD_EXECUTE:
                printf("[Server] Execute: %s\n", pPacket->bData);
                // 模拟执行命令
                sprintf((char*)pPacket->bData, "Executed: %d", pPacket->dwParam1);
                pPacket->dwDataLen = strlen((char*)pPacket->bData) + 1;
                break;
                
            case CMD_EXIT:
                printf("[Server] Exit command received\n");
                bRunning = FALSE;
                strcpy((char*)pPacket->bData, "Goodbye!");
                pPacket->dwDataLen = 9;
                break;
                
            default:
                printf("[Server] Unknown command: %d\n", pPacket->dwCommand);
                strcpy((char*)pPacket->bData, "Unknown command");
                pPacket->dwDataLen = 16;
        }
        
        ReleaseMutex(hMutex);
        
        // 通知客户端可以读取响应
        SetEvent(hEventRead);
    }
    
    // 清理
    UnmapViewOfFile(pPacket);
    CloseHandle(hMapFile);
    CloseHandle(hMutex);
    CloseHandle(hEventWrite);
    CloseHandle(hEventRead);
    
    return 0;
}
// SyncClient.c - 同步共享内存客户端
#include <windows.h>
#include <stdio.h>
#include "SyncSharedMem.h"

BOOL SendCommand(PCOMMAND_PACKET pPacket, HANDLE hMutex, 
                 HANDLE hEventWrite, HANDLE hEventRead,
                 DWORD dwCmd, DWORD dwP1, DWORD dwP2,
                 const char* data) {
    WaitForSingleObject(hMutex, INFINITE);
    
    pPacket->dwCommand = dwCmd;
    pPacket->dwParam1 = dwP1;
    pPacket->dwParam2 = dwP2;
    if (data) {
        strcpy((char*)pPacket->bData, data);
        pPacket->dwDataLen = strlen(data) + 1;
    }
    
    ReleaseMutex(hMutex);
    
    // 通知服务端
    SetEvent(hEventWrite);
    
    // 等待响应
    if (WaitForSingleObject(hEventRead, 5000) == WAIT_TIMEOUT) {
        printf("Timeout waiting for response\n");
        return FALSE;
    }
    
    return TRUE;
}

int main() {
    HANDLE hMapFile = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, SHM_NAME);
    HANDLE hMutex = OpenMutexA(SYNCHRONIZE | MUTEX_MODIFY_STATE, FALSE, MUTEX_NAME);
    HANDLE hEventWrite = OpenEventA(EVENT_MODIFY_STATE, FALSE, EVENT_WRITE);
    HANDLE hEventRead = OpenEventA(SYNCHRONIZE, FALSE, EVENT_READ);
    
    if (!hMapFile || !hMutex || !hEventWrite || !hEventRead) {
        printf("Failed to open resources. Start server first.\n");
        return 1;
    }
    
    PCOMMAND_PACKET pPacket = (PCOMMAND_PACKET)MapViewOfFile(
        hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, SHM_SIZE);
    
    printf("[Client] Connected to server\n");
    
    // 发送PING
    if (SendCommand(pPacket, hMutex, hEventWrite, hEventRead, 
                    CMD_PING, 0, 0, NULL)) {
        printf("[Client] Response: %s\n", pPacket->bData);
    }
    
    // 发送执行命令
    if (SendCommand(pPacket, hMutex, hEventWrite, hEventRead,
                    CMD_EXECUTE, 123, 0, "calc.exe")) {
        printf("[Client] Response: %s\n", pPacket->bData);
    }
    
    // 发送退出
    if (SendCommand(pPacket, hMutex, hEventWrite, hEventRead,
                    CMD_EXIT, 0, 0, NULL)) {
        printf("[Client] Response: %s\n", pPacket->bData);
    }
    
    UnmapViewOfFile(pPacket);
    CloseHandle(hMapFile);
    CloseHandle(hMutex);
    CloseHandle(hEventWrite);
    CloseHandle(hEventRead);
    
    return 0;
}

5.4、示例4:基于真实文件的内存映射

// FileMapping.c - 映射真实文件
#include <windows.h>
#include <stdio.h>

void ReadFileWithMapping(const char* filePath) {
    HANDLE hFile = CreateFileA(
        filePath,
        GENERIC_READ,
        FILE_SHARE_READ,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL);
    
    if (hFile == INVALID_HANDLE_VALUE) {
        printf("Cannot open file: %d\n", GetLastError());
        return;
    }
    
    DWORD dwFileSize = GetFileSize(hFile, NULL);
    printf("File size: %d bytes\n", dwFileSize);
    
    HANDLE hMap = CreateFileMappingA(
        hFile,
        NULL,
        PAGE_READONLY,
        0, 0,   // 映射整个文件
        NULL);  // 无名称
    
    if (hMap == NULL) {
        printf("CreateFileMapping failed: %d\n", GetLastError());
        CloseHandle(hFile);
        return;
    }
    
    // 映射视图
    LPVOID pView = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
    
    if (pView == NULL) {
        printf("MapViewOfFile failed: %d\n", GetLastError());
        CloseHandle(hMap);
        CloseHandle(hFile);
        return;
    }
    
    // 直接访问文件内容
    printf("First 100 bytes:\n");
    for (DWORD i = 0; i < min(100, dwFileSize); i++) {
        printf("%02X ", ((BYTE*)pView)[i]);
        if ((i + 1) % 16 == 0) printf("\n");
    }
    printf("\n");
    
    // 搜索特定模式
    BYTE pattern[] = { 0x4D, 0x5A };  // MZ
    if (dwFileSize >= 2 && 
        memcmp(pView, pattern, 2) == 0) {
        printf("This is a PE file!\n");
    }
    
    UnmapViewOfFile(pView);
    CloseHandle(hMap);
    CloseHandle(hFile);
}

void ModifyFileWithMapping(const char* filePath) {
    HANDLE hFile = CreateFileA(
        filePath,
        GENERIC_READ | GENERIC_WRITE,
        0,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL);
    
    if (hFile == INVALID_HANDLE_VALUE) {
        printf("Cannot open file: %d\n", GetLastError());
        return;
    }
    
    HANDLE hMap = CreateFileMappingA(
        hFile, NULL, PAGE_READWRITE,
        0, 0, NULL);
    
    LPVOID pView = MapViewOfFile(hMap, FILE_MAP_WRITE, 0, 0, 0);
    
    // 修改文件内容
    // 注意:修改会直接反映到文件
    ((BYTE*)pView)[0] = 0xFF;
    
    // 刷新到磁盘
    FlushViewOfFile(pView, 0);
    
    UnmapViewOfFile(pView);
    CloseHandle(hMap);
    CloseHandle(hFile);
}

int main(int argc, char* argv[]) {
    if (argc < 2) {
        printf("Usage: %s <file>\n", argv[0]);
        return 1;
    }
    
    ReadFileWithMapping(argv[1]);
    
    return 0;
}

6、课后作业

  1. 实现一个基于文件映射的进程间命令执行器
  2. 编写程序使用文件映射读取PE文件并解析DOS头和NT头
  3. 实现带超时和重试机制的共享内存通信库
  4. 对比文件映射和ReadFile/WriteFile的性能差异

7、扩展阅读

  • MSDN: File Mapping Functions
  • Windows Internals: Memory Management
  • 内存映射文件的安全性考虑