目录
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)给进程提供连续、统一的内存视图。进程的代码和数据以及变量是碎片化的存储在物理内存中的,有了虚拟地址空间,进程看到的地址是连续且统一的,无需关心物理内存的分布,可以直接使用地址编写代码,不会和其他进程产生使用同一个地址的冲突。
(3)实现虚拟内存技术。物理内存是有限的,程序的内存需求可能超过物理内存,操作系统将虚拟地址空间分为“页”为单位的块,只加载当前要用的代码和数据到物理内存中,当前不用的存储在磁盘的交换区中,进程访问未加载的代码和数据时,触发缺页中断,将暂时不需要的页唤出到磁盘,加载当前需要的页。
(4)内存保护。虚拟地址空间允许操作系统给不同的页设置访问权限(如只读、可写等),防止进程误操作。
知识点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一样被这样管理起来。
623

被折叠的 条评论
为什么被折叠?



