不要動輒滾粗,先看堆棧是否溢出
中國人是慣于精打細算的,魯迅先生說:“時間是海綿里的水,擠一擠總會有的!”
本文引用地址:http://www.butianyuan.cn/article/201910/406345.htm領導說,魯迅說得對!
于是,領導們經常帶著期盼的神情,忽悠苦逼的軟件工程師:“再多想想辦法吧,嗯,MCU的主頻是低了點,RAM資源是少了些,但是,考慮一下成本,MCU還是盡量不要換的吧?方法總比困難多,看著RAM資源好像不大夠,但是換個實現方式,還可以再擠一擠的吧?魯迅先生曾說......”
好吧,領導們肯定讀過華嚴經,深諳佛菩薩“螺螄殼里做道場”的本事:大即是小,小即是大,大小無二無別!嫌功能太多,RAM資源太少,多少算多呀?為啥子要生出那么多分別心撒?
可是,領導們可能不知道,魯迅先生寫錯了字不叫錯別字,叫“通假字”,我們寫錯了就是實實在在的“錯別字”,而且佛菩薩的境界也是“非汝邊事”。所以,用小馬拉大車,在資源一般般的MCU中塞入盡可能多的代碼,實現那么多功能,還能讓這些模塊配合無間地親密運轉,實在不是我等的境界了!
這不,同事小王又找我來訴苦了。
1
“馬步君,救救我吧??煲活I導折磨瘋了,這就是個16位的單片機,RAM總共512個字節(jié)。留給堆棧256個字節(jié),剩下的就只有256字節(jié)了,哪能實現那么多功能呢?可是領導說干嘛留給堆棧256個字節(jié),堆棧留少一點RAM不就夠用了嗎?”
說著說著,小王愈加地憤憤不平了:“明明有個管腳兼容的芯片,RAM有1k字節(jié),但是領導就是不讓換。說讓堆棧留少一點,哼,他知道個屁!滾粗,堆棧不夠的話系統(tǒng)會跑飛的呀!”
看著小王蠟黃黃的臉蛋和紅通通的眼睛,我心下有些不忍:‘萬般皆苦,做人最苦,難怪如來說為可憐愍者呀!’可是,領導說的也不無道理,對于堆棧該設置多少,很多人都是稀里糊涂,又有多少人能夠弄得明白呢?于是我竟而給領導辯護了起來:“也許領導說得對吧,因為你確實不知道該給堆棧留多少空間吧?”
我一邊小心翼翼地說著,一邊看著小王的臉慢慢地耷拉了下來,甚而就要拉到地上了。于是我趕忙提起萬般的精力找補一番,給他講起MCU中RAM資源和堆棧分配的矛盾性來:
“RAM資源確實很重要,領導的意思應該是說你對它的分配要照顧到應用、系統(tǒng)堆棧兩方面的需求,不可有所偏頗。
在MCU的地址空間中,RAM是連續(xù)分配一段線性地址空間,應用中用到的全局變量、中斷和系統(tǒng)調用用到的棧、動態(tài)分配用到的堆都要分配在這段有限的線性空間內,當然你可以選擇不用‘堆’。不過,如果有所富余,或者確實需要,你還得把存在程序存儲空間中的一段代碼復制到RAM空間內運行,以加快程序的運行速度,提高系統(tǒng)實時性。
所以,RAM資源確實是有限,不可能也不應該盲目得為堆棧分配太大的尺寸。不過話又說回來,如果堆棧設置地過小也不行,因為設置過小的話,一旦程序設計得不合理就很容易出問題。比如在函數調用中子函數中的局部變量太多、中斷優(yōu)先級設置得不合理導致高低中斷間的嵌套、中斷ISR程序過長導致本中斷被嵌套,或者出現函數調用層次過深等程序設計不當之處都可能導致堆棧溢出,改變臨近堆棧的RAM空間中的內容,從而造成程序運行異常,發(fā)生故障甚至導致重大事故。
從這個角度來說,在一定程度上,堆棧設置得大一些,有利于彌補程序設計的缺陷。話再說回來,程序設計地很完美,就不需要設置那么大的堆棧。歸結到底,這就是個平衡木啊!”
跟小王進行了這段科普后,他著實有些懵圈了。于是我把他晾在一邊,忙活起了自己的事兒。
我想,上面那番話夠他消化一段時間的了。
2
快到飯點了,辦公室里突然熱鬧了起來,有人在大聲講電話,有人被踩了尾巴似的叫上一聲,然后戛然而止,就像被一把剪刀剪斷了聲線一般,有人開始四處走動串聯,但是我卻感到背后有一種異樣的寂靜!果然,一回頭,小王又找上門來了。
“馬步君,你剛才說的是不錯,堆棧不能設置得過大,也不能設置得過小,可是這好像等于什么也沒有說一樣嘛。歸根到底,我該怎么設置堆棧的大小呢?”緩過神來的小王,突然發(fā)現我只是專業(yè)性地描述了問題,卻沒有給出問題的答案。
“孺子可教也,”小王的發(fā)問讓我不禁有些凜然,我一邊向他投去贊賞的目光,一邊心下思忖該怎么樣回答。思量片刻,我又開啟了說教模式:
“可以通過靜態(tài)分析的方式確定堆??臻g的尺寸。你需要根據源程序中每個函數的局部變量大小確定每個函數的堆棧使用量,然后根據編譯器生成的函數調用列表為每個函數建立調用樹,檢查每棵調用樹,確定從樹根到樹葉的調用路徑的堆棧使用量,從中選出最大堆棧使用量,同時,還要仔細分析系統(tǒng)用到的所有中斷,確定中斷服務程序的堆棧使用量?!?/span>
看著他再次陷入懵圈狀態(tài),我滿意地點了點頭,鼓起腮幫子繼續(xù)說教,
“但是,除了咱們自己寫的程序,你所調用的C標準庫函數以及大值整數的乘除、浮點運算等對應的運行庫函數也會消耗堆棧,它們的堆棧使用量具體是多少我也不是很清楚,但是應該可以查得到。講到這里你也看到了,這種靜態(tài)分析方式對開發(fā)者的技術水平、對產品代碼的理解程度要求非常高,得到的數據并不完善,而且這種方式依賴于具體的應用和源程序實現方式,所以,好麻煩!”
被說到懷疑人生的小王再次鎖緊了眉頭,抿著嘴唇一言不發(fā),他在想什么我不清楚,但是我想:“按照我剛才的說法,我不是也不知道該怎么設置堆棧大小的嘛?哎,做人難,做嵌入式軟件工程師更難啊!”
3
我本以為這件事到此結束了,沒曾想吃完飯后,小王又找上門來了,“馬步君,你剛才說了,通過靜態(tài)分析判斷堆棧使用量對程序員要求很高,而且不通用,那么,有沒有一種動態(tài)的判斷方式呢?”
“當然有了,可以在鏈接文件中,對RAM的空間分配做手腳?!蔽以俅钨┵┒勂饋?,這邊廂我吐沫飛濺,那邊廂小王兩眼放光。各位看官且先不要覺得筆者的思維實在敏捷、腦路不得了的靈光,而對筆者投來欽敬的目光。實際上,就在吃飯的空當,我就在苦苦地思索,到底該怎樣,堆棧的空間分配才算適當。
如果不在鏈接文件中做任何設置,RAM就是堆棧區(qū)+全局變量區(qū),這樣一來,堆棧區(qū)以下便是全局變量區(qū),堆棧的生長方向為自上而下,即向著RAM地址減小的方向增長,堆棧溢出時改變全局變量的值,可是很多情況下,你根本意識不到程序溢出,只有在特殊的觸發(fā)條件下程序運行某個功能時,你才可能意識到不對勁。
所以,為了第一時間就檢查到堆棧溢出,要加入一個緊鄰堆棧區(qū)的新區(qū),這個新區(qū)叫‘堆棧溢出緩沖區(qū)’。想一想哈,這時堆棧溢出時就會改變‘堆棧溢出緩沖區(qū)’的數據,只要我上電初始化時將‘堆棧溢出緩沖區(qū)’初始化為固定數據,然后定期查詢這個新區(qū)中的數據,就能判斷堆棧是否溢出,而且可以判斷這一段時間內的最大堆棧使用量?!?/p>
我緩緩著解釋著自己的思路,等著小王慢慢跟上來。過了一會兒,小王又猝不及防地發(fā)問了:“如果堆棧設置地比較大,不會發(fā)生溢出,那這個‘堆棧溢出緩沖區(qū)’也起不到什么作用,只會白白浪費RAM資源??!”
好吧,我承認,當時確實被他問住了,但是,既然之前的思路已經打開,再打個小補丁就不算什么難事了。我思量片刻,就給出了讓他滿意的答案:
“可以在MCU上電初始化時,將堆棧區(qū)和堆棧溢出緩沖區(qū)的數據全部初始化為一個固定數據,比如0xa5,將最大堆棧使用量記為stack_max,然后用一個周期定時器定時讀取堆棧溢出緩沖區(qū)和堆棧區(qū)的數據,就可以判斷堆棧設置是否過大。
而且,第二次讀取這兩個區(qū)的數據時,從stack_max個數據后開始讀取即可,比如上周期統(tǒng)計到堆棧用到100個字節(jié),stack_max=100,下個周期從第101個字節(jié)開始讀起就可以了。
如果你開始設置堆棧為384個字節(jié),跑了一天后,發(fā)現stack_max=210,那就把堆棧設置為256就可以了。這樣就能解決你的問題-科學合理地縮小堆棧分配了!”
4
過了幾天,小王終于發(fā)現,‘原來’自己的程序用到的堆棧從來都不會超過130個字節(jié),于是他乖乖地改小了堆棧,把空出來的100來個字節(jié)都分給了全局變量,RAM一下子綽綽有余了,他很開心地對我說:看來還是不要動輒滾粗,要先看看堆棧是否真的溢出!
評論