文章目录
- Linux虚拟内存空间分布
-
- 1. 程序和进程
- 2. 内存管理的基本概念&内存布局的过程
-
- 2.1 内存布局的过程
- 2.2 为什么要有这么多分区?
- 3. 内存分配的方式
-
- 3.1 静态和动态分配
- 3.2 静态和动态分布的区别
- 4. 内存管理函数
-
- 4.1 malloc/free 函数
- 4.2 realloc 函数
- 4.3 calloc 函数
- 4.4 alloca 函数
- 5. 堆与堆的区别
- 6. 数据存储区域 Demo
- 参考资料
Linux虚拟内存空间分布
1. 程序和进程
问题:程序和过程是什么?
程序 一般来说,这只是一个可执行的代码文件 linux 这是一个可执行的文件。当一个程序运行时,它被称为程序,即程序处于运行状态。
该程序存储了一系列关于如何在运行过程中创建过程的文件信息:
- : 描述可执行文件的元信息,内核利用信息解释文件中的其他信息
- : 对程序进行编码
- : 开始执行标记程序的起始指令位置
- : 程序中使用的变量的初始值和字面常量值
- : 描述函数和变量的位置,重定位表记录要修改的符号引用的位置,以及如何修改
- :列出程序运行中需要使用的共享库和加载共享库的动态链接器的路径名
- : 描述如何创建过程
在加载程序时,内核分配一个唯一的标识符,即进程号,linux 每当创建一个过程时,内核就会将下一个可用小于或等于32767,内核就会将下一个可用过程分配给其使用, 当进程号大于32767时,内核将重置进程号计数器,然后开始重新分配。 由于内核会运行一些保护过程和系统过程,一般会为这些过程预留一些过程号,所以一般从300开始重置, 类似于系统占用的端口号1-1024。
2. 内存管理的基本概念&内存布局的过程
2.1 内存布局的过程
代码区指令按程序设计流程依次执行。对于顺序指令,只执行一次(每个过程)。如果重复,则需要跳转指令。如果交付,则需要使用堆栈。
代码段: 代码段(code segment/textsegment )它通常是一个用于存储程序执行代码的内存区域。 程序运行前确定了这部分区域的大小,内存区域通常只读, 有些架构也允许编写代码段,即允许修改程序 在代码段中,也可能包含一些常数变量,如字符串常量。
代码区的指令包括操作代码和要操作的对象(或引用对象地址)。如果是立即数(即具体) 数值,如5),将直接包含在代码中;如果是局部数据,则在栈区分配空间,然后引用数据地址;如果是BSS区和数据区 该数据地址也将在代码中引用。此外,代码段还规划了局部数据申请的内存空间信息。
只初始化一次。
数据段: 数据段(data segment )通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
data静态数据区域存储在程序中已初始化的全球变量、静态变量和常量。
在运行过程中改变其值。
BSS 段: BSS 段(bss segment )它通常是一个内存区域,用于存储程序中未初始化的全局变量。BSS 是英文Block Started by Symbol 的简称。
BSS 该段属于静态内存分配, 也就是说,程序从一开始就被清除了。一般在初始化时BSS部分段将被清除。
用于动态内存分配。堆放在内存中bss区域和栈区之间。一般由程序员分配和释放。如果程序员不释放,程序可能会结束OS回收。
堆(heap): 堆是存储过程中动态分配的内存段,其尺寸不固定,可动态扩展或减少。当过程调用时malloc 当函数分配内存时,动态地将新分配的内存添加到堆上(堆被扩展);当使用时free 当函数释放内存时,从堆中删除释放的内存(堆减少)。
操作系统负责代码段、数据段和BSS段的加载, 并将空间分配到内存中。它还由操作系统分配和管理,无需程序员显示管理;由程序员自己管理,即显式申请和释放空间。
存储函数的参数值、局部变量值等由编译器自动分配释放。
存放函数的参数值、 局部变量值,以及在任务切换过程中存储当前任务的上下文。其操作模式类似于数据结构中的栈。 每当调用函数时,函数返回地址和一些关于调用的信息,如一些寄存器的内容,都会存储在堆栈区域。 然后,调用函数将自动变量和临时变量分配到栈区, 这就是C实现函数递归调用的方法。每次执行递归函数调用,都会使用新的栈架, 这样,新实例栈中的变量就不会与函数的另一个实例栈中的变量混淆。
栈(stack) :栈又称堆栈, 是用户存储程序临时创建的局部变量,即我们的函数括弧"{}"变量的定义 (但不包括static 声明的变量,static 变量存储在数据段中)。此外,当函数被调用时, 其参数也将被压入启动调用过程栈中,调用结束后,函数的返回值也将被存储 回栈中。由于栈的先进先出特点, 所以栈特别方便保存/ 恢复调用现场。
从这个意义上说,我们可以把堆栈看作是一个存储和交换临时数据的内存区。
2.2 为什么要有这么多分区?
这么多么多区域的主要原因是:
-
在运行过程中,代码是流程执行,只需访问一次, 当然,跳转和递归可能会多次执行代码,数据通常需要多次访问,所以单独开放空间,方便访问和节省空间.
-
运行时将临时数据和代码放入栈区,生命周期短。 整个程序执行过程中可能需要访问全局数据和静态数据,因此单独存储管理。
-
堆场由用户自由分配管理。
上图中我们看到的地址都是虚拟地址 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.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申请的内存位于堆中, 最终需要函数free来释放。 malloc函数并没有初始化申请的内存空间,因此调用malloc()函数之后,还需调用函数memset初始化这部分内存空间;alloca则将初始化这部分内存空间为0。
5. 堆和栈的区别
前面已经介绍过,栈是由编译器在需要时分配的,不需要时自动清除的变量存储区。里面的变量通常是局部变量、函数参数等。堆是由malloc()函数(C++语言为new运算符)分配的内存块,内存释放由程序员手动控制,在C语言为free函数完成(C++中为delete)
栈和堆的主要区别有以下几点:
栈编译器自动管理,无需程序员手工控制;而堆空间的申请释放工作由程序员控制,容易产生内存泄漏。
栈是向低地址扩展的数据结构,是一块连续的内存区域。 这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,当申请的空间超过栈的剩余空间时,将提示溢出。因此,用户能从栈获得的空间较小。
堆是向高地址扩展的数据结构,是不连续的内存区域。因为系统是用链表来存储空闲内存地址的,且链表的遍历方向是由低地址向高地址。由此可见,堆获得的空间较灵活,也较大。栈中元素都是一一对应的, 不会存在一个内存块从栈中间弹出的情况。
对于堆来讲,频繁的malloc/free(new/delete)势必会造成内存空间的不连续,从而造成大量的碎片, 使程序效率降低(虽然程序在退出后操作系统会对内存进行回收管理)。对于栈来讲,则不会存在这个问题。
堆的增长方向是向上的,即向着内存地址增加的方向;栈的增长方向是向下的,即向着内存地址减小的方向。
堆都是程序中由malloc()函数动态申请分配并由free()函数释放的;栈的分配和释放是由编译器完成的,栈的动态分配由alloca()函数完成,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行申请和释放的,无需手工实现
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行。堆则是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
一个进程在运行时不可能会用如此大的虚拟地址空间,它们只会用到其中的一部分,而且并不一定连成一片,可能会被分割成几块,每一块连续的虚拟内存块被称为虚拟内存段。
: .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