【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程序運行流程
  1. 檢查存文文件前 128字節中的一些魔數以確認可執行格式。如果魔數不匹配,則返回錯誤碼 -ENOEXEC。
  2. 讀可執行文件的首部,這個首部描述程序的段和所需要的共享庫。
  3. 從可執行文件獲得程序解釋器的路徑名,用程序解釋器來確定共享庫的位置並把它們映射到內存。
  4. 獲得程序解釋器的目錄項對象
  5. 檢查程序解釋器的執行許可權
  6. 把程序解釋器前128B 複製到緩衝區
  7. 對程序解釋器的類型執行一些一致性檢查
  8. 調用 flush_old_exec 函數釋放前一個計算所佔用的幾 乎所有資源
  9. 建立進程描述符的 PF_FORKNOEXEC 標誌
    10.爲進程的用戶態堆棧分配一個新的線性區描述符。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章