进程内存布局
一个程序本质上都是由 BSS 段、data段、text段三个组成的。
- BSS段:在采用段式内存管理的架构中,BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。
- 数据段:在采用段式内存管理的架构中,数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
- 代码段:在采用段式内存管理的架构中,代码段(text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
进程所分配的内存:
1. 程序代码区(.text) - 存放函数体的二进制代码 。
2. 文字常量区(.rodata) - 常量字符串就是放在这里的,程序结束后由系统释放(rodata—read only data)。
3. 全局区/静态区(static) - 全局变量 和 静态变量的存储是放在一块的。初始化的全局变量和静态变量在一块区域(.rwdata or .data),未初始化的全局变量和未初始化的静态变量在相邻的另一块区域(.bss), 程序结束后由系统释放。
4. 堆区(heap) - 一般由程序员分配释放(new/malloc/calloc delete/free),若程序员不释放,程序结束时可能由 OS 回收。
注意:它与数据结构中的堆是两回事,但分配方式倒类似于链表。
5. 栈区(stack) - 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
虚拟内存管理
像多数现代内核一样,Linux采用了虚拟内核内存管理技术。该技术利用了大多数程序的一个典型特征,即访问局部性,以高效率实用CPU和RAM资源。
空间局部性:程序倾向于访问在最近访问过的内存地址附近的内存。
时间局部性:程序倾向于在不久的将来再次访问最近刚访问过的内存地址。
正是由于访问局部性特征,使得程序即便仅有部分地址空间存在于RAM中,依然可能得到执行。
虚拟内存的规划之一是将没个程序使用的内存切割成小型的,固定大小的‘页(page)’单元。相应的,将RAM划分成一系列与虚拟页尺寸相同的页帧。在任一时刻,每个程序仅有部分页需要驻留在物理内存页帧中,这些页构成了所谓驻留集(resident set )。程序未使用的页拷贝在交换区(swap area)中内。交换区是磁盘的保留区域,作为计算机RAM的补充在需要的时候才会载入物理内存。
下面代码功能为查看本机页大小
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]){
int psize = getpagesize();
printf("%d\n", psize);
return 0;
}
可见我的电脑页大小为4K。
内核为每个进程维护一行页表。页表中的每个条目要么指出一个虚拟页面在RAM中的所在位置,要么表明其当前驻留在磁盘上。
虚拟内存的实现需要硬件中分页内存管理单元(PMMU)的支持,PMMU把要访问的每个虚拟内存地址转换成相应的物理内存地址,当特定虚拟内存地址所对应的页没有驻留于RAM中时,将页面错误通知内核。
栈和栈帧
函数的调用和返回使栈的增长和收缩成线性。栈驻留在内存高端开始向下增长。专用寄存器——栈指针,用于跟踪当前栈顶,每次调用函数时,会在栈上新分配一帧,每当函数返回时,从栈上将此帧移去。
有时,会用用户栈来表示此处所讨论的栈,以便与内核栈区分开来。内核栈是每个进程保留在内存中的内存区域,在执行系统调用的过程中供内部函数使用。
每个(用户)栈帧包括如下信息
- 函数实参和局部变量;由于这些变量都是在调用函数时自动创建的,因此在C语言中称其为自动变量。函数返回时将自动销毁这些变量(因为栈帧会被释放),这也是自动变量与静态变量主要的语义区别:后者与函数执行无关,且长期存在。
- 调用的链接信息:每个函数都会用到一些CPU寄存器,每当函数调用另一函数时,会在被调用函数的栈帧中>保存这些寄存器的副本,以便函数返回时能为函数调用者将寄存器恢复原状。
命令行参数与环境列表
每个C语言程序都必须有一个称为main()的函数作为程序启动的起点。
int main(int argc, char *argv[]){}
argc/argv参数机制的局限之一在于这些变量仅对main()函数可用。
如果想要从程序内任意位置访问这些信息的部分或者全部内容可以通过以下两个方法
- 通过linux系统转悠的/proc/PID/cmdline文件可以读取任意进程的命令行参数,每个参数都以空字节终止。
- GNU C语言库提供有两个全局变量,可在程序内任意位置使用以获取调用该程序时的程序名。第一个全局变量 program_invocation_name,提供了调用该程序的完整路径。第二个全局变量program _ invocation_ short _ name, 提供了不含目录的程序名称,即路径名的基本名称,定义 _ GNU_SOURCE宏后即可从< errno.h >中获取对这两个全局变量的声明。
如下图所示,argv和environ数组,以及这些参数最初指向的字符串,都驻留在进程栈之上的一个单一、连续的内存区域。
许多程序使用getopt()库函数解析命令行选项。
每个进程都有与其相关的称之为环境列变量的字符串数组,其中每个字符串都以名称=值形式定义。
通过Linux专有的proc/PID/environ文件检查任一进程的环境变量。每一个“NAME = value”对都以空字节终止。
在新进程创建之时,会继承其父进程的环境副本。环境提供了将信息从父进程传递给子进程的方法。由于子进程只有在创建时才获得其父进程的环境副本,所以这一信息是单向的,一次性的。子进程创建后,父子进程均可改变各自的环境变量,且这些变更对对方而言不可再见。
可以通过设置环境变量来改变一些库函数的行为。因此,用户无需修改程序代码或者重新链接相关库,就能控制调用该函数的应用程序行为。
getopt()函数就是一例,可设置POSIXLY_CORRECT环境变量来改变此函数的行为。
getenv()函数能够从进程环境中检索单个值。
想getenv()函数提供环境变量名称,该函数将返回相应的字符串指针。如果不存在制定名称的环境变量,那么getenv()函数将返回null
putenv()函数向调用进程的环境中添加一个新变量。
参数string是一指针,指向name=value形式的字符串。putenv()函数将设定environ变量中某一元素的指向与string参数的指向内容相同,而非string参数所指向字符串的复制副本。因此,string参数不应为自动变量。
setenv()函数可以替代putenv()函数,向环境中添加一个变量。
setenv()函数为形如name=value的字符串分配一块内存缓冲区,并将name和value所指向的字符串复制到此缓冲区,以此来创建一个新的环境变量。
unsetenv()函数从环境中移除由name参数表示的变量。
同setenv()函数一样,参数name不应包含等号字符。
有时需要清除整个环境,然后以所选值进行重建。可以通过将environ变量赋值为NULL来清除环境
environ = NULL;
在某些情况下,使用setenv()函数和clearenv()函数可能会导致程序内存泄漏。前面提到,setenv()函数所分配的一块内存缓冲区,随之会成为进程环境的一部分。而调用clearenv()时则没有释放该缓冲区(clearenv()函数并不知道该缓冲区的存在,也不知道它在哪里)。反复调用这两个程序会不断产生的内存泄漏。
非局部跳转
术语“非局部”是指跳转的目标为当前执行函数之外的某个位置。C语言提供库函数setjmp()函数和longjmp()执行非局部跳转功能。
//tu
setjmp()调用为后续由longjmp()调用执行的跳转确定了跳转目标。该目标正是程序发起setjmp()调用的位置。通过查看setjmp()返回的整数值,可以区分setjmp()调用是厨师调用还是第二次“返回”。初始调用返回值为0,后续伪返回的返回值为longjmp()调用中val参数所指定的任意值。如果指定val参数的值为0,则实际调用会将其替换为1。
setjmp()把当前进程环境的各种信息保存到env参数中,调用longjmp()时必须指定相同的env变量。调
用setjmp()时,env除了储存当前进程的其他信息,还保存了程序计数器和栈指针寄存器的副本。
longjmp()调用的两个关键步骤:
1. 将发起longjmp()调用的函数与之前调用setjmp()的函数之间的函数栈帧从栈上剥离,此过程又叫“解开栈”,这是通过将栈指针寄存器重置为env参数内保存的值来实现的。
2. 重置程序计数寄存器,使程序得以从初始setjmp()调用位置继续执行。
longjmp()调用不能跳转到一个已经返回的函数中!
优化编译器会重组程序的指令执行顺序,并且在CPU寄存器中存储某些变量。这种优化一般依赖于反应了程序词法结构的运行时控制流程。由于setjmp和longjmp的跳转操作需要在运行是得以确立和执行,并未在程序的此法结构中有所反应,故而编译器在进行优化时无法将其考虑在内。此外,某些ABI实现的语义要求longjmp函数西安恢复先前setjmp调用所保存的CPU寄存器副本。这意味着longjmp操作会使经过优化的变量被赋以错值。
比如下例:
#include <stdio.h>
#include <unistd.h>
#include <setjmp.h>
#include <stdlib.h>
static jmp_buf env;
static void dojump(int nvar, int rvar, int vvar){
printf("inside dojump(): nvar=%d, rvar=%d, vvar=%d\n", nvar, rvar, vvar);
longjmp(env, 1);
}
int main(){
int nvar;
register int rvar;
volatile int vvar; //告诉编译器不要对其进行优化。
nvar = 111;
rvar = 222;
vvar = 333;
if(setjmp(env) == 0){
nvar = 777;
rvar = 888;
vvar = 999;
dojump(nvar, rvar, vvar);
}
else{
printf("after longjmp():nvar=%d, rvar=%d, vvar=%d\n", nvar, rvar, vvar);
}
}
当以优化方式编译该程序时,在longjmp调用后,nvar和rvar参数被重置为setjmp初次调用的值,起因是优化器对代码的重组收到longjmp调用的干扰。