簡體   English   中英

如何將結構顯式加載到 L1d 緩存中?

[英]How to explicitly load a structure into L1d cache?

我的目標是將 static 結構加載到 L1D 緩存中。 之后使用這些結構成員執行一些操作並在完成操作后運行invd以丟棄所有修改的緩存行。 所以基本上我想在緩存內創建一個安全的環境,以便在緩存內執行操作時,數據不會泄漏到 RAM 中。

為此,我有一個 kernel 模塊。 我在結構成員上放置了一些固定值。 然后我禁用搶占,禁用所有其他 CPU(當前 CPU 除外)的緩存,禁用中斷,然后使用__builtin_prefetch()將我的 static 結構加載到緩存中。 之后,我用新值覆蓋之前放置的固定值。 之后,我執行invd (清除修改后的緩存行),然后對所有其他 CPU 啟用緩存,啟用中斷並啟用搶占。 我的理由是,當我在原子模式下執行此操作時, INVD將刪除所有更改。 從原子模式回來后,我應該會看到我之前放置的原始固定值。 然而,這並沒有發生。 退出原子模式后,我可以看到用於覆蓋之前放置的固定值的值。 這是我的模塊代碼,

奇怪的是重啟電腦后,我的output變了,我就是不明白為什么。 現在,我根本看不到任何變化。 我正在發布完整的代碼,包括@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), %%eax\n\t"
        "mov %%rax, %%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, change,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,0,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, word, 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,NULL,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);

我得到以下 output:

[ 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

有沒有想過我做錯了什么? 正如我之前所說,我希望我的 output 保持不變。

我能想到的一個原因是__builtin_prefetch()沒有將結構放入緩存中。 將某些內容放入緩存的另一種方法是在MTRRPAT的幫助下設置write-back區域。 但是,我對如何實現這一點一無所知。 我找到了12.6。 使用 ioctl() 從 C 程序創建 MTRR顯示了如何創建MTRR區域,但我不知道如何將結構的地址與該區域綁定。

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

Kernel 版本: 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 版本: 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()

output:

[ 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對 kernel 數據結構(如日志緩沖區)所做的任何修改都可能會被寫回 DRAM,或者如果它們在緩存中仍然是臟的,則可能會失效。 如果某些但不是所有存儲都進入 DRAM(因為緩存容量有限),您可以將 kernel 數據結構留在不一致的 state 中。

我猜您當前禁用 HT 的測試顯示一切工作都比您希望的還要好,包括丟棄printk完成的存儲,以及丟棄changeFixedValue完成的存儲。 這可以解釋代碼完成后沒有留給用戶空間讀取的日志消息。

要對此進行測試,理想情況下您希望對 printk 所做的一切進行clflush ,但沒有簡單的方法可以做到這一點。 也許wbinvd然后changeFixedValue然后invd (您沒有在此核心上進入無填充模式,因此您的 store / fillCache想法不需要使用 fillCache,請參見下文。)


啟用超線程:

CR0.CD 是每個物理內核的,因此讓您的 HT 同級內核禁用緩存也意味着隔離內核的 CD=1。 因此,啟用 HT 后,即使在隔離核心上,您處於無填充模式。

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


編譯時和運行時重新排序

asm volatile("invd\n":::); 沒有"memory" clobber 告訴編譯器允許對其進行重新排序。 memory 操作。 顯然這不是您的問題,但這是您應該修復的錯誤。

可能也是一個好主意asm("mfence; lfence"::: "memory"); 就在fillCache之前,以確保任何緩存未命中的加載和存儲都不會仍在運行中,並且可能會在代碼運行時分配新的緩存行。 甚至可能是一個完全序列化的指令,例如asm("xor %eax,%eax; cpuid"::: "eax", "ebx", "ecx", "edx", "memory"); ,但我不知道 CPUID 會阻止哪個 mfence; 沒有。


題目問題:觸摸 memory 將其放入緩存

PREFETCHT0(進入 L1d 緩存)是__builtin_prefetch(p,0,3); . 這個答案顯示了 args 如何映射到指令; 您正在使用prefetchw (寫入意圖)或者我認為prefetcht1 (L2 緩存)取決於編譯器選項。

但實際上,由於您需要它來確保正確性,因此您不應該使用硬件在忙碌時可能會丟棄的可選提示。 mfence; lfence mfence; lfence將使硬件不太可能真的很忙,但仍然不是一個壞主意。

使用READ_ONCE類的volatile讀取來獲取 GCC 以發出加載指令。 或者使用volatile char *buf*buf |= 0; 或真正的 RMW 而不是預取的東西,以確保該線路是專有的,而不必讓 GCC 發出prefetchw

也許值得運行 fillCache 幾次,只是為了更確定每一行都在你想要的 state 中正確。 但是由於您的環境小於 4k,因此每一行都將位於 L1d 緩存中的不同集合中,因此在分配另一行時不會有一條線被丟棄的風險(除非在 L3 緩存的 hash function 中存在別名的情況下? ,偽 LRU 驅逐應該可靠地保持最近的行。)


將數據對齊 128,一對對齊的緩存線

static struct CACHE_ENV {... } cacheEnv; 不保證與緩存行大小對齊; 您缺少 C11 _Alignas(64)或 GNU C __attribute__((aligned(64))) 所以它可能跨越超過sizeof(T)/64行。 或者為了更好的測量,L2 鄰線預取器對齊 128。 (在這里您可以而且應該簡單地對齊緩沖區,但是使用 function _mm_clflush 刷新大型結構的正確方法顯示了如何循環遍歷任意大小的可能未對齊結構的每個緩存行。)

這並不能解釋您的問題,因為唯一可能遺漏的部分是env.out的最后最多 48 個字節。 (我認為默認 ABI 規則下全局結構將對齊 16。)而且您只打印每個數組的前幾個字節。


更簡單的方法:memset(0) 以避免將數據泄漏回 DRAM

順便說一句,完成后通過 memset 用0覆蓋緩沖區還應該防止數據像 INVD 一樣可靠地寫回 DRAM,但速度更快。 (也許通過 asm 手動rep stosb以確保它不能優化為死存儲)。

無填充模式在這里也可能有用,以阻止緩存未命中退出現有行。 AFAIK,這基本上鎖定了緩存,因此不會發生新的分配,因此不會被驅逐。 (但您可能無法讀取或寫入其他正常的 memory,盡管您可以在寄存器中留下結果。)

無填充模式(對於當前核心)可以確保在重新啟用分配之前使用 memset 清除緩沖區絕對安全; 在導致驅逐期間沒有緩存未命中的風險。 盡管如果您的 fillCache 實際工作正常,並且在您開始工作之前將所有行放入 MESI Modified state 中,那么您的加載和存儲將在 L1d 緩存中命中,而不會有驅逐任何緩沖行的風險。

如果你擔心 DRAM 內容(而不是總線信號),那么在 memset 之后的每一行 clflushopt 都會減少 window 的漏洞 (或者如果0對您不起作用,則從原始副本的干凈副本中獲取 memcpy,但希望您可以只在私有副本中工作並且不修改原始文件。使用您當前的方法總是可以進行雜散回寫,所以我會不想依賴它來絕對始終保持未修改的大緩沖區。)

不要將 NT 存儲用於手動 memset 或 memcpy:這可能會在 NT 存儲之前刷新“秘密”臟數據。 一種選擇是使用普通存儲或rep stosb使用 memset(0) ,然后使用 NT 存儲再次循環。 或者也許每行做 8x movq 普通存儲,然后是 8x movnti,所以你在繼續之前對同一行背靠背地做這兩件事。


為什么要填充緩存?

如果您沒有使用無填充模式,那么在寫入之前是否緩存這些行甚至都無關緊要。 invd運行時,你只需要你的寫入在緩存中是臟的,即使它們是從緩存中丟失的存儲中獲得的,這也應該是正確的。

您在fillCache和 changeFixedValue 之間已經沒有任何障礙,例如changeFixedValue ,這很好,但意味着當您弄臟緩存時,由於啟動緩存而導致的任何緩存未命中仍然存在。

INVD 本身正在序列化,因此它應該在丟棄緩存內容之前等待存儲離開存儲緩沖區。 (所以將mfence;lfence放在你的工作之后,INVD 之前,應該沒有任何區別。)換句話說,INVD 應該丟棄仍在存儲緩沖區中的可緩存存儲,以及臟緩存行,除非提交其中一些商店碰巧驅逐任何東西。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM