嵌入式系統,記憶體不夠了該怎麼辦?

2023-10-18 15:00:11

liwen01 2023.10.01

前言

在嵌入式系統中,記憶體是比較緊缺的資源,特別是在消費類產品中, 為了節省成本,一般都會將硬體資源應用到極致。在開發過程中,就經常會遇到,執行記憶體(RAM)就還差一點,但就是不夠用的情況,比如:

  • 需要在原系統上新增一個小演演算法
  • OTA只能將韌體放到記憶體上時
  • 需要動態分配比較大的空間
  • 需要放置一些比較大的臨時檔案

如果你原來裝置記憶體已經使用到90%甚至更高,要實現上面功能,大概率會影響系統的整體效能,甚至會出現系統異常。那該怎麼辦?

在不考慮硬體增加RAM大小的情況下,軟體上還有沒有其它的方式,可以擠出一點點空間呢?答案是可以的,但是需要綜合評估帶來的問題和風險。

這裡以君正T31ZC為範例,來介紹它的記憶體使用情況,以及如何擠出更加多的記憶體空間

(一)記憶體使用情況分析

T31ZC 是一款基於MIPS架構的主處理器,上面整合了512MbitDDR2記憶體,使用的是linux作業系統。

(1)實體記憶體分佈

512Mbit的實體記憶體,也就是64MByte,在實際使用的時候,它被劃分為了兩塊,rmem 和 mem

  1. rmem 用於多媒體,比如音視訊編碼、裁剪、OSD等功能
  2. mem 是用於linux系統記憶體

它通過tag部分的cmdline來設定

[root@Zeratul:~]# cat /proc/cmdline 
console=ttyS0,115200n8 mem=40M@0x0 rmem=24M@0x2800000 root=/dev/ram0 rw rdinit=/linuxrc mtdparts=jz_sfc:256K(boot),352K(tag),5M(kernel),6M(rootfs),2560K(recovery),1440K(system),512K(config),16M@0(all) lpj=6955008 quiet senv;[HW];init_vw=1920;init_vh=1080;nrvbs=2;mode=0;eenv; lzo_size=5907415 rd_start=0x80600000 rd_size=0xd35c00
[root@Zeratul:~]#

這裡我們看到40M分配給了linux系統,24M分配給了多媒體。

通過 dmesg 命令可以看到更多記憶體的使用情況

rd_start=0x80600000 rd_size=0xd35c00

這個是用來存放根檔案系統的起始位置,以及這個空間的大小

Memory: 20680k/40960k

40960K 是linux系統可使用的總大小,也就是上面設定的40MB,20680k 是系統實際可以使用的記憶體

剩下的 40960K - 20680k = 20280K 記憶體到哪裡去了?

3868k kernel code, 20280k reserved, 1052k data, 196k init, 0k highmem

一部分是給核心使用,包括核心程式碼段、資料段、以及 init 段

剩下的20280k - 3868K -1052K - 196K = 15164K

這剩下的1516K 是預留給ramfs 使用的

(2)mem使用情況

linux 系統啟動後,不執行其它的應用程式,我們檢視的記憶體使用情況如下:

cat /proc/meminfo

可用的實際實體記憶體大小還剩餘12872 kB

分配給linux系統使用的記憶體有40M,但是應用程式都還沒開始執行,記憶體就只剩下12872K,記憶體都到哪裡去了!

總結歸納如下:

  1. 總實體記憶體64M,24M分配給了多媒體,40M分配給了linux系統
總大小 多媒體記憶體 linux系統
64M 24M 40M
  1. linux系統記憶體中,可使用的記憶體為20680k,預留的記憶體為20280k
linux系統 available reserved
40960k 20680k 20280k
  1. linux系統中可使用的記憶體,核心模組載入,緩衝快取等系統服務佔用7768K,剩餘的為應用可使用的記憶體
available MemFree modules/others
20680k 12912k 7768K
  1. linux系統中預留的記憶體,主要是預留給kernel和根檔案系統,其中kernel中主要有程式碼段,資料段,init段。剩下的主要是根檔案系統佔用的空間。
reserved kernel code kernel data kernel init ramfs others
20280k 3868K 1052K 196K 13527K 1637K

(二)優化方向

從上面的分析來看,可以優化的方向有:

  • 減少模組載入,系統服務
  • kernel 優化
  • rootfs 優化

(1)減少模組載入,系統服務

這個可優化的空間有限,這裡不做詳細討論,可以通過命令檢視實際模組載入情況:cat /proc/modules

(2)kernel 優化

將核心的偵錯資訊去除掉,可以在menuconfig 中取消 Load all symbols for debugging/ksymoops 選項,這裡可以省出少量的空間

(3)rootfs 優化

從上面的分析,我們看到為ramfs預留的空間為13527K,這是個非常大的空間。從啟動cmdline中我們看到Flash的分割區資訊如下:

256K(boot),352K(tag),5M(kernel),6M(rootfs),2560K(recovery),1440K(system),512K(config),16M@0(all)

1.  flash為rootfs預留的空間為6M,而記憶體為rootfs預留的是13527K,為何相差如此之大?

主要的原因是我們rootfs在燒錄到Flash的時候是壓縮過的,在載入rootfs的時候,首先是將rootfsFlash中讀取出來,再解壓到記憶體指定的地址去,然後再將解壓後的rootfs掛載成ramfs檔案系統載入起來。

rootfs的打包命令如下:

cd ./_rootfs_camera
find . | cpio -H newc -o > ../rootfs_camera.cpio
cd ..
lzop -9 -f rootfs_camera.cpio -o rootfs_camera.cpio.lzo
./mark_rootfs_pc rootfs_camera.cpio.lzo
  • _rootfs_camera 目錄下的所有檔案歸檔到上一目錄的rootfs_camera.cpio文件中
  • 使用lzop 命令使用最高等級(9)的壓縮方式把rootfs_camera.cpio 壓縮到rootfs_camera.cpio.lzo 檔案,-f 表示強制執行,如果rootfs_camera.cpio.lzo已經存在則覆蓋它
  • mark_rootfs_pc 是用來更新rootfs_camera.cpio.lzo的實際檔案大小,實際是將壓縮後的檔案大小寫入到rootfs_camera.cpio.lzo的最開始位置(4位元組)。

2. 為什麼要使用ramfs檔案系統?

ramfs 適合用於臨時儲存、臨時檔案、核心模組載入等臨時性應用,其中資料不需要長期儲存的場景,它是linux核心中的虛擬檔案系統,不需要指定特定的掛載選項。

優點有:

  • 快速讀寫操作
  • 零延遲
  • 輕量級
  • 易於建立和銷燬

缺點有:

  • 不具備永續性
  • 記憶體限制
  • 不適合大型檔案
  • 需要足夠的記憶體

君正T31ZC是屬於低功耗SOC,實時性,快速啟動要求比較高。一般應用場景是有另外的MCU來控制它上下電,有事件觸發的時候,SOC上電處理事件,事件處理完成後,SOC下電,只留MCU工作,以達到省功耗的目的,所以使用ramfs 也是合理的選擇。

官方手冊上的建議是這樣的:

rootfs中只存放對快起有要求的必要程式和庫檔案,如果對快起沒有要求的程式或庫檔案建議放到 system 分割區,以達到快速啟動和節省記憶體的目的。

(三)另類解決方案

(1)問題分析

根據上面的分析,如果系統使用的是ramfs,優化空間最大的就是根檔案系統,減小根檔案系統的大小可以直接節省記憶體。

上面官方給的建議中需要面對另外一個問題,就是需要在應用程式啟動之後,再去system分割區使用dl_open載入所需要的動態庫。如果你程式主要使用的是靜態庫,那這種方式就達不到想要的效果。

檢視rootfs 檔案系統裡面的內容,佔用空間大的,一個是stone目錄,另外一個是lib目錄。

  • ./stone 目錄下放置的是需要執行的main程式
  • ./lib 目錄下放置的主要是一些動態庫,ko驅動檔案,以及一個wifi使用的 bin 檔案
-rwxrwxr-x  1 biao biao 6.2M Oct 15 18:14 main
-rwxrwxrwx 1 biao biao 1003K Aug 21 10:04 cywdhd.ko
-rwxrwxrwx 1 biao biao 404K  Aug 21 10:04 fw_bcm43438a1.bin

(2)解決方案

main 執行程式在啟動的時候就執行

./etc/init.d/rcS:/stone/main &

wifi 驅動也是在啟動的時候被載入

insmod /lib/modules/cywdhd.ko firmware_path=/lib/firmware/fw_bcm43438a1.bin nvram_path=/lib/firmware/nvram.txt iface_name=wlan0

是否可以這樣:

在啟動的時候,main 執行檔案,cywdhd.ko驅動,驅動韌體fw_bcm43438a1.bin被載入完之後,就把它們刪除,以達到釋放記憶體的目的?

因為這些檔案都是放置在記憶體上,刪除它們並不會影響Flash中的檔案,下次上電可以重新從Flash中讀取出來,刪除它們也確實是可以釋放一部分記憶體。

以我上面的例子,刪除 main 檔案就可以釋放6.2M的空間,確實也是可以達到釋放記憶體的目的。

但是,這樣操作是否有風險?

(四)刪除ELF檔案是否有影響

我們知道,程式執行是被段頁式載入到記憶體的,那要怎麼知道在刪除main程式執行檔案的時候,main程式裡面的內容已經被全部載入到記憶體中去了呢?

同樣,刪除ko檔案和bin韌體資料檔案的時候同樣會遇到相同的疑問。

(1)刪除 cywdhd.ko 驅動

通過 cat /proc/modules 命令可以檢視模組的載入情況

[root@Zeratul:bin]# cat /proc/modules | grep cywdhd
cywdhd 671712 0 - Live 0xc0215000
jzmmc 17113 1 cywdhd, Live 0xc010f000
mmc_core 90139 3 mmc_block,cywdhd,jzmmc, Live 0xc00e5000

我們看到 cywdhd.ko被載入到記憶體的0xc0215000 這個地址,Live 表示已經載入到核心並且在在執行。671712 表示它的大小。

為什麼我們前面看cywdhd.ko 大小是1003K ,載入到核心中去卻只剩下67171 ?

我們使用 file 命令檢視cywdhd.ko檔案資訊,發現它是ELF檔案格式,並且是not stripped,也就是它裡面還包含一些偵錯資訊和偵錯符號表等內容

biao@ubuntu: file cywdhd.ko 
cywdhd.ko: ELF 32-bit LSB relocatable, MIPS, MIPS32 version 1 (SYSV), BuildID[sha1]=a00e1928dd77ac04bb813b22cd485156f3392741, not stripped

strip 處理之後,檔案大小變成了657788

biao@ubuntu: mips-linux-uclibc-gnu-strip cywdhd.ko 
biao@ubuntu:ll
-rwxrwxrwx 1 biao biao 657788 Oct 15 19:42 cywdhd.ko

綜合上面資訊,我們可以大致判斷刪除cywdhd.ko檔案是沒有風險的。

還有一種確認方式是:通過檢視 /proc/kallsyms 檔案資訊

/proc/kallsyms 包含了核心符號表資訊,其中包括已載入模組的地址範圍。如果模組的地址範圍包括整個模組,那麼它通常已經完全載入到記憶體中。

(2)刪除fw_bcm43438a1.bin韌體

fw_bcm43438a1.bin 是作為cywdhd.ko 驅動的一個韌體被載入到核心上的,至於它是什麼時候使用,或者說是否一次全部載入到記憶體上了,應該取決於cywdhd.ko驅動

因此我個人認為刪除fw_bcm43438a1.bin是會有比較大的風險的

(3)刪除main程式

Linux系統中, 執行檔案,動態庫,驅動檔案都是屬於ELF檔案格式。ELF檔案裡面分很多段,常見的有程式碼段,資料段,BSS 段。

可以使用命令 readelf -S your_program 來具體檢視

我們看到程式碼段的大小是0x46a6b0,在很多作業系統中,為了節省記憶體空間,都是使用動態載入的方式,也就是段頁式載入方式。

根據區域性性原理,一般程式在執行的時候,都是將當前執行位置附近的資料載入到記憶體上執行。

但是對於ramfs檔案系統,因為它已經是被全部載入到記憶體上了,那在它上面的main檔案,在執行的時候,它是一次性載入(全域性載入)還是按需載入(段頁式載入)呢?

如果它是在執行的時候一次性載入到記憶體上,那麼,在它執行之後,把main檔案刪除是沒有風險的,否則會引入系統風險。

對於執行檔案在ramfs檔案系統上的載入,暫時沒有找到更詳細的介紹資料,熟悉這一領域的同學可以給點建議。

(五)手動釋放記憶體

可以使用命令手動釋放記憶體:

echo 1 > /proc/sys/vm/drop_caches

關於drop_caches 的介紹,可以通過 man proc 的介紹來了解,這裡不做過多說明

(六)結尾

綜合上面介紹的內容,在嵌入式Linux系統記憶體不足的情況下,可以使用下面幾種方式進行優化

  • kernel的偵錯資訊符號表去除,減少核心映象檔案大小
  • 將檔案系統上的ELF檔案(包括執行檔案、動態庫和ko驅動檔案)strip處理,去除多餘的資訊
  • 將動態庫放置到其它分割區上,程式執行後再通過dl_open來載入
  • 執行檔案或是驅動檔案,在使用過後把它們刪除(可能存在風險)
  • 手動釋放記憶體

具體使用哪種方式,可以根據實際使用情況進行評估和選擇。

以上如有分析錯誤的地方,歡迎批評指正。

 

---------------------------End---------------------------
如需獲取更多內容
請關注 liwen01 公眾號