最近工作中,經(jīng)歷了很多項(xiàng)目問題的調(diào)試,把這些問題歸總起來,其中和存儲(chǔ)有關(guān)的,獨(dú)占一半。而存儲(chǔ)對(duì)計(jì)算機(jī)系統(tǒng)造成的影響又可分為兩塊:一是系統(tǒng)功能的穩(wěn)健性;二是程序的執(zhí)行效率。
存儲(chǔ)器結(jié)構(gòu)
1.1 存儲(chǔ)器層次結(jié)構(gòu)
由于訪問速度、成本、功耗等指標(biāo)的制約,計(jì)算機(jī)系統(tǒng)中的存儲(chǔ)往往不是作為一個(gè)單一的大塊存在,而是被設(shè)置成一個(gè)多級(jí)的層次結(jié)構(gòu)。作為一個(gè)程序員,需要理解存儲(chǔ)器層次結(jié)構(gòu),因?yàn)樗鼘?duì)應(yīng)用程序的性能有著巨大的影響。
圖1中展示了一個(gè)典型 計(jì)算機(jī)系統(tǒng)的存儲(chǔ)層級(jí)結(jié)構(gòu)。和金字塔模型很像,越靠近金字塔頂端的存儲(chǔ)器,離CPU越近,訪問更便利且訪問速度快,但同時(shí)它們的容量也越來越?。环粗娇拷讓?,存儲(chǔ)容量越大,訪問速度卻越來越慢。
最高層的L0,是CPU寄存器,CPU可以在一個(gè)時(shí)鐘周期內(nèi)訪問他們;接下來是一個(gè)或多個(gè)小型到中型的基于SRAM的高速緩存存儲(chǔ)器,可以在幾個(gè)CPU時(shí)鐘周期內(nèi)訪問它們;然后是一個(gè)大的基于DRAM的主存,可以在幾十到幾百個(gè)時(shí)鐘周期內(nèi)訪問它們;接下來是慢速但容量很大的本地磁盤;最后,甚至有些系統(tǒng)會(huì)通過網(wǎng)絡(luò)訪問其遠(yuǎn)程服務(wù)器上的磁盤。
圖1 存儲(chǔ)器層次結(jié)構(gòu)
高速緩存可以存在一級(jí)或多級(jí),甚至不存在于一些低性能的單片機(jī)中。TI C64X架構(gòu)中設(shè)計(jì)有2級(jí)的高速緩存,其數(shù)據(jù)和代碼的存取機(jī)制在本訂閱號(hào)之前的文章“TI C6000 數(shù)據(jù)存儲(chǔ)處理與性能優(yōu)化”中有過描述。
1.2 存儲(chǔ)器的訪問
一個(gè)編寫良好的計(jì)算機(jī)程序常常具有良好的局部性(locality)。
局部性通常有兩種不同的形式:時(shí)間局部性和空間局部性。
時(shí)間局部性:在一個(gè)具有良好時(shí)間局部性的程序中,被訪問過一次的內(nèi)存位置很可能在不遠(yuǎn)的將來再被多次訪問 。
空間局部性:在一個(gè)具有良好空間局部性的程序中,如果一個(gè)內(nèi)存位置被訪問 了一次,那么程序很可能在不遠(yuǎn)的將來訪問 附近的一個(gè)內(nèi)存位置。
例如對(duì)一個(gè)二維數(shù)組求和:
for (i=0; i《M; i++)
for(j=0; j《N; j++)
sum += data_tab[i][j];
其中,sum在循環(huán)迭代中被多次訪問,它具有良好的時(shí)間局部性,但因其為標(biāo)量,因此沒有空間局部性。對(duì)二維數(shù)組data_tab而言,其元素在存儲(chǔ)中順序存儲(chǔ),因此有好的空間局部性,但由于其每個(gè)元素只訪問一次,因此時(shí)間局部性很差。
另外,假設(shè)把上面第三行替換成:
sum += data_tab[j][i];
由于二維數(shù)組是按行依次存儲(chǔ),則每次循環(huán)對(duì)內(nèi)存訪問的步長(zhǎng)由1變?yōu)镹,空間局部性變差。像這樣看上去對(duì)程序很小的改動(dòng)對(duì)它的局部性有很大的影響。
存儲(chǔ)類型
2.1 隨機(jī)訪問存儲(chǔ)器(RAM)
上電使用,掉電存儲(chǔ)內(nèi)容丟失。
靜態(tài)RAM(SRAM):存儲(chǔ)單元具有雙穩(wěn)態(tài)特性,上電就能保持它的值,對(duì)光和電噪聲等干擾不敏感。它使用晶體管多,因而密集度低,更貴,功耗更大。
動(dòng)態(tài)RAM(DRAM):對(duì)每個(gè)位存儲(chǔ)為對(duì)一個(gè)電容的充電,因漏電的原因需要不斷刷新來保持存儲(chǔ)值。
SDRAM(Synchronous DRAM):用與驅(qū)動(dòng)內(nèi)存控制器相同的外部時(shí)鐘信號(hào)的上升沿,來作為控制信號(hào)。
DDR SDRAM(Double Data-Rate Synchronous DRAM):通過使用兩個(gè)時(shí)鐘沿作為控制信號(hào),從而使DRAM的速度翻倍。它是用提高有效帶寬的很小的預(yù)緩沖區(qū)的大小來劃分的,如DDR(2位)、DDR2(4位)、DDR3(8位)。
表1 SRAM與DRAM性能比較
2.2 非易失性存儲(chǔ)器
非易失性存儲(chǔ)器,即使在關(guān)電后仍能保存著它們的信息。由于歷史的原因,它們也被整體成稱為ROM(Read Only Memory),但很多ROM類型是即可以讀也可以寫的。
PROM(Programble ROM):只能被編程一次,PROM的每個(gè)存儲(chǔ)器單元有一種熔絲,只能用高電流熔斷一次。
EPROM(Erasable Programble ROM):可擦寫可編程,對(duì)它的編程是通過一種把1寫入EPROM的特殊設(shè)備來完成的。
EEPROM(Electrically Erasable Programble ROM):電子可擦除,不需要一個(gè)物理上獨(dú)立的編程設(shè)備,因此可以直接在芯片上編程,能被編程的次數(shù)可以達(dá)到10e5次。
閃存(flash):基于EEPROM的一類非易失性存儲(chǔ)器。對(duì)它寫數(shù)據(jù)必須先將芯片中對(duì)應(yīng)的內(nèi)容清空,然后再寫入,也就是通常說的“先擦后寫 ”。
NAND flash:對(duì)它的操作以“塊 ”為基本單位,因此要修改一個(gè)字節(jié),必須重寫整個(gè)數(shù)據(jù)塊。它的容量大,適合做大量數(shù)據(jù)的存儲(chǔ)。
NOR flash:對(duì)它的操作以“字 ”為基本單位。它的容量相對(duì)較小,成本大,一般用來存儲(chǔ)程序。
2.3 磁盤
磁盤由盤片構(gòu)成,它是廣為應(yīng)用的保存大量數(shù)據(jù)的存儲(chǔ)設(shè)備。不過從磁盤上讀信息的時(shí)間為毫秒級(jí),比從DRAM讀慢了10萬倍,比從SRAM讀慢了100萬倍。
棧和堆
3.1 棧(stack)
棧是一個(gè)“后進(jìn)先出”的存儲(chǔ)空間,程序運(yùn)行中,代碼“過程 ”涉及到的返回地址、過程參數(shù)、需要保存的寄存器信息都被壓入棧中,過程中聲明的臨時(shí)變量也是在棧中開辟存儲(chǔ)。
過程的形式多樣:函數(shù)、方法、子例程、中斷處理函數(shù)等。當(dāng)過程需要的存儲(chǔ)空間超出寄存器能夠存放的大小時(shí),就會(huì)在棧上分配空間,這部分空間稱為過程的棧幀。
通常,棧底位于高地址,隨著數(shù)據(jù)的壓入,棧向低地址方向增長(zhǎng),而棧指針指向棧頂。將棧指針減少一個(gè)適當(dāng)?shù)牧?,可以為沒有指定初始值的數(shù)據(jù),在棧上分配空間;類似的,可以通過增加棧指針來釋放空間。因?yàn)闂5奈恢萌Q于.stack段分配在什么地方,所以棧的實(shí)際地址是在連接時(shí)決定的。
假設(shè)過程P調(diào)用過程Q時(shí),相關(guān)的控制和數(shù)據(jù)信息添加到棧尾,當(dāng)Q返回時(shí),這些信息會(huì)釋放掉。如果存在多級(jí)調(diào)用(包括遞歸調(diào)用),較早的棧幀也會(huì)累積下來,直到它對(duì)應(yīng)的過程結(jié)束。
圖2 通用的棧幀結(jié)構(gòu)
棧空間的大小可以在鏈接命令文件中,使用-stack選項(xiàng)進(jìn)行設(shè)定。在對(duì)TI C64X架構(gòu)芯片進(jìn)行編程時(shí),如果用戶沒有自己設(shè)定,系統(tǒng)默認(rèn)棧的尺寸為1KB。注意:編譯器在編譯期間和運(yùn)行期間都不進(jìn)行對(duì)棧溢出的檢查,因此對(duì)??臻g的管理上需格外小心。
3.2 堆(heap)
堆是用于動(dòng)態(tài)內(nèi)存分配的一個(gè)存儲(chǔ)區(qū)域,動(dòng)態(tài)分配的對(duì)象不是可直接尋址的,它們總是用指針來訪問。動(dòng)態(tài)內(nèi)存分配器將堆視為一組不同大小的塊的集合來維護(hù)。每個(gè)塊就是一個(gè)連續(xù)的虛擬內(nèi)存片,要么是已分配的,要么是空閑的。一個(gè)已分配的塊保持已分配狀態(tài),直到他被釋放,這種釋放要么是應(yīng)用程序顯式執(zhí)行的,要么是內(nèi)存分配器自身隱式執(zhí)行的。
C標(biāo)準(zhǔn)庫提供一個(gè)稱為malloc程序包的顯式分配器,程序通過調(diào)用malloc函數(shù)來從堆中分配塊。malloc函數(shù)返回一個(gè)指針,指向大小至少為size字節(jié)的內(nèi)存塊,這個(gè)塊會(huì)為可能包含在這個(gè)塊內(nèi)的任何數(shù)據(jù)對(duì)象類型做對(duì)齊。與malloc相對(duì)應(yīng)的是free程序包,用于釋放已分配的內(nèi)存塊。
下圖直接截取深入理解計(jì)算機(jī)系統(tǒng)這本書中,用一個(gè)16字的小堆來展示的malloc和free是如何管理堆空間的:
程序使用動(dòng)態(tài)內(nèi)存分配的最重要原因是,經(jīng)常直到程序?qū)嶋H運(yùn)行時(shí)才知道某些數(shù)據(jù)結(jié)構(gòu)的大小。
堆位于段.sysmem中,可分配動(dòng)態(tài)存儲(chǔ)池的大小,僅僅受限于系統(tǒng)中實(shí)際的存儲(chǔ)容量。 堆空間的大小可以在鏈接命令文件中,使用-heap選項(xiàng)進(jìn)行設(shè)定。在對(duì)TI C64X架構(gòu)芯片進(jìn)行編程時(shí),如果用戶沒有自己設(shè)定,系統(tǒng)默認(rèn)堆的尺寸為1KB。
C編程中常見的與內(nèi)存有關(guān)的錯(cuò)誤
在我們項(xiàng)目的調(diào)試中,死機(jī)現(xiàn)象被認(rèn)為是最讓人頭疼的問題,因?yàn)檫@類問題往往很難復(fù)現(xiàn),而即便是找到了復(fù)現(xiàn)的規(guī)律,也很難通過跟蹤調(diào)試來斷定問題所屬的區(qū)域。由軟件造成死機(jī)的因素,可大致分為兩類:內(nèi)存操作錯(cuò)誤和線程阻塞。
線程阻塞也有很多表現(xiàn)方式,如高優(yōu)先級(jí)線程堆積、程序陷入異常處理流程、死循環(huán)等等。所幸的是,線程阻塞要么很好復(fù)現(xiàn),要么被追蹤到一次就能立馬找出癥結(jié)所在,可面對(duì)內(nèi)存錯(cuò)誤造成的死機(jī)就不那么容易解決了?!渡钊肜斫庥?jì)算機(jī)系統(tǒng)》這本書中也講到:與內(nèi)存有關(guān)的錯(cuò)誤,屬于那些最令人驚恐的錯(cuò)誤,因?yàn)樗麄冊(cè)跁r(shí)間和空間上,經(jīng)常在舉措無緣一段距離之后才表現(xiàn)出來,將錯(cuò)誤的數(shù)據(jù)選擇錯(cuò)誤的位置,你的程序可能在最終失敗之前運(yùn)行了好幾個(gè)小時(shí),其實(shí)程序終止的位置距離處的位置已經(jīng)很遠(yuǎn)了。
內(nèi)存操作錯(cuò)誤為什么會(huì)造成死機(jī)?舉個(gè)簡(jiǎn)單的類比:一套性能完整的代碼就好比是一幢設(shè)計(jì)建造完好的房子,而內(nèi)存操作錯(cuò)誤就像是突然拆除或篡改了支撐房屋結(jié)構(gòu)或運(yùn)轉(zhuǎn)的關(guān)鍵部位,這一突發(fā)錯(cuò)誤完全超出設(shè)計(jì)意料之外,引發(fā)一連串的惡果也就在所難免。下面是一些常見的內(nèi)存錯(cuò)誤操作的例子(真實(shí)的情況遠(yuǎn)不止這些,更多情況可參考《深入理解計(jì)算機(jī)系統(tǒng)》這本書):
4.1 數(shù)組越界
int data_tab[100];
int i;
for(i=0; i《=100; i++)
{
data_tab[i] = i;
}
這個(gè)例子中data_tab占100個(gè)int型長(zhǎng)度,但循環(huán)卻進(jìn)行了101次。C對(duì)于數(shù)組應(yīng)用不進(jìn)行任何邊界檢查,而且局部變量和狀態(tài)信息(例如保存的寄存器值和返回地址)存放在棧中,這兩種情況結(jié)合到一起就能導(dǎo)致嚴(yán)重的程序錯(cuò)誤,對(duì)越界數(shù)組元素的寫操作,會(huì)破壞存儲(chǔ)在棧中的狀態(tài)信息。
另一種情況是data_tab[]是全局?jǐn)?shù)組,這樣的越界將引起對(duì)數(shù)組尾部其它存儲(chǔ)內(nèi)容的修改,倘若被篡改的存儲(chǔ)區(qū)域剛好涉及到代碼的關(guān)鍵流程判斷,意味著異常流程的出現(xiàn)。
4.2 間接引用壞指針
scanf(“%d”, &val);
錯(cuò)誤寫成:
scanf(“%d”, val);
在這種情況下,scanf將val的內(nèi)容解釋為一個(gè)地址,并試圖將一個(gè)字節(jié)寫到這個(gè)位置。在最好的情況下,程序立即異常終止,在最糟糕的情況下,val的內(nèi)容對(duì)應(yīng)于內(nèi)存的某個(gè)合法的讀寫區(qū)域,于是我們就覆蓋了這塊內(nèi)存,就通常會(huì)在相當(dāng)長(zhǎng)的一段時(shí)間以后,造成災(zāi)難性的令人困惑的后果。
4.3 誤解指針運(yùn)算
int *search(int *p, int val)
{
while(*p && *p != val)
p += sizeof(int) // should be p++
return p;
}
這種錯(cuò)誤是忘記了指針的算術(shù)操作是以它指向的對(duì)象的大小為單位來進(jìn)行的。假設(shè)這里int占四個(gè)字節(jié),原本掃描每個(gè)int變成了每四個(gè)int掃描一次。
4.4 引用不存在的變量
int *stackref()
{
int val;
int *p;
p = &val;
return p;
}
這個(gè)函數(shù)返回一個(gè)指針,指向棧里的一個(gè)局部變量。盡管p仍指向一個(gè)合法的內(nèi)存地址,是它已經(jīng)不再指向一個(gè)合法的變量了。要知道,局部變量val在函數(shù)返回時(shí),將會(huì)被釋掉。
4.5 引用空閑堆塊中的數(shù)據(jù)
int *heapref()
{
int *x, *y;
x = (int *)malloc(sizeof(int));
。。。
free(x);
y = x;
。。。
return y;
}
這里y錯(cuò)誤的引用已經(jīng)被釋放的堆塊的數(shù)據(jù)和指針x。申請(qǐng)的堆塊一旦被釋放,雖然之前指向該堆塊的指針x并沒有變化,但它指向的區(qū)域數(shù)據(jù)已經(jīng)不再有效了。
4.6 引起內(nèi)存泄漏
void leak(int n)
{
x = (int *)malloc(n*sizeof(int));
return;
}
這個(gè)函數(shù)中分配了一個(gè)堆塊,但沒釋放就返回。如果經(jīng)常調(diào)用leak,那么漸漸的堆里就會(huì)充滿了垃圾,最糟糕的情況下會(huì)占用整個(gè)內(nèi)存地址空間。內(nèi)存泄漏是緩慢、隱性的殺手。
評(píng)論
查看更多