C 语言的内存模型
C语言是一门比较偏底层的语言,所以它的内存模型与操作系统的一些东西(进程)的内存模型相同,了解了C语言的内存模型对以后的学习很有帮助。
简单内存模型
-
栈(stack):存放程序中的局部变量(但不包括static声明的变量,static变量放在静态常量区中)。同时,在函数被调用时,栈用来传递参数和返回值。由于栈先进后出特点。所以栈特别方便用来保存/恢复调用现场(适合递归)。
-
堆(heap):用来存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用
malloc
分配内存时,新分配的内存就被动态添加到堆上,当进程调用free
释放内存时,会从堆中剔除。 -
静态常量区:用于存放一些全局变量,静态变量,字符串常量…
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
int localVar = 1;
int num1[10] = { 1, 2, 3, 4 };
char char2[] = "abcd";
const char* pChar3 = "abcd";
int* ptr1 = (int*)malloc(sizeof(int) * 4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr2);
free(ptr3);
}
接下来看一下详细的C内存模型
C内存模型
kernel
-
进程管理:内核负责管理计算机系统中的进程(process)。它创建、调度和终止进程,分配和管理进程所需的资源,以及处理进程间的通信和同步。
-
内存管理:内核负责管理计算机系统的物理内存和虚拟内存。它分配和回收内存空间,将虚拟地址映射到物理地址,处理页面置换和内存分页等操作。
-
文件系统管理:内核提供文件系统的管理功能。它负责文件和目录的创建、修改和删除,以及文件的读取和写入操作。内核还管理文件的访问权限和文件的元数据信息。
-
设备管理:内核负责管理计算机系统中的硬件设备。它提供设备驱动程序,用于与硬件设备进行通信和控制。内核管理设备的初始化、中断处理、设备驱动程序的加载和卸载等操作。
-
系统调用接口:内核提供系统调用接口,允许用户程序通过特定的函数调用请求内核提供的服务。这些服务包括文件操作、进程管理、网络通信等,用户程序可以通过系统调用接口与内核进行交互。
总之,内核是 C 内存模型中的关键组成部分,它负责管理系统的资源、提供对硬件的访问接口,并提供系统调用接口供用户程序使用。内核在操作系统中扮演着非常重要的角色,确保计算机系统的正常运行和资源的有效管理。
栈
ulimit -s//查看栈的大小
栈的特点
-
后进先出(LIFO):栈遵循后进先出的原则,最后进入栈的元素首先被访问和移除。
-
有限大小:栈的大小是有限的,通常由操作系统或编程语言定义。当栈的容量达到上限时,继续向栈中添加元素会导致栈溢出。
-
自动分配和释放:栈上的内存空间由编译器自动分配和释放。当函数被调用时,函数的局部变量和函数调用的上下文信息(如返回地址、参数等)被分配在栈上。当函数执行完毕时,栈上的这些数据会被自动释放。
栈在程序中的主要作用是管理函数的调用和返回。当一个函数被调用时,它的局部变量和其他相关信息被压入栈中,函数执行完毕后,这些信息被弹出栈。这样可以确保函数的局部变量和上下文信息在函数调用过程中的正确性和独立性。
此外,栈还可以用于存储临时数据、递归算法、表达式求值等。栈的大小相对较小,但访问速度较快,因此在需要快速分配和释放内存的场景下,栈是一个常用的数据结构。
我们在解决问题时,可试着去模拟栈,可以用线性表和链表去模拟。
栈帧
每一次函数的调用,都会在调用栈(call stack)上维护一个独立的栈帧空间(stack frame),每个独立的栈帧一般包括:
- 函数的返回地址和参数
- 临时变量: 包括函数的非静态局部变量以及编译器自动生成的其他临时变量
rsp
、rbp
这两个寄存器中存放的是地址,这两个地址是用来确认各变量,用来维护函数栈帧的rbp
(栈底指针):该指针永远指向系统栈最上面一个栈帧的底部rsp
(栈顶指针):该指针永远指向系统栈最上面一个栈帧的栈顶- 栈是从高地址向低地址延伸,一个函数的栈帧用
rbp
和rsp
这两个寄存器来划定范围,rbp
指向当前的栈帧的底部,rsp
始终指向栈帧的顶部 - 压栈
push
:rsp
上移朝低地址移动;出栈pop
:栈顶元素弹出,rsp
下移高地址
堆
堆(Heap)是计算机内存中用于动态分配内存的一部分。它是在程序运行时动态分配和释放内存的区域,用于存储程序运行时创建的对象、数据结构和动态分配的内存块。
堆的特点
-
动态分配:堆内存的大小可以在程序运行时动态地增长或缩小,根据程序的需要进行内存分配和释放。
-
随机访问:堆中的内存块可以通过指针进行随机访问,程序可以根据需要在堆中分配和访问任意大小的内存块。
-
持久性:堆中分配的内存块在分配后会一直存在,直到显式释放或程序结束。
-
不连续分配:堆中的内存块不一定是连续的,可以是散布在堆内存区域的不同位置。
在大多数编程语言中,如C、C++、Java等,提供了堆内存的动态分配和释放机制。程序可以使用特定的函数或操作符(如malloc
、new
等)来在堆中分配内存,并使用相应的函数或操作符(如free
、delete
等)来释放已分配的内存。
malloc
和free
的原理
1)当开辟的空间小于 128K 时,调用 brk()函数,malloc 的底层实现是系统调用函数 brk(),其主要移动指针 _enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)
int brk(void *addr);
void *sbrk(intptr_t increment);
brk 用于返回堆的顶部地址;sbrk 用于扩展堆,通过参数 increment 指定要增加的大小,如果扩展成功,返回 brk 的旧值。如果 increment 为零,返回 brk 的当前值。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
void *current_brk;
int *dynamic_memory;
// 获取当前的 brk 指针位置
current_brk = sbrk(0);
if (current_brk == (void *)-1) {
perror("sbrk");
exit(1);
}
printf("Current brk pointer: %p\n", current_brk);
// 扩展堆空间
if (sbrk(sizeof(int)) == (void *)-1) {
perror("sbrk");
exit(1);
}
// 在新分配的内存中存储值
dynamic_memory = (int *)current_brk;
*dynamic_memory = 42;
printf("Dynamic memory value: %d\n", *dynamic_memory);
return 0;
}
我们不会直接通过 brk 或 sbrk 来分配堆内存,而是先通过 sbrk 扩展堆,将这部分空闲内存空间作为缓冲池,然后通过 malloc / free 管理缓冲池中的内存。这是一种池化思想,能够避免频繁的系统调用,提高程序性能。
2)当开辟的空间大于 128K 时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟。
malloc和free的简易模拟
#include <stdio.h>
#include <string.h>
#include <unistd.h>
typedef struct mallocBlock
{
struct mallocBlock *before;
int size;
int use;
} mb;
static mb *mb_last = NULL;
void *my_malloc(int size)
{
mb *mbp;
for (mbp = mb_last; mbp != NULL; mbp = mbp->before)
{
if (mbp->use == 0 && mbp->size >= size)
{
break;
}
}
if (mbp == NULL)
{
mbp = sbrk(sizeof(mb) + size);
if (mbp == (void *)-1)
return (void *)0;
mbp->size = size;
mbp->before = mb_last;
mb_last = mbp;
}
mbp->use = 1;
return mbp + 1;
}
int my_free(void *p)
{
if (p == NULL)
return 0;
mb *mbp = (mb *)p - 1;
mbp->use = 0;
return 0;
}
int main(void)
{
char *a = my_malloc(1);
char *b = my_malloc(2);
char *c = my_malloc(3);
my_free(a);
my_free(b);
my_free(c);
printf("%p,%p,%p\n", a, b, c);
b = my_malloc(1);
printf("%p,%p,%p\n", a, b, c);
return 0;
}
Valgrind的简易使用
valgrind --leak-check=yes ./a.out arg1 arg2
数据段
BSS段
BSS段储存的是未初始化的全局变量或初始化为0的全局变量, BSS段不占据执行文件空间,但占据程序运行时的内存空间。
执行期间必须将BSS段内容全部设为0。
rodata段
rodata段存储常量数据,比如程序中定义为const的全局变量,#define定义的常量,以及诸如“Hello World”的字符串常量。只读数据,存储在ROM中。
const修饰的全局变量在常量区;const修饰的局部变量只是为了防止修改,没有放入常量区。
编译器会去掉重复的字符串常量,程序的每个字符串常量只有一份。
wqdata段
data存储已经初始化的全局变量,属于静态内存分配。(注意:初始化为0的全局变量还是被保存在BSS段)
static声明的变量也存储在数据段,链接时初值加入执行文件。
代码段
text段存放程序代码,运行前就已经确定(编译时确定),通常为只读。