如何在gem5中正确模拟两个进程的共享内存场景?
在gem5中模拟进程共享内存缓存命中的问题排查与解决
我需要在gem5中模拟两个进程共享内存的场景,真实机器上通过mmap映射同一文件可以实现,但在gem5中测试时,第二个进程访问同一内存地址的延迟没有下降,说明缓存没有共享。具体表现为:第一个进程访问内存后第二次延迟从291降到22,但第二个进程访问同样地址的延迟还是246,未命中缓存。
相关代码
program1(sender)
#define _GNU_SOURCE #include<stdio.h> #include <stdint.h> #include <stdlib.h> #include <unistd.h> #include <assert.h> #include <sched.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/wait.h> #include <pthread.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #include "../util.h" #define arr_len (64*8*8*8) volatile int test; int main(){ uint64_t *addr = (uint64_t *) mmap_file(0,"shared"); int tt = time_mread(addr); mfence(); int tt2 = time_mread(addr); printf("%p\n", addr); printf("time %d %d\n", tt,tt2); return 0; }
program2(receiver)
#define _GNU_SOURCE #include<stdio.h> #include <stdint.h> #include <stdlib.h> #include <unistd.h> #include <assert.h> #include <sched.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/wait.h> #include <pthread.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #include "../util.h" int main(){ uint64_t *addr = (uint64_t *) mmap_file(0,"shared"); uint64_t *synchronization = (uint64_t *) mmap_file(64,"shared"); delayloop(5000); int tt = time_mread(addr); printf("%p\n", addr); printf("time %d\n", tt); return 0; }
util.h
#ifndef UTIL_H_ #define UTIL_H_ #include <fcntl.h> #include <getopt.h> #include <inttypes.h> #include <math.h> #include <pthread.h> #include <semaphore.h> #include <stdarg.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> #include <sys/shm.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/wait.h> #include <time.h> #include <unistd.h> #include <x86intrin.h> #define PAGE_SHIFT 12 #define PAGEMAP_LENGTH 8 #define PAGE 4096 #define FILE_SIZE 4096 uint64_t rdtscp64() { uint32_t low, high; asm volatile ("rdtscp": "=a" (low), "=d" (high) :: "ecx"); return (((uint64_t)high) << 32) | low; } uint64_t mmap_file(int offset, char *fileName) { uint64_t addr; // Map file to virtual memory and extract the address at the file offset if (fileName != NULL) { int inFile = open(fileName, O_RDONLY); if(inFile == -1) { printf("Failed to Open File\n"); exit(1); } void *mapaddr = mmap(NULL,FILE_SIZE,PROT_READ,MAP_SHARED,inFile,0); if (mapaddr == (void*) -1 ) { printf("Failed to Map Address\n"); exit(1); } addr = (uint64_t) mapaddr + offset; } return addr; } int // Attribution: https://cs.adelaide.edu.au/~yval/Mastik/ time_mread(void *adrs) { volatile unsigned long time; asm volatile ( // "lfence\n" "mfence\n" "rdtscp\n" "lfence\n" "mov %%eax, %%esi\n" "mov (%1), %%eax\n" "rdtscp\n" "sub %%esi, %%eax\n" : "=&a" (time) // output : "r" (adrs) // input : "ecx", "edx", "esi"); // clobber registers return (int) time; } void mfence() { asm volatile("mfence"); } void delayloop(uint32_t cycles) { uint64_t start = rdtscp64(); while ((rdtscp64()-start) < cycles) ; } #endif
运行输出
0x7ffff7e03000 time 291 22 0x7ffff7e03000 time 246
gem5运行命令
/usr/local/src/gem5/build/X86/gem5.opt --debug-flags=CacheAll,ExecAll,CacheBlock,LSQ --debug-file=/usr/local/src/gem5/runScripts/attack_spectre/trace.out --debug-start=168800000 /usr/local/src/gem5/configs/example/se.py --num-cpus=2 --l1d_replacement=LRU --mem-size=4GB --l1d_size=32kB --l1d_assoc=8 --l1i_size=32kB --l1i_assoc=4 --l2_assoc=16 --l2_size=2MB --caches --l2cache --cpu-type=DerivO3CPU -c '/usr/local/src/gem5/programs/spectre/sender;/usr/local/src/gem5/programs/spectre/receiver'
问题原因与解决方法
核心原因
gem5的SE(Syscall Emulation)模式下,默认每个进程有独立的地址空间,且文件映射的物理页不会在进程间共享——即使两个进程mmap同一个文件,gem5会为每个进程分配独立的物理页副本,导致缓存无法共享。另外,SE模式下进程的执行是串行的(即使指定了多CPU),第一个进程退出后其缓存状态会被清空,第二个进程启动时缓存是冷的。
解决步骤
- 切换到FS(Full System)模式:FS模式模拟完整的操作系统,进程间共享物理内存和缓存的行为和真实机器一致。需要准备对应的磁盘镜像和内核。
- 调整进程调度与同步:如果坚持用SE模式,需要修改配置让两个进程同时运行在不同CPU上,并且确保第一个进程的缓存状态不会被清空。具体修改:
- 修改
se.py配置,让两个进程绑定到不同CPU,且同时启动而非串行执行。 - 使用gem5的
fork系统调用模拟父子进程,而非独立的两个进程——父子进程在SE模式下会共享地址空间和缓存。
- 修改
- 验证物理页映射:在FS模式下,确保两个进程mmap的虚拟地址映射到同一物理页。可以通过读取
/proc/self/pagemap验证物理页是否一致。 - 修正同步逻辑:当前的
delayloop(5000)在gem5中可能不足以等待第一个进程完成内存访问,改用基于共享内存的同步(比如第一个进程写入共享内存标记,第二个进程轮询等待标记),确保第一个进程已经将数据加载到缓存后,第二个进程再发起访问。
具体修改示例(SE模式下父子进程方案)
修改program1,通过fork创建子进程(集成原program2逻辑),这样父子进程共享地址空间和缓存:
// 修改后的program1 #define _GNU_SOURCE #include<stdio.h> #include <stdint.h> #include <stdlib.h> #include <unistd.h> #include <assert.h> #include <sched.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/wait.h> #include <pthread.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #include "../util.h" volatile int test; int main(){ uint64_t *addr = (uint64_t *) mmap_file(0,"shared"); uint64_t *sync_flag = (uint64_t *) mmap_file(64,"shared"); // 创建子进程 pid_t pid = fork(); if(pid == 0){ // 子进程等待父进程完成第一次访问 while(*sync_flag == 0); int tt = time_mread(addr); printf("Child: %p\n", addr); printf("Child time %d\n", tt); return 0; } else { // 父进程先访问内存 int tt = time_mread(addr); mfence(); int tt2 = time_mread(addr); printf("Parent: %p\n", addr); printf("Parent time %d %d\n", tt,tt2); // 设置同步标记,通知子进程可以访问 *sync_flag = 1; wait(NULL); return 0; } }
运行时只需要指定这一个程序作为gem5的-c参数,SE模式下父子进程会共享物理页和缓存,子进程访问时会命中父进程加载的缓存。
调试建议
- 查看gem5的debug trace(
CacheAll标记),确认两个进程访问的物理地址是否一致。如果物理地址不同,说明页没有共享。 - 在FS模式下,使用自定义的rdtscp工具测量缓存命中情况,和真实机器结果对比。
内容的提问来源于stack exchange,提问作者Gerrie
相关产品推荐
相关产品推荐

