【Android Linux内存及性能优化】(二) 进程内存的优化 - 栈段 - 环境变量 - ELF


本文接着《【Android Linux内存及性能优化】(一) 进程内存的优化 - 堆段


一、内存篇

1.1 系统当前可用内存

1.2 进程的内存使用

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. 建立进程新的个性,即建立进程描述符的 personality 字段
  10. 清进程描述符的 PF_FORKNOEXEC 标志
  11. 为进程的用户态堆栈分配一个新的线性区描述符。
  12. 使用 do_mmap 函数创建一个线性区来对可执行文件正文段(即代码)进行映射
  13. 使用do_mmap 函数创建一个线性区来对可执行文件数据段进行映射
  14. 为可执行文件的其他专用段分配另外的线性区
  15. 调用一个装入程序解释器的函数
  16. 把可执行格式的 linux_binfmt 对象的地址存放在进程描述符的binfmt字段中
  17. 确定进程新的权能
  18. 创建特定的程序解释器表并把他们存放在用户堆栈
  19. 设置进程的内存描述符start_code, end_code, end_data, start_brk, brk,以及 start_stack 字段。
  20. 调用 do_brk 函数创建一个新的匿名线性区来映射程序的bss 段。这个线性区的大小是在可执行程序被链接时计算出来的。
  21. 转到程序的入口
  22. 开始执行代码段的指令。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

"小夜猫&小懒虫&小财迷"的男人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值