嵌入式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執行相應的功能,在執行完畢後,傳回控制給父程序。
APPLET_NOEXEC
系統将調用fork建立子程序,然後執行BusyBox中相應的功能,在執行完畢後,傳回控制給父程序。
APPLET_NOFORK
它相當于builts-in,隻是執行BusyBox的内部函數,不必建立子程序,是以其效率極高。
衆所周知,在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
屬于APPLET_NOEXEC的功能有:
awk,chgrp,chmod,chown,cp,cut,dd,find,hexdump,ln,soort,test,xargs......
是以優化shell腳本的政策一般有:
1. 去掉無用的腳本
2. 盡可能的使用BusyBox内部的指令
3. 盡量不要使用管道pipe
4. 減少管道中的指令數目
5. 盡量不要使用·
優化程序啟動速度
程序的啟動過程如下:
1 搜尋其所依賴的動态庫
2 加載動态庫
3 初始化動态庫
4 初始化程序
5 将程式的控制權移交給main函數
要加快的程序的啟動速度,可以從以下幾方面入手:
1 減少加載的動态庫的數量
a) 使用dlopen,将啟動時不需要的動态庫延後加載
b) 将一些動态庫改為靜态庫
優點:
- 減少了加載動态庫的數量
- 在與其他動态庫合并之後,動态庫内部的函數之間不必再進行動态連結、符号查找,進而提高速度
缺點:
- 該動态庫如果被多個動态庫或程序所依賴的話,那麼該動态庫将被複制多份合并到新的動态庫中,導緻整體的檔案大小增加,占用更多的Flash。
- 失去了動态庫原有的代碼段記憶體共享,是以可能會導緻記憶體使用上的增加
2 優化加載動态庫時的搜尋路徑
a) 設定LD_HWCAP_MASK,禁掉一些不用的硬體特性
b) 将所有的動态庫都放在一個目錄下,并且将目錄放在LD_LIBRARY_PATH的開始
c) 不能放在一個目錄的,在程序中加入-rpath選項,指定搜尋路徑
如果做了之前的工作仍然無法滿足程序啟動速度的要求,那就從程序的排程上下功夫,可以:
- 程序改為線程
-
可以把原來的程序分割為兩個部分:
常駐記憶體部分:其為daemon程序,主要負責加載程序所需要的動态庫,偵聽使用者信号,建立和銷毀使用者邏輯線程
完成使用者邏輯部分: 由daemon部分建立線程,按使用者需求完成使用者邏輯
- 這樣就節省掉了加載動态庫、初始化動态庫和全局變量部分,可以縮短程序的響應時間,來滿足使用者的需求
- 還可以再引申一下,将原來的多個daemon程序的常駐記憶體部分進行合并,根據使用者邏輯需求,建立不同的程序。
-
優點:
建立線程時,不需要重新加載動态庫,故縮短了程序的響應時間
多個業務邏輯共享動态庫時,避免了系統為每個業務邏輯建立動态庫的資料段,進而節省了大量的記憶體。
-
缺點:
由原來的程序改為線程,工作量比較大,代碼修改上存在一定的風險
多個業務邏輯線程之間共享動态庫時,有可能會帶來全局變量的沖突
由于還是存在daemon程序部分,是以其堆棧記憶體不會被釋放,多個業務邏輯線程所存在記憶體洩露會糾纏在一起,進而使問題更加複雜。
-
- preload程序
-
在程序的main函數中插入一行語句:
pause();
- 這樣,當程序啟動時,加載完動态庫後,就會停在這裡,不會運作使用者邏輯。
-
當我們需要相應使用者時,向該程序發送一個信号,這樣使用者就會繼續前進,處理使用者邏輯,這樣就節省了程序加載動态庫的過程。
這裡需要一個信号處理函數:
- 當使用者邏輯執行完成後,就退出程序,同時再啟動該程序,這是程序會在加載完動态庫後,停留在那裡
-
void sigCont( int unused)
{
return;
}
int main(int argc, char** argv)
{
signal(SIGCONT,sigCont);
pause();
}
-
提前加載,延後退出
當程序啟動需要較長時間時,很多程式員僅僅想到了将其提前加載(在開機時啟動),卻沒有想到起退出條件,而導緻程序中又多了一個daemon程序。 是以提前加載,延後退出需要更加精确的控制程序的生命周期。
- 調整CPU頻率
- 嵌入式裝置中,CPU一般有幾個工作頻率
- CPU頻率越高,運作速度越快,耗電量越高
- 可以再啟動前調高CPU頻率,在完成後再調低CPU頻率
- 這種方法以耗電量增加為代價,在某些場合下不适用
優化代碼
-
if表達式
從左到右對表達式求值,當結果确定後也就不在需要計算其他的表達式,也就是常說的“短路”機制,是以對于if語句可以做以下優化:
- 删除備援條件
- 删除肯定不成立的條件
- 利用短路機制,将計算速度最快的表達式放在左邊
- 循環語句的優化
- 将不變的代碼移到循環之外
- 将分支語句提到循環的外面
- 通過循環分支的展開,可以降低循環次數,進而減少分支語句對循環的影響
- 用減1指令替代循環加1指令
#将分支語句提到循環的外面的例子
for (i=nloop; i>; i--)
{
if(n == )
j += ;
else
j += ;
}
#改為:
if (n == )
{
for (i=nloop; i>; i--)
{
j += ;
}
}
else
{
for (i=nloop; i>; i--)
{
j += ;
}
}
#############################################################################################################
# 展開循環語句的例子
#方式1
for (n = ; n < *; n++)
{
n++;
}
#方式2
for (n = ; n < *; n++)
{
n++;
n++;
}
#方式3
for (n = ; n < *; n++)
{
n++;
n++;
n++;
n++;
}
#以上三種方法,方式三所用的時間最短,效率最高
-
寄存器的使用遵循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傳回,以此類推。
- 子程式間通過寄存器R0——R3來傳遞參數。
- 函數參數優化
- 函數的參數最好不超過4個
- 4個以下的形參可以通過寄存器來傳遞,4個以上的參數,則需要通過棧來傳遞。
- 同僚如果參數小于4個,R0-R4中剩餘的寄存器可以儲存函數中的局部變量。
- 減少局部變量的個數
- 盡量限制函數内部循環所用的局部變量的數目,最多不超過12個,以便編譯器能把變量配置設定到寄存器。
- 如果沒有局部變量儲存到棧中,系統也将不必設定和恢複棧指針。
- 當函數内部寄存器變量多于12個時,并不意味着隻是将前面的12個臨時變量配置設定寄存器,之後的臨時變量都是通過棧記憶體來操作。
- 當寄存器配置設定完記憶體後,遇到新的臨時變量時,先檢視已配置設定寄存器的局部變量是否有在後面的代碼中不會被使用,則新的局部變量使用其所占用的寄存器。
- 如果也紛紛寄存器的局部變量在後面的代碼中都要使用,則要選擇出一個臨時變量,将其儲存到棧中,之後将其使用的寄存器配置設定給局部變量。
- 函數的參數最好不超過4個
- 檔案操作的優化
- 讀寫檔案時,緩沖區的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程序在第一次使用時啟動。