1. 程式人生 > >電腦科學基礎知識(一):The Memory Hierarchy

電腦科學基礎知識(一):The Memory Hierarchy

作者:linuxer 釋出於:2014-6-16 19:54 分類:基礎學科

一、前言

最近一個問題經常縈繞在我的腦海:一個學習電子工程的機械師如何稱為優秀的程式設計師?(注:本文作者本科學習機械設計,研究生轉到電子工程系學習,畢業後卻選擇了系統程式設計師這樣的職業)。經過思考,我認為阻擋我稱為一個優秀程式設計師的障礙是電腦科學的理論知識。自然辯證法告訴我們:理論源於實踐,又指導實踐,她們是相輔相成的關係。雖然從業十餘年,閱code無數,但計算機的理論不成體系,無法指導工程面具體技能的進一步提升。

電腦科學博大精深,CPU體系結構、離散數學、編譯器原理、軟體工程等等。最終選擇從下面這本書作為起點:

s2547828

本文就是在閱讀了該書的第六章的一個讀數筆記,方便日後查閱。

二、儲存技術

本節主要介紹SRAM,SDRAM,FLASH以及磁碟這集中儲存技術,這些技術是後面學習的基礎。

1、SRAM

SRAM是RAM的一種,和SDRAM不同的是SRAM不需要refresh的動作,只要保持供電,其memory cell儲存的資料就不會丟失。一個SRAM的memory cell是由六個場效電晶體組成,如下:memory cell

具體bit的資訊是儲存在M1、M2、M3、M4這四個場效電晶體中。M1和M2組成一個反相器,我們稱之C1。Q(有上劃線的那個Q)是C1的輸出。M3和M4組成另外一個反相器C2,Q是C2的輸出。C1的輸出連線到C2的輸入,C2的輸出連線到C1的輸入,通過這樣的方法實現兩個反相器的輸出狀態的鎖定、儲存,即儲存了1個bit的資料。M5和M6是用來控制資料訪問的。一個SRAM的memory cell有三個狀態:

(1)idle狀態。這種狀態下,Word Line(圖中標識為WL)為低電平的時候,M5和M6都處於截止狀態,儲存bit資訊的cell和外界是隔絕的。這時候,只有有供電,cell保持原來的狀態。

(2)reading狀態。我們假設cell中儲存的資料是1(Q點是高電平),當進行讀操作的時候,首先把兩根bit line(BL和BL)設定為高電平。之後assert WL,以便導通M5和M6。M5和M6導通之後,我們分成兩個部分來看。右邊的BL和Q都是高電平,因此狀態不變。對於左邊,BL是高電平,而Q是低電平,這時候,BL就會通過M5、M1進行放電,如果時間足夠長,BL最終會變成低電平。cell儲存資料0的情況是類似的,只不過這時候最終BL

會保持高電平,而BL最終會被放電成低電平,具體的過程這裡不再詳述。BL和BL會接到sense amplifier上,sense amplifier可以感知BL和BL之間的電壓差從而判斷cell中儲存的是0還是1。

(3)writing狀態。假設我們要向cell中寫入1,首先將BL設定為高電平,BL設定為低電平。之後assert WL,以便導通M5和M6。M5和M6導通之後,如果原來cell儲存1,那麼狀態不會變化。如果原來cell儲存0,這時候Q是低電平,M1截止,M2導通,Q是高電平,M4截止,M3導通。一旦assert WL使得M5和M6導通後,Q變成高電平(跟隨BL點的電平),從而導致M1導通,M2截止。一旦M1導通,原來Q點的高電平會通過M1進行放電,使Q點變成低電平。而Q點的低電平又導致M4導通,M3截止,使得Q點鎖定在高電平上。將cell的內容從1變成0也是相似的過程,這裡不再詳述。

瞭解了一個cell的結構和操作過程之後,就很容易瞭解SRAM晶片的結構和原理了。一般都是將cell組成陣列,再加上一些地址譯碼邏輯,資料讀寫buffer等block。

3、Flash。具體請參考FLASH internals。

4、Disk(硬碟)

嵌入式軟體工程師多半對FLASH器件比較熟悉,而對Hard Disk相對陌生一些。這裡我們只是簡單介紹一些基本的資訊,不深入研究。儲存資料的硬碟是由一個個的“盤子”(platter)組成,每個盤子都有兩面(surface),都可以用來儲存資料。磁碟被堆疊在一起沿著共同的主軸(spindle)旋轉。每個盤面都有一個磁頭(header)用來讀取資料,這些磁頭被固定在一起可以沿著盤面徑向移動。盤面的資料是儲存在一個一個的環形的磁軌(track)上,磁軌又被分成了一個個的sector。還有一個術語叫做柱面(cylinder),柱面是由若干的track組成,這些track分佈在每一個盤面上,有共同的特點就是到主軸的距離相等。

我們可以從容量和存取速度兩個方面來理解Disk Drive。容量計算比較簡單,特別是理解了上面描述的硬碟的幾何結構之後。磁碟容量=(每個sector有多少個Byte)x(每個磁軌有多少個sector)x(每個盤面有多少個磁軌)x(每個盤子有多少個盤面)x(該硬碟有多少個盤子)。

由於各個盤面的讀取磁頭是固定在一起的,因此,磁頭的移動導致訪問的柱面的變化。因此,同一時刻,我們可以同時讀取位於同一柱面上的sector的資料。對於硬碟,資料的訪問是按照sector進行的,當我們要訪問一個sector的時候需要考慮下面的時間:

(1)Seek time。這個時間就是磁頭定位到磁軌的時間。這個時間和上次訪問的磁軌以及移動磁頭的速度有關。大約在10ms左右。

(2)Rotational latency。磁頭移動到了磁軌後,還不能讀取sector的資料,因為儲存資料的盤面都是按照固定的速率旋轉的,有可能我們想要訪問的sector剛好轉過磁頭,這時候,只能等下次旋轉到磁頭位置的時候才能開始資料讀取。這個是時間和磁碟的轉速有關,數量級和seek time類似。

(3)Transfer time。當想要訪問的sector移動到磁頭下面,資料訪問正式啟動。這時候,資料訪問的速度是和磁碟轉速以及磁軌的sector數目相關。

舉一個實際的例子可能會更直觀:Seek time:9ms,Rotational latency:4ms,Transfer time:0.02ms。從這些資料可以知道,硬碟訪問的速度主要受限在Seek time和Rotational latency,一旦磁頭和sector相遇,資料訪問就非常的快了。此外,我們還可以看出,RAM的訪問都是ns級別的,而磁碟要到ms級別,可見RAM的訪問速度要遠遠高於磁碟。

三、區域性性原理(Principle of Locality)

好的程式要展現出好的區域性性(locality),以便讓系統(軟體+硬體)展現出好的效能。到底是memory hierarchy、pipeline等硬體設計導致軟體必須具備區域性性,還是本身軟體就是具有區域性性特點從而推動硬體進行相關的設計?這個看似雞生蛋、蛋生雞的問題,我傾向於邏輯的本身就是有序的,是區域性性原理的本質。此外,區域性性原理不一定涉及硬體。例如對於AP軟體和OS軟體,OS軟體會把AP軟體最近訪問的virtual address space的資料存在記憶體中作為cache。

區域性性被分成兩種型別:

(1)時間區域性性(Temporal Locality)。如果程式有好的時間區域性性,那麼在某一時刻訪問的memory,在該時刻隨後比較短的時間內還多次訪問到。

(2)空間區域性性(Spatial Locality)。如果程式有好的空間區域性性,那麼一旦某個地址的memory被訪問,在隨後比較短的時間內,該memory附近的memory也會被訪問。

1、資料訪問的區域性性分析

int sumvec(int v[N])
{
  int i, sum = 0;
  
  for (i = 0; i < N; i++)
    sum += v[i];
    
  return sum;
}

i和sum都是棧上的臨時變數,由於i和sum都是標量,不可能表現出好的空間區域性性,不過對於sumvec的主loop,i、sum以及陣列v都可以表現出很好的時間區域性性。雖然,i和sum沒有很好的空間區域性性,不過編譯器會把i和sum放到暫存器中,從而優化效能。陣列v在loop中是順序訪問的,因此可以表現出很好的空間區域性性。總結一下,上面的程式可以表現出很好的區域性性。

按照上面的例子對陣列v進行訪問的模式叫做stride-1 reference pattern。下面的例子可以說明stride-N reference pattern:

int sumarraycols(int v[M][N])
{
  int i, j; sum = 0;
  
  for(j = 0; j < N; j++)
    for (i = 0; i < M; i++)
      sum += a[i][j];
      
  return sum;
}

二維陣列v[i][j]是一個i行j列的陣列,在記憶體中,v是按行儲存的,首先是第1行的j個列資料,之後是第二行的j個列資料……依次排列。在實際的計算機程式中,我們總會有計算陣列中所有元素的和的需求,在這個問題上有兩種方式,一種是按照行計算,另外一種方式是按照列計算。按照行計算的程式可以表現出好的區域性性,不過sumarraycols函式中是按照列進行計算的。在內迴圈中,累計一個列的資料需要不斷的訪問各個行,這就導致了資料訪問不是連續的,而是每次相隔N x sizeof(int),這種情況下,N越大,空間區域性性越差。

2、程式訪問的區域性性分析

對於指令執行而言,順序執行的程式具備好的空間區域性性。我們可以回頭看看sumvec函式的執行情況,這個程式中,loop內的指令都是順序執行的,因此,有好的空間區域性性,而loop被多次執行,因此同時又具備了好的時間區域性性。

3、經驗總結

我們可以總結出3條經驗:

(1)不斷的重複訪問同一個變數的程式有好的時間區域性性

(2)對於stride-N reference pattern的程式,N越小,空間區域性性越好,stride-1 reference pattern的程式最優。好的程式要避免以跳來跳去的方式來訪問memory,這樣程式的空間區域性性會很差

(3)迴圈有好的時間和空間區域性性。迴圈體越小,迴圈次數越多,區域性性越好

四、儲存體系

1、分層的儲存體系

現代計算機系統的儲存系統是分層的,主要有六個層次:

(1)CPU暫存器

(2)On-chip L1 Cache (一般由static RAM組成,size較小,例如16KB)

(3)Off-chip L2 Cache (一般由static RAM組成,size相對大些,例如2MB)

(4)Main memory(一般是由Dynamic RAM組成,幾百MB到幾個GB)

(5)本地磁碟(磁介質,幾百GB到若干TB)

(6)Remote disk(網路儲存、分散式檔案系統)

而決定這個分層結構的因素主要是:容量(capacity),價格(cost)和訪問速度(access time)。位於金字塔頂端的CPU暫存器訪問速度最快(一個clock就可以完成訪問)、容量最小。金字塔底部的儲存介質訪問速度最慢,但是容量可以非常的大。

2、儲存體系中的cache

快取機制可以發生在儲存體系的k層和k+1層,也就是說,在k層的儲存裝置可以作為low level儲存裝置(k+1層)的cache。例如訪問main memory的時候,L2 cache可以快取main memory的部分資料,也就是說,原來需要訪問較慢的SDRAM,現在可以通過L2 cache直接命中,提高了效率。同樣的道理,訪問本地磁碟的時候,linux核心也會建立page cache、buffer cache等用來快取disk上資料。

下面我們使用第三層的L2 cache和第四層的Main memory來說明一些cache的基本概念。一般而言,Cache是按照cache line組織的,當訪問主儲存器的時候,有可能資料或者指令位於cache line中,我們稱之cache hit,這種情況下,不需要訪問外部慢速的主儲存器,從而加快了儲存器的訪問速度。也有可能資料或者指令沒有位於cache line中,我們稱之cache miss,這種情況下,需要從外部的主儲存器載入資料或指令到cache中來。由於時間區域性性(tmpporal locality)和空間區域性性(spatial locality)原理,load 到cache中的資料和指令往往是最近要使用的內容,因此可以提高整體的效能。當cache miss的時候,我們需要從main memory載入cache,如果這時候cache已經滿了(畢竟cache的size要小於main memory的size),這時候還要考慮替換演算法。比較簡單的例子是隨機替換演算法,也就是隨機的選擇一個cache line進行替換。也可以採用Least-recently used(LRU)演算法,這種演算法會選擇最近最少使用的那個cache line來載入新的cache資料,cache line中舊的資料就會被覆蓋。

cache miss有三種:

(1)在系統初始化的時候,cache中沒有任何的資料,這時候,我們稱這個cache是cold cache。這時候,由於cache還沒有warn up導致的cache miss叫做compulsory miss或者cold miss。

(2)當載入一個cache line的時候,有兩種策略,一種是從main memory載入的資料可以放在cache中的任何一個cache line。這個方案判斷cache hit的開銷太大,需要scan整個cache。因此,在實際中,指定的main memory的資料只能載入到cache中的一個subset中。正因此如此,就產生了另外一種cache miss叫做conflict miss。也就是說,雖然目前cache中仍然有空閒的cacheline,但是由於main memory要載入的資料對映的那個subset已經滿了,這種情況導致的cache miss叫做conflict miss。

(3)對於程式的執行,有時候會在若干個指令中迴圈,而在這個迴圈中有可能不斷的反覆訪問一個或者多個數據block(例如:一個靜態定義的陣列)。這些資料block就叫做這個迴圈過程的Working Set。當Working Set的size大於cache的size之後,就會產生capacity miss。加大cache的size是解決capacit miss的唯一方法(或者減少working set的size)。

前面我們已經描述過,分層的memory hierarchy的精髓就是每層的儲存裝置都是可以作為下層裝置的cache,而在每一層的儲存裝置都要有一些邏輯(可以是軟體的,也可以是硬體的)來管理cache。例如:cache的size是多少?如何定義k層的cache和k+1層儲存裝置之間的transfer block size(也就是cache line),如何確定cache hit or miss,替換策略為何?對於cpu register而言,編譯器提供了了cache的管理策略。對於L1和L2,cache管理策略是由HW logic來控管,不過軟體工程師在程式設計的時候需要了解這個層次上的cache機制,以便寫出比較優化的程式碼。我們都有用瀏覽器訪問Internet的經歷,我們都會有這樣的常識,一般最近訪問的網頁都會比較快,因此這些網頁是從本地載入而不是遠端的主機。這就是本地磁碟對網路磁碟的cache機制,是用軟體邏輯來控制的。

五、cache內幕

本節用ARM926的cache為例,描述一些cache memory相關的基礎知識,對於其他level上的cache,概念是類似的。

1、ARM926 Cache的組織

ARM926的地址線是32個bit,可以訪問的地址空間是4G。現在,我們要設計CPU暫存器和4G main memory空間之間的一個cache。毫無疑問,我們的cache不能那麼大,因此我們考慮設計一個16K size的cache。首先考慮cache line的size,一般選擇32個Bytes,這個也是和硬體相關,對於支援burst mode的SDRAM,一次burst可以完成32B的傳輸,也就是完成了一次cache line的填充。16K size的cache可以分成若干個set,對於ARM926,這個數字是128個。綜上所述,16KB的cache被組織成128個cache set,每個cache set中有4個cache line,每個cache line中儲存了32B位元組的資料block。瞭解了Cache的組織,我們現在看看每個cache line的組成。一個cache line由下面的內容組成:

1、該cache line是否有效的標識。

2、Tag。聽起來很神祕,其實一般是地址的若干MSB部分組成,用來判斷是否cache hit的

3、資料塊(具體的資料或者指令)。如果命中,並且有效,CPU就直接從cache中取走資料,不必再去訪問memory了。

瞭解了上述資訊之後,我們再看看virutal memory address的組成,具體如下圖所示:

addr

當CPU訪問一個32 bit的地址的時候,首先要去cache中檢視是否hit。由於cache line的size(指資料塊部分,不包括tag和flag)是32位元組,因此最低的5個bits是用來定位cache line offset的。中間的7個bit是用來尋找cache set的。7個bit可以定址128個cache set。找到了cache set之後,我們就要對該cache set中的四個cache line進行逐一比對,是否valid,如果valid,那麼要訪問地址的Tag是否和cache line中的Tag一樣?如果一樣,那麼就cache hit,否則cache miss,需要發起訪問main memory的操作。

總結一下:一個cache的組織可以由下面的四個引數組來標識(S,E,B,m)。S是cache set的數目;E是每個cache set中cache line的數目;B是一個cache line中儲存的資料塊的位元組數;m是實體地址的bit數目。

2、Direct-Mapped Cache和Set Associative Cache

如果每個cache set中只有一個cache line,也就是說E等於1,這樣組織的cache就叫做Direct-Mapped Cache。這種cache的各種操作比較簡單,例如判斷是否cache hit。通過Set index後就定位了一個cache set,而一個cache set只有一個cache line,因此,只有該cache line的valid有效並且tag是匹配的,那麼就是cache hit,否則cache miss。替換策略也簡單,因為就一個cache line,當cache miss的時候,只能把當前的cache line換出。雖然硬體設計比較簡單了,但是conflict Miss會比較突出。我們可以舉一個簡單的例子:

float dot_product(float x[8], float y[8])  {    int i;         float sum = 0.0;    for(i=0; i<8; i++)    {       sum += x[i]*y[i];         }            return sum;  }

上面的程式是求兩個向量的dot product,這個程式有很好的區域性性,按理說應該有較高的cache hit,但是實際中未必總是這樣。假設32 byte的cache被組織成2個cache set,每個cache line是16B。假設x陣列放在0x0地址開始的32B中,4B表示一個float資料,y陣列放在0x20開始的地址中。第一個迴圈中,當訪問x[0]的時候(set index等於0),cache的set 0被載入了x[0]~x[3]的資料。當訪問y[0]的時候,由於set index也是0,因此y[0]~y[3]被載入到set 0,從而替換了之前載入到set 0的x[0]~x[3]資料。第二個迴圈的時候,當訪問x[1],不能cache命中,於是重新將x[0]~x[3]的資料載入set 0,而訪問y[1]的時候,仍然不能cache hit,因為y[0]~y[3]已經被flush掉了。有一個術語叫做Thrashing就是描述這種情況。

正是因為E=1導致了cache thrashing,加大E可以解決上面的問題。當一個cache set中有多於1個cache line的時候,這種cache就叫做Set Associative Cache。ARM926的cache被稱為four-way set associative cache,也就是說一個cache set中包括4個cache line。一旦有了多個cache line,判斷cache hit就稍顯麻煩了,因為這時候必須要逐個比對了,直到找到匹配的Tag並且是valid的那個cache line。如果cache miss,這時候就需要從main memory載入cache,如果有空當然好,選擇那個flag是invalid的cache line就OK了,如果所有的cache line都是有效的,那麼替換哪一個cache line呢?當然,硬體設計可以有多種選擇,但是毫無疑問增加了複雜度。

還有一種cache被叫做fully Associative cache,這種cache只有一個cache set。這種cache匹配非常耗時,因為所有的cache line都在一個set中,硬體要逐個比對才能判斷出cache miss or hit。這種cache只適合容量較小的cache,例如TLB。

3、寫操作帶來的問題

上面的章節主要描述讀操作,對於寫操作其實也存在cache hit和cache miss,這時候,系統的行為又是怎樣的呢?我們首先來看看當cache hit時候的行為(也就是說想要寫入資料的地址單元已經在cache中了)。根據寫的行為,cache分成三種類型:

(1)write through。CPU向cache寫入資料時,同時向memory也寫一份,使cache和memory的資料保持一致。優點是簡單,缺點是每次都要訪問memory,速度比較慢。

(2)帶write buffer的write through。策略同上,只不過不是直接寫memory,而是把更新的資料寫入到write buffer,在合適的時候才對memory進行更新。

(3)write back。CPU更新cache line時,只是把更新的cache line標記為dirty,並不同步更新memory。只有在該cache line要被替換掉的時候,才更新 memory。這樣做的原因是考慮到很多時候cache存入的是中間結果(根據區域性性原理,程式可能隨後還會訪問該單元的資料),沒有必要同步更新memory(這可以降低bus transaction)。優點是CPU執行的效率提高,缺點是實現起來技術比較複雜。

在write操作時發生cache miss的時候有兩種策略可以選擇:

(1)no-write-allocate cache。當write cache miss的時候,簡單的寫入main memory而沒有cache的操作。一般而言,write through的cache會採用no-write-allocate的策略。

(2)write allocate cache。當write cache miss的時候,分配cache line並將資料從main memory讀入,之後再進行資料更新的動作。一般而言,write back的cache會採用write allocate的策略。

4、實體地址還是虛擬地址?

當CPU發出地址訪問的時候,從CPU出去的地址是虛擬地址,經過MMU的對映,最終變成實體地址。這時候,問題來了,我們是用虛擬地址還是實體地址(中的cache set index)來尋找cache set?此外,當找到了cache set,那麼我們用虛擬地址還是實體地址(中的Tag)來匹配cache line?

根據使用的是實體地址還是虛擬地址,cache可以分成下面幾個類別:

(1)VIVT(Virtual index Virtual tag)。尋找cache set的index和匹配cache line的tag都是使用虛擬地址。

(2)PIPT(Physical index Physical tag)。尋找cache set的index和匹配cache line的tag都是使用實體地址。

(3)VIPT(Virtual index Physical tag)。尋找cache set的index使用虛擬地址,而匹配cache line的tag使用的是實體地址。

對於一個計算機系統,CPU core、MMU和Cache是三個不同的HW block。採用PIPT的話,CPU發出的虛擬地址要先經過MMU翻譯成實體地址之後,再輸入到cache中進行cache hit or miss的判斷,毫無疑問,這個序列化的操作損害了效能。但是這樣簡單而粗暴的使用實體地址沒有歧義,不會有cache alias。VIVT的方式毫無疑問是最快的,不需要MMU的翻譯直接進入cache判斷hit or miss,不過會引入其他問題,例如:一個實體地址的內容可以出現在多個cache line中,這就需要更多的cache flush操作。反而影響了速度(這就是傳說中的cache alias,具體請參考下面的章節)。採用VIPT的話,CPU輸出的虛擬地址可以同時送到MMU(進行翻譯)和cache(進行cache set的選擇)。這樣cache 和MMU可以同時工作,而MMU完成地址翻譯後,再用物理的tag來匹配cache line。這種方法比不上VIVT 的cache 速度, 但是比PIPT 要好。在某些情況下,VIPT也會有cache alias的問題,但可以用巧妙的方法避過,後文會詳細描述。

對於ARM而言,隨著技術進步,MMU的翻譯速度提高了,在cache 用index查詢cache set的過程中MMU已經可以完成虛擬地址到實體地址的轉換工作,因此在cache比較tag的時候實體地址已經可以使用了,就是說採用 physical tag可以和cache並行工作,不會影響cache的速度。因此,在新的ARM構建中(如ARMv6和ARMv7中),採用了VIPT的方式。

5、cache alias

在linux核心中可能有這樣的場景:同一個實體地址被對映到多個不同的虛擬地址上。在這樣的場景下,我們可以研究一下cache是如何處理的。

對於PIPT,沒有alias,因為cache set selection和tag匹配都是用實體地址,對於一個實體地址,cache中只會有一個cache line的資料與之對應。

對於VIPT的cache system,雖然在匹配tag的時候使用physical address tag,但是卻使用virtual address的set index進行cache set查詢,這時候由於使用不同的虛擬地址而導致多個cache line針對一個實體地址。對於linux的記憶體管理子系統而言,virtual address space是通過4k的page進行管理的。對於實體地址和虛擬地址,其低12 bit是完全一樣。 因此,即使是不同的虛擬地址對映到同一個實體地址,這些虛擬地址的低12bit也是一樣的。在這種情況下,如果查詢cache set的index位於低12 bit的範圍內,那麼alais不會發生,因為不同的虛擬地址對應同一個cache line。當index超過低12 bit的範圍,就會產生alais。在實際中,cache line一般是32B,佔5個bit,在VIPT的情況下,set index佔用7bit(包括7bit)一下,VIPT就不存在alias的問題。在我接觸到的專案中,ARM的16K cache都是採用了128個cache set,也就是7個bit的set index,恰好滿足了no alias的需求。

對於VIVT,cache中總是存在多於一個的cache line 包含這個實體地址的資料,總是存在cache alias的問題。

cache alias會影響cache flush的介面,特別是當flush某個或者某些實體地址的時候。這時候,系統軟體需要找到該實體地址對應的所有的cache line進行flush的動作。

6、Cache Ambiguity

Cache Ambiguity是指將不同的實體地址對映到相同的虛擬地址而造成的混亂。這種情況下在linux核心中只有在不同程序的使用者空間的頁面才可能發生。 Cache Ambiguity會造成同一個cache line在不同的程序中代表不同的資料, 切換程序的時候需要進行處理。

對於PIPT,不存在Cache Ambiguity,雖然虛擬地址一樣,但是實體地址是不同的。對於VIPT,由於使用實體地址來檢查是否cache hit,因此不需要在程序切換的時候flush使用者空間的cache來解決Cache Ambiguity的問題。VIVT會有Cache Ambiguity的問題,一般會在程序切換或者exit mm的時候flush使用者空間的cache