內(nèi)存的歷史
現(xiàn)代的intel處理器可以追溯到最早期的intel芯片几苍。
1.8085處理器充分利用了芯片整合技術(shù)蚀浆,它將三塊芯片組合成一塊。在本質(zhì)上香追,它是把8080處理器合瓢、8224時鐘驅(qū)動器和8228控制器整合到一塊芯片上。雖然它內(nèi)部的數(shù)據(jù)總線寬度仍然是8位透典,但它使用了16位的地址總線晴楔,所以能夠訪問2^16也就是64KB的內(nèi)存顿苇。
2.8086處理器于1978年誕生,它對8085作了改進(jìn)税弃,允許16位的數(shù)據(jù)總線和20位的地址總線纪岁,可以訪問多大1MB的內(nèi)存。它通過重疊兩個16位的字來形成20位的地址则果,而不是通過簡單的鏈接兩個字來形成32位的地址幔翰。第一個16位值稱為偏移量,第二個16位字經(jīng)過移位后稱為段西壮,8086芯片有4個段寄存器遗增,用于存儲段地址的值,并能自動進(jìn)入移位和加法操作來產(chǎn)生20位的地址茸时。8086有代碼寄存器CS贡定,數(shù)據(jù)寄存器DS和堆棧寄存器SS,分別存放代碼段可都、數(shù)據(jù)段和堆棧段的首地址缓待,另外還有一個附加段ES。
3.80826差不多就是80186渠牲,只是內(nèi)置了一些微不足道的外設(shè)端口支持旋炒,但它第一次試圖擴展內(nèi)存地址空間。它把內(nèi)存控制器移到處理器芯片的外面签杈,并提供了一種內(nèi)存模式瘫镇,稱為虛擬模式(virtual mode)。在虛擬模式中答姥,段寄存器并不與偏移地址相加铣除,而是為一個存放實際段地址的表提供索引。這種地址模式也被稱作保護模式(protected mode)鹦付,它依然是16位的尚粘。
4.80386在80286的基礎(chǔ)上增加了兩種新的地址模式:32位的保護模式和虛擬的8086模式。Microsoft的windows NT操作系統(tǒng)以及增強模式下的windows都采用了32位的保護模式敲长。這就是為什么windos NT至少需要386才能運行的原因郎嫁。另一種內(nèi)存模式,虛擬8086模式祈噪,可以創(chuàng)建一種內(nèi)存空間為1MB的8086虛擬機,幾個虛擬機可以同時運行泽铛,從而支持MS-DOS的虛擬多任務(wù)系統(tǒng)。
5.80486是一種經(jīng)過重新包裝的80386辑鲤,它的速度更快一些盔腔,因為總線缺乏允許安裝協(xié)處理器的狀態(tài)。486適當(dāng)?shù)脑黾恿艘恍┲噶睿⒃谔幚砥鲀?nèi)部集成了cache(高速的處理器內(nèi)存)铲觉。
intel 80x86內(nèi)存模型以及它的工作原理
段(segment)這個術(shù)語至少有兩種不同的含義:
在UNIX中澈蝙,段就是一塊以二進(jìn)制形式出現(xiàn)的相關(guān)內(nèi)容。
在intel 80x86內(nèi)存模型中撵幽,段是內(nèi)存模型設(shè)計的結(jié)果灯荧,在80x86內(nèi)存模型中,各處理器的地址空間并不一致盐杂,但它們都被分割成以64K為單位的區(qū)域逗载,每個這樣的區(qū)域便稱為段。
作為80x86內(nèi)存模型最基本的形式链烈,8086中的段是一塊64k的內(nèi)存區(qū)域厉斟,由一個段寄存器所指向。內(nèi)存地址的形成過程是:取得段寄存器得值强衡,左移4位擦秽,然后就是16位得偏移地址,它表示段內(nèi)得地址漩勤。如果把段寄存器的值(經(jīng)過移位)加上偏移地址感挥,就得到最終的地址。這就意味著許多不同的段地址/偏移地址組合可能指向同一個內(nèi)存地址越败。
今天触幼,計算機系統(tǒng)結(jié)構(gòu)的真正挑戰(zhàn)不在于內(nèi)存的容量,而是內(nèi)存的速度究飞。在巨型地址空間的機器中置谦,主存訪問時間的重要性將進(jìn)一步凸現(xiàn)。當(dāng)訪問海量數(shù)據(jù)時亿傅,它所耗費的內(nèi)存訪問時間將左右軟件的性能媒峡。
虛擬內(nèi)存
很早的時候,在計算機領(lǐng)域中人們就提出了虛擬內(nèi)存的概念葵擎。它的基本思路是利用廉價但緩慢的磁盤來擴充內(nèi)存谅阿。在任一給定時刻,程序?qū)嶋H需要使用的虛擬內(nèi)存區(qū)段的內(nèi)容就被載入物理內(nèi)存中坪蚁。當(dāng)物理內(nèi)存中的數(shù)據(jù)有一段時間未被使用,它們就可能被轉(zhuǎn)移到硬盤中镜沽,節(jié)省下來的物理內(nèi)存空間用于載入需要使用的其他數(shù)據(jù)敏晤。
在計算機領(lǐng)域的早期,把未使用的部分?jǐn)?shù)據(jù)從內(nèi)存轉(zhuǎn)移到磁盤的任務(wù)是由程序員手工完成的缅茉。程序員必須花費極大的精力追蹤任一時刻哪些數(shù)據(jù)是在物理內(nèi)存中嘴脾,并根據(jù)需要在段之間來回切換。這種方法實在是太過時了,它對于當(dāng)代的程序員而言根本不具備可操作性译打。
多層存儲是一個類似的概念耗拓,我們可以在一臺計算機中到處看到它的存在(如寄存器 vs 主存)。從理論上說奏司,內(nèi)存的每個位置都可以用寄存器來代替乔询,但在實際上,這樣做的成本將是不切實際的昂貴韵洋,所以必須犧牲一些訪問速度來大幅降低存儲系統(tǒng)的實現(xiàn)成本竿刁。虛擬內(nèi)存只是對多層存儲進(jìn)行擴充,使用磁盤而不是主存來保存運行進(jìn)程的映像搪缨,所以說它們實際上是同一種策略食拜。
SunOS中的進(jìn)程執(zhí)行于32位地址空間。操作系統(tǒng)負(fù)責(zé)具體細(xì)節(jié)副编,使每個進(jìn)程都以為自己擁有整個地址空間的獨家訪問權(quán)负甸。這個幻覺是通過虛擬內(nèi)存實現(xiàn)的,所有進(jìn)程共享機器的物理內(nèi)存痹届,當(dāng)內(nèi)存用完時就用磁盤保存數(shù)據(jù)呻待。在進(jìn)程運行時,數(shù)據(jù)在磁盤和內(nèi)存之間來回移動短纵。內(nèi)存管理硬件負(fù)責(zé)把虛擬地址翻譯為物理地址带污,并讓一個進(jìn)程始終運行于系統(tǒng)的真正內(nèi)存中。應(yīng)用程序程序員只看到虛擬地址香到,并不知道自己的進(jìn)程在磁盤和內(nèi)存之間來回切換鱼冀,除非他們觀察運行時間或者查看諸如ps之類的系統(tǒng)指令。
虛擬內(nèi)存通過“頁”的形式組織悠就。頁就是操作系統(tǒng)在磁盤和內(nèi)存之間移來移去或進(jìn)行保護的單位千绪,一般為幾K字節(jié)」Fⅲ可以通過鍵入/usr/ucb/pagesize來觀察系統(tǒng)中的頁面大小荸型。當(dāng)內(nèi)存的映像在磁盤和物理內(nèi)存間來回移動時,稱它們是page in(移入內(nèi)存)或page out(移到磁盤)炸茧。
從潛在的可能性上說瑞妇,與進(jìn)程有關(guān)的所有內(nèi)存都將被系統(tǒng)所使用。如果該進(jìn)程可能不會馬上運行(可能它的優(yōu)先級低梭冠,也可能是它處于睡眠狀態(tài))辕狰,操作系統(tǒng)可以暫時取回所有分配給它的物理內(nèi)存資源,將該進(jìn)程的所有相關(guān)信息都備份到磁盤上控漠。這樣蔓倍,這個進(jìn)程就被“換出”悬钳。在磁盤中有一個特殊的交換區(qū),用于保存從內(nèi)存中被換出的進(jìn)程偶翅。在一臺機器中默勾,交換區(qū)的大小一般是物理內(nèi)存的幾倍。只有用戶進(jìn)程才會被換進(jìn)換出聚谁,SunOS內(nèi)核常駐于內(nèi)存中母剥。
進(jìn)程只能操作位于物理內(nèi)存中的頁。當(dāng)進(jìn)程引用一個不在物理內(nèi)存中的頁面時垦巴,MMU就會產(chǎn)生一個頁錯誤媳搪。內(nèi)核對此事件做出響應(yīng),并判斷該引用是否有效骤宣。如果無效秦爆,內(nèi)核向進(jìn)程發(fā)出一個“segmentation violation(段違規(guī))”的信號。如果有效憔披,內(nèi)核從磁盤取回該頁等限,換入到內(nèi)存中。一旦頁進(jìn)入內(nèi)存芬膝,進(jìn)程便被解鎖望门,可以重新運行——進(jìn)程本身并不知道它曾經(jīng)因為頁換入事件等待了一會。
SunOS對于磁盤的文件系統(tǒng)和主存有一種統(tǒng)一的觀點锰霜。操作系統(tǒng)使用相同的底層數(shù)據(jù)結(jié)構(gòu)(vnode
,或稱虛擬結(jié)點)來操縱這兩者筹误。所有的虛擬內(nèi)存操作都出于同樣的設(shè)計哲學(xué),就是把文件區(qū)域映射到內(nèi)存區(qū)域中癣缅。這可以提高性能厨剪,并允許可觀的代碼復(fù)用。你可能聽說過“hat layer(帽子層)“——就是驅(qū)動MMU的”硬件地址翻譯“軟件友存。它極度依賴硬件祷膳,每出現(xiàn)一個新的計算機架構(gòu),它都必須重新改寫屡立。
虛擬內(nèi)存現(xiàn)在已成為一項操作系統(tǒng)中不可或缺的技術(shù)直晨,它允許多個進(jìn)程運行于較小的物理內(nèi)存中。
Cache存儲器
Cache存儲器是多層存儲概念的更深擴展膨俐。它的特點是容量小勇皇、價格高、速度快焚刺。Cache位于CPU和內(nèi)存之間敛摘,是一種極快的存儲緩沖區(qū)。從內(nèi)存管理單元(MMU)的角度看檩坚,有些機器的Cache是屬于CPU一側(cè)的着撩。在這種情況下,Cache使用的是虛擬地址匾委,在每次進(jìn)程切換時拖叙,它的內(nèi)容必須進(jìn)行刷新。也有一些機器的Cache從MMU的角度看是屬于物理內(nèi)存一側(cè)的赂乐。在這種情況下薯鳍,Cache使用的是物理地址,這就容易使多處理器CPU共享同一個Cache挨措。
所有的現(xiàn)代處理器都使用了Cache存儲器挖滤。當(dāng)數(shù)據(jù)從內(nèi)存讀入時,整行(一般16或者32個字節(jié))的數(shù)據(jù)被裝入Cache浅役。如果程序具有良好的地址引用局部性(如:它順序瀏覽一個字符串)斩松,那么CPU以后對鄰近數(shù)據(jù)的引用就可以從快速的Cache讀取,而不用從緩慢的內(nèi)存中讀取觉既。Cache操作的速度與系統(tǒng)的周期時間相同惧盹。與常規(guī)的內(nèi)存相比,Cache要貴的多瞪讼,所以在系統(tǒng)中我們把它作為存儲系統(tǒng)的附加部分钧椰,而不是把它作為唯一的存儲形式。
Cache包含一個地址的列表以及它們的內(nèi)容符欠。隨著處理器不斷引用新的存儲地址嫡霞,Cache的地址列表也一直處于變化中。所有對內(nèi)存的讀取和寫入操作都要經(jīng)過Cache希柿。當(dāng)處理器需要從一個特定的地址提取數(shù)據(jù)時诊沪,這個請求首先遞交給Cache。如果數(shù)據(jù)已經(jīng)存在于Cache中狡汉,它就可以立即被提取娄徊,否則,Cache向內(nèi)存?zhèn)鬟f這個請求盾戴,于是就要進(jìn)行緩慢的訪問內(nèi)存操作寄锐。內(nèi)存讀取的數(shù)據(jù)以行為單位,在讀取的同時也裝入到Cache中尖啡。
如果你的程序的行為頗為怪異橄仆,以致每次都無法命中Cache,那么衅斩,程序的性能比不采用Cache還要差盆顾。因為每次判斷Cache是否命中的額外邏輯需要時間。
Sun使用兩種類型的Cache:
- 全寫法(write-through)Cache——每次寫入Cache時總是同時寫入到內(nèi)存中畏梆,使內(nèi)存和Cache始終保持一致您宪。
- 寫回法(write-back)Cache——當(dāng)?shù)谝淮螌懭霑r奈懒,只對Cache進(jìn)行寫入。如果已經(jīng)寫入過的Cache行再次需要寫入時宪巨,此時第一次寫入的結(jié)果尚未保存磷杏,所以要先把它寫入到內(nèi)存中。當(dāng)內(nèi)核切換進(jìn)程時捏卓,Cache中的所有數(shù)據(jù)也都要先寫入到內(nèi)存中极祸。
在兩種情況下,一旦對Cache的訪問結(jié)束怠晴,指令流都將繼續(xù)執(zhí)行遥金,不用等待緩慢的內(nèi)存操作全部完成。
如果處理器使用內(nèi)存映射(memory-mapped)的I/O,可能會出現(xiàn)提供I/O總線使用的Cache蒜田。而且現(xiàn)在經(jīng)常出現(xiàn)分離的指令Cache和數(shù)據(jù)Cache稿械。事實上還可能出現(xiàn)多層的Cache,而且Cache可以出現(xiàn)在任何存在快速/慢速設(shè)備的接口上(如磁盤和內(nèi)存)冲粤。PC經(jīng)常使用由主存構(gòu)成的Cache來提高速度較慢的磁盤的存取速度溜哮。在UNIX中,內(nèi)存就是磁盤的Cache色解,因此切斷機器電源前如果不使用sync命令把Cache(內(nèi)存)的內(nèi)容刷新到磁盤中茂嗓,文件系統(tǒng)就有可能損壞。
對于編寫應(yīng)用程序的程序員而言科阎,Cache和虛擬內(nèi)存都是透明的述吸,但知道它們所提供的好處以及它們可以影響系統(tǒng)性能的行為是非常重要的。
Cache的組成:
術(shù)語 | 定義 |
---|---|
行(line) | 行就是對Cache進(jìn)行訪問的單位锣笨。每行由兩部分組成:一個數(shù)據(jù)部分以及一個標(biāo)簽蝌矛,用于指定它所代表的地址。 |
塊(block) | 一個Cache行內(nèi)的數(shù)據(jù)被稱作塊错英。塊保存來回移動于Cache行和內(nèi)存之間的字節(jié)數(shù)據(jù)入撒,一個典型的塊為32字節(jié)。 |
一個Cache行的內(nèi)容代表特定的內(nèi)存塊椭岩,如果處理器試圖訪問屬于該塊地址范圍的內(nèi)存茅逮,它就會作出反應(yīng),速度自然要比訪問內(nèi)存快的多判哥。 | |
在計算機行業(yè)中献雅,對大多數(shù)人而言,”塊“和”行“的概念分的并不特別清塌计,兩者常惩ι恚可以交換使用。 | |
Cache | 一個Cache(一般為64K到1M之間锌仅,也可能更多)由許多行組成章钾。有時也使用相關(guān)的硬件來加速對標(biāo)簽的訪問墙贱。為了提高速度,Cache的位置離Cache很近贱傀,而且內(nèi)存系統(tǒng)和總線經(jīng)過高度優(yōu)化嫩痰,盡可能的提高大小等于Cache塊的數(shù)據(jù)塊的移動速度。 |
數(shù)據(jù)段和堆
我們已經(jīng)討論了跟系統(tǒng)相關(guān)的內(nèi)存話題的背景信息窍箍,現(xiàn)在是重新訪問每個進(jìn)程內(nèi)部的內(nèi)存布局的時候了。
就像堆棧段能夠根據(jù)需要自動增長一樣丽旅,數(shù)據(jù)段也包含了一個對象椰棘,用于完成這項工作,這就是堆(heap)榄笙。堆的結(jié)構(gòu)如下圖所示:
堆區(qū)域用于動態(tài)分配的存儲邪狞,也就是通過malloc(內(nèi)存分配)函數(shù)獲得的內(nèi)存,并通過指針訪問茅撞。堆中的所有東西都是匿名的——不能按名字直接訪問帆卓,只能通過指針間接訪問。從堆中獲取內(nèi)存的唯一辦法就是通過調(diào)用malloc(以及同類的calloc米丘、realloc等)庫函數(shù)剑令。calloc函數(shù)與malloc類似,但它在返回指針之前先把分配好的內(nèi)存的內(nèi)容都清空為0拄查,不要以為calloc函數(shù)中的c跟C語言編程有關(guān)——它的意思是”分配清零后的內(nèi)存“吁津。realloc函數(shù)改變一個指針?biāo)赶虻膬?nèi)存塊的大小,既可以將其擴大堕扶,也可以把它縮小碍脏,它經(jīng)常把內(nèi)存拷貝到別的地方然后將指向新地址的指針返回給你,這在動態(tài)增長表的大小時很有用稍算。
堆內(nèi)存的回收不必與它所分配的順序一致(它甚至可以不回收)典尾,所以無序的malloc/free最終會產(chǎn)生堆碎片。堆對它的每塊區(qū)域都需要密切留心糊探,哪些是已經(jīng)分配了的钾埂,哪些是尚未分配的。其中一種策略就是建立一個可用塊(”自由存儲區(qū)“)的鏈表科平,每塊由malloc分配的內(nèi)存塊都在自己的前面標(biāo)明自己的大小勃教。有些人用arena這個術(shù)語描述由內(nèi)存分配器(memory allocator)管理的內(nèi)存塊的集合(在SunOS中,就是從當(dāng)前break的位置到數(shù)據(jù)段結(jié)尾之間的區(qū)域)匠抗。
被分配的內(nèi)存總是經(jīng)過對齊故源,以適合機器上最大尺寸的原子訪問,一個malloc請求申請的內(nèi)存大小為方便起見一般被規(guī)整為2的乘方汞贸∩回收的內(nèi)存可供重新使用印机,但并沒有(方便的)辦法把它從你的進(jìn)程移出交還給操作系統(tǒng)。
堆的末端由一個稱為break的指針來標(biāo)識门驾,當(dāng)堆管理器需要更多內(nèi)存時射赛,它可以通過系統(tǒng)調(diào)用brk和sbrk來移動break指針。一般情況下奶是,不必由自己顯示的調(diào)用brk楣责,如果分配的內(nèi)存容量很大,brk最終會被自動調(diào)用聂沙。
用于管理內(nèi)存的調(diào)用是:
- malloc和free——從堆中獲得內(nèi)存以及把內(nèi)存返回給堆秆麸。
- brk和sbrk——調(diào)整數(shù)據(jù)段的大小至一個絕對值(通過某個增量)。
警告:你的程序可能無法同時調(diào)用malloc()和brk()及汉。如果你使用malloc沮趣,malloc希望當(dāng)你調(diào)用brk和sbrk時,它具有唯一的控制權(quán)坷随。由于sbrk向進(jìn)程提供了唯一的方法將數(shù)據(jù)段內(nèi)存返回給系統(tǒng)內(nèi)核房铭,所以如果使用了malloc,就有效的防止了程序的數(shù)據(jù)段縮小的可能性温眉。要想獲得以后能夠返回給系統(tǒng)內(nèi)核的內(nèi)存缸匪,可以使用mmap系統(tǒng)調(diào)用來映射/dev/zero文件。需要返回這種內(nèi)存時类溢,可以使用munmap系統(tǒng)調(diào)用豪嗽。
內(nèi)存泄漏
由于C語言通常并不使用垃圾收集器(自動確認(rèn)并回收不再使用的內(nèi)存塊),在使用malloc()和free()時不得不非常慎重豌骏。堆經(jīng)常會出現(xiàn)兩種類型的問題:
- 釋放或改寫仍在使用的內(nèi)存(稱為”內(nèi)存損壞“)龟梦。
- 未釋放不再使用的內(nèi)存(稱為”內(nèi)存泄漏“)。
這是最難被調(diào)試發(fā)現(xiàn)的問題之一窃躲。如果每次已分配的內(nèi)存塊不再使用而程序員并不釋放它們计贰,進(jìn)程就會一邊分配越來越多的內(nèi)存,一邊卻并不釋放不再使用的那部分內(nèi)存蒂窒。我們使用”內(nèi)存泄漏”這個詞是因為一種稀有的資源正被一個進(jìn)程榨干躁倒。內(nèi)存泄漏的主要可見癥狀就是罪魁進(jìn)程的速度會減慢。原因是體積大的進(jìn)程更有可能被系統(tǒng)換出洒琢,讓別的進(jìn)程運行秧秉,而且大的進(jìn)程在換進(jìn)換出時花費的時間也更多。
即使(從定義上說)泄漏的內(nèi)存本身并不被引用衰抑,但它仍可能存在于頁中(內(nèi)容自然是垃圾)象迎,這樣就增加了進(jìn)程的工作頁數(shù)量,降低了性能。另外需要注意的一點是砾淌,泄漏的內(nèi)存往往比忘記釋放的數(shù)據(jù)結(jié)構(gòu)要大啦撮,在資源有限的情況下,即使引起內(nèi)存泄漏的進(jìn)程并不運行汪厨,整個系統(tǒng)的運行速度也會被拖慢赃春。
避免內(nèi)存泄漏
每次當(dāng)調(diào)用malloc分配內(nèi)存時,注意在以后要調(diào)用相應(yīng)的free來釋放它劫乱。
如果不知道如何調(diào)用free與先前的malloc相對應(yīng)织中,那么很可能已經(jīng)造成了內(nèi)存泄漏。
一種簡單的方法就是在可能的時候使用alloca()來分配動態(tài)內(nèi)存衷戈,以避免上述情況狭吼。當(dāng)離開調(diào)用alloca的函數(shù)時,它所分配的內(nèi)存會被自動釋放脱惰。顯然,這并不適用于那些比創(chuàng)建它們的函數(shù)生命期更長的結(jié)構(gòu)窿春。但如果對象的生命期在該函數(shù)結(jié)束前便已終止拉一,這種建立在堆棧上的動態(tài)內(nèi)存分配是一種開銷很小的選擇。有些人不提倡使用alloca旧乞,因為它并不是一種可移植的方法蔚润。如果處理器在硬件上不支持堆棧,alloca()就很難高效的實現(xiàn)尺栖。
如何檢測內(nèi)存泄漏
觀察內(nèi)存泄漏是一個兩步驟的過程嫡纠。
首先,使用free命令觀察還有多少可用的交換空間:
在一兩分鐘內(nèi)鍵入該命令三到四次延赌,看看可用的交換區(qū)是否在減少除盏。還可以使用其他一些/usr/bin/*stat工具如netstat、vmstat等挫以。如果發(fā)現(xiàn)不斷有內(nèi)存被分配且從不釋放者蠕,一個可能的解釋就是有個進(jìn)程出現(xiàn)了內(nèi)存泄漏。
操作系統(tǒng)同時動態(tài)管理它的內(nèi)存使用掐松。內(nèi)核中的許多數(shù)據(jù)表是動態(tài)分配的踱侣,所以預(yù)先沒有固定的限制。如果一個內(nèi)核程序錯誤引起內(nèi)存泄漏大磺,機器的速度便會慢下來抡句,有時機器干脆掛起或甚至不知所措。如果出現(xiàn)內(nèi)存泄漏杠愧,最終可能導(dǎo)致可以分配的內(nèi)存無法滿足內(nèi)核的需要待榔,結(jié)果每個內(nèi)核程序都無限制的等待——于是機器便被掛起。內(nèi)核中的內(nèi)存泄漏往往很快便被發(fā)現(xiàn)流济,因為絕大多數(shù)內(nèi)核程序的使用都相當(dāng)頻繁究抓。
總線錯誤與段錯誤
在UNIX上編程時猾担,兩個常見的運行時錯誤:
bus error(core dumped) 總線錯誤(信息已轉(zhuǎn)儲)
segmentation fault(core dumped) 段錯誤(信息已轉(zhuǎn)儲)
錯誤信息對引起這兩種錯誤的源代碼錯誤并沒有作簡單的解釋,上面的信息并未提供如何從代碼中尋找錯誤的線索刺下,而且兩者之間的區(qū)別也并不是十分清楚绑嘹。
大多數(shù)的問題都是出于這樣一個事實:錯誤就是操作系統(tǒng)所檢測到的異常,而這個異常是盡可能的以操作系統(tǒng)方便的原則來報告的橘茉」ひ福總線錯誤和段錯誤的準(zhǔn)確原因在不同的操作系統(tǒng)版本上各不相同。這里畅卓,描述的是運行于SPARC架構(gòu)的SunOS出現(xiàn)的這兩類錯誤以及產(chǎn)生錯誤的原因:
當(dāng)硬件告訴操作系統(tǒng)一個有問題的內(nèi)存引用時擅腰,就會出現(xiàn)這兩種錯誤。操作系統(tǒng)通過向出錯的進(jìn)程發(fā)送一個信號與之交流翁潘。信號就是一種事件通知或一個軟件中斷趁冈,在UNIX系統(tǒng)編程中使用很廣。在缺省情況下拜马,進(jìn)程在收到“總線錯誤”或“段錯誤”信號后將進(jìn)行信息轉(zhuǎn)儲并終止渗勘。不過可以為這些信號設(shè)置一個信號處理程序(signal handler),用于修改進(jìn)程的缺省反應(yīng)俩莽。
總線錯誤
事實上旺坠,總線錯誤幾乎都是由于未對齊的讀或?qū)?/strong>引起的。它之所以稱為總線錯誤扮超,是因為出現(xiàn)未對齊的內(nèi)存訪問請求時取刃,被堵塞的組件就是地址總線。對齊(alignment)的意思就是數(shù)據(jù)項只能存儲在地址是數(shù)據(jù)項大小的整數(shù)倍的內(nèi)存位置上出刷。在現(xiàn)代的計算機架構(gòu)中璧疗,尤其是RISC架構(gòu),都需要數(shù)據(jù)對齊馁龟,因為與任意的對齊有關(guān)的額外邏輯會使整個內(nèi)存系統(tǒng)更大且更慢病毡。通過迫使每個內(nèi)存訪問局限在一個Cache行或一個單獨的頁面內(nèi),可以極大的簡化(并加速)如Cache控制器和內(nèi)存管理單元這樣的硬件屁柏。
我們表達(dá)“數(shù)據(jù)項不能跨越頁面或Cache邊界”規(guī)則的方法多少有些間接啦膜,因為我們用地址對齊這個術(shù)語來稱述這個問題,而不是直截了當(dāng)說是禁止內(nèi)存跨頁訪問淌喻,但它們說的是同一回事僧家。例如,訪問一個8字節(jié)的double數(shù)據(jù)時裸删,地址只允許是8的整數(shù)倍八拱。所以一個double數(shù)據(jù)可以存儲于地址24、8008或32768,但不能存儲于地址1006(因為它無法被8整除)肌稻。頁和Cache的大小是經(jīng)過精心設(shè)計的清蚀,這樣只要遵守對齊規(guī)則就可以保證一個原子數(shù)據(jù)項不會跨越一個頁或Cache塊的邊界。
一個會引起總線錯誤的代碼是:
union {
char a[10];
int i;
}u;
int *p = (int *)&(u.a[1]);
*p = 17;
因為數(shù)組和int的聯(lián)合確保數(shù)組a是按照int的4字節(jié)對齊的爹谭,所以“a+1”的地址肯定未按int對齊枷邪。然后我們試圖往這個地址存儲4個字節(jié)的數(shù)據(jù),但這個訪問只是按照單字節(jié)的char對齊诺凡,這就違反了規(guī)則东揣。一個好的編譯器發(fā)現(xiàn)不對齊的情況時會發(fā)出警告,但它并不能檢測到所有不對齊的情況腹泌。
段錯誤
段錯誤或段違規(guī)(segmentation violation)應(yīng)該已經(jīng)很清楚嘶卧,因為前面對段模型已經(jīng)作了解釋。在Sun的硬件中凉袱,段錯誤是由于內(nèi)存管理單元(負(fù)責(zé)支持虛擬內(nèi)存的硬件)的異常所致芥吟,而該異常則通常是由于解除引用一個未初始化或非法值得指針引起的。如果指針引用一個并不位于你的地址空間中的地址专甩,操作系統(tǒng)便會對此進(jìn)行干涉钟鸵。
一個會引起段錯誤的代碼如下:
int *p = 0;
*p = 17;
一個微妙之處是,導(dǎo)致指針具有非法的值通常是由于不同的編程錯誤所引起的配深。和總線錯誤不同携添,段錯誤更像是一個間接的癥狀而不是引起錯誤的原因嫁盲。
一個更糟糕的微妙之處是篓叶,如果未初始化的指針恰好具有未對齊的值(對于指針?biāo)L問的數(shù)據(jù)而言),它將會產(chǎn)生總線錯誤羞秤,而不是段錯誤缸托。對于絕大多數(shù)架構(gòu)的計算機而言確實如此,因為CPU先看到地址瘾蛋,然后再把它發(fā)送給MMU俐镐。
在你的代碼中,對非法指針值得解引用操作可能會像上面這樣顯式得的出現(xiàn)哺哼,也可能在庫函數(shù)中出現(xiàn)(傳遞給它一個非法值)佩抹。令人不快的是,你的程序如果進(jìn)行了修改(如在調(diào)試狀態(tài)下編譯或增加額外的調(diào)試語句)取董,內(nèi)存的內(nèi)容便很容易改變棍苹,于是這個問題被轉(zhuǎn)移到別處或干脆消失测萎。段錯誤是非常難于解決的裸诽,而且只有非常頑固的段錯誤才會一直存在岖常。
通常導(dǎo)致段錯誤的幾個直接原因:
- 解除引用一個包含非法值的指針笼痛。
- 解除引用一個空指針(常常由于從系統(tǒng)程序中返回空指針颂暇,并未檢查就使用)。
- 在未得到正確的權(quán)限時進(jìn)行訪問钦幔。例如窃诉,試圖往一個只讀的文本段存儲值就會引起段錯誤。
- 用完了堆棸峦荩或堆空間(虛擬內(nèi)存雖然巨大但絕非無限)巷疼。
下面這個說法可能過于簡單,但在絕大多數(shù)架構(gòu)的絕大多數(shù)情況下溉卓,總線錯誤意味著CPU對進(jìn)程引用內(nèi)存的一些做法不滿皮迟,而段錯誤則是MMU對進(jìn)程引用內(nèi)存的一些情況發(fā)出抱怨。
以發(fā)生頻率為序桑寨,最終可能導(dǎo)致段錯誤的常見編程錯誤是:
- 指針值錯誤: 在指針初始化之前就用它來引用內(nèi)存(野指針)伏尼,或者向庫函數(shù)傳送一個野指針(如果調(diào)試器顯式系統(tǒng)程序中出現(xiàn)了段錯誤,并不是因為系統(tǒng)程序引起了段錯誤尉尾,問題很可能還存在于自己的代碼中)爆阶;對指針進(jìn)行釋放之后再訪問它的內(nèi)容(空懸指針)(可以在free語句后再將指針置為空值)。
- 改寫(overwrite)錯誤: 越過數(shù)組邊界寫入數(shù)據(jù)沙咏,在動態(tài)分配的內(nèi)存兩端之外寫入數(shù)據(jù)辨图,或改寫一些堆管理數(shù)據(jù)結(jié)構(gòu)(在動態(tài)分配的內(nèi)存之前的區(qū)域?qū)懭霐?shù)據(jù)就很容易發(fā)生這種情況)。
- 指針釋放引起的錯誤: 釋放同一個內(nèi)存塊兩次肢藐,或釋放一塊未曾使用malloc分配的內(nèi)存故河,或釋放仍在使用中的內(nèi)存,或釋放一個無效的指針吆豹。一個極為常見的與釋放內(nèi)存有關(guān)的錯誤就是在for(p=start;p;p=p->next)這樣的循環(huán)中迭代一個鏈表鱼的,并在循環(huán)體內(nèi)使用free(p)語句。這樣痘煤,在下一次循環(huán)迭代時凑阶,程序就會對已經(jīng)釋放的指針進(jìn)行解除引用操作,從而導(dǎo)致不可預(yù)料的結(jié)果衷快。
當(dāng)程序出現(xiàn)壞指針值時(野指針宙橱、空懸指針),什么樣的結(jié)果都有可能發(fā)生蘸拔。一種廣被接受的說法是师郑,如果你走運,指針將指向你的地址空間之外调窍,這樣第一次使用該指針時就會使程序進(jìn)行信息轉(zhuǎn)儲后終止宝冕。如果你不走運,指針將指向你的地址空間之內(nèi)陨晶,并損壞(改寫)它所指向的內(nèi)存的任何信息猬仁。這將引起隱晦的BUG帝璧,非常難以捕捉。
虛擬內(nèi)存規(guī)則總結(jié)
規(guī)則:
- 1.每個進(jìn)程擁有很多的字節(jié)湿刽。
- 2.字節(jié)存放于頁中的烁,每頁4096個字節(jié)。位于同一頁上的字節(jié)具有本地引用關(guān)系诈闺。
- 3.頁可以存放在內(nèi)存中渴庆,也可以存放在磁盤中。內(nèi)存一般不夠大雅镊,無法容納所有的頁襟雷。
- 4.總共只有一塊內(nèi)存,但可以有幾個磁盤仁烹,所有進(jìn)程共享內(nèi)存和磁盤耸弄。
- 5.每個字節(jié)都有自己的虛擬地址。
- 6.進(jìn)程可以對一個字節(jié)進(jìn)行引用操作卓缰。每個進(jìn)程輪流進(jìn)行引用操作计呈。
- 7.每個進(jìn)程只能引用自己的字節(jié),不能引用其它進(jìn)程的字節(jié)征唬。
- 8.字節(jié)只有當(dāng)它們位于內(nèi)存中時才能被引用捌显。
- 9.只有虛擬內(nèi)存管理器知道某個字節(jié)位于內(nèi)存還是位于磁盤。
- 10.一個字節(jié)不被引用的時間越長总寒,它就被稱為越“舊”扶歪。
- 11.進(jìn)程必須通過虛擬內(nèi)存管理器得到字節(jié)。它所給的字節(jié)數(shù)量是2的倍數(shù)或乘方數(shù)摄闸,這有助于減少開銷善镰。
- 12.進(jìn)程引用字節(jié)的方法就是給出它的虛擬地址。如果進(jìn)程所給出的虛擬地址恰好位于內(nèi)存中贪薪,那么進(jìn)程就可以立即引用它媳禁。如果它位于磁盤中眠副,虛擬內(nèi)存管理器會把包含該字節(jié)的頁移入到內(nèi)存中画切。如果內(nèi)存空間已滿,它就尋找內(nèi)存中最舊的頁(可能是該進(jìn)程自己的囱怕,也可能是其他進(jìn)程的)霍弹,把它換到磁盤中,騰出來的空間就存放包含你需要字節(jié)的頁娃弓。然后典格,進(jìn)程就可以引用該字節(jié),但進(jìn)程并不知道該頁原先位于磁盤中台丛。
- 13.每個進(jìn)程擁有的字節(jié)的虛擬地址與其他進(jìn)程一樣耍缴。虛擬內(nèi)存管理器始終知道誰擁有哪個字節(jié)以及該輪到誰進(jìn)行引用操作砾肺,所以一個進(jìn)程不會無意引用其他進(jìn)程的字節(jié),即使兩者的虛擬地址相同防嗡。
說明:
- 1.根據(jù)傳統(tǒng)变汪,虛擬內(nèi)存管理器使用一張很大且分段的表,另外還有頁表用于記住所有字節(jié)的位置以及它們的主人蚁趁。
- 2.規(guī)則13的一個結(jié)果就是各次運行中每位進(jìn)程的虛擬地址都類似裙盾,即使進(jìn)程的數(shù)量有所變化。
- 3.虛擬內(nèi)存管理器也擁有自己的一些字節(jié)他嫡,它們中的有些也和一般進(jìn)程的字節(jié)一樣在內(nèi)存和磁盤中移來移去番官。但是,它的有些字節(jié)使用頻率非常之高钢属,所以常駐內(nèi)存徘熔。
- 4.按照上述規(guī)則,經(jīng)常被引用的字節(jié)更有可能被存放在內(nèi)存中淆党,而不太被引用的字節(jié)則更可能被存放在磁盤中近顷,這可以提高內(nèi)存的使用效率。