同核心下缓存隐通道进程间负载Latency差异原因及优化问询
基于缓存的隐通道延迟差异问题分析与解决
问题背景
我正尝试用C语言实现基于缓存的隐通道,通过mmap()的MAP_SHARED选项映射同一文件实现发送方与接收方的物理地址共享。发送方逻辑:传输1时会clflush目标地址并测量负载延迟,传输0时预加载目标地址再测延迟;同一进程内load操作延迟在0-1000周期(缓存命中/未命中)。接收方手动在发送方终止后运行,绑定至同一core-id,加载同一共享物理地址测量延迟,但此时load操作延迟为1000-5000周期。
延迟差异的核心原因
- 进程退出导致缓存状态丢失:发送方进程终止后,操作系统会回收其所有资源,包括该进程关联的CPU缓存条目。即使绑定到同一核心,新启动的接收方进程访问共享地址时,该地址的缓存已被清理,必然触发缓存未命中,需要从内存甚至磁盘重新加载,导致延迟大幅升高。
- 缓存与进程上下文强关联:虽然
MAP_SHARED保证了物理地址一致,但CPU缓存是基于核心和进程上下文管理的(部分架构下缓存为核心私有,但进程退出时核心会标记对应缓存行为无效)。发送方退出后,对应缓存行已失效,接收方第一次访问必然是冷启动。 - 发送方的收尾操作主动清空缓存:发送方传输1时最后执行了
clflush((void *)addr),主动清除了目标地址的缓存,接收方访问时自然只能从内存加载,延迟更高。
解决方法
要让两个进程的负载延迟一致,核心是保留发送方设置的缓存状态,直到接收方完成读取,具体调整如下:
1. 让发送方保持存活,避免缓存被回收
发送方在完成比特发送后不要立即退出,通过等待信号或睡眠保持进程存活,直到接收方完成读取。这样缓存状态会被保留在核心的缓存中,不会被系统清理。
2. 调整发送方的缓存操作逻辑
- 传输1时,移除最后的
clflush操作,保留缓存未命中的状态给接收方; - 传输0时,确保预加载的缓存行在接收方访问时仍有效(发送方保持存活即可)。
3. 接收方添加预热与多次采样
接收方第一次访问共享地址必然是冷缓存,需要先执行一次无计时的load操作预热缓存,之后多次采样取平均值,才能得到与发送方一致的延迟范围。
4. 严格控制进程绑定与缓存环境
- 绑定两个进程到同一物理核心(注意不是超线程的逻辑核心);
- 禁用目标核心的超线程功能,避免缓存被其他逻辑核心干扰;
- 确保目标核心没有其他高负载进程,减少缓存竞争。
修改后的代码示例
发送方代码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <x86intrin.h> typedef unsigned long long CYCLES; volatile int load, x; CYCLES load__latency; void *address; #define DEFAULT_FILE_NAME "shared_file" // 计算load操作延迟 static inline CYCLES load_latency(volatile void* p) { CYCLES t1 = rdtscp(); load = *((int *)p); CYCLES t2 = rdtscp(); return (t2 - t1); } void send_bit(int one, void *addr) { if (one) { _mm_clflush(addr); // 使用标准GCC内置函数,替代直接调用clflush load__latency = load_latency(addr); printf("发送方load延迟 = %llu.\n", load__latency); // 移除最后的clflush,保留缓存未命中状态 } else { x = *((int *)addr); load__latency = load_latency(addr); printf("发送方load延迟 = %llu.\n", load__latency); } } // 初始化共享内存地址 void init_address(const char *filename) { FILE *fp = fopen(filename, "w+"); if (!fp) { perror("fopen failed"); exit(1); } int dummy = 0; fwrite(&dummy, sizeof(int), 1, fp); fflush(fp); address = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fileno(fp), 0); if (address == MAP_FAILED) { perror("mmap failed"); exit(1); } fclose(fp); } // 信号处理函数:接收接收方的退出通知 void sig_handler(int sig) { printf("发送方收到通知,即将退出\n"); munmap(address, sizeof(int)); exit(0); } int main(int argc, char **argv) { int bit = 0; if (argc == 2) { bit = atoi(argv[1]); } // 注册信号处理,接收退出通知 signal(SIGUSR1, sig_handler); init_address(DEFAULT_FILE_NAME); send_bit(bit, address); printf("发送方已完成发送,等待接收方读取...\n"); pause(); // 暂停进程,等待信号 return 0; }
接收方代码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <x86intrin.h> #include <sched.h> typedef unsigned long long CYCLES; volatile int load; CYCLES load__latency; void *address; #define DEFAULT_FILE_NAME "shared_file" #define TARGET_CORE 0 // 指定绑定的物理核心ID // 计算load操作延迟 static inline CYCLES load_latency(volatile void* p) { CYCLES t1 = rdtscp(); load = *((int *)p); CYCLES t2 = rdtscp(); return (t2 - t1); } // 初始化共享内存地址 void init_address(const char *filename) { FILE *fp = fopen(filename, "r+"); if (!fp) { perror("fopen failed"); exit(1); } address = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fileno(fp), 0); if (address == MAP_FAILED) { perror("mmap failed"); exit(1); } fclose(fp); } // 绑定进程到指定物理核心 void bind_to_core(int core_id) { cpu_set_t cpuset; CPU_ZERO(&cpuset); CPU_SET(core_id, &cpuset); if (sched_setaffinity(0, sizeof(cpu_set_t), &cpuset) == -1) { perror("sched_setaffinity failed"); exit(1); } } int main(int argc, char **argv) { // 绑定到目标核心 bind_to_core(TARGET_CORE); init_address(DEFAULT_FILE_NAME); // 预热缓存:第一次访问不计入测量 load = *((int *)address); // 多次采样取平均值,减少误差 CYCLES total = 0; const int samples = 10; for (int i = 0; i < samples; i++) { total += load_latency(address); } load__latency = total / samples; printf("接收方平均load延迟 = %llu\n", load__latency); // 发送信号通知发送方可以退出(需替换为发送方的PID,此处假设发送方是父进程) kill(getppid(), SIGUSR1); munmap(address, sizeof(int)); return 0; }
内容的提问来源于stack exchange,提问作者Sathvik Swaminathan
相关产品推荐
相关产品推荐

