微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

如何将结构显式加载到 L1d 缓存中? 启用超线程:编译时和运行时重新排序标题问题:触摸内存将其放入缓存将您的数据按 128 对齐,这是一对对齐的缓存行更简单的方法:memset(0) 避免将数据泄漏回 DRAM为什么要fillCache?

如何解决如何将结构显式加载到 L1d 缓存中? 启用超线程:编译时和运行时重新排序标题问题:触摸内存将其放入缓存将您的数据按 128 对齐,这是一对对齐的缓存行更简单的方法:memset(0) 避免将数据泄漏回 DRAM为什么要fillCache?

我的目标是将静态结构加载到 L1D 缓存中。之后使用这些结构成员执行一些操作,并在完成操作后运行 invd 以丢弃所有修改过的缓存行。所以基本上我想使用在缓存内部创建一个安全的环境,以便在缓存内部执行操作时,数据不会泄漏到RAM中。

为此,我有一个内核模块。我在结构的成员上放置了一些固定值。然后我禁用抢占,禁用所有其他 cpu(当前 cpu 除外)的缓存,禁用中断,然后使用 __builtin_prefetch() 将我的静态结构加载到缓存中。之后,我用新值覆盖之前放置的固定值。之后,我执行invd(清除修改后的缓存行),然后为所有其他cpu启用缓存,启用中断并启用抢占。我的理由是,当我在原子模式下执行此操作时,INVD删除所有更改。从原子模式回来后,我应该看到我之前放置的原始固定值。然而,这并没有发生。退出原子模式后,我可以看到用于覆盖先前放置的固定值的值。这是我的模块代码

奇怪的是,在重新启动 PC 后,我的输出发生了变化,我只是不明白为什么。现在,我根本没有看到任何变化。我正在发布完整的代码包括@Peter Cordes 建议的一些修复,

#include <linux/module.h>    
#include <linux/kernel.h>    
#include <linux/init.h>      
#include <linux/moduleparam.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Author");
MODULE_DESCRIPTION("test INVD");

static struct CACHE_ENV{
    unsigned char in[128];
    unsigned char out[128];
}cacheEnv __attribute__((aligned(64)));

#define cacheEnvSize (sizeof(cacheEnv)/64)
//#define change "Hello"
unsigned char change[]="hello";


void disCache(void *p){
    __asm__ __volatile__ (
        "wbinvd\n"
        "mov %%cr0,%%rax\n\t"
        "or $(1<<30),%%eax\n\t"
        "mov %%rax,%%cr0\n\t"
        "wbinvd\n"
        ::
        :"%rax"
    );

    printk(KERN_INFO "cpuid %d --> cache disable\n",smp_processor_id());

}


void enaCache(void *p){
    __asm__ __volatile__ (
        "mov %%cr0,%%rax\n\t"
        "and $~(1<<30),%%cr0\n\t"
        ::
        :"%rax"
    );

    printk(KERN_INFO "cpuid %d --> cache enable\n",smp_processor_id());

}

int changeFixedValue (struct CACHE_ENV *env){
    int ret=1;
    //memcpy(env->in,change,sizeof (change));
    //memcpy(env->out,sizeof (change));

    strcpy(env->in,change);
    strcpy(env->out,change);
    return ret;
}

void fillCache(unsigned char *p,int num){
    int i;
    //unsigned char *buf = p;
    volatile unsigned char *buf=p;

    for(i=0;i<num;++i){
    
/*
        asm volatile(
        "movq $0,(%0)\n"
        :
        :"r"(buf)
        :
        );
*/
        //__builtin_prefetch(buf,1,1);
        //__builtin_prefetch(buf,3);
        *buf += 0;
        buf += 64;   
     }
    printk(KERN_INFO "Inside fillCache,num is %d\n",num);
}

static int __init device_init(void){
    unsigned long flags;
    int result;

    struct CACHE_ENV env;

    //setup Fixed values
    char word[] ="0xabcd";
    memcpy(env.in,word,sizeof(word) );
    memcpy(env.out,sizeof (word));
    printk(KERN_INFO "env.in fixed is %s\n",env.in);
    printk(KERN_INFO "env.out fixed is %s\n",env.out);

    printk(KERN_INFO "Current cpu %s\n",smp_processor_id());

    // start atomic
    preempt_disable();
    smp_call_function(disCache,NULL,1);
    local_irq_save(flags);

    asm("lfence; mfence" ::: "memory");
    fillCache(&env,cacheEnvSize);
    
    result=changeFixedValue(&env);

    //asm volatile("invd\n":::);
    asm volatile("invd\n":::"memory");

    // exit atomic
    smp_call_function(enaCache,1);
    local_irq_restore(flags);
    preempt_enable();

    printk(KERN_INFO "After: env.in is %s\n",env.in);
    printk(KERN_INFO "After: env.out is %s\n",env.out);

    return 0;
}

static void __exit device_cleanup(void){
    printk(KERN_ALERT "Removing invd_driver.\n");
}

module_init(device_init);
module_exit(device_cleanup);

我得到以下输出

[ 3306.345292] env.in fixed is 0xabcd
[ 3306.345321] env.out fixed is 0xabcd
[ 3306.345322] Current cpu (null)
[ 3306.346390] cpuid 1 --> cache disable
[ 3306.346611] cpuid 3 --> cache disable
[ 3306.346844] cpuid 2 --> cache disable
[ 3306.347065] cpuid 0 --> cache disable
[ 3306.347313] cpuid 4 --> cache disable
[ 3306.347522] cpuid 5 --> cache disable
[ 3306.347755] cpuid 6 --> cache disable
[ 3306.351235] Inside fillCache,num is 4
[ 3306.352250] cpuid 3 --> cache enable
[ 3306.352997] cpuid 5 --> cache enable
[ 3306.353197] cpuid 4 --> cache enable
[ 3306.353220] cpuid 6 --> cache enable
[ 3306.353221] cpuid 2 --> cache enable
[ 3306.353221] cpuid 1 --> cache enable
[ 3306.353541] cpuid 0 --> cache enable
[ 3306.353608] After: env.in is hello
[ 3306.353609] After: env.out is hello

我的 Makefile

obj-m += invdMod.o
CFLAGS_invdMod.o := -o0
invdMod-objs := disable_cache.o  

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
    rm -f *.o

有没有想过我做错了什么?正如我之前所说,我希望我的输出保持不变。

我能想到的一个原因是 __builtin_prefetch() 没有将结构放入缓存中。另一种将内容放入缓存的方法是在 write-backMTRR 的帮助下设置 PAT 区域。但是,我对如何实现这一目标一无所知。我发现 12.6. Creating MTRRs from a C programme using ioctl()’s 显示了如何创建 MTRR 区域,但我不知道如何将我的结构地址与该区域绑定。

我的 cpu 是:Intel(R) Core(TM) i7-7700HQ cpu @ 2.80GHz

内核版本:Linux xxx 4.4.0-200-generic #232-Ubuntu SMP Wed Jan 13 10:18:39 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

海湾合作委员会版本:gcc (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609

我已经用 -O0 参数编译了这个模块

更新 2:关闭超线程

我使用 echo off > /sys/devices/system/cpu/smt/control 关闭了超线程。之后,运行我的模块似乎没有调用 changeFixedValue()fillCache()

输出

[ 3971.480133] env.in fixed is 0xabcd
[ 3971.480134] env.out fixed is 0xabcd
[ 3971.480135] Current cpu 3
[ 3971.480739] cpuid 2 --> cache disable
[ 3971.480956] cpuid 1 --> cache disable
[ 3971.481175] cpuid 0 --> cache disable
[ 3971.482771] cpuid 2 --> cache enable
[ 3971.482774] cpuid 0 --> cache enable
[ 3971.483043] cpuid 1 --> cache enable
[ 3971.483065] After: env.in is 0xabcd
[ 3971.483066] After: env.out is 0xabcd

解决方法

在fillCache 底部调用printk 看起来很不安全。您将要运行更多的存储然后 invd,因此 printk 对内核数据结构(如日志缓冲区)所做的任何修改都可能会被写回 DRAM 或者可能会失效,如果它们'在缓存中仍然很脏。如果某些但不是所有存储都进入 DRAM(因为缓存容量有限),您可能会使内核数据结构处于不一致状态。

我猜您当前在禁用 HT 的情况下进行的测试表明,一切都比您希望的要好,包括丢弃 printk 完成的存储,以及丢弃 changeFixedValue 完成的存储{1}}。这可以解释为什么代码完成后没有留给用户空间阅读的日志消息。

要对此进行测试,您最好clflush 执行printk 所做的一切,但没有简单的方法可以做到这一点。也许wbinvd然后changeFixedValue然后invd。 (您不会在此核心上进入无填充模式,因此 fillCache 对您的商店/invd 创意没有必要,见下文。)


启用超线程:

CR0.CD 是每个物理核心,所以让你的 HT 兄弟核心禁用缓存也意味着隔离核心的 CD=1。 因此,启用 HT 后,即使在隔离的核心上,您也处于无填充模式。

禁用HT后,隔离核心仍然正常。


编译时和运行时重新排序

asm volatile("invd\n":::); 没有 "memory" clobber 告诉编译器它允许重新排序它wrt。内存操作。显然,这不是您的问题,但这是您应该修复的错误。

asm("mfence; lfence" ::: "memory"); 放在 fillCache 之前可能也是一个好主意,以确保任何缓存未命中的加载和存储都不会仍在运行中,并且可能会在您的代码运行时分配新的缓存行.或者甚至可能是像 asm("xor %eax,%eax; cpuid" ::: "eax","ebx","ecx","edx","memory"); 这样的完全序列化指令,但我不知道 CPUID 阻止了哪个 mfence;围栏不会。


标题问题:触摸内存将其放入缓存

PREFETCHT0(进入 L1d 缓存)是 __builtin_prefetch(p,3);This answer 展示了 args 如何映射到指令;您正在使用 prefetchw(写入意图),或者我认为 prefetcht1(L2 缓存)取决于编译器选项。

但实际上,由于您需要这样做以确保正确性,因此您不应该使用硬件在繁忙时可以丢弃的可选提示。 mfence; lfence 会使硬件不太可能实际很忙,但仍然不是一个坏主意。

使用类似 volatileREAD_ONCE 来让 GCC 发出加载指令。或者使用 volatile char *buf*buf |= 0; 或其他东西来真正 RMW 而不是预取,以确保该行是独家拥有的,而不必让 GCC 发出 prefetchw

也许值得运行 fillCache 几次,只是为了更确保每一行都正确地处于您想要的状态。但是由于您的 env 小于 4k,所以每一行都将位于 L1d 缓存中的不同集合中,因此在分配另一行时不会有一行被丢弃的风险(除非 L3 缓存的哈希函数中有别名?但即便如此,伪 LRU 驱逐应该可靠地保留最近的行。)


将您的数据按 128 对齐,这是一对对齐的缓存行

static struct CACHE_ENV { ... } cacheEnv; 不保证按缓存行大小对齐;您缺少 C11 _Alignas(64) 或 GNU C __attribute__((aligned(64)))。所以它可能跨越多于 sizeof(T)/64 行。或者,为了更好的衡量,将 L2 相邻行预取器对齐 128。 (在这里,您可以而且应该简单地对齐缓冲区,但 The right way to use function _mm_clflush to flush a large struct 显示了如何循环遍历任意大小的可能未对齐结构的每个缓存行。)

这并不能解释您的问题,因为唯一可能遗漏的部分是 env.out 的最后最多 48 个字节。 (我认为默认 ABI 规则下全局结构将按 16 对齐。)而且您只打印每个数组的前几个字节。


更简单的方法:memset(0) 避免将数据泄漏回 DRAM

顺便说一句,完成后通过 memset 用 0 覆盖您的缓冲区也应该可以防止您的数据像 INVD 一样可靠地写回 DRAM,但速度更快。 (也许是通过 asm 的手动 rep stosb,以确保它不会像死商店一样优化)。

无填充模式在这里也可能有用,以阻止缓存未命中驱逐现有行。 AFAIK,这基本上锁定了缓存,因此不会发生新的分配,因此不会被驱逐。 (但您可能无法读取或写入其他普通内存,尽管您可以将结果留在寄存器中。)

无填充模式(对于当前内核)可以确保在重新启用分配之前使用 memset 清除缓冲区绝对安全;在导致驱逐期间没有缓存未命中的风险。尽管如果您的 fillCache 实际上工作正常,并且在您开始工作之前让您的所有线路进入 MESI Modified 状态,您的加载和存储将在 L1d 缓存中命中,而不会有驱逐任何缓冲线路的风险。

如果您担心 DRAM 内容(而不是总线信号),那么 在 memset 之后的每一行 clflushopt 将减少漏洞窗口。 (或者,如果 0 对您不起作用,则从原始副本的干净副本中进行 memcpy,但希望您可以只在私人副本中工作并且保持原稿不变。您的当前文档始终可以进行杂散回写方法,所以我不想依赖它来确定总是保持一个大缓冲区不变。)

不要将 NT 存储用于手动 memset 或 memcpy:这可能会在 NT 存储之前刷新“秘密”脏数据。一种选择是使用普通存储或 rep stosb memset(0),然后使用 NT 存储再次循环。或者,也许每行执行 8 次 movq 常规存储,然后执行 8 次 movnti,因此您可以在继续之前对同一行背靠背执行这两项操作。


为什么要fillCache?

如果您没有使用无填充模式,那么在您写入它们之前 是否缓存这些行都无关紧要。您只需要在 invd 运行时您的写入在缓存中是脏的,即使它们是从您的缓存中丢失的存储中获得的,也应该如此。

您已经没有 fillCachechangeFixedValue 之间的 mfence 之类的障碍,这很好,但意味着当您弄脏缓存时,任何因启动缓存而导致的缓存未命中仍在运行中。

>

INVD 本身 is serializing,所以它应该在丢弃缓存内容之前等待存储离开存储缓冲区。 (因此将 mfence;lfence 放在你的工作之后,在 INVD 之前,应该没有任何区别。)换句话说,INVD 应该丢弃仍在存储缓冲区中的可缓存存储,以及脏缓存行,除非提交一些的商店碰巧驱逐任何东西。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。