【Android Linux內存及性能優化】二 進程內存的優化 - 棧段 - 環境變量 - ELF
本文接着《【Android Linux內存及性能優化】(一) 進程內存的優化 - 堆段》
一、內存篇
1.3 進程內存優化
1.3.1 執行文件
1.3.1.2 棧段
棧中的內存是由程序自動來維護的,棧段內存緊密排列,不會出現內存碎片的問題,不需要手動申請和釋放。
在進程進入函數時,會自動將參數程局部變量加入棧中,而在函數返回時,會自動將這塊內存返回給系統。
1.3.1.2.1 棧上申請內存
大家都知道動態分配的內存,一定要釋放,否則會有內存泄漏。
可能鮮有人知,在棧中動態分配的內存可以不用釋放。
alloca 就是這樣一個函數,最後一個a 代表 auto,即自動釋放的意思。
例如:
#include <stdio.h>
int main(){
int n = 0;
int *p = alloca(1024);
printf("&n=%p, p=%p", &n, p);
return 0;
}
結果爲: &n=0xbefffe6c p=0xbefffa60
在棧上分配內存的好處,就是不會有內存泄漏的問題。
1.3.1.2.2 棧的擴展
在前面講過,進程通過系統調用 brk 和 sbrk 調整堆頂的地址來擴展或釋放堆段內存。
但 棧不一樣。
棧的分配是這樣的:
棧需要多少空間,就給多少空間,不需要通過系統調用去擴展棧頂指針。當進程採取壓線操作後,棧頂指針減小,
如果進程訪問相應的內存時,會觸發頁故障,通過頁故障來擴展棧段內存。
我們來看下LInux 內核中,處理頁故障的函數。
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
// 查找 address < vm_end 的線性區,如果沒有返回空
vma = find_vma(mm, address);
if(!vma)
goto bad_area;
// 如果這個地址在一個線性區期間,那麼說明這是個合法的地址,採用調用頁等處理方式
if( !(vma->vm_flags & VM_GROWSDOWN) )
goto bad_area;
// 如果該地址不在這個線性區,檢測該線性區是否可以各下擴展
if(expand_stack(vma, address))
goto bad_area;
// 如果可以向下擴展,那就可以擴展這塊棧內存了
}
expand_stack()
函數用來擴展棧內存。
從代碼可以看出,進程不需要系統調用來擴展棧段所在的內存空間,而是隨着壓棧的操作,棧頂指針向下擴展。
當訪問棧變量時,觸發頁故障,在Linux 內核中的頁故障處理函數中,擴展棧段所在的內存空間。
由於不涉及系統調用,所以棧段內存的擴展比堆段內存擴展更加方便、快捷。
1.3.1.2.3 棧的釋放
利用函數壓棧時,棧頂指針減少,訪問棧變量時觸發頁故障來擴展棧內存空間是有效的,可這個機制並不能解決棧段內存回收的問題。
在進程從函數中返回時,釋放臨時變量所以返回到上一級函數,棧頂寄存器 esp 將自動增大,
這時候,進程應該釋放這塊內存,同時調整Linux內核中該進程的棧段所對應的線性區,這時候,進程應該釋放這塊內存,同時調整在 Linux 內核中該進程的棧段所對應的線性區。
需要一個事件來觸發上面的動作。
但經過遞歸實測,在LInux 系統中進程棧段所使用的物理內存只能增長,不會減少,其大小等於其在運行過程中所使用的最大的棧空間。
猜測原因如下:
(1)確實沒有合適的事件來觸發棧段內存的回收。
(2)LInux 的棧段雖然在函數退出時不會被釋放,但下次進入函數時可以複用,因此可能認爲 Linux 的棧段內存釋放問題對整個進程的內存使用影響不大。
而對程序員來講,應慎用遞歸函數 及在函數內分配大內存,因爲那是有代價的:
棧段所對應的物理內存,一旦使用就不會再釋放。
1.3.1.3 環境變量及參數
首先環境變量與進程有關,在系統fork 出一個子進程後,會同時將自已的環境變量複製到子進程中,將其存儲在棧的頂端,
從而實現了環境變量在父子進程之間的繼承關係。
在LInux 內核中,環境變量的存儲位置如下:
從上圖中來看,環境變量是與棧估合用一個線性區的。
有時候你會發現,剛進入 main函數,根本沒有用到什麼棧空間,但其棧頂已經用了2 個物理頁面,那兩個物理頁面就應該是用來保存環境變量字符串和命令行參數的。
1.3.1.3.1 環境變量的存儲
#include <stdio.h>
extern char ** environ;
int main(){
char **env = environ;
printf("environ: %p \n", environ);
while(*env){
printf("env: %p %p %s\n", env , *env, *env);
env++;
}
}
environ 是一個字符串指什數組,將數組的內容和地址,連同字符串信息一同打印出來。
運行結果如下:
environ: 0xbefffeac
env: 0xbefffeac 0xbeffff6b USER=root
env: 0xbefffeb0 0xbeffff75 OLDPWD=/root
env: 0xbefffeb4 0xbeffff82 HOME=/root
env: 0xbefffeb8 0xbeffff8d PS1=#
env: 0xbefffec0 0xbeffff84 LOGNAME=root
env: 0xbefffec4 0xbeffffd3 SHELL=/bin/bash
env: 0xbefffec8 0xbeffffe2 PWD=/mnt/msc_int0
從結果可以看出,環境變量的字符串全部順序從地址:0xbeffff6b 開始,保存在棧段的頂端。
environ 保存着環境變量字符串指針數組的地址 0xbeffffea ,接下來的地址,均保存着對應的環境變量字符串地址。
可以看出,環境變量的字符串排列非常緊密,沒有一點空隙,但如果新增,刪除,修改環境變量,又應該怎麼操作呢?
1.3.1.3.2 新增環境變量
調用 setenv("xxx", "xxx", 1);
來新增環境變量
運行結果爲,程序無法在棧的頂部保存新的環境變量,便在堆段申請了一段內存用來保存環境變量。
因此,如果進程新增一個環境變量,
系統將消耗的內存 = 4 x 系統環境變量總數 + 新增環境變量的長度 + 1
1.3.1.3.3 修改環境變量
不論環境變量字符串增大還是減少,都會在堆段分配出一塊內存來保存環境變量字符串。
不會重新分配環境變量字符串指針數組的內存,會更新對應字符串指針的指向,從棧段指向堆段量新分配的內存。
如果,當前環境變量已經保存在堆段,如果要修改該環境變量的值,
還是會從堆段,重新再申請一塊新內存,用於保存新的環境變量值。
glibc 不會去判斷對應的環境變量是否保存在堆中,且不會試圖釋放它,因此這一點存在內存泄漏的風險。
1.3.1.3.4 釋放環境變量
可以通過使用unsetenv 來釋放環境變量。
但刪除環境變量時,只是簡單地更新環境變量字符串指針數組,並沒有釋放相應的字符串資源,
所心unsetenv 並不會釋放內存。
1.3.1.3.5 環境變量的內存優化
應儘可能在程序啓動前設置好環境變量,這樣環境變量緊密排列在棧空間。
在程序內增加,修改環境變量將會導致在堆中申請內存。
1.3.1.4 ELF 文件
在LInux 系統中,可以使用 file 命令來得知文件的格式,以 /bin/ls 爲例
ciellee@sh:~$ file ./ls
./ls: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=d0bc0fb9b3f60f72bbad3c5a1d24c9e2a1fde775, stripped
1.3.1.4.1 常用工具(屬於 binutils 工具包)
命令 | 說明 |
---|---|
strings | 輸出ELF 文件中所有字符串 |
strip | 刪除ELF文件中一些無用的信息 |
nm | 列舉目標文件符號 |
size | 顯示目標文件段(section)大小,以及目標文件大小 |
readelf | 顯示ELF 格式文件的內容 |
objdump | 顯示目標文件信息,可作爲反彙編用 |
ar | 建立static library(insert delete list extract) |
addr2line | 將地址轉換文件、行號 |
1.3.1.4.2 readelf -a 讀取文件內容
ELF頭部文件視圖 | |
---|---|
鏈接視圖 | 執行視圖 |
ELF 頭部 | ELF 頭部 |
程序頭部表(可選) | 程序頭部表 |
節區 1 | 段1 |
… | |
節區 a | 段2 |
… | … |
節區頭部表 | 節區頭部表(可選) |
ciellee@sh:~$ readelf -a ./ls
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x4049a0
Start of program headers: 64 (bytes into file)
Start of section headers: 124728 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 29
Section header string table index: 28
1.3.1.4.3 strip 程序瘦身
以把 .comment 節從ELF 文件中刪除爲例
strip --remove-section=.comment xxxx
or
strip --remove-section=.comment --strip-all xxxxx
注意,strip 雖然可以減小 ELF 文件的大小,但並不會修改代碼段和數據段中的節,所以它不會減少進程運行時的內存使用。
1.3.1.4.4 ELF程序運行流程
- 檢查存文文件前 128字節中的一些魔數以確認可執行格式。如果魔數不匹配,則返回錯誤碼 -ENOEXEC。
- 讀可執行文件的首部,這個首部描述程序的段和所需要的共享庫。
- 從可執行文件獲得程序解釋器的路徑名,用程序解釋器來確定共享庫的位置並把它們映射到內存。
- 獲得程序解釋器的目錄項對象
- 檢查程序解釋器的執行許可權
- 把程序解釋器前128B 複製到緩衝區
- 對程序解釋器的類型執行一些一致性檢查
- 調用 flush_old_exec 函數釋放前一個計算所佔用的幾 乎所有資源
- 建立進程描述符的 PF_FORKNOEXEC 標誌
10.爲進程的用戶態堆棧分配一個新的線性區描述符。