|
我是老溫,一名熱愛(ài)學(xué)習(xí)的嵌入式工程師
% U" _- u" J w, |- n5 q8 ]# y關(guān)注我,一起變得更加優(yōu)秀!9 R7 K" f7 \5 u" X' b! s
工程師們似乎認(rèn)為編寫(xiě)垃圾回收機(jī)制是很難的,是一種只有少數(shù)智者和Hans Boehm(et al)才能理解的高深魔法。我認(rèn)為編寫(xiě)垃圾回收最難的地方就是內(nèi)存分配,這和閱讀 K&R 所寫(xiě)的 malloc 樣例難度是相當(dāng)?shù)摹?font class="jammer">5 u% R# ~# r0 C. T; ]
在開(kāi)始之前有一些重要的事情需要說(shuō)明一下:8 u) }5 X! G& A
第一,我們所寫(xiě)的代碼是基于Linux Kernel的,注意是Linux Kernel而不是GNU/Linux。
7 j/ [. B! v5 `5 x% w, U1 A$ j0 ?第二,我們的代碼是32bit的。
# E1 R: o9 t9 `: d8 F3 h8 |第三,請(qǐng)不要直接使用這些代碼。我并不保證這些代碼完全正確,可能其中有一些我還未發(fā)現(xiàn)的小的bug,但是整體思路仍然是正確的。好了,讓我們開(kāi)始吧。' C; u8 |' o# {
1編寫(xiě)malloc$ V, C6 m, m+ E/ s5 ^" m5 V1 F
最開(kāi)始,我們需要寫(xiě)一個(gè)內(nèi)存分配器(memmory allocator),也可以叫做內(nèi)存分配函數(shù)(malloc function)。2 v7 ?7 Y. d& b4 V
最簡(jiǎn)單的內(nèi)存分配實(shí)現(xiàn)方法就是維護(hù)一個(gè)由空閑內(nèi)存塊組成的鏈表,這些空閑內(nèi)存塊在需要的時(shí)候被分割或分配。0 X% Y. k7 f' L- a
當(dāng)用戶(hù)請(qǐng)求一塊內(nèi)存時(shí),一塊合適大小的內(nèi)存塊就會(huì)從鏈表中被移除并分配給用戶(hù)。
; [* f9 F1 V/ B6 L5 z0 v如果鏈表中沒(méi)有合適的空閑內(nèi)存塊存在,而且更大的空閑內(nèi)存塊已經(jīng)被分割成小的內(nèi)存塊了或內(nèi)核也正在請(qǐng)求更多的內(nèi)存(譯者注:就是鏈表中的空閑內(nèi)存塊都太小不足以分配給用戶(hù)的情況)。
0 D0 {6 i. ]$ r: ^& p那么此時(shí),會(huì)釋放掉一塊內(nèi)存并把它添加到空閑塊鏈表中。3 A0 e+ S1 V; t# S# y- h& `
在鏈表中的每個(gè)空閑內(nèi)存塊都有一個(gè)頭(header)用來(lái)描述內(nèi)存塊的信息。我們的header包含兩個(gè)部分,第一部分表示內(nèi)存塊的大小,第二部分指向下一個(gè)空閑內(nèi)存塊。
: J1 C& C9 I" w! V將頭(header)內(nèi)嵌進(jìn)內(nèi)存塊中是唯一明智的做法,而且這樣還可以享有字節(jié)自動(dòng)對(duì)齊的好處,這很重要。
6 D5 Q3 x8 }3 D4 o/ d由于我們需要同時(shí)跟蹤我們“當(dāng)前使用過(guò)的內(nèi)存塊”和“未使用的內(nèi)存塊”,因此除了維護(hù)空閑內(nèi)存的鏈表外,我們還需要一條維護(hù)當(dāng)前已用內(nèi)存塊的鏈表(為了方便,這兩條鏈表后面分別寫(xiě)為“空閑塊鏈表”和“已用塊鏈表”)。4 D* E* [4 M; y! d
我們從空閑塊鏈表中移除的內(nèi)存塊會(huì)被添加到已用塊鏈表中,反之亦然。
$ V5 e- p" ]2 ^' b7 `現(xiàn)在我們差不多已經(jīng)做好準(zhǔn)備來(lái)完成malloc實(shí)現(xiàn)的第一步了。但是再那之前,我們需要知道怎樣向內(nèi)核申請(qǐng)內(nèi)存。
2 t- J3 u( Y- f7 o6 B, F動(dòng)態(tài)分配的內(nèi)存會(huì)駐留在一個(gè)叫做堆(heap)的地方,堆是介于棧(stack)和BSS(未初始化的數(shù)據(jù)段-你所有的全局變量都存放在這里且具有默認(rèn)值為0)之間的一塊內(nèi)存。
4 _1 e d, p; N, v# m堆(heap)的內(nèi)存地址起始于(低地址)BSS段的邊界,結(jié)束于一個(gè)分隔地址(這個(gè)分隔地址是已建立映射的內(nèi)存和未建立映射的內(nèi)存的分隔線(xiàn))。1 P3 i0 x9 |* e! |) p
為了能夠從內(nèi)核中獲取更多的內(nèi)存,我們只需提高這個(gè)分隔地址。為了提高這個(gè)分隔地址我們需要調(diào)用一個(gè)叫作 sbrk 的Unix系統(tǒng)的系統(tǒng)調(diào)用,$ T3 k- ^1 r; n, M6 f9 ~ ~
這個(gè)函數(shù)可以根據(jù)我們提供的參數(shù)來(lái)提高分隔地址,如果函數(shù)執(zhí)行成功則會(huì)返回以前的分隔地址,如果失敗將會(huì)返回-1。- l. s5 h P Z; q) s( s
利用我們現(xiàn)在知道的知識(shí),我們可以創(chuàng)建兩個(gè)函數(shù):morecore()和add_to_free_list()。
8 z$ y( h# n1 M) f; F( ?當(dāng)空閑塊鏈表缺少內(nèi)存塊時(shí),我們調(diào)用morecore()函數(shù)來(lái)申請(qǐng)更多的內(nèi)存。由于每次向內(nèi)核申請(qǐng)內(nèi)存的代價(jià)是昂貴的,我們以頁(yè)(page-size)為單位申請(qǐng)內(nèi)存。 m0 y: h5 U$ i' |9 F& ]
頁(yè)的大小在這并不是很重要的知識(shí)點(diǎn),不過(guò)這有一個(gè)很簡(jiǎn)單解釋?zhuān)喉?yè)是虛擬內(nèi)存映射到物理內(nèi)存的最小內(nèi)存單位。
! e c8 F* j( Y5 `& }1 `接下來(lái)我們就可以使用add_to_list()將申請(qǐng)到的內(nèi)存塊加入空閑塊鏈表。$ C% r7 Q. g1 z# @8 L% W: R
現(xiàn)在我們有了兩個(gè)有力的函數(shù),接下來(lái)我們就可以直接編寫(xiě)malloc函數(shù)了。
7 Y' ~! \# j- z8 b6 s1 j1 f% j0 W6 U我們掃描空閑塊鏈表當(dāng)遇到第一塊滿(mǎn)足要求的內(nèi)存塊(內(nèi)存塊比所需內(nèi)存大即滿(mǎn)足要求)時(shí),停止掃描,而不是掃描整個(gè)鏈表來(lái)尋找大小最合適的內(nèi)存塊,我們所采用的這種算法思想其實(shí)就是首次適應(yīng)(與最佳適應(yīng)相對(duì))。/ P3 c6 d+ i. v" j
注意:有件事情需要說(shuō)明一下,內(nèi)存塊頭部結(jié)構(gòu)中size這一部分的計(jì)數(shù)單位是塊(Block),而不是Byte。- a+ T* w& d* A# H. [+ Z9 v: O7 s
注意這個(gè)函數(shù)的成功與否,取決于我們第一次使用時(shí)是否使 freep = &base 。這點(diǎn)我們會(huì)在初始化函數(shù)中進(jìn)行設(shè)置。/ d. x; x' i* A8 w
盡管我們的代碼完全沒(méi)有考慮到內(nèi)存碎片,但是它能工作。既然它可以工作,我們就可以開(kāi)始下一個(gè)有趣的部分-垃圾回收!1 N( D* K( C; o3 G" w! ` e
2標(biāo)記與清掃) O Q6 E- ~" s+ m* Y) m5 R6 a
我們說(shuō)過(guò)垃圾回收器會(huì)很簡(jiǎn)單,因此我們盡可能的使用簡(jiǎn)單的方法:標(biāo)記和清除方式。這個(gè)算法分為兩個(gè)部分:4 M- |1 G9 U/ |! ]
首先,我們需要掃描所有可能存在指向堆中數(shù)據(jù)(heap data)的變量的內(nèi)存空間并確認(rèn)這些內(nèi)存空間中的變量是否指向堆中的數(shù)據(jù)。8 P7 E3 C% e/ m$ w& b
為了做到這點(diǎn),對(duì)于可能內(nèi)存空間中的每個(gè)字長(zhǎng)(word-size)的數(shù)據(jù)塊,我們遍歷已用塊鏈表中的內(nèi)存塊。% D, l i" w* o1 c+ O# y7 R0 t# t
如果數(shù)據(jù)塊所指向的內(nèi)存是在已用鏈表塊中的某一內(nèi)存塊中,我們對(duì)這個(gè)內(nèi)存塊進(jìn)行標(biāo)記。( G+ D& w: R- I$ u, P, f
第二部分是,當(dāng)掃描完所有可能的內(nèi)存空間后,我們遍歷已用塊鏈表將所有未被標(biāo)記的內(nèi)存塊移到空閑塊鏈表中。
9 ?# a# L( S3 o現(xiàn)在很多人會(huì)開(kāi)始認(rèn)為只是靠編寫(xiě)類(lèi)似于malloc那樣的簡(jiǎn)單函數(shù)來(lái)實(shí)現(xiàn)C的垃圾回收是不可行的,因?yàn)樵诤瘮?shù)中我們無(wú)法獲得其外面的很多信息。% `; u9 G4 P5 A% T1 `
例如,在C語(yǔ)言中沒(méi)有函數(shù)可以返回分配到堆棧中的所有變量的哈希映射。但是只要我們意識(shí)到兩個(gè)重要的事實(shí),我們就可以繞過(guò)這些東西:/ v; }: l8 @5 y
第一,在C中,你可以嘗試訪問(wèn)任何你想訪問(wèn)的內(nèi)存地址。因?yàn)椴豢赡苡幸粋(gè)數(shù)據(jù)塊編譯器可以訪問(wèn)但是其地址卻不能被表示成一個(gè)可以賦值給指針的整數(shù)。' H' B8 R% r J% e
如果一塊內(nèi)存在C程序中被使用了,那么它一定可以被這個(gè)程序訪問(wèn)。這是一個(gè)令不熟悉C的編程者很困惑的概念,因?yàn)楹芏嗑幊陶Z(yǔ)言都會(huì)限制程序訪問(wèn)虛擬內(nèi)存,但是C不會(huì)。
( ]; V6 O* w' M8 ^, |' @5 h第二,所有的變量都存儲(chǔ)在內(nèi)存的某個(gè)地方。這意味著如果我們可以知道變量們的通常存儲(chǔ)位置,我們可以遍歷這些內(nèi)存位置來(lái)尋找每個(gè)變量的所有可能值。
( k4 t6 z8 c' V) m另外,因?yàn)閮?nèi)存的訪問(wèn)通常是字(word-size)對(duì)齊的,因此我們僅需要遍歷內(nèi)存區(qū)域中的每個(gè)字(word)即可。4 f. S5 |8 \, i5 y- o
局部變量也可以被存儲(chǔ)在寄存器中,但是我們并不需要擔(dān)心這些因?yàn)榧拇嫫鹘?jīng)常會(huì)用于存儲(chǔ)局部變量,而且當(dāng)函數(shù)被調(diào)用的時(shí)候他們通常會(huì)被存儲(chǔ)在堆棧中。6 }) X, x. S `$ M
現(xiàn)在我們有一個(gè)標(biāo)記階段的策略:遍歷一系列的內(nèi)存區(qū)域并查看是否有內(nèi)存可能指向已用塊鏈表。編寫(xiě)這樣的一個(gè)函數(shù)非常的簡(jiǎn)潔明了:7 H% f& o& Y! j
為了確保我們只使用頭(header)中的兩個(gè)字長(zhǎng)(two words)我們使用一種叫做標(biāo)記指針(tagged pointer)的技術(shù)。' n) r: S. l3 n& n
利用header中的next指針指向的地址總是字對(duì)齊(word aligned)這一特點(diǎn),我們可以得出指針低位的幾個(gè)有效位總會(huì)是0。
) S d( Q( m5 q2 K因此我們將next指針的最低位進(jìn)行標(biāo)記來(lái)表示當(dāng)前塊是否被標(biāo)記。
\3 P8 Y7 ` X+ \$ g- z# O- X6 H現(xiàn)在,我們可以?huà)呙鑳?nèi)存區(qū)域了,但是我們應(yīng)該掃描哪些內(nèi)存區(qū)域呢?我們要掃描的有以下這些:( i3 L9 M( n; o: o& @( r
BBS(未初始化數(shù)據(jù)段)和初始化數(shù)據(jù)段。這里包含了程序的全局變量和局部變量。因?yàn)樗麄冇锌赡軕?yīng)用堆(heap)中的一些東西,所以我們需要掃描BSS與初始化數(shù)據(jù)段,已用的數(shù)據(jù)塊。
& e+ E6 {4 c' I( \/ |$ b- i當(dāng)然,如果用戶(hù)分配一個(gè)指針來(lái)指向另一個(gè)已經(jīng)被分配的內(nèi)存塊,我們不會(huì)想去釋放掉那個(gè)被指向的內(nèi)存塊。堆棧。因?yàn)槎褩V邪械木植孔兞,因此這可以說(shuō)是最需要掃描的區(qū)域了。; b' F2 N# ]0 Q
我們已經(jīng)了解了關(guān)于堆(heap)的一切,因此編寫(xiě)一個(gè)mark_from_heap函數(shù)將會(huì)非常簡(jiǎn)單:
# j2 u* @; l: Z. j! J8 u幸運(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)。" }5 T# d1 k3 ^$ b2 X( V
因此,BSS和已初始化數(shù)據(jù)段位于 &etext 與 &end 之間。這個(gè)方法足夠簡(jiǎn)單,當(dāng)不是平臺(tái)獨(dú)立的。
2 W% n( u' Z" D9 a: Z; ^( l/ {堆棧這部分有一點(diǎn)困難。堆棧的棧頂非常容易找到,只需要使用一點(diǎn)內(nèi)聯(lián)匯編即可,因?yàn)樗鎯?chǔ)在 sp 這個(gè)寄存器中。但是我們將會(huì)使用的是 bp 這個(gè)寄存器,因?yàn)樗雎粤艘恍┚植孔兞俊?br />
" `# d! F' j* Q. \6 I2 b尋找堆棧的的棧底(堆棧的起點(diǎn))涉及到一些技巧。出于安全因素的考慮,內(nèi)核傾向于將堆棧的起點(diǎn)隨機(jī)化,因此我們很難得到一個(gè)地址。2 B; l! n G1 K' c
老實(shí)說(shuō),我在尋找棧底方面并不是專(zhuān)家,但是我有一些點(diǎn)子可以幫你找到一個(gè)準(zhǔn)確的地址。) V- R! F2 w' X% M
一個(gè)可能的方法是,你可以?huà)呙枵{(diào)用棧(call stack)來(lái)尋找 env 指針,這個(gè)指針會(huì)被作為一個(gè)參數(shù)傳遞給主程序。
. _. S8 t" \2 Q, P8 P1 S另一種方法是從棧頂開(kāi)始讀取每個(gè)更大的后續(xù)地址并處理inexorible SIGSEGV。
& ]( O% }. g! |6 o# J; S Z但是我們并不打算采用這兩種方法中的任何一種,我們將利用linux會(huì)將棧底放入一個(gè)字符串并存于proc目錄下表示該進(jìn)程的文件中這一事實(shí)。這聽(tīng)起來(lái)很愚蠢而且非常間接。
, M% x& d A8 Z. O7 S值得慶幸的是,我并不感覺(jué)這樣做是滑稽的,因?yàn)樗虰oehm GC中尋找棧底所用的方法完全相同。5 b: [5 O7 X/ z, h6 x
現(xiàn)在我們可以編寫(xiě)一個(gè)簡(jiǎn)單的初始化函數(shù)。
0 a. `+ y+ O2 D6 X# z: ~6 N在函數(shù)中,我們打開(kāi)proc文件并找到棧底。棧底是文件中第28個(gè)值,因此我們忽略前27個(gè)值。Boehm GC和我們的做法不同的是他僅使用系統(tǒng)調(diào)用來(lái)讀取文件來(lái)避免讓stdlib庫(kù)使用堆(heap),但是我們并不在意這些。
' e% [' ?# [* Z; S: H7 `現(xiàn)在我們知道了每個(gè)我們需要掃描的內(nèi)存區(qū)域的位置,所以我們終于可以編寫(xiě)顯示調(diào)用的回收函數(shù)了:6 T/ H* r1 B4 t" i- m M6 G
朋友們,所有的東西都已經(jīng)在這了,一個(gè)用C為C程序編寫(xiě)的垃圾回收器。這些代碼自身并不是完整的,它還需要一些微調(diào)來(lái)使它可以正常工作,但是大部分代碼是可以獨(dú)立工作的。/ i& e! g3 A2 b7 q+ o3 _
3總結(jié)
4 b+ A- q* k- x" Z3 v* A# w一開(kāi)始就打算編寫(xiě)完整的程序是很困難的,你編程的唯一算法就是分而治之。
' H5 ^; K. k) W- T! `# ^% X" g先編寫(xiě)內(nèi)存分配函數(shù),然后編寫(xiě)查詢(xún)內(nèi)存的函數(shù),然后是清除內(nèi)存的函數(shù)。最后將它們合在一起。! b5 q& r$ N: k1 M* X0 Q
當(dāng)你在編程方面克服這個(gè)障礙后,就再也沒(méi)有困難的實(shí)踐了。你可能有一個(gè)算法不太了解,但是任何人只要有足夠的時(shí)間就肯定可以通過(guò)論文或書(shū)理解這個(gè)算法。
' Y0 ]( e) S2 w& I* Q& s$ T, j如果有一個(gè)項(xiàng)目看起來(lái)令人生畏,那么將它分成完全獨(dú)立的幾個(gè)部分。
* E, A8 H+ i3 ?# p! c4 l你可能不懂如何編寫(xiě)一個(gè)解釋器,但你絕對(duì)可以編寫(xiě)一個(gè)分析器,然后看一下你還有什么需要添加的,添上它。相信自己,終會(huì)成功!: w0 P- A2 r, J0 q# t
來(lái)源:https://www.lmlphp.com/user/1774/article/item/19294/
; H% D, ~& I! i$ M; P-END-* x; w9 l. E+ L9 [
往期推薦:點(diǎn)擊圖片即可跳轉(zhuǎn)閱讀
, E. T" i5 ]$ ?' ]. M
3 S1 r- d# X$ m; b . }8 F4 u0 F% @1 h) R" w4 `
& W$ V" R" Q# _1 N. R
# S5 z5 k, x( {4 E+ }. W1 P, v0 ^
pu4p3cz5lu464070330720.jpg (197.44 KB, 下載次數(shù): 1)
下載附件
保存到相冊(cè)
pu4p3cz5lu464070330720.jpg
2024-12-6 23:01 上傳
: s4 ], Q" Z* z ~
4 W$ F7 b3 ?$ ? 嵌入式軟件和硬件在互相扯皮,項(xiàng)目進(jìn)行不下去了!
% N1 l3 {/ y/ W+ Z- _5 }
V. |# {+ d; Q7 B1 Z 7 _- W$ {5 X; j" R* k% a4 P4 c
2 Q0 {" T9 j! s3 }/ x: b# H Q' z# W1 e4 B7 x ^
3 n. K! a5 M! F/ J5 n! z
4 Z; |- F6 M2 [% u2 f + }2 r3 l9 m* m: T0 r
) X5 F% `$ _/ m8 Q$ H6 w# R
) Z$ x; C' s% \" V' c- e2 y
F: W' K+ P& z
g0ge04pzthn64070330821.jpg (198.96 KB, 下載次數(shù): 2)
下載附件
保存到相冊(cè)
g0ge04pzthn64070330821.jpg
2024-12-6 23:01 上傳
4 P8 b) y' W5 a4 R
$ d( H* a1 n; R4 b9 W! E1 h 嵌入式經(jīng)常用到串口,如何判斷串口數(shù)據(jù)接收完成?# c0 r2 x. P# B, n0 v; G: U' N
9 R; r a6 S5 k2 a% V A, p + ?. n! ~) f) q& ]# W5 a; Z
: M+ N/ T: x1 k( Z0 M7 M + W! _3 l& o# O' z1 I# {) n$ m: B
" ?8 q( I9 K8 E. L' E
9 `- y; f r8 `+ n
. e' d% r$ L5 X$ c4 A. A
) w3 ^% j3 i5 X: w
+ X' p- t1 P2 L# b9 d9 n2 ^
( A1 N! h1 v: {! k) @
rasa1wv0qid64070330921.jpg (215.94 KB, 下載次數(shù): 1)
下載附件
保存到相冊(cè)
rasa1wv0qid64070330921.jpg
2024-12-6 23:01 上傳
6 Q/ n* j. A4 P
8 R$ J0 r0 Z& W! U( b 嵌入式和單片機(jī),這兩者之間是什么關(guān)系?) \% `" K1 ^& q8 m" n
+ l. w8 H& q4 P/ [
4 Y2 e }5 g( Z5 c
- K% T- ~: l7 s2 t9 u* k# X 7 U( M9 c' m% P- r0 }% }( ?( @
, j: s8 E) _3 X3 T我是老溫,一名熱愛(ài)學(xué)習(xí)的嵌入式工程師- V- `" s {/ T$ S; s* J
關(guān)注我,一起變得更加優(yōu)秀!
3 w/ W2 E8 E n
tbn2zhgon4j64070331021.png (665.93 KB, 下載次數(shù): 1)
下載附件
保存到相冊(cè)
tbn2zhgon4j64070331021.png
2024-12-6 23:01 上傳
|
|