嵌入式linux啟動時間優化
嵌入式系統的啟動速度因裝置的效能和程式碼的質量而異,但總體而言,從消費者的角度考慮,系統的啟動速度肯定是越快越好。因此,對嵌入式系統進行效能優化,加快裝置的啟動時間為專案後期必須進行的一項工作。需要注意的是:嵌入式Linux裝置的優化不是一蹴而就的,而是一個不斷優化,不斷改進的過程。
現將自己掌握的嵌入式裝置的效能優化策略進行總結,如有不對的地方,還望批評指正。
啟動快慢的標準
裝置啟動的快慢目前還沒有一個統一的標準。在專案中一般按照客戶的標準。
效能的評測
對於開發人員來說,評價裝置的效能一般是通過在程式碼中增加log的方式。這種方式具有以下幾點優點:
精確度高。
通常能夠精確到毫秒。有特殊需求的情況下,可以精確到毫秒,比如使用gettimeofday函式。
靈活性強。
可以測出程式碼中任意部分的程式碼執行所耗費的時間。
導致效能低下的原因
在嵌入式裝置中,匯入裝置啟動時間過長,效能低下的原因一般包括如下幾個方面:
硬體的原因
硬體的原因一般指的是裝置的CPU及Flash效能。如果程式碼的運算量很大,礙於CPU和Flash的效能,會導致CPU過於繁忙。有些裝置礙於成本的原因,Flash太小,很多東西都需要壓縮存放,那麼在裝置啟動過程中,解壓也需要一定的時間。
程式的原因
程式碼需要進行大量的IO操作,比如讀寫檔案,記憶體訪問等等,CPU更多的時候處於等待狀態。而有些程式碼,由於編寫的原因,導師各個程序之間相互等待,CPU利用率低下,制約了裝置的效能。
優化的原則
優化並不能盲目的優化,盲目追求效能,還要統籌考慮。一般要遵循以下原則:
等效性原則
優化前後的程式碼實現的功能要完全一致
有效性原則
優化後的程式碼一定要比原先的程式碼執行速度快活著佔用儲存空間小,或者二者兼有,否則就是毫無意義的優化
經濟性原則
很多程式碼效能低下的部分原因也是由於硬體效能的限制,比如將檔案壓縮存放以節約儲存成本。優化要在現有的條件下考慮,不要以更換儲存空間的大小來換取解壓的時間。優化要付出較小的代價,很多程式設計師在做優化的時候,抱怨裝置的效能有限,要求提高裝置的效能,這樣只能是本末倒置。
優化的方法
此處提出的優化的方法主要是從程式碼的角度考慮,不包括升級硬體。
shell 指令碼優化
絕大多數的嵌入式裝置都會使用busybox作為實現Linux命令的工具,因此BusyBox提供了一個比較完善的環境,可以適用於任何小的嵌入式系統。
BusyBox 是一個集成了一百多個最常用linux命令和工具的軟體。BusyBox 包含了一些簡單的工具,例如ls、cat和echo等等,還包含了一些更大、更復雜的工具,例grep、find、mount以及telnet。有些人將BusyBox稱為Linux工具裡的瑞士軍刀。簡單的說BusyBox就好像是個大工具箱,它整合壓縮了Linux的許多工具和命令,也包含了Android系統的自帶的shell。
BusyBox包含三種類型的命令:
APPLET
即為人所熟知的applets,它由BusyBox建立一個子程序,然後呼叫exec執行相應的功能,在執行完畢後,返回控制給父程序。
- 1
APPLET_NOEXEC
系統將呼叫fork建立子程序,然後執行BusyBox中相應的功能,在執行完畢後,返回控制給父程序。
- 1
APPLET_NOFORK
它相當於builts-in,只是執行BusyBox的內部函式,不必建立子程序,所以其效率極高。
- 1
眾所周知,在Linux中呼叫fork,exec是很耗費時間的,所以我們應該儘可能的使用APPLET_NOFORK命令,其次是APPLET_NOEXEC,最後是APPLET。
在BusyBox1.9中,屬於APPLET_NOFORK的功能有:
basename,cat,dirname,echo,false,hostid,length,logname,mkdir,pwd,rm,rmdir,deq,sleep,sync,touch,true,usleep,whoami,yes
- 1
屬於APPLET_NOEXEC的功能有:
awk,chgrp,chmod,chown,cp,cut,dd,find,hexdump,ln,soort,test,xargs......
- 1
所以優化shell指令碼的策略一般有:
1. 去掉無用的指令碼
2. 儘可能的使用BusyBox內部的命令
3. 儘量不要使用管道pipe
4. 減少管道中的命令數目
5. 儘量不要使用·
- 1
- 2
- 3
- 4
- 5
優化程序啟動速度
程序的啟動過程如下:
1 搜尋其所依賴的動態庫
2 載入動態庫
3 初始化動態庫
4 初始化程序
5 將程式的控制權移交給main函式
- 1
- 2
- 3
- 4
- 5
要加快的程序的啟動速度,可以從以下幾方面入手:
1 減少載入的動態庫的數量
a) 使用dlopen,將啟動時不需要的動態庫延後載入
b) 將一些動態庫改為靜態庫
優點:
- 減少了載入動態庫的數量
- 在與其他動態庫合併之後,動態庫內部的函式之間不必再進行動態連結、符號查詢,從而提高速度
缺點:
- 該動態庫如果被多個動態庫或程序所依賴的話,那麼該動態庫將被複制多份合併到新的動態庫中,導致整體的檔案大小增加,佔用更多的Flash。
- 失去了動態庫原有的程式碼段記憶體共享,因此可能會導致記憶體使用上的增加
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
2 優化載入動態庫時的搜尋路徑
a) 設定LD_HWCAP_MASK,禁掉一些不用的硬體特性
b) 將所有的動態庫都放在一個目錄下,並且將目錄放在LD_LIBRARY_PATH的開始
c) 不能放在一個目錄的,在程序中加入-rpath選項,指定搜尋路徑
- 1
- 2
- 3
- 4
- 5
如果做了之前的工作仍然無法滿足程序啟動速度的要求,那就從程序的排程上下功夫,可以:
程序改為執行緒
可以把原來的程序分割為兩個部分:
常駐記憶體部分:其為daemon程序,主要負責載入程序所需要的動態庫,偵聽使用者訊號,建立和銷燬使用者邏輯執行緒
完成使用者邏輯部分: 由daemon部分建立執行緒,按使用者需求完成使用者邏輯
這樣就節省掉了載入動態庫、初始化動態庫和全域性變數部分,可以縮短程序的響應時間,來滿足使用者的需求
還可以再引申一下,將原來的多個daemon程序的常駐記憶體部分進行合併,根據使用者邏輯需求,建立不同的程序。
優點:
建立執行緒時,不需要重新載入動態庫,故縮短了程序的響應時間
多個業務邏輯共享動態庫時,避免了系統為每個業務邏輯建立動態庫的資料段,從而節省了大量的記憶體。
缺點:
由原來的程序改為執行緒,工作量比較大,程式碼修改上存在一定的風險
多個業務邏輯執行緒之間共享動態庫時,有可能會帶來全域性變數的衝突
由於還是存在daemon程序部分,所以其堆疊記憶體不會被釋放,多個業務邏輯執行緒所存在記憶體洩露會糾纏在一起,從而使問題更加複雜。
preload程序
在程序的main函式中插入一行語句:
pause();
這樣,當程序啟動時,載入完動態庫後,就會停在這裡,不會執行使用者邏輯。
當我們需要相應使用者時,向該程序傳送一個訊號,這樣使用者就會繼續前進,處理使用者邏輯,這樣就節省了程序載入動態庫的過程。
這裡需要一個訊號處理函式:
當用戶邏輯執行完成後,就退出程序,同時再啟動該程序,這是程序會在載入完動態庫後,停留在那裡
void sigCont( int unused)
{
return;
}
int main(int argc, char** argv)
{
signal(SIGCONT,sigCont);
pause();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
提前載入,延後退出
當程序啟動需要較長時間時,很多程式設計師僅僅想到了將其提前載入(在開機時啟動),卻沒有想到起退出條件,而導致程序中又多了一個daemon程序。 因此提前載入,延後退出需要更加精確的控制程序的生命週期。
調整CPU頻率
- 嵌入式裝置中,CPU一般有幾個工作頻率
- CPU頻率越高,執行速度越快,耗電量越高
- 可以再啟動前調高CPU頻率,在完成後再調低CPU頻率
- 這種方法以耗電量增加為代價,在某些場合下不適用
優化程式碼
if表示式
從左到右對錶達式求值,當結果確定後也就不在需要計算其他的表示式,也就是常說的“短路”機制,因此對於if語句可以做以下優化:
- 刪除冗餘條件
- 刪除肯定不成立的條件
- 利用短路機制,將計算速度最快的表示式放在左邊
迴圈語句的優化
- 將不變的程式碼移到迴圈之外
- 將分支語句提到迴圈的外面
- 通過迴圈分支的展開,可以降低迴圈次數,從而減少分支語句對迴圈的影響
- 用減1指令替代迴圈加1指令
#將分支語句提到迴圈的外面的例子
for (i=nloop; i>0; i--)
{
if(n == 1)
j += 2;
else
j += 1;
}
#改為:
if (n == 1)
{
for (i=nloop; i>0; i--)
{
j += 2;
}
}
else
{
for (i=nloop; i>0; i--)
{
j += 1;
}
}
#############################################################################################################
# 展開迴圈語句的例子
#方式1
for (n = 0; n < 1024*1024; n++)
{
n++;
}
#方式2
for (n = 0; n < 1024*512; n++)
{
n++;
n++;
}
#方式3
for (n = 0; n < 1024*256; n++)
{
n++;
n++;
n++;
n++;
}
#以上三種方法,方式三所用的時間最短,效率最高
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
暫存器的使用遵循ATPCS標準
ATPCS標準是嵌入式開發應儘量遵循的標準,主要內容如下:
子程式間通過暫存器R0——R3來傳遞引數。
被呼叫的子程式在返回前無需恢復暫存器R0——R3的內容
在子程式中,使用暫存器R4——R11來儲存區域性變數。
如果在子程式中使用了暫存器R4——R11的某些暫存器,子程式進入時必須儲存這些暫存器的值,在返回前必須恢復這些暫存器的值,對於子程式中沒有用到的暫存器則不必進行這些操作。
R12用作子程式間scratch暫存器,記作ip。
在子程式間的連線程式碼段經常使用這些規則
R13用作資料棧指標,記作sp。
在子程式間暫存器R13不能用作其他用途。
R14成為連線暫存器,記作lr。
它用來儲存子程式的返回地址。
R15是程式計數器,記作pc。
子程式返回結果為一個32位整數時,可以通過暫存器R0返回;結果為一個64位整數時,可以通過暫存器R0和R1返回,以此類推。
函式引數優化
函式的引數最好不超過4個
4個以下的形參可以通過暫存器來傳遞,4個以上的引數,則需要通過棧來傳遞。
同事如果引數小於4個,R0-R4中剩餘的暫存器可以儲存函式中的區域性變數。
減少區域性變數的個數
- 儘量限制函式內部迴圈所用的區域性變數的數目,最多不超過12個,以便編譯器能把變數分配到暫存器。
- 如果沒有區域性變數儲存到棧中,系統也將不必設定和恢復棧指標。
當函式內部暫存器變數多於12個時,並不意味著只是將前面的12個臨時變數分配暫存器,之後的臨時變數都是通過棧記憶體來操作。
- 當暫存器分配完記憶體後,遇到新的臨時變數時,先檢視已分配暫存器的區域性變數是否有在後面的程式碼中不會被使用,則新的區域性變數使用其所佔用的暫存器。
- 如果也紛紛暫存器的區域性變數在後面的程式碼中都要使用,則要選擇出一個臨時變數,將其儲存到棧中,之後將其使用的暫存器分配給區域性變數。
- 讀寫檔案時,緩衝區的buffer為2048或4096時,速度最快。
- 利用mmap讀寫檔案
mmap的基本流程是:
- 建立一個與原始檔相同的目標檔案
- 使用mmap,分別將原始檔和目標檔案對映到記憶體中
- 使用memcpy,將檔案讀寫操作轉換成記憶體的拷貝操作
執行緒的優化
- 執行緒的建立是要付出代價的,如果建立的執行緒只做很少的事情,而又頻繁的建立和銷燬執行緒,是得不償失的
- 使用非同步IO,來取代多執行緒+同步IO的方式
- 使用執行緒池取代執行緒的建立和銷燬
記憶體操作的優化
記憶體訪問流程
+ CPU試圖訪問一塊記憶體
+ CPU首先確認該記憶體是否已經被載入到cache中
+ 如果載入到cache中,則直接在cache中定位
+ 如果未載入到cache中,則通過CPU和記憶體直接的地址匯流排,向記憶體傳送地址的高27位地址
+ 當記憶體收到高27位地址後,利用SDRAM的突發交換模式,將連續的32個位元組傳送給CPU的cache,填充一個快取行
+ CPU可以通過地址的高27位來定位cache的快取行,利用地址的低5位定位到快取行中具體的位元組
- 儘量使用佔用記憶體少的演算法
- 利用流水線記憶體存取與計算並行的特點,組合記憶體訪問與計算
調整程序的優先順序
- linux支援兩種程序:實時程序和普通程序
- 實時程序的優先順序是靜態設定的,而且始終大於普通程序的優先順序。對於實時程序來講,其使用絕對優先順序的概念,絕對優先順序的取值範圍是0——99,數字越大,優先順序越高。
- 普通程序的絕對優先順序取值是0.在普通程序之間,其又具備靜態優先順序和動態優先順序之分。靜態優先順序,我們可以通過程式來修改。同事系統在執行過程中,會在靜態優先順序基礎上,不斷動態計算出每個程序的動態優先順序,擁有最高動態優先順序的程序程序被排程器選中。一般來講,靜態優先順序越高,程序所能分配的時間片越長。
- 儘量不要把某些程序放到啟動指令碼中,嘗試daemon程序在第一次使用時啟動。