mysql 内存越界_linux内存管理浅析
[
地址映射
](圖:左中)
linux內(nèi)核使用頁(yè)式內(nèi)存管理,應(yīng)用程序給出的內(nèi)存地址是虛擬地址,它需要經(jīng)過(guò)若干級(jí)頁(yè)表一級(jí)一級(jí)的變換,才變成真正的物理地址。
想一下,地址映射還是一件很恐怖的事情。當(dāng)訪問(wèn)一個(gè)由虛擬地址表示的內(nèi)存空間時(shí),需要先經(jīng)過(guò)若干次的內(nèi)存訪問(wèn),得到每一級(jí)頁(yè)表中用于轉(zhuǎn)換的頁(yè)表項(xiàng)(頁(yè)表是存放在內(nèi)存里面的),才能完成映射。也就是說(shuō),要實(shí)現(xiàn)一次內(nèi)存訪問(wèn),實(shí)際上內(nèi)存被訪問(wèn)了N+1次(N=頁(yè)表級(jí)數(shù)),并且還需要做N次加法運(yùn)算。
所以,地址映射必須要有硬件支持,mmu(內(nèi)存管
理單元)就是這個(gè)硬件。并且需要有cache來(lái)保存頁(yè)表,這個(gè)cache就是TLB(Translation
lookaside
buffer)。
盡管如此,地址映射還是有著不小的開(kāi)銷(xiāo)。假設(shè)cache的訪存速度是內(nèi)存的10倍,命中率是40%,頁(yè)表有三級(jí),那么平均一次虛擬地址訪問(wèn)大概就消耗了兩次物理內(nèi)存訪問(wèn)的時(shí)間。
于是,一些嵌入式硬件上可能會(huì)放棄使用mmu,這樣的硬件能夠運(yùn)行VxWorks(一個(gè)很高效的嵌入式實(shí)時(shí)操作系統(tǒng))、linux(linux也有禁用mmu的編譯選項(xiàng))、等系統(tǒng)。
但是使用mmu的優(yōu)勢(shì)也是很大的,最主要的是出于安全性考慮。各個(gè)進(jìn)程都是相互獨(dú)立的虛擬地址空間,互不干擾。而放棄地址映射之后,所有程序?qū)⑦\(yùn)行在同一個(gè)地址空間。于是,在沒(méi)有mmu的機(jī)器上,一個(gè)進(jìn)程越界訪存,可能引起其他進(jìn)程莫名其妙的錯(cuò)誤,甚至導(dǎo)致內(nèi)核崩潰。
在地址映射這個(gè)問(wèn)題上,內(nèi)核只提供頁(yè)表,實(shí)際的轉(zhuǎn)換是由硬件去完成的。那么內(nèi)核如何生成這些頁(yè)表呢?這就有兩方面的內(nèi)容,虛擬地址空間的管理和物理內(nèi)存的管理。(實(shí)際上只有用戶態(tài)的地址映射才需要管理,內(nèi)核態(tài)的地址映射是寫(xiě)死的。)
[
虛擬地址管理
](圖:左下)
每個(gè)進(jìn)程對(duì)應(yīng)一個(gè)task結(jié)構(gòu),它指向一個(gè)mm結(jié)構(gòu),這就是該進(jìn)程的內(nèi)存管理器。(對(duì)于線程來(lái)說(shuō),每個(gè)線程也都有一個(gè)task結(jié)構(gòu),但是它們都指向同一個(gè)mm,所以地址空間是共享的。)
mm->pgd指向容納頁(yè)表的內(nèi)存,每個(gè)進(jìn)程有自已的mm,每個(gè)mm有自己的頁(yè)表。于是,進(jìn)程調(diào)度時(shí),頁(yè)表被切換(一般會(huì)有一個(gè)CPU寄存器來(lái)保存頁(yè)表的地址,比如X86下的CR3,頁(yè)表切換就是改變?cè)摷拇嫫鞯闹?。所以,各個(gè)進(jìn)程的地址空間互不影響(因?yàn)轫?yè)表都不一樣了,當(dāng)然無(wú)法訪問(wèn)到別人的地址空間上。但是共享內(nèi)存除外,這是故意讓不同的頁(yè)表能夠訪問(wèn)到相同的物理地址上)。
用戶程序?qū)?nèi)存的操作(分配、回收、映射、等)都是對(duì)mm的操作,具體來(lái)說(shuō)是對(duì)mm上的vma(虛擬內(nèi)存空間)的操作。這些vma代表著進(jìn)程空間的各個(gè)區(qū)域,比如堆、棧、代碼區(qū)、數(shù)據(jù)區(qū)、各種映射區(qū)、等等。
用戶程序?qū)?nèi)存的操作并不會(huì)直接影響到頁(yè)表,更不會(huì)直接影響到物理內(nèi)存的分配。比如malloc成功,僅僅是改變了某個(gè)vma,頁(yè)表不會(huì)變,物理內(nèi)存的分配也不會(huì)變。
假設(shè)用戶分配了內(nèi)存,然后訪問(wèn)這塊內(nèi)存。由于頁(yè)表里面并沒(méi)有記錄相關(guān)的映射,CPU產(chǎn)生一次缺頁(yè)異常。內(nèi)核捕捉異常,檢查產(chǎn)生異常的地址是不是存在于一個(gè)合法的vma中。如果不是,則給進(jìn)程一個(gè)"段錯(cuò)誤",讓其崩潰;如果是,則分配一個(gè)物理頁(yè),并為之建立映射。
[
物理內(nèi)存管理
](圖:右上)
那么物理內(nèi)存是如何分配的呢?
首先,linux支持NUMA(非均質(zhì)存儲(chǔ)結(jié)構(gòu)),物理內(nèi)存管理的第一個(gè)層次就是介質(zhì)的管理。pg_data_t結(jié)構(gòu)就描述了介質(zhì)。一般而言,我們的內(nèi)存管理介質(zhì)只有內(nèi)存,并且它是均勻的,所以可以簡(jiǎn)單地認(rèn)為系統(tǒng)中只有一個(gè)pg_data_t對(duì)象。
每一種介質(zhì)下面有若干個(gè)zone。一般是三個(gè),DMA、NORMAL和HIGH。
DMA:因?yàn)橛行┯布到y(tǒng)的DMA總線比系統(tǒng)總線窄,所以只有一部分地址空間能夠用作DMA,這部分地址被管理在DMA區(qū)域(這屬于是高級(jí)貨了);
HIGH:高端內(nèi)存。在32位系統(tǒng)中,地址空間是4G,其中內(nèi)核規(guī)定3~4G的范圍是內(nèi)核空間,0~3G是用戶空間(每個(gè)用戶進(jìn)程都有這么大的虛擬空間)(圖:中下)。前面提到過(guò)內(nèi)核的地址映射是寫(xiě)死的,就是指這3~4G的對(duì)應(yīng)的頁(yè)表是寫(xiě)死的,它映射到了物理地址的0~1G上。(實(shí)際上沒(méi)有映射1G,只映射了896M。剩下的空間留下來(lái)映射大于1G的物理地址,而這一部分顯然不是寫(xiě)死的)。所以,大于896M的物理地址是沒(méi)有寫(xiě)死的頁(yè)表來(lái)對(duì)應(yīng)的,內(nèi)核不能直接訪問(wèn)它們(必須要建立映射),稱(chēng)它們?yōu)楦叨藘?nèi)存(當(dāng)然,如果機(jī)器內(nèi)存不足896M,就不存在高端內(nèi)存。如果是64位機(jī)器,也不存在高端內(nèi)存,因?yàn)榈刂房臻g很大很大,屬于內(nèi)核的空間也不止1G了);
NORMAL:不屬于DMA或HIGH的內(nèi)存就叫NORMAL。
在zone之上的zone_list代表了分配策略,即內(nèi)存分配時(shí)的zone優(yōu)先級(jí)。一種內(nèi)存分配往往不是只能在一個(gè)zone里進(jìn)行分配的,比如分配一個(gè)頁(yè)給內(nèi)核使用時(shí),最優(yōu)先是從NORMAL里面分配,不行的話就分配DMA里面的好了(HIGH就不行,因?yàn)檫€沒(méi)建立映射),這就是一種分配策略。
每個(gè)內(nèi)存介質(zhì)維護(hù)了一個(gè)mem_map,為介質(zhì)中的每一個(gè)物理頁(yè)面建立了一個(gè)page結(jié)構(gòu)與之對(duì)應(yīng),以便管理物理內(nèi)存。
每個(gè)zone記錄著它在mem_map上的起始位置。并且通過(guò)free_area串連著這個(gè)zone上空閑的page。物理內(nèi)存的分配就是從這里來(lái)的,從
free_area上把page摘下,就算是分配了。(內(nèi)核的內(nèi)存分配與用戶進(jìn)程不同,用戶使用內(nèi)存會(huì)被內(nèi)核監(jiān)督,使用不當(dāng)就"段錯(cuò)誤";而內(nèi)核則無(wú)人監(jiān)督,只能靠自覺(jué),不是自己從free_area摘下的page就不要亂用。)
[
建立地址映射
]
內(nèi)核需要物理內(nèi)存時(shí),很多情況是整頁(yè)分配的,這在上面的mem_map中摘一個(gè)page下來(lái)就好了。比如前面說(shuō)到的內(nèi)核捕捉缺頁(yè)異常,然后需要分配一個(gè)page以建立映射。
說(shuō)到這里,會(huì)有一個(gè)疑問(wèn),內(nèi)核在分配page、建立地址映射的過(guò)程中,使用的是虛擬地址還是物理地址呢?首先,內(nèi)核代碼所訪問(wèn)的地址都是虛擬地址,因?yàn)镃PU指令接收的就是虛擬地址(地址映射對(duì)于CPU指令是透明的)。但是,建立地址映射時(shí),內(nèi)核在頁(yè)表里面填寫(xiě)的內(nèi)容卻是物理地址,因?yàn)榈刂酚成涞哪繕?biāo)就是要得到物理地址。
那么,內(nèi)核怎么得到這個(gè)物理地址呢?其實(shí),上面也提到了,mem_map中的page就是根據(jù)物理內(nèi)存來(lái)建立的,每一個(gè)page就對(duì)應(yīng)了一個(gè)物理頁(yè)。
于是我們可以說(shuō),虛擬地址的映射是靠這里page結(jié)構(gòu)來(lái)完成的,是它們給出了最終的物理地址。然而,page結(jié)構(gòu)顯然是通過(guò)虛擬地址來(lái)管理的(前面已經(jīng)說(shuō)過(guò),CPU指令接收的就是虛擬地址)。那么,page結(jié)構(gòu)實(shí)現(xiàn)了別人的虛擬地址映射,誰(shuí)又來(lái)實(shí)現(xiàn)page結(jié)構(gòu)自己的虛擬地址映射呢?沒(méi)人能夠?qū)崿F(xiàn)。
這就引出了前面提到的一個(gè)問(wèn)題,內(nèi)核空間的頁(yè)表項(xiàng)是寫(xiě)死的。在內(nèi)核初始化時(shí),內(nèi)核的地址空間就已經(jīng)把地址映射寫(xiě)死了。page結(jié)構(gòu)顯然存在于內(nèi)核空間,所以它的地址映射問(wèn)題已經(jīng)通過(guò)“寫(xiě)死”解決了。
由于內(nèi)核空間的頁(yè)表項(xiàng)是寫(xiě)死的,又引出另一個(gè)問(wèn)題,NORMAL(或DMA)區(qū)域的內(nèi)存可能被同時(shí)映射到內(nèi)核空間和用戶空間。被映射到內(nèi)核空間是顯然的,因?yàn)檫@個(gè)映射已經(jīng)寫(xiě)死了。而這些頁(yè)面也可能被映射到用戶空間的,在前面提到的缺頁(yè)異常的場(chǎng)景里面就有這樣的可能。映射到用戶空間的頁(yè)面應(yīng)該優(yōu)先從HIGH區(qū)域獲取,因?yàn)檫@些內(nèi)存被內(nèi)核訪問(wèn)起來(lái)很不方便,拿給用戶空間再合適不過(guò)了。但是HIGH區(qū)域可能會(huì)耗盡,或者可能因?yàn)樵O(shè)備上物理內(nèi)存不足導(dǎo)致系統(tǒng)里面根本就沒(méi)有HIGH區(qū)域,所以,將NORMAL區(qū)域映射給用戶空間是必然存在的。
但是NORMAL區(qū)域的內(nèi)存被同時(shí)映射到內(nèi)核空間和用戶空間并沒(méi)有問(wèn)題,因?yàn)槿绻硞€(gè)頁(yè)面正在被內(nèi)核使用,對(duì)應(yīng)的page應(yīng)該已經(jīng)從free_area被摘下,于是缺頁(yè)異常處理代碼中不會(huì)再將該頁(yè)映射到用戶空間。反過(guò)來(lái)也一樣,被映射到用戶空間的page自然已經(jīng)從free_area被摘下,內(nèi)核不會(huì)再去使用這個(gè)頁(yè)面。
[
內(nèi)核空間管理
](圖:右下)
除了對(duì)內(nèi)存整頁(yè)的使用,有些時(shí)候,內(nèi)核也需要像用戶程序使用malloc一樣,分配一塊任意大小的空間。這個(gè)功能是由slab系統(tǒng)來(lái)實(shí)現(xiàn)的。
slab相當(dāng)于為內(nèi)核中常用的一些結(jié)構(gòu)體對(duì)象建立了對(duì)象池,比如對(duì)應(yīng)task結(jié)構(gòu)的池、對(duì)應(yīng)mm結(jié)構(gòu)的池、等等。
而slab也維護(hù)有通用的對(duì)象池,比如"32字節(jié)大小"的對(duì)象池、"64字節(jié)大小"的對(duì)象池、等等。內(nèi)核中常用的kmalloc函數(shù)(類(lèi)似于用戶態(tài)的malloc)就是在這些通用的對(duì)象池中實(shí)現(xiàn)分配的。
slab除了對(duì)象實(shí)際使用的內(nèi)存空間外,還有其對(duì)應(yīng)的控制結(jié)構(gòu)。有兩種組織方式,如果對(duì)象較大,則控制結(jié)構(gòu)使用專(zhuān)門(mén)的頁(yè)面來(lái)保存;如果對(duì)象較小,控制結(jié)構(gòu)與對(duì)象空間使用相同的頁(yè)面。
除了slab,linux
2.6還引入了mempool(內(nèi)存池)。其意圖是:某些對(duì)象我們不希望它會(huì)因?yàn)閮?nèi)存不足而分配失敗,于是我們預(yù)先分配若干個(gè),放在mempool中存起來(lái)。正常情況下,分配對(duì)象時(shí)是不會(huì)去動(dòng)mempool里面的資源的,照常通過(guò)slab去分配。到系統(tǒng)內(nèi)存緊缺,已經(jīng)無(wú)法通過(guò)slab分配內(nèi)存時(shí),才會(huì)使用
mempool中的內(nèi)容。
[
頁(yè)面換入換出
](圖:左上)(圖:右上)
頁(yè)面換入換出又是一個(gè)很復(fù)雜的系統(tǒng)。內(nèi)存頁(yè)面被換出到磁盤(pán),與磁盤(pán)文件被映射到內(nèi)存,是很相似的兩個(gè)過(guò)程(內(nèi)存頁(yè)被換出到磁盤(pán)的動(dòng)機(jī),就是今后還要從磁盤(pán)將其載回內(nèi)存)。所以swap復(fù)用了文件子系統(tǒng)的一些機(jī)制。
頁(yè)面換入換出是一件很費(fèi)CPU和IO的事情,但是由于內(nèi)存昂貴這一歷史原因,我們只好拿磁盤(pán)來(lái)擴(kuò)展內(nèi)存。但是現(xiàn)在內(nèi)存越來(lái)越便宜了,我們可以輕松安裝數(shù)G的內(nèi)存,然后將swap系統(tǒng)關(guān)閉。于是swap的實(shí)現(xiàn)實(shí)在讓人難有探索的欲望,在這里就不贅述了。(另見(jiàn):《linux內(nèi)核頁(yè)面回收淺析
》)
[
用戶空間內(nèi)存管理
]
malloc是libc的庫(kù)函數(shù),用戶程序一般通過(guò)它(或類(lèi)似函數(shù))來(lái)分配內(nèi)存空間。
libc對(duì)內(nèi)存的分配有兩種途徑,一是調(diào)整堆的大小,二是mmap一個(gè)新的虛擬內(nèi)存區(qū)域(堆也是一個(gè)vma)。
在內(nèi)核中,堆是一個(gè)一端固定、一端可伸縮的vma(圖:左中)。可伸縮的一端通過(guò)系統(tǒng)調(diào)用brk來(lái)調(diào)整。libc管理著堆的空間,用戶調(diào)用malloc分配內(nèi)存時(shí),libc盡量從現(xiàn)有的堆中去分配。如果堆空間不夠,則通過(guò)brk增大堆空間。
當(dāng)用戶將已分配的空間free時(shí),libc可能會(huì)通過(guò)brk減小堆空間。但是堆空間增大容易減小卻難,考慮這樣一種情況,用戶空間連續(xù)分配了10塊內(nèi)存,前9塊已經(jīng)free。這時(shí),未free的第10塊哪怕只有1字節(jié)大,libc也不能夠去減小堆的大小。因?yàn)槎阎挥幸欢丝缮炜s,并且中間不能掏空。而第10塊內(nèi)存就死死地占據(jù)著堆可伸縮的那一端,堆的大小沒(méi)法減小,相關(guān)資源也沒(méi)法歸還內(nèi)核。
當(dāng)用戶malloc一塊很大的內(nèi)存時(shí),libc會(huì)通過(guò)mmap系統(tǒng)調(diào)用映射一個(gè)新的vma。因?yàn)閷?duì)于堆的大小調(diào)整和空間管理還是比較麻煩的,重新建一個(gè)vma會(huì)更方便(上面提到的free的問(wèn)題也是原因之一)。
那么為什么不總是在malloc的時(shí)候去mmap一個(gè)新的vma呢?第一,對(duì)于小空間的分配與回收,被libc管理的堆空間已經(jīng)能夠滿足需要,不必每次都去進(jìn)行系統(tǒng)調(diào)用。并且vma是以page為單位的,最小就是分配一個(gè)頁(yè);第二,太多的vma會(huì)降低系統(tǒng)性能。缺頁(yè)異常、vma的新建與銷(xiāo)毀、堆空間的大小調(diào)整、等等情況下,都需要對(duì)vma進(jìn)行操作,需要在當(dāng)前進(jìn)程的所有vma中找到需要被操作的那個(gè)(或那些)vma。vma數(shù)目太多,必然導(dǎo)致性能下降。(在進(jìn)程的vma較少時(shí),內(nèi)核采用鏈表來(lái)管理vma;vma較多時(shí),改用紅黑樹(shù)來(lái)管理。)
[
用戶的棧
]
與堆一樣,棧也是一個(gè)vma(圖:左中),這個(gè)vma是一端固定、一端可伸(注意,不能縮)的。這個(gè)vma比較特殊,沒(méi)有類(lèi)似brk的系統(tǒng)調(diào)用讓這個(gè)vma伸展,它是自動(dòng)伸展的。
當(dāng)用戶訪問(wèn)的虛擬地址越過(guò)這個(gè)vma時(shí),內(nèi)核會(huì)在處理缺頁(yè)異常的時(shí)候?qū)⒆詣?dòng)將這個(gè)vma增大。內(nèi)核會(huì)檢查當(dāng)時(shí)的棧寄存器(如:ESP),訪問(wèn)的虛擬地址不能超過(guò)ESP加n(n為CPU壓棧指令一次性壓棧的最大字節(jié)數(shù))。也就是說(shuō),內(nèi)核是以ESP為基準(zhǔn)來(lái)檢查訪問(wèn)是否越界。
但是,ESP的值是可以由用戶態(tài)程序自由讀寫(xiě)的,用戶程序如果調(diào)整ESP,將棧劃得很大很大怎么辦呢?內(nèi)核中有一套關(guān)于進(jìn)程限制的配置,其中就有棧大小的配置,棧只能這么大,再大就出錯(cuò)。
對(duì)于一個(gè)進(jìn)程來(lái)說(shuō),棧一般是可以被伸展得比較大(如:8MB)。然而對(duì)于線程呢?
首先線程的棧是怎么回事?前面說(shuō)過(guò),線程的mm是共享其父進(jìn)程的。雖然棧是mm中的一個(gè)vma,但是線程不能與其父進(jìn)程共用這個(gè)vma(兩個(gè)運(yùn)行實(shí)體顯然不用共用一個(gè)棧)。于是,在線程創(chuàng)建時(shí),線程庫(kù)通過(guò)mmap新建了一個(gè)vma,以此作為線程的棧(大于一般為:2M)。
可見(jiàn),線程的棧在某種意義上并不是真正棧,它是一個(gè)固定的區(qū)域,并且容量很有限。
總結(jié)
以上是生活随笔為你收集整理的mysql 内存越界_linux内存管理浅析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: info java module_JAV
- 下一篇: mysql 按照in id顺序_Mysq