You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

同核心下缓存隐通道进程间负载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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 15:54:17