Linux虚拟内存空间分布

Linux虚拟内存空间分布

1. 程序和进程

问题:程序和进程各是什么?

程序 只是一段可以执行的代码文件,通俗讲在 linux 上就是一个可执行文件。当一个程序运行时就被称为进程,即进程是运行状态的程序。

程序存储了一系列文件信息,这些信息描述了如何在运行时创建一个进程,包含了下面的内容:

  • 二进制格式标识:  描述可执行文件的元信息,内核利用该信息解释文件中的其他信息
  • 机器语言指令:   对程序进行编码
  • 程序入口地址:   标示程序开始执行的起始指令位置
  • 数据:   程序所包含变量的初始值和程序所使用的字面常量值
  • 符号表和重定位表: 描述程序中函数和变量的位置,重定位表记录要修改的符号引用的位置,以及如何修改
  • 共享库和动态链接信息:列出程序运行时需要使用的共享库以及加载共享库的动态链接器的路径名
  • 其他信息: 描述如何创建进程

内核在加载程序的时候会为其分配一个唯一标识符即进程号,linux 内核限制进程号需要小于等于32767每当创建一个进程的时候,内核就会顺序将下一个可用进程分配给其使用, 当进程号大于32767时,内核会重置进程号计数器,然后开始重新分配。 因为内核会运行一些守护进程和系统进程,所有一般会预留一些进程号给这些程序使用,所以一般从300开始重置, 类似于端口号1-1024为系统占用。

2. 内存管理基本概念&进程的内存布局

2.1 进程内存布局

1) 代码区(Text egment):

代码区指令根据程序设计流程依次执行,对于顺序指令,则只会执行一次(每个进程),如果反复,则需要使用跳转指令,如果进行递归,则需要借助栈来实现。

代码段: 代码段(code segment/textsegment )通常是指用来存放程序执行代码的一块内存区域。 这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序 在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

代码区的指令中包括操作码和要操作的对象(或对象地址引用)。如果是立即数(即具体的 数值,如5),将直接包含在代码中;如果是局部数据,将在栈区分配空间,然后引用该数据地址;如果是BSS区和数据区 在代码中同样将引用该数据地址。另外,代码段还规划了局部数据所申请的内存空间信息。

2) 全局初始化数据区/静态数据区(Data Segment)

只初始化一次。

数据段: 数据段(data segment )通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。

data段中的静态数据区存放的是程序中已初始化的全局变量、静态变量和常量。

3) 未初始化数据区(BSS):

在运行时改变其值。

BSS 段: BSS 段(bss segment )通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS 是英文Block Started by Symbol 的简称。

BSS 段属于静态内存分配, 即程序一开始就将其清零了。一般在初始化时BSS段部分将会清零。

4) 堆区(heap):

用于动态内存分配。堆在内存中位于bss区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时有可能由OS回收。

堆(heap): 堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc 等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。

在将应用程序加载到内存空间执行时,操作系统负责代码段、数据段和BSS段的加载, 并将在内存中为这些段分配空间。栈段亦由操作系统分配和管理,而不需要程序员显示地管理;堆段由程序员自己管理,即显式地申请和释放空间。

5) 栈区(stack):

由编译器自动分配释放,存放函数的参数值、局部变量的值等。

存放函数的参数值、 局部变量的值,以及在进行任务切换时存放当前任务的上下文内容。其操作方式类似于数据结构中的栈。 每当一个函数被调用,该函数返回地址和一些关于调用的信息,比如某些寄存器的内容,被存储到栈区。 然后这个被调用的函数再为它的自动变量和临时变量在栈区上分配空间, 这就是C实现函数递归调用的方法。每执行一次递归函数调用,一个新的栈框架就会被使用, 这样这个新实例栈里的变量就不会和该函数的另一个实例栈里面的变量混淆。

栈(stack) :栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧"{}"中定义的变量 (但不包括static 声明的变量,static 意味着在数据段中存放变量)。除此以外,在函数被调用时, 其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放 回栈中。由于栈的先进先出特点, 所以栈特别方便用来保存/ 恢复调用现场。

从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

2.2 为什么要这么多的分区?

之所以分这么多区域,主要是因为:

  1. 一个进程在运行过程中,代码是根据流程依次执行的,只需要访问一次, 当然跳转和递归有可能使代码执行多次,而数据一般都需要访问多次,因此单独开辟空间以方便访问和节约空间.

  2. 临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。 全局数据和静态数据有可能在整个程序执行过程中都需要访问,因此单独存储管理。

  3. 堆区由用户自由分配,以便管理。

上图我们看到的地址都是虚拟地址 linux 32 位操作系统中,每个进程理论上有4G的独立内存空间,进程
的内存空间按照text,data, bss, heap, stack由低到高分配,但是只增值到0xC0000000, 最后1G留给了
内核。0 ~ 3G属于用户空间,3~4G为内核空间以上我们描述了一个运行的可执行程序 即一个进程的内
存分布。

下面我们比较一下经过编译后的可执行程序(不运行)内存分布。 首先我们编译生成一个可执行程序(mem),用命令size 查看这个可执行二进制文件结构情况
在这里插入图片描述

  • text:  代码区
  • data:  全局初始化区/静态数据区
  • bss:  未初始化区
  • dec:  十进制总和
  • hex:  十六进制总和
  • filename:文件名

可以看到,可执行程序在存储时(没有调入到内存)只有代码区(text), 数据区(data), 和未初始化数据区(bss) 3个部分。下面贴上一图,对比可执行代码存储结构和运行时(进程)的内存结构对照(图片是
盗用_,鸣谢时贴出原创~)

3. 内存分配的方式

3.1 静态和动态分配

在C语言中,可以静态或动态的为一个数据分配内存空间。

  • 静态分配:编译器在处理程序的源代码的时候由编译器分配。
  • 动态分配:程序在执行过程中由程序员自己分配,堆内存调用malloc库函数申请分配,栈内存使用alloca申请分配

3.2 静态和动态分配的区别

1)静态对象是有名字的变量,可以直接对其进行操作;动态对象是没有名字的变量,需要通过指针间接的对它进行操作。

2)静态对象的分配与释放由编译器自动处理;动态对象的分配和释放必须由程序员自己显示的管理, 通过 malloc 和free 两个函数来完成。

4. 内存管理函数

4.1 malloc/free 函数

malloc()函数用来在堆中申请内存空间,free()函数释放原先申请的内存空间。malloc()函数是在内存的动态存储区中分配一个长度为size字节的连续空间。其参数是一个无符号整型数,返回一个指向所分配的连续存储域的起始地址的指针。当函数未能成功分配存储空间时(如内存不足)则返回一个NULL指针。

由于内存区域总是有限的,不能无限制地分配下去,而且程序应尽量节省资源 所以当分配的内存区域不用时,则要释放它,以便其他的变量或程序使用。

#include <stdlib.h> 
 
void *malloc(size_t size);
void free(void (ptr);

示例

int *p1,*p2;

p1 = (int *)malloc(10*sizeof(int));
p2 = p1;
....
free(p2);   /* 或者free(p1) */
p1 = NULL;  /* 或者p2 = NULL ,避免p1称为野指针 */

malloc()函数返回值赋给p1,又把p1的值赋给p2,所以此时p1,p2都可作为free函数的参数。使用free()函数时,

需要特别注意下面几点:
1)调用free()释放内存后,不能再去访问被释放的内存空间。内存被释放后, 很有可能该指针仍然指向该内存单元,但这块内存已经不再属于原来的应用程序,此时的指针为悬挂指针(可以赋值为NULL)。

2)不能两次释放相同的指针。因为释放内存空间后,该空间就交给了内存分配子程序,再次释放内存空间会导致错误。也不能用free来释放非malloc()、calloc()和realloc()函数创建的指针空间,在编程时,也不要将指针进 行自加操作,使其指向动态分配的内存空间中间的某个位置,然后直接释放,这样也有可能引起错误。

3)在进行C语言程序开发中,malloc/free是配套使用的,即不需要的内存空间都需要释放回收。

4.2 realloc 函数

realloc()函数用来从堆上分配内存,当需要扩大一块内存空间时,realloc()试图直接从堆上当前内存段后面的字节中获得更多的内存空间,如果能够满足,则返回原指针;如果当前内存段后面的空闲字节不够,那么就使用堆上第一个能够满足这一要求的内存块,将目前的数据复制到新的位置,而将原来的数据块释放掉 如果内存不足,重新申请空间失败,则返回NULL。此函数定义如下:

#include <stdlib.h>

/*
 * ptr:  参数ptr为先前由malloc,calloc和realloc所返回的内存指针
 * size: 新分配的内存大小
 */
void *realloc(void *ptr, size_t size);

当调用realloc()函数重新分配内存时,如果申请失败,将返回NULL,此时原来指针仍然有效,因此在程序编写时需要进行判断,如果调用成功,realloc()函数会重新分配一块新内存,并将原来的数据拷贝到新位置 返回新内存的指针,而释放掉原来指针(realloc()函数的参数指针)指向的空间, 原来的指针变为不可用(即不需要再释放,也不能再释放)因此,一般不使用以下语句:

ptr=realloc(ptr,new_amount)

4.3 calloc 函数

calloc是malloc函数的简单包装,它的主要优点是把动态分配的内存进行初始化,全部清零。其操作及语法类似malloc()函数。

#include <stdlib.h>

/*
 * 在堆内存上动态分配 nmemb个连续的单元,每个单元大小为size
 * 与malloc的区别:calloc 在动态分配内存后,自动初始化该内存空间为0,而malloc不初始化,里面数据是随机的垃圾值
 */
// 函数原型 
void *calloc(size_t nmemb, size_t size);

// 函数实现
void *
calloc(size_t nmemb, size_t size)
{
    void *p;
    size_t total;
    total = nmemb *size;

    p = malloc(total); /* 申请空间 */
    if(p != NULL) 
        memset(p,'\0',total);

    return p;
}

4.4 alloca 函数

alloca()函数用来在栈中动态分配size个字节的内存空间,因此函数返回时会自动释放掉空间。alloca函数原型及库头文件如下:

#include <alloca.h>

void *alloca(size_t size);

alloca 与 malloc 的区别主要在于
alloca是向栈申请内存,无需释放,malloc申请的内存位于堆中, 最终需要函数free来释放。
malloc函数并没有初始化申请的内存空间,因此调用malloc()函数之后,还需调用函数memset初始化这部分内存空间;alloca则将初始化这部分内存空间为0。

5. 堆和栈的区别

前面已经介绍过,栈是由编译器在需要时分配的,不需要时自动清除的变量存储区。里面的变量通常是局部变量、函数参数等。堆是由malloc()函数(C++语言为new运算符)分配的内存块,内存释放由程序员手动控制,在C语言为free函数完成(C++中为delete)

栈和堆的主要区别有以下几点:

1)管理方式不同
栈编译器自动管理,无需程序员手工控制;而堆空间的申请释放工作由程序员控制,容易产生内存泄漏。

2)空间大小不同
栈是向低地址扩展的数据结构,是一块连续的内存区域。 这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,当申请的空间超过栈的剩余空间时,将提示溢出。因此,用户能从栈获得的空间较小。

堆是向高地址扩展的数据结构,是不连续的内存区域。因为系统是用链表来存储空闲内存地址的,且链表的遍历方向是由低地址向高地址。由此可见,堆获得的空间较灵活,也较大。栈中元素都是一一对应的, 不会存在一个内存块从栈中间弹出的情况。

3)是否产生碎片
对于堆来讲,频繁的malloc/free(new/delete)势必会造成内存空间的不连续,从而造成大量的碎片, 使程序效率降低(虽然程序在退出后操作系统会对内存进行回收管理)。对于栈来讲,则不会存在这个问题。

4)增长方向不同
堆的增长方向是向上的,即向着内存地址增加的方向;栈的增长方向是向下的,即向着内存地址减小的方向。

5)分配方式不同
堆都是程序中由malloc()函数动态申请分配并由free()函数释放的;栈的分配和释放是由编译器完成的,栈的动态分配由alloca()函数完成,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行申请和释放的,无需手工实现

6)分配效率不同
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行。堆则是C函数库提供的,它的机制很复杂,例如为了分配一块内存, 库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大的空间,如果没有足够大的空间(可能是由于内存碎片太多),就有需要操作系统来重新整理内存空间,这样就有机会分到足够大小的内存, 然后返回。

显然,堆的效率比栈要低得多。

6. 数据存储区域 Demo

mem_add.c

#include <stdio.h>
#include <stdlib.h>

extern int etext, edata, end;   /* 用户进程相关的虚拟地址为int 类型*/

int bss_var;                  /* 未初始化全局数据存储在BSS区 */
int data_var = 42;            /* 初始化的全局数据域存储在数据区 */

void afunc();

/* 打印地址 */
  #define SHW_ADDR(ID, I) \
    printf("the %s\t is at addr:%p\n",ID, &I);

int main(int argc, char *argv[])
{
    char *p, *b, *nb;                           /* p,b,nb 都在栈上 */
    printf("length of p is %d\n", sizeof(p));   /* 指针的长度为4个字节 */
    printf("the p add is  [%p]\n", &p);
    printf("the b add is  [%p]\n", &b);
    printf("the nb add is [%p]\n", &nb);
    printf("\nAddr etext:%p\t Addr edata:%p\t Addr end:%p\t\n", &etext, &edata, &end);
    
    /* text 段地址 */
    printf("\ntext Location:\n");
    SHW_ADDR("main", main);         /* 查看代码段main函数位置 */
    SHW_ADDR("afunc", afunc);       /* 查看代码段afunc函数位置 */
    
    /* BSS 段地址 */
    printf("\nbss Location:\n");
    SHW_ADDR("bass_var", bss_var);  /* 查看BSS段变量的位置 */
    
    /* data 段地址 */
    printf("\ndata Location:\n");
    SHW_ADDR("data_var", data_var); /* 查看data段变量的位置 */
    
    /* 栈上地址 */  
    printf("\nStack Location:\n");
    afunc();
    p = (char *)alloca(32);         /* 从栈中动态分配空间用alloca函数 */
    if(p != NULL) {
        SHW_ADDR("start", p);       /* 栈上第一个变量地址 */
        SHW_ADDR("end", p+31);
    }

    /* 堆地址 */
    b = (char *)malloc(32*sizeof(char));    /* malloc动态从堆中分配空间 */
    nb = (char *)malloc(16*sizeof(char));   /* malloc动态从堆中分配空间 */
    printf("\nHeap Location:\n");
    printf("the Heap start: %p\n", b);      /* 堆的起始位置 */
    printf("the Heap end: %p\n", (nb+16*sizeof(char))); /* 堆的结束位置 */
    printf("\nb and nb in Stack\n");
    
    SHW_ADDR("b", b);   /* 显示栈中数据b的位置 */
    SHW_ADDR("nb", nb); /* 显示栈中数据nb的位置 */
    
    free(b);
    b = NULL;   /* b 赋为NULL,防止成为野指针 */
    free(nb);
    nb = NULL;  /* nb 赋为NULL,防止成为野指针 */

    return 0;
}

void afunc()
{
    static int level = 0;    /* 静态数据存储在数据段中 */
    int stack_var;           /* 局部变量存储在栈区 */
    
    if(++level == 5) return;
    
    /* 栈内存区 */
    printf("stack_var%d is at: %p\n", level, &stack_var);
    SHW_ADDR("stack_var in stack section", stack_var);
    /* 静态变量数据区 */
    SHW_ADDR("level in data section", level);
    
    afunc();
}

Makefile

mem : mem_add.c
	gcc -W -Wall -o $@ $^
clean:
	rm -fr mem 

运行:

[root@zhao Memory]# ./mem
length of p is 4
the p add is  [0xbf802b2c]
the b add is  [0xbf802b28]
the nb add is [0xbf802b24]

Addr etext:0x80487e8     Addr edata:0x8049b34     Addr end:0x8049b44    

text Location:  // text 段地址
the main     is at addr:0x8048454
the afunc     is at addr:0x80486a3

bss Location:   // bss 段地址
the bass_var     is at addr:0x8049b40

data Location:  // data段地址
the data_var     is at addr:0x8049b30

Stack Location:  // 栈地址
stack_var1 is at: 0xbf802afc
the stack_var in stack section     is at addr:0xbf802afc
the level in data section     is at addr:0x8049b3c  // 静态变量
stack_var2 is at: 0xbf802acc
the stack_var in stack section     is at addr:0xbf802acc
the level in data section     is at addr:0x8049b3c  // 
stack_var3 is at: 0xbf802a9c
the stack_var in stack section     is at addr:0xbf802a9c
the level in data section     is at addr:0x8049b3c
stack_var4 is at: 0xbf802a6c
the stack_var in stack section     is at addr:0xbf802a6c
the level in data section     is at addr:0x8049b3c
the start     is at addr:0xbf802b2c
the end     is at addr:0xbf802ba8

Heap Location:  // 堆地址
the Heap start: 0x98eb008
the Heap end: 0x98eb040

b and nb in Stack
the b     is at addr:0xbf802b28
the nb     is at addr:0xbf802b24

各地址对应内存区间:

补充:
平常总说cpu的位数,其实说的是cpu一次能运算的最长整数的宽度,既ALU(算术逻辑单元)的宽度。
cpu的位数也是数据总线的条数
数据总线:数据线的总和,数据线就是cpu与内存进行数据传递的通道,一条数据线,一次可以传送1位二进制数,8条数据线一次就可以传8位(1个字节)
地址总线:CPU是通过地址总线来指定存储单元的,地址总线决定了cpu能访问的最大内存大小,比如,10位的地址线能访问的内存为1024位(1B)二进制数据
在这里插入图片描述
操作系统为了屏蔽I/O底层的差异,创建了VFS(虚拟文件系统)为了屏蔽I/O层与内存之间的差异,产生了虚拟内存。为了屏蔽cpu与内存之间的差异,创建了进程。每个程序运行起来都会拥有一个自己的虚拟地址空间,32位cpu的操作系统,其地址线也为32位,所以虚拟地址空间为2^32 -1= 4G

一个进程在运行时不可能会用如此大的虚拟地址空间,它们只会用到其中的一部分,而且并不一定连成一片,可能会被分割成几块,每一块连续的虚拟内存块被称为虚拟内存段。

Linux虚拟内存空间布局如下
在这里插入图片描述
.reserve(预留)段
一共占用128M,属于预留空间,进程是禁止访问的

.text(代码段)
可执行文件加载到内存中的只有数据和指令之分,而指令被存放在.text段中,一般是共享的,编译时确定,只读,不允许修改

.data
存放在编译阶段(而非运行时)就能确定的数据,可读可写。也就是通常所说的静态存储区,赋了初值的全局变量和赋初值的静态变量存放在这个区域,常量也存放在这个区域

.bss段
通常用来存放程序中未初始化以及初始化为0的全局/静态变量的一块内存区域,在程序载入时由内核清0

.heap(堆)
用于存放进程运行时动态分配的内存,可动态扩张或缩减,这块内存由程序员自己管理,通过malloc/new可以申请内存,free/delete用来释放内存,heap的地址从低向高扩展,是不连续的空间

.stack(栈)
记录函数调用过程相关的维护性信息,栈的地址从高地址向低地址扩展,是连续的内存区域

共享库(libc.so)

静态库和动态库的区别:
(1)、不同操作系统下后缀不一样
windows linux
静态库 .lib .a
动态/共享库 .dll .so

(2)、加载方法的时间点不同
*.a 在程序生成链接的时候已经包含(拷贝)进来了
*.so 程序在运行的时候才加载使用

(3)静态库把包含调用函数的库是一次性全部加载进去的,动态库是在运行的时候,把用到的函数的定义加载进去,所以包含静态库的程序所以用静态库编译的文件比较大,如果静态库改变了,程序得重新编译,相反的,动态库编译的可执行文件较小,但.so改变了,不影响程序,动态库的开发很方便

(4)程序对静态库没有依赖性,对动态库有依赖性。
cat命令可以查看进程的虚拟地址空间布局
cat /proc/pid/maps
该输出命令一共有六列,分别为:
虚拟内存开始地址-结束地址、访问权限(r读-w写-x可执行-s共享-p私有) 、偏移量 、主设备号:次设备号、映像文件i节点 、映像文件路径

参考资料

Linux虚拟内存空间分布
https://blog.csdn.net/qq_18144747/article/details/88089870

linux查看虚拟内存和cpu占用率
https://blog.csdn.net/hhh3h/article/details/42150005

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值