1. 一個具有快表的分頁存儲系統。訪問一次內存需要100納秒,訪問一次快表需要20納
設命中率為x
則120=100x+(100+180*2)(1-x)
x=94.4%
應該對哈~~
2. 內存管理
在一段時間內,程序的執行僅限於某個部分,相應地,它所訪問的存儲空間也局限於某個區域。
局部性原理的 分類 :
將編譯後的目標模塊裝配成一個可執行程序。
可執行程序以 二進制可執行文件 的形式存儲在磁碟上。
鏈接程序的 任務 :
程序的鏈接,可劃分為:
重定位 :將邏輯地址(相對地址)轉換為物理地址(絕對地址)的過程。
物理地址 = 邏輯地址 + 程序在內存中的起始地址
程序的裝入,可劃分為:
任何時刻主存儲器 最多隻有一個作業 。
每個分區 大小固定不變 :分區大小相等、分區大小不等。
每個分區可以且 僅可以裝入一個作業 。
使用 下限寄存器 和 上限寄存器 來保存當前作業的起始位置和結束位置。
使用 固定分區說明表 區分各分區的狀態。
分區 大小不是預先固定的 ,而是按作業(進程)的實際需求來劃分的。
分區 個數也不是預先固定的 ,而是由裝入的作業數決定的。
使用 空閑分區表 說明空閑分區的位置。
使用 空閑分區鏈 說明空閑分區的位置。
首次適應演算法的 過程 :
外部碎片:空閑內存 沒有在 分配的 進程 中。
內部碎片:空閑內存 在 分配的 進程 中。
從 上次找到的 空閑分區的 下一個 空閑分區開始查找。
優點:空閑區分布均勻、查找開銷較小。
缺點:缺乏大空閑區。
最佳適應演算法的 過程 :
優點:提高內存利用率。
注意點:每次在進行空閑區的修改前,需要先進行 分區大小遞增 的排序。
頁 :將一個 進程 的 邏輯地址空間 分成若干個 大小相等 的 片 。
頁框 :將 物理內存空間 分成與頁大小相同的若干個 存儲塊 。
分頁存儲 :將進程的若干 頁 分別裝入多個 可以不相鄰 的 頁框 中。
頁內碎片 :進程 最後一頁 一般裝不滿一個頁框,形成 頁內碎片 。
頁表 :記錄描述頁的各種數據,實現從 頁號 到 頁框號 的映射。
注意: 頁內偏移量 的單位是 位元組 。
分頁地址變換指是: 邏輯地址 通過 地址變換機構 變換為 物理地址 。
分頁地址變換的 過程 :
操作系統在修改或裝入頁表寄存器的值時,使用的是 特權級 指令。
頁大小:512B ~ 4KB,目前的計算機系統中,大多選擇 4KB 大小的頁。
頁大小的 選擇因素 :
快表也稱為「轉換後援緩沖」,是為了提高CPU訪問速度而採用的專用緩存,用來存放 最近被訪問過的頁表項 。
英文縮寫:TLB。
組成: 鍵和值 。
在TLB中找到某一個頁號對應的頁表項的百分比稱為 TLB命中率 。
當 能 在TLB中找到所需要的頁表項時:
有效訪問時間 = 一次訪問TLB 的時間 + 一次訪問內存 的時間(訪問內存讀寫數據或指令)
當 不能 在TLB中找到所需要的頁表項時:
有效訪問時間 = 一次訪問TLB 的時間 + 兩次訪問內存 的時間(一次訪問內存頁表,一次訪問內存讀寫數據或指令)
將頁表再分頁,形成兩級或多級頁表,將頁表離散地存放在物理內存中。
在進程切換時,要運行的進程的頁目錄表歧視地址被寫入 頁表寄存器 。
在二級分頁系統中,為頁表再建立一個頁目錄表的目的是為了能在地址映射時得到頁表在物理內存中的地址,在頁目錄表的表項中存放了每一個 頁表 在物理內存中所在的 頁框號 。
虛擬存儲器 :是指具有 請求調入功能 和 置換功能 ,能 從邏輯上對內存容量進行擴充 的一種存儲系統。
請求調入 :就是說,先將進程一部分裝入內存,其餘的部分什麼時候需要,什麼時候請求系統裝入。
置換 :如果請求調入時,沒有足夠的內存,則由操作系統選擇一部分內存中的進程內容移到外存,以騰出空間把當前需要裝入的內存調入。
為了實現請求分頁,需要:
保證進程正常運行的所需要的最小頁框數。
最小頁框數與進程的大小沒有關系,它與計算機的 硬體結構 有關,取決於 指令的格式、功能和定址方式 。
內存不夠時,從進程本身選擇淘汰頁,還是從系統中所有進程中選擇?:
採用什麼樣的演算法為不同進程分配頁框?:
常用的兩種 置換策略 : 局部置換 和 全局置換 。
從分配給進程的頁框數量上看,常使用的兩種 分配策略 : 固定分配 和 可變分配 。
用新調入的頁替換 最長時間沒有訪問 的頁面。
找到 未來最晚被訪問 的那個頁換出。
,P為缺頁率。
有效訪問時間與缺頁率成 正比 ,缺頁率越高,有效訪問時間越長,訪問效率越低。
工作集 :某段時間間隔里,進程實際要訪問的頁的集合。
引入工作集的 目的 :降低缺頁率,提高訪問內存效率。
抖動 :運行進程的大部分時間都用於頁的換入換出,幾乎不能完成任何有效果工作的狀態。
抖動的 產生原因 :
抖動的 預防方法 :
在分段存儲管理的系統中,程序使用 二維 的邏輯地址,一個數用來表示 段 ,另一個數用來表示 段內偏移量 。
引入分段的 目的 :
引入分段的 優點 :
進程的地址空間被劃分成 若干個段 。
每個段定義了一組邏輯信息,每個段的大小由相應的邏輯信息組的長度確定, 段的大小不一樣 ,每個段的邏輯地址從0開始,採用一段 連續的地址空間 。
系統為每個段分配一個 連續的物理內存區域 ,各個 不同的段可以離散 地放入物理內存不同的區域。
系統為 每個進程建立一張段表 ,段表的每一個表項記錄的信息包括: 段號、段長和該段的基址 ,段表存放在內存中。
分段的 邏輯地址結構 :
段表是由操作系統維護的用於支持分段存儲管理 地址映射 的數據結構。
每個進程有一個段表,段表由段表項構成。每個段表項包括: 段號、段長(段的大小)和該段的基址(段的起始地址) 。
若已知邏輯單元的地址為 S:D (段號:段內偏移量),求相應物理地址的步驟如下:
相同點 :分頁和分段都屬於 離散 分配方式,都要通過數據結構與硬體的配合來實現 邏輯地址到物理地址 的映射。
不同點 :
將用戶進程的邏輯空間 先劃分為若干個段 , 每個段再劃分成若干個頁 。
進程以頁為單位在物理內存中 離散 存放,每個段中被離散存放的頁具有 邏輯相關性 。
為了實現地址映射,操作系統為 每個進程建立一個段表 ,再為 每個段建立一個頁表 。
進程段表的段表項組成:
滿足以下條件的兩個塊稱為 夥伴 :
3. 假定一個將頁表放在內存的分頁系統,如果一次內存訪問用200ns,訪問一頁內存需要多少時間
訪問一頁內存的時間為:200ns+200ns=400ns
4. c語言訪問內存沖突,這該怎麼辦啊
、C中內存分為四個區
棧:用來存放函數的形參和函數內的局部變數。由編譯器分配空間,在函數執行完後由編譯器自動釋放。
堆:用來存放由動態分配函數(如malloc)分配的空間。是由程序員自己手動分配的,並且必須由程序員使用free釋放。如果忘記用free釋放,會導致所分配的空間一直占著不放,導致內存泄露。
全局局:用來存放全局變數和靜態變數。存在於程序的整個運行期間,是由編譯器分配和釋放的。
文字常量區:例如char *c = 「123456」;則」123456」為文字常量,存放於文字常量區。也由編譯器控制分配和釋放。
程序代碼區:用來存放程序的二進制代碼。
例子(一)
int a = 0; //全局區
void main()
{
int b; //棧
char s[] = abc; //s在棧,abc在文字常量區
char *p1,*p2; //棧
char *p3 = 123456; //123456在常量區,p3在棧上
static int c =0; //全局區
p1 = (char *)malloc(10); //p1在棧,分配的10位元組在堆
p2 = (char *)malloc(20); //p2在棧,分配的20位元組在堆
strcpy(p1, 123456); //123456放在常量區
}
例子(二)
//返回char型指針
char *f()
{
//s數組存放於棧上
char s[4] = {'1','2','3','0'};
return s; //返回s數組的地址,但程序運行完s數組就被釋放了
}
void main()
{
char *s;
s = f();
printf (%s, s); //列印出來亂碼。因為s所指向地址已經沒有數據
}
2、動態分配釋放內存
用malloc動態分配內存後一定要判斷一下分配是否成功,判斷指針的值是否為NULL。
內存分配成功後要對內存單元進行初始化。
內存分配成功且初始化後使用時別越界了。
內存使用完後要用free(p)釋放,注意,釋放後,p的值是不會變的,仍然是一個地址值,仍然指向那塊內存區,只是這塊內存區的值變成垃圾了。為了防止後面繼續使用這塊內存,應在free(p)後,立即p=NULL,這樣後面如果要使用,判斷p是否為NULL時就會判斷出來。
NO.1
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str,hello world);
printf(str);
}
請問運行Test函數後會是什麼樣的結果?
NO.2
char *GetMemory(void)
{
char p[] = hello world;
retrun p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
問題同NO.1
NO.3
void GetMemory2(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str,100);
strcpy(str,hello);
printf(str);
}
問題同NO.1
NO.4
void Test(void)
{
char *str = (char *)malloc(100);
strcpy(str,hello);
free(str);
if(str != NULL)
{
strcpy(str,world);
printf(str);
}
}
問題同NO.1
我對以上問題的分析:
NO.1:程序首先申請一個char類型的指針str,並把str指向NULL(即str里存的是NULL的地址,*str為NULL中的值為0),調用函數的過程中做了如下動作:1申請一個char 類型的指針p,2把str的內容到了p里(這是參數傳遞過程中系統所做的),3為p指針申請了100個空間,4返回Test函數.最後程序把字元串hello world拷貝到str指向的內存空間里.到這里錯誤出現了!str的空間始終為NULL而並沒有實際的空間.深刻理解函數調用的第2步,將不難發現問題所在!(建議:畫圖理解)
NO.2:程序首先申請一個char類型的指針str,並把str指向NULL.調用函數的過程中做了如下動作:1申請一數組p[]並將其賦值為hello world(數組的空間大小為12),2返回數組名p付給str指針(即返回了數組的首地址).那麼這樣就可以列印出字元串"hello world"了么?當然是不能的!因為在函數調用的時候漏掉了最後一步.也就是在第2步return數組名後,函數調用還要進行一步操作,也就是釋放內存空間.當一個函數被調用結束後它會釋放掉它裡面所有的變數所佔用的空間.所以數組空間被釋放掉了,也就是說str所指向的內容將不確定是什麼東西.
NO.3:正確答案為可以列印出hello.但內存泄漏了!
NO.4:申請空間,拷貝字元串,釋放空間.前三步操作都沒有任何問題.到if語句里的判斷條件開始出錯了,因為一個指針被釋放之後其內容並不是NULL,而是一個不確定的值.所以if語句永遠都不能被執行.這也是著名的"野"指針問題.所以我們在編寫程序釋放一個指針之後一定要人為的將指針付成NULL.這樣就會避免出現"野"指針的出現.有人說"野"指針很可怕,會帶來意想不到的錯誤.
C語言內存對齊
C99規定int、unsigned int和bool可以作為位域類型,但編譯器幾乎都對此作了擴展,允許其它類型類型的存在。
使用位域的主要目的是壓縮存儲,其大致規則為:
1) 如果相鄰位域欄位的類型相同,且其位寬之和小於類型的sizeof大小,則後面的欄位將緊鄰前一個欄位存儲,直到不能容納為止;
2) 如果相鄰位域欄位的類型相同,但其位寬之和大於類型的sizeof大小,則後面的欄位將從新的存儲單元開始,其偏移量為其類型大小的整數倍;
3) 如果相鄰的位域欄位的類型不同,則各編譯器的具體實現有差異,VC6採取不壓縮方式,Dev-C++採取壓縮方式;
4) 如果位域欄位之間穿插著非位域欄位,則不進行壓縮;
5) 整個結構體的總大小為最寬基本類型成員大小的整數倍。
還是讓我們來看看例子。
示例1:
struct BF1
{
char f1 : 3;
char f2 : 4;
char f3 : 5;
};
其內存布局為:
|_f1__|__f2__|_|____f3___|____|
|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|
0 3 7 8 1316
位域類型為char,第1個位元組僅能容納下f1和f2,所以f2被壓縮到第1個位元組中,而f3隻能從下一個位元組開始。因此sizeof(BF1)的結果為2。
示例2:
struct BF2
{
char f1 : 3;
short f2 : 4;
char f3 : 5;
};
由於相鄰位域類型不同,在VC6中其sizeof為6,在Dev-C++中為2。
示例3:
struct BF3
{
char f1 : 3;
char f2;
char f3 : 5;
};
什麼是內存對齊
考慮下面的結構:
struct foo
{
char c1;
short s;
char c2;
int i;
};
假設這個結構的成員在內存中是緊湊排列的,假設c1的地址是0,那麼s的地址就應該是1,c2的地址就是3,i的地址就是4。也就是
c1 00000000, s 00000001, c2 00000003, i 00000004。
可是,我們在Visual c/c++ 6中寫一個簡單的程序:
struct foo a;
printf("c1 %p, s %p, c2 %p, i %p/n",
(unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.i - (unsigned int)(void*)&a);
運行,輸出:
c1 00000000, s 00000002, c2 00000004, i 00000008。
為什麼會這樣?這就是內存對齊而導致的問題。
為什麼會有內存對齊
以下內容節選自《Intel Architecture 32 Manual》。
字,雙字,和四字在自然邊界上不需要在內存中對齊。(對字,雙字,和四字來說,自然邊界分別是偶數地址,可以被4整除的地址,和可以被8整除的地址。)
無論如何,為了提高程序的性能,數據結構(尤其是棧)應該盡可能地在自然邊界上對齊。原因在於,為了訪問未對齊的內存,處理器需要作兩次內存訪問;然而,對齊的內存訪問僅需要一次訪問。
一個字或雙字操作數跨越了4位元組邊界,或者一個四字操作數跨越了8位元組邊界,被認為是未對齊的,從而需要兩次匯流排周期來訪問內存。一個字起始地址是奇數但卻沒有跨越字邊界被認為是對齊的,能夠在一個匯流排周期中被訪問。
某些操作雙四字的指令需要內存操作數在自然邊界上對齊。如果操作數沒有對齊,這些指令將會產生一個通用保護異常(#GP)。雙四字的自然邊界是能夠被16整除的地址。其他的操作雙四字的指令允許未對齊的訪問(不會產生通用保護異常),然而,需要額外的內存匯流排周期來訪問內存中未對齊的數據。
編譯器對內存對齊的處理
預設情況下,c/c++編譯器默認將結構、棧中的成員數據進行內存對齊。因此,上面的程序輸出就變成了:
c1 00000000, s 00000002, c2 00000004, i 00000008。
編譯器將未對齊的成員向後移,將每一個都成員對齊到自然邊界上,從而也導致了整個結構的尺寸變大。盡管會犧牲一點空間(成員之間有空洞),但提高了性能。
也正是這個原因,我們不可以斷言sizeof(foo) == 8。在這個例子中,sizeof(foo) == 12。
如何避免內存對齊的影響
那麼,能不能既達到提高性能的目的,又能節約一點空間呢?有一點小技巧可以使用。比如我們可以將上面的結構改成:
struct bar
{
char c1;
char c2;
short s;
int i;
};
這樣一來,每個成員都對齊在其自然邊界上,從而避免了編譯器自動對齊。在這個例子中,sizeof(bar) == 8。
這個技巧有一個重要的作用,尤其是這個結構作為API的一部分提供給第三方開發使用的時候。第三方開發者可能將編譯器的默認對齊選項改變,從而造成這個結構在你的發行的DLL中使用某種對齊方式,而在第三方開發者哪裡卻使用另外一種對齊方式。這將會導致重大問題。
比如,foo結構,我們的DLL使用默認對齊選項,對齊為
c1 00000000, s 00000002, c2 00000004, i 00000008,同時sizeof(foo) == 12。
而第三方將對齊選項關閉,導致
c1 00000000, s 00000001, c2 00000003, i 00000004,同時sizeof(foo) == 8。
如何使用c/c++中的對齊選項
vc6中的編譯選項有 /Zp[1|2|4|8|16] ,/Zp1表示以1位元組邊界對齊,相應的,/Zpn表示以n位元組邊界對齊。n位元組邊界對齊的意思是說,一個成員的地址必須安排在成員的尺寸的整數倍地址上或者是n的整數倍地址上,取它們中的最小值。也就是:
min ( sizeof ( member ), n)
實際上,1位元組邊界對齊也就表示了結構成員之間沒有空洞。
/Zpn選項是應用於整個工程的,影響所有的參與編譯的結構。
要使用這個選項,可以在vc6中打開工程屬性頁,c/c++頁,選擇Code Generation分類,在Struct member alignment可以選擇。
要專門針對某些結構定義使用對齊選項,可以使用#pragma pack編譯指令。指令語法如下:
#pragma pack( [ show ] | [ push | pop ] [, identifier ] , n )
意義和/Zpn選項相同。比如:
#pragma pack(1)
struct foo_pack
{
char c1;
short s;
char c2;
int i;
};
#pragma pack()
棧內存對齊
我們可以觀察到,在vc6中棧的對齊方式不受結構成員對齊選項的影響。(本來就是兩碼事)。它總是保持對齊,而且對齊在4位元組邊界上。
驗證代碼
#include <stdio.h>
struct foo
{
char c1;
short s;
char c2;
int i;
};
struct bar
{
char c1;
char c2;
short s;
int i;
};
#pragma pack(1)
struct foo_pack
{
char c1;
short s;
char c2;
int i;
};
#pragma pack()
int main(int argc, char* argv[])
{
char c1;
short s;
char c2;
int i;
struct foo a;
struct bar b;
struct foo_pack p;
printf("stack c1 %p, s %p, c2 %p, i %p/n",
(unsigned int)(void*)&c1 - (unsigned int)(void*)&i,
(unsigned int)(void*)&s - (unsigned int)(void*)&i,
(unsigned int)(void*)&c2 - (unsigned int)(void*)&i,
(unsigned int)(void*)&i - (unsigned int)(void*)&i);
printf("struct foo c1 %p, s %p, c2 %p, i %p/n",
(unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.i - (unsigned int)(void*)&a);
printf("struct bar c1 %p, c2 %p, s %p, i %p/n",
(unsigned int)(void*)&b.c1 - (unsigned int)(void*)&b,
(unsigned int)(void*)&b.c2 - (unsigned int)(void*)&b,
(unsigned int)(void*)&b.s - (unsigned int)(void*)&b,
(unsigned int)(void*)&b.i - (unsigned int)(void*)&b);
printf("struct foo_pack c1 %p, s %p, c2 %p, i %p/n",
(unsigned int)(void*)&p.c1 - (unsigned int)(void*)&p,
(unsigned int)(void*)&p.s - (unsigned int)(void*)&p,
(unsigned int)(void*)&p.c2 - (unsigned int)(void*)&p,
(unsigned int)(void*)&p.i - (unsigned int)(void*)&p);
printf("sizeof foo is %d/n", sizeof(foo));
printf("sizeof bar is %d/n", sizeof(bar));
printf("sizeof foo_pack is %d/n", sizeof(foo_pack));
return 0;
}
5. 案例分析一: 假定CPU的主頻是500MHz。硬碟採用DMA方式進行數據傳送,其
案例一:
在DMA方式下,每秒進行DMA操作為: 5MB/5000B=5×10 6 /5000=1×10 3 次 因為DMA預處理和後處理的總開銷為500個時鍾周期,所以1秒之內用於DMA操作的時鍾周期數為: 500×1×10 3 =5×10 5
案例分析二:
(1)頁面大小為4KB=212 ,則得到頁內位移占虛地址的低12位,頁號占剩餘高位。
31A2H:頁號P=3,有效位為1,存在內存中。先訪問快表2ns,因初始為空,不在快表中,因此,需要訪問頁表200ns得到頁框號,合成物理地址後訪問主存200ns,共計2ns+200ns+200ns=402ns。
24C2H:頁號P=2,有效位為0,不存在內存中。先訪問快表2ns,落空,訪問頁表200ns落空,進行缺頁中斷處理107ns,合成物理地址後訪問主存200ns,共計2ns+200ns+107ns+200ns≈107ns。
36B4H:頁號P=3,有效位為1,存在內存中。訪問快表,因第一次訪問已將該頁號放入快表,因此花費2ns便可合成物理地址,訪問主存200ns,共計2ns+200ns=202ns。
(2)基於上述訪問序列,當訪問虛地址24C2H時產生缺頁中斷,合法駐留集為2,必須從表中淘汰一個頁面,根據題目的置換演算法,應淘汰1號頁面,因此24C2H的對應頁框號為906H。由此可得24C2H的物理地址為9064C2H。
6. 緩存一致性
在現代的 CPU(大多數)上,所有的內存訪問都需要通過層層的緩存來進行。CPU 的讀 / 寫(以及取指令)單元正常情況下甚至都不能直接訪問內存——這是物理結構決定的;CPU 都沒有管腳直接連到內存。相反,CPU 和一級緩存(L1 Cache)通訊,而一級緩存才能和內存通訊。大約二十年前,一級緩存可以直接和內存傳輸數據。如今,更多級別的緩存加入到設計中,一級緩存已經不能直接和內存通訊了,它和二級緩存通訊——而二級緩存才能和內存通訊。或者還可能有三級緩存。
緩存是分「段」(line)的,一個段對應一塊存儲空間,大小是 32、64或128位元組,每個緩存段知道自己對應什麼范圍的物理內存地址。
當 CPU 看到一條讀內存的指令時,它會把內存地址傳遞給一級數據緩存。一級數據緩存會檢查它是否有這個內存地址對應的緩存段。如果沒有,它會把整個緩存段從內存(或者從更高一級的緩存,如果有的話)中載入進來。是的,一次載入整個緩存段,這是基於這樣一個假設:內存訪問傾向於本地化(localized),如果我們當前需要某個地址的數據,那麼很可能我們馬上要訪問它的鄰近地址。一旦緩存段被載入到緩存中,讀指令就可以正常進行讀取。
如果我們只處理讀操作,那麼事情會很簡單,因為所有級別的緩存都遵守以下規律—— 在任意時刻,任意級別緩存中的緩存段的內容,等同於它對應的內存中的內容。 。
一旦我們允許寫操作,事情就變得復雜一點了。這里有兩種基本的寫模式:直寫(write-through)和回寫(write-back)。直寫更簡單一點:我們透過本級緩存,直接把數據寫到下一級緩存(或直接到內存)中,如果對應的段被緩存了,我們同時更新緩存中的內容(甚至直接丟棄),就這么簡單。這也遵守前面的定律: 緩存中的段永遠和它對應的內存內容匹配。
回寫模式就有點復雜了。緩存不會立即把寫操作傳遞到下一級,而是僅修改本級緩存中的數據,並且把對應的緩存段標記為「臟」段。臟段會觸發回寫,也就是把裡面的內容寫到對應的內存或下一級緩存中。回寫後,臟段又變「干凈」了。當一個臟段被丟棄的時候,總是先要進行一次回寫。回寫所遵循的規律有點不同。 當所有的臟段被回寫後,任意級別緩存中的緩存段的內容,等同於它對應的內存中的內容。
換句話說,回寫模式的定律中,我們去掉了「在任意時刻」這個修飾語,代之以弱化一點的條件:要麼緩存段的內容和內存一致(如果緩存段是干凈的話),要麼緩存段中的內容最終要回寫到內存中(對於臟緩存段來說)。
只要系統只有一個 CPU 核在工作,一切都沒問題。如果有多個核,每個核又都有自己的緩存,那麼我們就遇到問題了,因為如果一個 CPU 緩存了某塊內存,那麼在其他 CPU 修改這塊內存的時候,我們希望得到通知。系統的內存在各個 CPU 之間無法做到與生俱來的同步,我們需要一個大家都能遵守的方法來達到同步的目的。
緩存一致性協議有多種,但是日常處理的大多數計算機設備使用的都屬於「窺探(snooping)」協議。
窺探」背後的基本思想是,所有內存傳輸都發生在一條共享的匯流排上,而所有的處理器都能看到這條匯流排:緩存本身是獨立的,但是內存是共享資源,所有的內存訪問都要經過仲裁(arbitrate):同一個指令周期中,只有一個緩存可以讀寫內存。窺探協議的思想是,緩存不僅僅在做內存傳輸的時候才和匯流排打交道,而是不停地在窺探匯流排上發生的數據交換,跟蹤其他緩存在做什麼。所以當一個緩存代表它所屬的處理器去讀寫內存時,其他處理器都會得到通知,它們以此來使自己的緩存保持同步。只要某個處理器一寫內存,其他處理器馬上就知道這塊內存在它們自己的緩存中對應的段已經失效。
在直寫模式下,這是很直接的,因為寫操作一旦發生,它的效果馬上會被「公布」出去。但是如果混著回寫模式,就有問題了。因為有可能在寫指令執行過後很久,數據才會被真正回寫到物理內存中——在這段時間內,其他處理器的緩存也可能會傻乎乎地去寫同一塊內存地址,導致沖突。在回寫模型中,簡單把內存寫操作的信息廣播給其他處理器是不夠的,我們需要做的是,在修改本地緩存之前,就要告知其他處理器。
MESI 是四種緩存段狀態的首字母縮寫,任何多核系統中的緩存段都處於這四種狀態之一。
從CPU讀寫角度來說:
上圖的切換解釋:
緩存的一致性消息傳遞是要時間的,這就使其切換時會產生延遲。當一個緩存被切換狀態時其他緩存收到消息完成各自的切換並且發出回應消息這么一長串的時間中CPU都會等待所有緩存響應完成。可能出現的阻塞都會導致各種各樣的性能問題和穩定性問題。
比如你需要修改本地緩存中的一條信息,那麼你必須將I(無效)狀態通知到其他擁有該緩存數據的CPU緩存中,並且等待確認。等待確認的過程會阻塞處理器,這會降低處理器的性能。因為這個等待遠遠比一個指令的執行時間長的多。
為了避免這種CPU運算能力的浪費,Store Bufferes被引入使用。處理器把它想要寫入到主存的值寫到緩存,然後繼續去處理其他事情。當所有失效確認(Invalidate Acknowledge)都接收到時,數據才會最終被提交。
執行失效也不是一個簡單的操作,它需要處理器去處理。另外,存儲緩存(Store Buffers)並不是無窮大的,所以處理器有時需要等待失效確認的返回。這兩個操作都會使得性能大幅降低。為了應付這種情況,引入了失效隊列——對於所有的收到的Invalidate請求,Invalidate Acknowlege消息必須立刻發送,Invalidate並不真正執行,而是被放在一個特殊的隊列中,在方便的時候才會去執行,處理器不會發送任何消息給所處理的緩存條目,直到它處理Invalidate。
7. 一個分頁表系統,頁表存放在內存,如果一次內存的訪問時間是200ns,引入快表,並且75%的頁表引用發生在快
75%×200+25%×200×2=250ns(因為需要先查頁表判斷內存具體地址再訪問,隨意需要訪問兩次)。磁碟設備在工作時以恆定速率旋轉。為了讀或寫,磁頭必須能移動到所要求的磁軌上,並等待所要求的扇區的開始位置旋轉到磁頭下,然後再開始讀或寫數據。
具體講,從一次讀操作命令發出到該指令完成,將數據讀入數據緩沖寄存器為止所經歷的時間,存儲訪問時間略小於存儲周期。存儲訪問時間和存儲周期反映了主存速度的指標。
(7)假設一次內存訪問擴展閱讀:
磁碟存儲訪問時間:
磁碟設備在工作時以恆定速率旋轉。為了讀或寫,磁頭必須能移動到所要求的磁軌上,並等待所要求的扇區的開始位置旋轉到磁頭下,然後再開始讀或寫數據。故可把對磁碟的訪問時間分成以下三部分。
存儲周期,是指對存儲器進行連續兩次存取操作所需要的最小時間間隔。由於有些存儲器在一次存取操作後需要一定的恢復時間,所以通常存取周期大於或等於取數時間。讀寫周期一般與存儲器的類型有關,在一定程度上體現存儲器的速度。
8. 操作系統題目
兩種情況。
第一種:直接在快表中找到,只需訪問一次內存。所需時間為200+10ns。
第二種,快表中找不到,還得去內存中找。所需時間為,200+10+200ns。
則平均時間為=0.9*210+0.1*410=230ns
9. C++對未對齊的內存訪問為什麼要訪問兩次。講深點。謝謝
我是這樣理解的:
假設地址0x90000000-0x90000003存放int型數據,然後cpu用整型讀寫這個地址時,因為這個地址是32位對齊的,所以cpu直接用0x90000000訪問即可,只訪問一次。
假設地址0x90000002-0x90000005存放int型數據,然後cpu用整型讀寫這個地址時,因為這個地址不是32位對齊的,所以cpu先用0x90000000訪問操作數據的高兩個位元組,再用0x90000004操作數據的低兩位位元組,需要訪問兩次。