|
我是老溫,一名熱愛學(xué)習(xí)的嵌入式工程師0 m% h4 z, w$ ?4 i5 f- b0 D
關(guān)注我,一起變得更加優(yōu)秀!. b0 }( q. i; G7 ^# b( h
工程師們似乎認(rèn)為編寫垃圾回收機(jī)制是很難的,是一種只有少數(shù)智者和Hans Boehm(et al)才能理解的高深魔法。我認(rèn)為編寫垃圾回收最難的地方就是內(nèi)存分配,這和閱讀 K&R 所寫的 malloc 樣例難度是相當(dāng)?shù)摹?font class="jammer">/ S( \! f" O- ]. M7 ^5 ?
在開始之前有一些重要的事情需要說明一下:" y" m" d2 O2 e: y: T; Z
第一,我們所寫的代碼是基于Linux Kernel的,注意是Linux Kernel而不是GNU/Linux。
2 Q' H1 i3 X, X# _第二,我們的代碼是32bit的。
) E) N- v7 R+ \) B7 z第三,請(qǐng)不要直接使用這些代碼。我并不保證這些代碼完全正確,可能其中有一些我還未發(fā)現(xiàn)的小的bug,但是整體思路仍然是正確的。好了,讓我們開始吧。
7 d! C5 |& t' @7 m+ Y: w& ]1編寫malloc( \/ Z A0 |, n5 f
最開始,我們需要寫一個(gè)內(nèi)存分配器(memmory allocator),也可以叫做內(nèi)存分配函數(shù)(malloc function)。
+ X: {$ Z ^( B# o9 i! Y! w6 |" g最簡(jiǎn)單的內(nèi)存分配實(shí)現(xiàn)方法就是維護(hù)一個(gè)由空閑內(nèi)存塊組成的鏈表,這些空閑內(nèi)存塊在需要的時(shí)候被分割或分配。
: m6 L8 x% q0 J( O7 H/ v當(dāng)用戶請(qǐng)求一塊內(nèi)存時(shí),一塊合適大小的內(nèi)存塊就會(huì)從鏈表中被移除并分配給用戶。+ y2 A J. O+ X3 m1 k
如果鏈表中沒有合適的空閑內(nèi)存塊存在,而且更大的空閑內(nèi)存塊已經(jīng)被分割成小的內(nèi)存塊了或內(nèi)核也正在請(qǐng)求更多的內(nèi)存(譯者注:就是鏈表中的空閑內(nèi)存塊都太小不足以分配給用戶的情況)。- L& _; h4 I6 N' ]6 g" z- {/ T
那么此時(shí),會(huì)釋放掉一塊內(nèi)存并把它添加到空閑塊鏈表中。
8 ?; g/ ?! {- ~! |在鏈表中的每個(gè)空閑內(nèi)存塊都有一個(gè)頭(header)用來描述內(nèi)存塊的信息。我們的header包含兩個(gè)部分,第一部分表示內(nèi)存塊的大小,第二部分指向下一個(gè)空閑內(nèi)存塊。5 _3 @4 M0 y- Q7 ?
將頭(header)內(nèi)嵌進(jìn)內(nèi)存塊中是唯一明智的做法,而且這樣還可以享有字節(jié)自動(dòng)對(duì)齊的好處,這很重要。
: v" w; q! O8 q Q由于我們需要同時(shí)跟蹤我們“當(dāng)前使用過的內(nèi)存塊”和“未使用的內(nèi)存塊”,因此除了維護(hù)空閑內(nèi)存的鏈表外,我們還需要一條維護(hù)當(dāng)前已用內(nèi)存塊的鏈表(為了方便,這兩條鏈表后面分別寫為“空閑塊鏈表”和“已用塊鏈表”)。
$ }& U; d! a) f) u1 _8 Y: L, y我們從空閑塊鏈表中移除的內(nèi)存塊會(huì)被添加到已用塊鏈表中,反之亦然。; W h8 B) g/ [
現(xiàn)在我們差不多已經(jīng)做好準(zhǔn)備來完成malloc實(shí)現(xiàn)的第一步了。但是再那之前,我們需要知道怎樣向內(nèi)核申請(qǐng)內(nèi)存。
% T& }. G( Z" [! K4 N動(dòng)態(tài)分配的內(nèi)存會(huì)駐留在一個(gè)叫做堆(heap)的地方,堆是介于棧(stack)和BSS(未初始化的數(shù)據(jù)段-你所有的全局變量都存放在這里且具有默認(rèn)值為0)之間的一塊內(nèi)存。
, Q0 M8 l5 o2 w7 h0 a x堆(heap)的內(nèi)存地址起始于(低地址)BSS段的邊界,結(jié)束于一個(gè)分隔地址(這個(gè)分隔地址是已建立映射的內(nèi)存和未建立映射的內(nèi)存的分隔線)。/ o$ ~3 L4 G2 N, g; ]" a
為了能夠從內(nèi)核中獲取更多的內(nèi)存,我們只需提高這個(gè)分隔地址。為了提高這個(gè)分隔地址我們需要調(diào)用一個(gè)叫作 sbrk 的Unix系統(tǒng)的系統(tǒng)調(diào)用,# T: t2 Q: J6 h3 I' T4 M S4 B# F
這個(gè)函數(shù)可以根據(jù)我們提供的參數(shù)來提高分隔地址,如果函數(shù)執(zhí)行成功則會(huì)返回以前的分隔地址,如果失敗將會(huì)返回-1。 a. N9 C0 ~3 }9 U9 O
利用我們現(xiàn)在知道的知識(shí),我們可以創(chuàng)建兩個(gè)函數(shù):morecore()和add_to_free_list()。
! Y2 ]. \: b: M( M當(dāng)空閑塊鏈表缺少內(nèi)存塊時(shí),我們調(diào)用morecore()函數(shù)來申請(qǐng)更多的內(nèi)存。由于每次向內(nèi)核申請(qǐng)內(nèi)存的代價(jià)是昂貴的,我們以頁(yè)(page-size)為單位申請(qǐng)內(nèi)存。 k* J$ }7 K& A) ?- H @6 B
頁(yè)的大小在這并不是很重要的知識(shí)點(diǎn),不過這有一個(gè)很簡(jiǎn)單解釋:頁(yè)是虛擬內(nèi)存映射到物理內(nèi)存的最小內(nèi)存單位。0 X5 }% M6 e% f
接下來我們就可以使用add_to_list()將申請(qǐng)到的內(nèi)存塊加入空閑塊鏈表。
! a/ H: @9 {" z" g7 M, O7 G現(xiàn)在我們有了兩個(gè)有力的函數(shù),接下來我們就可以直接編寫malloc函數(shù)了。+ Y/ Z u. j$ c. F" W% j$ c
我們掃描空閑塊鏈表當(dāng)遇到第一塊滿足要求的內(nèi)存塊(內(nèi)存塊比所需內(nèi)存大即滿足要求)時(shí),停止掃描,而不是掃描整個(gè)鏈表來尋找大小最合適的內(nèi)存塊,我們所采用的這種算法思想其實(shí)就是首次適應(yīng)(與最佳適應(yīng)相對(duì))。' q( n7 F# Z% i: w$ ~& J& M. k
注意:有件事情需要說明一下,內(nèi)存塊頭部結(jié)構(gòu)中size這一部分的計(jì)數(shù)單位是塊(Block),而不是Byte。
& N6 k' q$ C7 E( A4 b8 L注意這個(gè)函數(shù)的成功與否,取決于我們第一次使用時(shí)是否使 freep = &base 。這點(diǎn)我們會(huì)在初始化函數(shù)中進(jìn)行設(shè)置。% x5 c( m3 f/ I+ W
盡管我們的代碼完全沒有考慮到內(nèi)存碎片,但是它能工作。既然它可以工作,我們就可以開始下一個(gè)有趣的部分-垃圾回收!! I; M' k3 ]% y! R
2標(biāo)記與清掃/ x$ a$ k5 R' ?
我們說過垃圾回收器會(huì)很簡(jiǎn)單,因此我們盡可能的使用簡(jiǎn)單的方法:標(biāo)記和清除方式。這個(gè)算法分為兩個(gè)部分:
& v8 T* {- N4 G8 Z) I! ?) F) X* X首先,我們需要掃描所有可能存在指向堆中數(shù)據(jù)(heap data)的變量的內(nèi)存空間并確認(rèn)這些內(nèi)存空間中的變量是否指向堆中的數(shù)據(jù)。
) T6 |2 j5 |6 W+ v( P為了做到這點(diǎn),對(duì)于可能內(nèi)存空間中的每個(gè)字長(zhǎng)(word-size)的數(shù)據(jù)塊,我們遍歷已用塊鏈表中的內(nèi)存塊。
3 W( d5 N& [7 K* b, h如果數(shù)據(jù)塊所指向的內(nèi)存是在已用鏈表塊中的某一內(nèi)存塊中,我們對(duì)這個(gè)內(nèi)存塊進(jìn)行標(biāo)記。
7 A- T$ |. Y% N$ `( x, J' ^第二部分是,當(dāng)掃描完所有可能的內(nèi)存空間后,我們遍歷已用塊鏈表將所有未被標(biāo)記的內(nèi)存塊移到空閑塊鏈表中。* ~, B4 p g8 R' C' J# |
現(xiàn)在很多人會(huì)開始認(rèn)為只是靠編寫類似于malloc那樣的簡(jiǎn)單函數(shù)來實(shí)現(xiàn)C的垃圾回收是不可行的,因?yàn)樵诤瘮?shù)中我們無法獲得其外面的很多信息。* r8 I z7 w% e: e$ b( k& p, ~
例如,在C語言中沒有函數(shù)可以返回分配到堆棧中的所有變量的哈希映射。但是只要我們意識(shí)到兩個(gè)重要的事實(shí),我們就可以繞過這些東西:. W: o9 h+ Q4 _7 l, G; X
第一,在C中,你可以嘗試訪問任何你想訪問的內(nèi)存地址。因?yàn)椴豢赡苡幸粋(gè)數(shù)據(jù)塊編譯器可以訪問但是其地址卻不能被表示成一個(gè)可以賦值給指針的整數(shù)。
1 `. R- l6 A& [如果一塊內(nèi)存在C程序中被使用了,那么它一定可以被這個(gè)程序訪問。這是一個(gè)令不熟悉C的編程者很困惑的概念,因?yàn)楹芏嗑幊陶Z言都會(huì)限制程序訪問虛擬內(nèi)存,但是C不會(huì)。& ~6 h8 R" r7 z- I( b! M
第二,所有的變量都存儲(chǔ)在內(nèi)存的某個(gè)地方。這意味著如果我們可以知道變量們的通常存儲(chǔ)位置,我們可以遍歷這些內(nèi)存位置來尋找每個(gè)變量的所有可能值。* B" Y% h/ \- f5 @2 D
另外,因?yàn)閮?nèi)存的訪問通常是字(word-size)對(duì)齊的,因此我們僅需要遍歷內(nèi)存區(qū)域中的每個(gè)字(word)即可。
( Q: F6 Y" R2 x; C. b局部變量也可以被存儲(chǔ)在寄存器中,但是我們并不需要擔(dān)心這些因?yàn)榧拇嫫鹘?jīng)常會(huì)用于存儲(chǔ)局部變量,而且當(dāng)函數(shù)被調(diào)用的時(shí)候他們通常會(huì)被存儲(chǔ)在堆棧中。
U8 L$ m8 l: G1 L% j; P( X現(xiàn)在我們有一個(gè)標(biāo)記階段的策略:遍歷一系列的內(nèi)存區(qū)域并查看是否有內(nèi)存可能指向已用塊鏈表。編寫這樣的一個(gè)函數(shù)非常的簡(jiǎn)潔明了:. Z# V/ i5 ~, O% J1 n' i
為了確保我們只使用頭(header)中的兩個(gè)字長(zhǎng)(two words)我們使用一種叫做標(biāo)記指針(tagged pointer)的技術(shù)。9 ]) ^# y% ]/ J q% Q7 ?+ |
利用header中的next指針指向的地址總是字對(duì)齊(word aligned)這一特點(diǎn),我們可以得出指針低位的幾個(gè)有效位總會(huì)是0。% ^9 a3 q2 Z3 N( v8 m# F& Q$ m
因此我們將next指針的最低位進(jìn)行標(biāo)記來表示當(dāng)前塊是否被標(biāo)記。
1 K4 ]5 E, v7 D, M" o現(xiàn)在,我們可以掃描內(nèi)存區(qū)域了,但是我們應(yīng)該掃描哪些內(nèi)存區(qū)域呢?我們要掃描的有以下這些:
/ v% b: N% W$ \BBS(未初始化數(shù)據(jù)段)和初始化數(shù)據(jù)段。這里包含了程序的全局變量和局部變量。因?yàn)樗麄冇锌赡軕?yīng)用堆(heap)中的一些東西,所以我們需要掃描BSS與初始化數(shù)據(jù)段,已用的數(shù)據(jù)塊。% V" e: u$ f3 w. U; x- z
當(dāng)然,如果用戶分配一個(gè)指針來指向另一個(gè)已經(jīng)被分配的內(nèi)存塊,我們不會(huì)想去釋放掉那個(gè)被指向的內(nèi)存塊。堆棧。因?yàn)槎褩V邪械木植孔兞,因此這可以說是最需要掃描的區(qū)域了。- h1 L; E! L# \; A/ a
我們已經(jīng)了解了關(guān)于堆(heap)的一切,因此編寫一個(gè)mark_from_heap函數(shù)將會(huì)非常簡(jiǎn)單:
3 l* u8 g; G( o. P$ u6 w' J幸運(yùn)的是對(duì)于BSS段和已初始化數(shù)據(jù)段,大部分的現(xiàn)代unix鏈接器可以導(dǎo)出 etext 和 end 符號(hào)。etext符號(hào)的地址是初始化數(shù)據(jù)段的起點(diǎn)(the last address past the text segment,這個(gè)段中包含了程序的機(jī)器碼),end符號(hào)是堆(heap)的起點(diǎn)。
" [; c3 ~3 f* P7 Z因此,BSS和已初始化數(shù)據(jù)段位于 &etext 與 &end 之間。這個(gè)方法足夠簡(jiǎn)單,當(dāng)不是平臺(tái)獨(dú)立的。
# w' \- ?$ Q* Y% c G堆棧這部分有一點(diǎn)困難。堆棧的棧頂非常容易找到,只需要使用一點(diǎn)內(nèi)聯(lián)匯編即可,因?yàn)樗鎯?chǔ)在 sp 這個(gè)寄存器中。但是我們將會(huì)使用的是 bp 這個(gè)寄存器,因?yàn)樗雎粤艘恍┚植孔兞俊?font class="jammer">3 W7 H9 F. t: O s
尋找堆棧的的棧底(堆棧的起點(diǎn))涉及到一些技巧。出于安全因素的考慮,內(nèi)核傾向于將堆棧的起點(diǎn)隨機(jī)化,因此我們很難得到一個(gè)地址。5 N. d- D9 d3 Q7 U
老實(shí)說,我在尋找棧底方面并不是專家,但是我有一些點(diǎn)子可以幫你找到一個(gè)準(zhǔn)確的地址。
/ m3 z! z8 L- _' H1 o! E一個(gè)可能的方法是,你可以掃描調(diào)用棧(call stack)來尋找 env 指針,這個(gè)指針會(huì)被作為一個(gè)參數(shù)傳遞給主程序。
1 O9 V; Y. O& f" H. p另一種方法是從棧頂開始讀取每個(gè)更大的后續(xù)地址并處理inexorible SIGSEGV。
: b& n" K$ ?- m/ K, {" r E& G- \- I但是我們并不打算采用這兩種方法中的任何一種,我們將利用linux會(huì)將棧底放入一個(gè)字符串并存于proc目錄下表示該進(jìn)程的文件中這一事實(shí)。這聽起來很愚蠢而且非常間接。
+ A5 U& Q0 B0 s. h1 J7 b值得慶幸的是,我并不感覺這樣做是滑稽的,因?yàn)樗虰oehm GC中尋找棧底所用的方法完全相同。1 s$ W/ N A2 }* C# X
現(xiàn)在我們可以編寫一個(gè)簡(jiǎn)單的初始化函數(shù)。3 i K X' c% t8 J `0 ~
在函數(shù)中,我們打開proc文件并找到棧底。棧底是文件中第28個(gè)值,因此我們忽略前27個(gè)值。Boehm GC和我們的做法不同的是他僅使用系統(tǒng)調(diào)用來讀取文件來避免讓stdlib庫(kù)使用堆(heap),但是我們并不在意這些。
7 N, l& K- K9 ]4 c3 }& V9 {現(xiàn)在我們知道了每個(gè)我們需要掃描的內(nèi)存區(qū)域的位置,所以我們終于可以編寫顯示調(diào)用的回收函數(shù)了:9 n: z" ^9 u5 Q
朋友們,所有的東西都已經(jīng)在這了,一個(gè)用C為C程序編寫的垃圾回收器。這些代碼自身并不是完整的,它還需要一些微調(diào)來使它可以正常工作,但是大部分代碼是可以獨(dú)立工作的。
- \- h& `" ]5 P- @3總結(jié)
: a" C0 `. E( y' E2 e一開始就打算編寫完整的程序是很困難的,你編程的唯一算法就是分而治之。( d! n: v- `7 S
先編寫內(nèi)存分配函數(shù),然后編寫查詢內(nèi)存的函數(shù),然后是清除內(nèi)存的函數(shù)。最后將它們合在一起。( T7 T" s9 U! {/ H! u. Y
當(dāng)你在編程方面克服這個(gè)障礙后,就再也沒有困難的實(shí)踐了。你可能有一個(gè)算法不太了解,但是任何人只要有足夠的時(shí)間就肯定可以通過論文或書理解這個(gè)算法。
( U1 }! J& ]8 A1 ~/ v" Z7 n1 D& T如果有一個(gè)項(xiàng)目看起來令人生畏,那么將它分成完全獨(dú)立的幾個(gè)部分。7 z8 e4 |5 a' }( @
你可能不懂如何編寫一個(gè)解釋器,但你絕對(duì)可以編寫一個(gè)分析器,然后看一下你還有什么需要添加的,添上它。相信自己,終會(huì)成功!+ W: c8 }- @" m. y
來源:https://www.lmlphp.com/user/1774/article/item/19294/3 L4 D9 C1 p% t& Q" N
-END-
- Y3 a7 u7 u# s6 }2 p往期推薦:點(diǎn)擊圖片即可跳轉(zhuǎn)閱讀
7 ^! P' s, g7 p& H' U4 r9 O% L
5 H4 V+ o; J$ D4 Z6 {
% y" Z$ i' \; H* b( [; A X3 n
( M8 u: h0 k, `7 m( q& o
. S' r2 |+ U6 ]9 s7 {2 ]
pu4p3cz5lu464070330720.jpg (197.44 KB, 下載次數(shù): 1)
下載附件
保存到相冊(cè)
pu4p3cz5lu464070330720.jpg
2024-12-6 23:01 上傳
9 B9 |( [& t& C s' C e - ^8 c0 I+ i/ _9 C2 ^2 q1 u
嵌入式軟件和硬件在互相扯皮,項(xiàng)目進(jìn)行不下去了!
" y7 G) o; O! t* v* z$ Q: m* Z 3 r/ }/ E o8 T% i* G
5 c) E3 }, ]+ s& u
6 D0 f. F5 j/ m, K8 N# v" y 5 ^$ X# Z/ g8 w2 g+ k* c
8 F" \& [# R# o8 t
; y* D& j- f5 d
0 t3 q+ i1 g3 } ! M6 B0 ^0 a& `: c* v" M
3 I F& d' }- `
4 {. b; d) V8 y9 o, n# @/ p
g0ge04pzthn64070330821.jpg (198.96 KB, 下載次數(shù): 2)
下載附件
保存到相冊(cè)
g0ge04pzthn64070330821.jpg
2024-12-6 23:01 上傳
* H0 O& V+ s1 B9 Z$ ]
% N8 q" ]) X7 p" I1 H 嵌入式經(jīng)常用到串口,如何判斷串口數(shù)據(jù)接收完成?
" n+ J& e% O' v & _( a2 _, B! D( \0 L" q
/ ]# J9 |& `, V: ^9 Z
4 l3 j7 N) O& p. k# n3 P; m
% Y4 |# Y+ n. [; {$ Q. ? 8 R, h' z& R! l4 a) v
2 K4 E: F F" [. l 2 j$ m* W9 o% I0 H
$ ~0 X; M1 W- l6 {+ W
7 X2 R8 \& a4 c! C) }& H7 b
) D" E7 ?6 x: p2 J
rasa1wv0qid64070330921.jpg (215.94 KB, 下載次數(shù): 1)
下載附件
保存到相冊(cè)
rasa1wv0qid64070330921.jpg
2024-12-6 23:01 上傳
; |3 _4 a$ H$ a0 q4 E
7 k4 w- a% @0 G8 C. ~ 嵌入式和單片機(jī),這兩者之間是什么關(guān)系?
8 e# W# j W; ~, M 0 U1 d5 l0 I J! q
$ X: V/ U$ i/ C5 j. q* ?
# Z O& J; k4 Y, r+ Z. I! M% R7 ] 7 \( b/ L3 V9 `1 U6 R; ]8 ?
7 U- U3 @, J+ G& I3 Z我是老溫,一名熱愛學(xué)習(xí)的嵌入式工程師
/ b6 ?! L% A. U, `. l關(guān)注我,一起變得更加優(yōu)秀!
/ o. y9 j3 A7 }" D8 e, S8 |4 ~, b
tbn2zhgon4j64070331021.png (665.93 KB, 下載次數(shù): 1)
下載附件
保存到相冊(cè)
tbn2zhgon4j64070331021.png
2024-12-6 23:01 上傳
|
|