如何解决如何将结构显式加载到 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-back
和 MTRR
的帮助下设置 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
会使硬件不太可能实际很忙,但仍然不是一个坏主意。
使用类似 volatile
的 READ_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
运行时您的写入在缓存中是脏的,即使它们是从您的缓存中丢失的存储中获得的,也应该如此。
您已经没有 fillCache
和 changeFixedValue
之间的 mfence 之类的障碍,这很好,但意味着当您弄脏缓存时,任何因启动缓存而导致的缓存未命中仍在运行中。
INVD 本身 is serializing,所以它应该在丢弃缓存内容之前等待存储离开存储缓冲区。 (因此将 mfence;lfence
放在你的工作之后,在 INVD 之前,应该没有任何区别。)换句话说,INVD 应该丢弃仍在存储缓冲区中的可缓存存储,以及脏缓存行,除非提交一些的商店碰巧驱逐任何东西。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。