在【C++内存管理】时,我们有提到过内存分布,可是我们对他并不理解!这次我们再来回顾一下,可以先对其进行各区域分布验证:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_unval;
int g_val = 100;
int main(int argc, char *argv[], char *env[])
{
const char *str = "helloworld";
printf("code addr: %p\n", main);
printf("init global addr: %p\n", &g_val);
printf("uninit global addr: %p\n", &g_unval);
static int test = 10;
char *heap_mem = (char*)malloc(10);
char *heap_mem1 = (char*)malloc(10);
char *heap_mem2 = (char*)malloc(10);
char *heap_mem3 = (char*)malloc(10);
printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1)
printf("test static addr: %p\n", &test); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem2); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem3); //heap_mem(0), &heap_mem(1)
printf("read only string addr: %p\n", str);
for(int i = 0 ;i < argc; i++)
{
printf("argv[%d]: %p\n", i, argv[i]);
}
for(int i = 0; env[i]; i++)
{
printf("env[%d]: %p\n", i, env[i]);
}
return 0;
}运行结果;
ltx@hcss-ecs-d90d:~/lesson5$ touch code.c
ltx@hcss-ecs-d90d:~/lesson5$ vim code.c
ltx@hcss-ecs-d90d:~/lesson5$ touch Makefile
ltx@hcss-ecs-d90d:~/lesson5$ vim Makefile
ltx@hcss-ecs-d90d:~/lesson5$ make
gcc -o code code.c
ltx@hcss-ecs-d90d:~/lesson5$ ./code
code addr: 0x55fbceb76189
init global addr: 0x55fbceb79010
uninit global addr: 0x55fbceb7901c
heap addr: 0x55fbd04566b0
heap addr: 0x55fbd04566d0
heap addr: 0x55fbd04566f0
heap addr: 0x55fbd0456710
test static addr: 0x55fbceb79014
stack addr: 0x7fff7f9936c0
stack addr: 0x7fff7f9936c8
stack addr: 0x7fff7f9936d0
stack addr: 0x7fff7f9936d8
read only string addr: 0x55fbceb77004
argv[0]: 0x7fff7f99473b
env[0]: 0x7fff7f994742
env[1]: 0x7fff7f994752
env[2]: 0x7fff7f994760
env[3]: 0x7fff7f99477a
env[4]: 0x7fff7f994790
env[5]: 0x7fff7f99479c
env[6]: 0x7fff7f9947b1
env[7]: 0x7fff7f9947c0
env[8]: 0x7fff7f9947cf
env[9]: 0x7fff7f9947e0
env[10]: 0x7fff7f994dcf
env[11]: 0x7fff7f994e04
env[12]: 0x7fff7f994e26
env[13]: 0x7fff7f994e3d
env[14]: 0x7fff7f994e48
env[15]: 0x7fff7f994e68
env[16]: 0x7fff7f994e71
env[17]: 0x7fff7f994e88
env[18]: 0x7fff7f994e90
env[19]: 0x7fff7f994ea3
env[20]: 0x7fff7f994ec2
env[21]: 0x7fff7f994ee5
env[22]: 0x7fff7f994f26
env[23]: 0x7fff7f994f8e
env[24]: 0x7fff7f994fc4
env[25]: 0x7fff7f994fd7
env[26]: 0x7fff7f994fe0通过结果可以看到,地址时依次增大的

来段代码感受一下
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int gval = 0;
int main()
{
pid_t id = fork();
if(id == 0)
{
while(1)
{
printf("子: gval: %d, &gval: %p, pid: %d, ppid: %d\n", gval, &gval, getpid(), getppid());
sleep(1);
gval++;
}
}
else
{
while(1)
{
printf("父: gval: %d, &gval: %p, pid: %d, ppid: %d\n", gval, &gval, getpid(), getppid());
sleep(1);
}
}
return 0;
}运行结果:
ltx@hcss-ecs-d90d:~/lesson5$ ./code
父: gval: 0, &gval: 0x55ab2eaf6014, pid: 878687, ppid: 878659
子: gval: 0, &gval: 0x55ab2eaf6014, pid: 878688, ppid: 878687
父: gval: 0, &gval: 0x55ab2eaf6014, pid: 878687, ppid: 878659
子: gval: 1, &gval: 0x55ab2eaf6014, pid: 878688, ppid: 878687
子: gval: 2, &gval: 0x55ab2eaf6014, pid: 878688, ppid: 878687
父: gval: 0, &gval: 0x55ab2eaf6014, pid: 878687, ppid: 878659
父: gval: 0, &gval: 0x55ab2eaf6014, pid: 878687, ppid: 878659
子: gval: 3, &gval: 0x55ab2eaf6014, pid: 878688, ppid: 878687
子: gval: 4, &gval: 0x55ab2eaf6014, pid: 878688, ppid: 878687
父: gval: 0, &gval: 0x55ab2eaf6014, pid: 878687, ppid: 878659
子: gval: 5, &gval: 0x55ab2eaf6014, pid: 878688, ppid: 878687
父: gval: 0, &gval: 0x55ab2eaf6014, pid: 878687, ppid: 878659
子: gval: 6, &gval: 0x55ab2eaf6014, pid: 878688, ppid: 878687
父: gval: 0, &gval: 0x55ab2eaf6014, pid: 878687, ppid: 878659
子: gval: 7, &gval: 0x55ab2eaf6014, pid: 878688, ppid: 878687
父: gval: 0, &gval: 0x55ab2eaf6014, pid: 878687, ppid: 878659
子: gval: 8, &gval: 0x55ab2eaf6014, pid: 878688, ppid: 878687
父: gval: 0, &gval: 0x55ab2eaf6014, pid: 878687, ppid: 878659
^C&gval: 0x55ab2eaf6014),但gval的值不同:父进程始终为0,子进程从0开始递增。为什么地址相同但值不同?
所以之前说‘程序的地址空间’是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?看
图:
分页&虚拟地址空间

虚拟地址到物理地址的转换
0x55ab2eaf6014),MMU拦截该访问,查询页表找到对应的物理地址。父子进程可能有相同的虚拟地址,但页表映射到不同的物理地址,因此变量值独立。物理地址,用户一概看不到,由OS统一管理,OS必须负责将虚拟地址转化成物理地址:OS通过MMU硬件组件实现转换。MMU使用页表(page table)映射虚拟地址到物理地址,用户程序无法直接访问物理地址
写时拷贝是一种内存管理技术,用于在进程创建子进程时(例如通过fork()系统调用),避免立即复制父进程的内存页。具体步骤如下:
fork()调用后,父进程和子进程共享相同的内存页。这意味着它们的虚拟地址空间中的某些区域(如已初始化数据区)指向相同的物理内存页。
虚拟地址空间的概念
虚拟地址空间就像是一个虚拟的“地图”,程序中的每个变量、函数等都位于这个虚拟地图上的某个位置。这个“地图”对每个进程来说都是独立的,就像每个家庭都有自己的独立地址簿一样。
示例:
想象一下,一个城市中有许多图书馆。每个图书馆都有自己的书架编号系统,这些编号就像是虚拟地址。读者(程序)只需要根据书架编号(虚拟地址)就能找到书(数据)。不同图书馆的书架编号(虚拟地址)可能相同,但它们指向的是不同的书(不同的物理内存位置)。就好像在两个不同的图书馆中,都有编号为“123”的书架,但这两个书架上的书是完全不同的。图书馆管理员(操作系统)会将这些虚拟的书架编号(虚拟地址)映射到实际存放书籍的仓库位置(物理内存地址)。这样,读者(程序)不需要知道书籍实际存放的仓库位置,只需要知道图书馆内的书架编号(虚拟地址)就能访问书籍(数据)。
区域划分的概念
区域划分是将虚拟地址空间按功能分割成不同区间(如代码区、堆区、栈区),每个区域通过 起始地址(start)和结束地址(end) 标记边界,由内核数据结构(如Linux的mm_struct)管理。区域可动态调整:扩大时修改end指针(如malloc申请堆空间),缩小时反向操作。
示例:
同桌两人共用一张100cm的课桌(虚拟地址空间)。他们在桌上画"三八线"划分区域:
start=0cm, end=50cmstart=50cm, end=100cm
这就像操作系统的mm_struct结构体记录了每个区域的边界。当小美想扩大地盘时,她将三八线移到30cm处,此时:
start=0cm, end=30cm(缩小)start=30cm, end=100cm(扩大)
这对应malloc扩大堆空间时,内核修改堆区的end值。而小胖在50cm内放书包(堆区存变量)、小美在70cm处放水杯(栈区存局部变量),就像进程在不同虚拟区域存取数据。
由于刚开始学习,所以本篇文章只站在进程的角度去看待虚拟内存。
描述linux下进程的地址空间的所有的信息的结构体是 mm_struct (内存描述符)。每个进程只有一个mm_struct结构,在每个进程的 task_struct 结构中,有一个指向该进程的mm_struct结构体指 针。
mm_struct 的核心地位作用:mm_struct 是描述整个进程用户空间虚拟地址空间的核心结构体,定义在 include/linux/mm_types.h 中 。
独立性:每个进程拥有独立的 mm_struct,确保进程地址空间隔离 。
与进程关联:在进程描述符 task_struct 中,通过指针 mm 指向该进程的 mm_struct:
struct task_struct {
struct mm_struct *mm; // 用户进程的地址空间描述符
struct mm_struct *active_mm; // 内核线程借用前一个进程的地址空间
};mm 指向自身的 mm_struct。
内核线程:对于内核线程来说,mm字段通常为NULL。内核线程没有独立的用户空间地址,但它们可以使用任意进程的地址空间。此时,内核线程会使用active_mm字段来指向一个有效的地址空间。可以说,mm_struct 结构是对整个用户空间的完整描述。在Linux内核中,每个进程都会拥有自己独立的mm_struct结构体实例,这个结构体包含了该进程所有内存管理相关的信息。正是由于这种独立性,才保证了每个进程都能拥有专属的虚拟地址空间,实现进程间的内存隔离。
从进程控制块(task_struct)到内存描述符(mm_struct)的关联关系如下:
进程的地址空间的分布情况:

mm_struct 关键成员解析(1) 虚拟内存区域管理
mmap:指向一个单链表 —— vm_area_struct 链表的头部,链表中的每个节点都是一个vm_area_struct结构体,表示一个虚拟内存区域(VMA)。当进程的虚拟内存区域较少时,使用这种单链表的方式来组织。mm_rb:指向 红黑树根节点,树中的每个节点也是一个vm_area_struct结构体。当进程的虚拟内存区域较多时,使用红黑树来组织,以便更高效地进行查找、插入和删除操作。mmap_cache:缓存最近访问的 VMA,加速局部性访问 。
(2) 地址空间范围定义
task_size:用户空间大小(如 32 位系统为 3GB)。
分段地址边界:
unsigned long start_code, end_code; // 代码段
unsigned long start_data, end_data; // 数据段
unsigned long start_brk, brk; // 堆(brk 动态扩展)
unsigned long start_stack; // 栈起始地址
unsigned long arg_start, arg_end; // 命令行参数
unsigned long env_start, env_end; // 环境变量这些字段明确划分用户空间各区域 。
(3) 页表与计数
pgd:指向进程 页全局目录(Page Global Directory) ,管理虚拟到物理地址转换 。
mm_users:共享该地址空间的进程数(如线程共享)。mm_count:mm_struct 的主引用计数,为 0 时释放结构体 。map_count:当前 VMA 的数量 。(4) 同步与保护
mmap_sem:读写信号量,保护 VMA 修改操作(如 mmap 系统调用)。page_table_lock:自旋锁,保护页表和 RSS(常驻内存集)统计 。struct mm_struct
{
/*...*/
struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */
struct rb_root mm_rb; /* red_black树 */
unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*/
/*...*/
// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
/*...*/
}vm_area_structvm_area_struct(简称 VMA)是 Linux 内核中描述进程虚拟地址空间中连续内存区域的核心数据结构。每个 VMA 代表一个具有相同属性(如访问权限、映射类型)的独立内存区间,例如代码段、堆、栈或内存映射文件。
.text):VM_READ | VM_EXEC.data):VM_READ | VM_WRITEVM_READ | VM_SHAREDvm_start ≤ 地址 < vm_end)。vm_flags 校验)。vm_ops 函数表实现按需分配(Demand Paging)、写时复制(Copy-on-Write)等机制。
关键字段包括:
struct vm_area_struct {
unsigned long vm_start; //虚存区起始
unsigned long vm_end; //虚存区结束
struct vm_area_struct *vm_next, *vm_prev; //前后指针
struct rb_node vm_rb; //红⿊树中的位置
unsigned long rb_subtree_gap;
struct mm_struct *vm_mm; //所属的 mm_struct
pgprot_t vm_page_prot;
unsigned long vm_flags; //标志位
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
struct list_head anon_vma_chain;
struct anon_vma *anon_vma;
const struct vm_operations_struct *vm_ops; //vma对应的实际操作
unsigned long vm_pgoff; //⽂件映射偏移量
struct file * vm_file; //映射的⽂件
void * vm_private_data; //私有数据
atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;vm_ops 实现按需分配物理页(Demand Paging)。所以我们可以对上图在进行更细致的描述:


假设一个进程调用了mmap系统调用,将一个文件映射到其虚拟地址空间中。此时,内核会执行以下步骤:
vm_area_struct结构体,设置其vm_start和vm_end为映射区域的起始和结束地址。
vm_file指向该文件对象,并设置vm_pgoff为文件中的起始偏移量。
vm_flags和vm_page_prot。
vm_area_struct结构体插入到进程的mm_struct结构体中,可能是插入到单链表或红黑树中。
通过vm_area_struct,Linux内核能够灵活地管理进程的虚拟内存区域,支持各种复杂的内存操作,如动态内存分配、文件映射、共享内存等。
0x08048000开始)。0x08048000,物理地址由OS动态分配。malloc()申请1GB虚拟空间,实际物理内存可能为0。0x4000)。.text段从0x08048000)。char *p = malloc(1024 * 1024 * 1024); // 申请1GB虚拟空间
// 实际物理内存可能尚未分配机制 | 作用 |
|---|---|
延迟分配 | 减少物理内存占用(如未初始化的数组不分配物理页) |
共享内存映射 | 多个进程通过页表映射到同一物理页(如共享库、IPC) |
内存碎片优化 | 物理页可分散存放,虚拟地址空间仍连续 |
虚拟地址空间的实现依赖硬件MMU和OS维护的页表:
虚拟地址 → MMU查询页表 → 物理地址0x1a00000,B的堆地址:0x1a00000(相同虚拟地址)。100,B映射到物理页帧200。0x1a00000 → MMU通过A的页表找到物理页帧100。200。虚拟地址空间通过 “间接层(Indirection)” 实现了:
task_struct)与内存管理(mm_struct)分离。设计哲学:虚拟化是计算机系统的核心思想——用软件抽象(虚拟地址空间)解决硬件限制(物理内存缺陷),正如虚拟机抽象物理机器、文件抽象磁盘块。