1. C/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);
printf("heap addr: %p\n", heap_mem1);
printf("heap addr: %p\n", heap_mem2);
printf("heap addr: %p\n", heap_mem3);
printf("test static addr: %p\n", &test);
printf("stack addr: %p\n", &heap_mem);
printf("stack addr: %p\n", &heap_mem1);
printf("stack addr: %p\n", &heap_mem2);
printf("stack addr: %p\n", &heap_mem3);
printf("read only string add: %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;
}
上述打印的地址,从正文代码区到命令行参数环境变量的地址,依次增大。
上述内存空间分布并不是实际的物理内存,而是进程地址空间,也叫做虚拟地址空间。
2. 虚拟地址
用下列一段代码证明上述所说的地址为虚拟地址。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int gval = 100;
int main(int argc, char *argv[], char *env[])
{
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;
}
知识点1:
C/C++等语言中输出的地址,全都是虚拟地址。虚拟地址是提供给上层用户使用的。
知识点2:
一个进程对应一个虚拟地址空间。一个虚拟地址对应一个字节,32位机器下有2的32次方(4GB)个地址,64位机器下有2的64次方个地址。
3. 进程地址空间
一个进程启动之后,就会有一套页表,该页表存储的是进程中各变量的进程地址空间中的虚拟地址和物理内存的地址的映射关系。
下图中是上述例子中父进程和子进程gval变量的虚拟地址空间和物理内存的关系。子进程的代码和数据以及页表都是拷贝父进程的。所以子进程中虚拟地址和物理地址的映射关系和父进程一样,当gval在子进程中被修改时,发生写时拷贝,在物理内存中就会开辟一块新的物理地址来存储子进程的gval变量,然后修改子进程中页表的映射关系,但是gval变量在子进程中的虚拟地址没有改变,所以出现了上述子进程和父进程gval变量虚拟地址一样而变量的值不一样的情况。
上述打印的子进程和父进程同一个变量地址相同,是虚拟地址相同,内容不同其实是虚拟地址和物理地址的映射关系被修改了。
知识点1:
在32位机器下,每个进程的虚拟地址空间都是4GB,每个进程都认为自己独占全部的物理内存。
4. 虚拟内存管理
虚拟地址空间本质就是一个结构体对象,名为mm_struct(内存描述符),描述Linux下进程地址空间的所有信息。每个进程只有一个mm_struct结构,在每个进程的task_struct结构中,有一个指向该进程mm_struct的指针。mm_struct结构体中存储的是对进程地址空间中代码区、堆区、栈区等每个区域进行区域划分的信息,存储的是每个区域的开始位置和结束位置。
struct task_struct
{
/*...*/
struct mm_struc *mm;
//对于普通的⽤⼾进程来说该字段指向他的虚拟地址空间的⽤⼾空间部分
//对于内核线程来说这部分为NULL。
struct mm_struct *active_mm;
//该字段是内核线程使⽤的。当该进程是内核线程时
//它的mm字段为NULL,表⽰没有内存地址空间,可也并不是真正的没有
//这是因为所有进程关于内核的映射都是⼀样的,内核线程可以使⽤任意进程的地址空间。
/*...*/
}
知识点1:
为什么要有虚拟地址空间?
(1) 可以使一个程序的代码和数据以及变量随机存储在物理内存的不同地方,通过页表的映射关系进行查找,并且使上层用户(进程视角)看到这些数据时,地址(虚拟地址)顺序是有序的。
(2) 将虚拟地址通过页表转化为物理地址的时候,每个区域在映射的过程中还有rwx等权限,可以使程序在访问虚拟内存的时候就对一些操作进行判定拦截,保护物理内存。
例如字符常量区定义的字符串常量是不允许被修改的,如果在程序中对其进行修改,在地址映射的时候没有w权限,直接进行了拦截,所以在字符常量区进行写入时程序会发生崩溃。
(3) 通过页表来建立虚拟地址和物理地址的映射关系,让进程管理和内存管理进行一定程度的解耦合。
知识点2:
创建一个进程的时候先有task_struct这样的内核数据结构,再加载进程对应的代码和数据。所以一个进程可以不用加载程序的代码和数据,只先创建进程的task_struct,mm_struct,页表。
4.1 mm_struct结构体
下图在逻辑上表示mm_struct中存储的信息,存储的信息为各个区域在虚拟内存空间中的起始位置和结束位置。
每一个进程都会有自己独立的mm_struct,操作系统要将全部进程的虚拟地址分区组织起来。所以在每个进程的task_struct中有mm_struct,mm_struct中有以下两种方式组织该进程对应的虚拟内存分区:
(1)当虚拟分区较少时采取单链表进行管理,由mmap指针指向这个链表。
(2)当虚拟分区较多时采用红黑树进行管理,由mm_rb指向这棵树。
Linux内核使用 vm_area_struct 结构体表示一个独立的虚拟内存区域(VMA),由于每个不同的虚拟内存区域功能和内部机制不同,因此一个进程使用多个vm_area_struct来分别表示不同类型的虚拟内存区域。上述两种组织方式使用的就是vm_area_struct来连接各个VMA,方便程序快速访问。
上图表示mm_struct中有一张链表,用于维护该进程中所以的虚拟内存区域。
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;
每个虚拟内存区域中也存有自己分区的起始位置和结束位置,还有一个mm_struct类型的指针,指向自己所属的mm_struct结构体。上图表示用双链表连接每个VMA。
知识点1:
堆区在虚拟内存中会存在多个,并且是离散的,上图中只画了一个,一个进程中实际会存在多个堆区,也和其他VMA一样被这样管理起来。