
作者主页
| 现有项目专栏 | |
| 算法篇 | Linux篇 |
| QT百日筑基篇 | 数据结构篇 |
| python篇 |
Linux——程序地址空间

目录/索引:
目录
Linux——程序地址空间
目录/索引:
程序地址空间
程序地址空间的回顾:
深度理解物理空间和虚拟空间:
进程地址空间的理解:
浅浅理解区域划分:
虚拟内存管理:
浅聊一下共享区:
内存之间的工作机制:
深度理解区域划分:
为什么要有虚拟地址空间
虚拟空间的意义:
缺页中断:
深度了解进程挂起:
程序地址空间
程序地址空间的回顾:
我们在学习c语言的时候,都或多或少的听说过:"栈区,堆区,啥的"那我们就写一下代码来看一下我们程序在内存中的分布。 下面这个代码能够清晰的看见我们写代码能遇到很多变量的地址:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.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;
}
运行:

深度分析:

通过这样的代码我们能分析出这里的空间划分图:

这个图和我们上面分析得来的很相似。我们的全局变量和静态变量都存在正文代码和堆之间的拿一块空间。
我们每个内存的一个字节只存一个地址。
那么我们在32位机器下: 能有2的32次方个地址。4GB
我们64位的机器下: 能有2的64次方个地址。
用户空间(3GB)是给我们下代码来保存数据的,后续我们拿到地址可直接访问对应的代码和数据。
深度理解物理空间和虚拟空间:
思考: 我们日常使用的是物理内存还是虚拟空间呢???
哎?先别急着回答我们一下下面的代码进行感受一下:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
int test_tmp = 0;
pid_t pi = fork();
while (1)
{
if (pi == 0)
{
printf("这是一个子进程对应的pid为%d, 对应变量的地址为%p, 值为%d\\n", getppid(), &test_tmp, test_tmp);
test_tmp++;
sleep(3);
}
else if(pi >0)
{
printf("这是一个父进程对应的pid为%d, 对应变量的地址为%p, 值为%d\\n", getpid(), &test_tmp, test_tmp);
sleep(3);
}
else
{
//错误:
exit(1);
}
}
}
我们看到我们的子进程的值一直在变化我们的地址一点都不发生改变。所以我们可以大胆的进行猜测我们之前能看到的地址绝对不是物理地址。

结论:
变量内容不一样,所以父子进程输出的变量绝对不是同一个变量 但地址值是一样的,说明,该地址绝对不是物理地址! 在Linux地址下,这种地址叫做 虚拟地址 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理
OS必须负责将 虚拟地址 转化成 物理地址。
进程地址空间的理解:
我们在创建进程的时候他就会给我们创建一个虚拟地址空间, 并且还会创建一个页表。
页表的作用:用来进行物理地址和虚拟地址关系之间的映射的, 后续我们加载进来对应的代码和数据我们就会在页表中填入对应的虚拟空间的地址和物理空间的地址。

进程通过什么创建虚拟空间???
通过结构体,在我们Linux中有对应的结构体(mm_struct)来创建对应的虚拟地址。
问题1: 有了虚拟地址空间和页表,我们的物理地址还要划分栈区,堆区吗???
答案已经显而易见了, 我们只要在对应的虚拟地址进行创建对应的分区, 就可以了, 我们的虚拟地址空间会通过页表进行映射对应的物理地址的。所以物理地址怎么存储对我们来说已经不是很重要了,因为我们不关心。
现在给大家讲个故事
有一个“有钱人”, 他有很多的私生子,他家财万贯, 有10个亿, 他给每个自己的孩子说说我死后你将继承我的所有遗产,他就给每一个私生子画饼了,我们每个私生子可以通过这个饼去打电话访问这这个有钱人。那么我们这些私生子就是一个个进程, 这个饼就是我们的虚拟内存空间,我们的这个有钱人就是我们的物理内存。电话就是一个个页表。

我们的虚拟内存空间会有一个个的分区, 所以我们要深刻理解这个分区:
浅浅理解区域划分:
我们上学的时候有的时候会和自己的同桌在课桌上画38线。有的时候当我们一直进行越界的话,同桌可能忍无可忍就将三八线向我们这移动让我们的区域变小。我们就有自己的起始位置和结束位置。我们将桌子画出一个个刻度, 然后我现在1这个刻度放尺子,在2这个位置放铅笔……。
所以虚拟内存也是这样的我们只要记住一个区域起始和结束。我们将桌子划分成一个个刻度。就是区域的划分。

虚拟内存管理:
描述linux下进程的地址空间的所有的信息的结构体是 个mm_struct结构,在每个进程的 mm_struct (内存描述符)。每个进程只有⼀ task_struct 结构中,有⼀个指向该进程的mm_struct结构体指 针。
浅聊一下共享区:
我们的后面会有动态库的制作,这个库的制作不是直接加载到内存中,而是加载到共享区中供我们进行使用。
内存之间的工作机制:
我们对应的进程会创建对应的进程地址空间,创建页表。然后我们的磁盘会将我们的代码和数据加载到我们的物理内存和虚拟内存中,并将两个起始地址加载到页表中。然后我们访问到某一个程序时我们的进程会拿着虚拟空间的地址同过页表找到对应的物理空间的内存。

深度理解区域划分:
描述linux下进程的地址空间的所有的信息的结构体是 个mm_struct结构,在每个进程的 mm_struct (内存描述符)。每个进程只有⼀ task_struct 结构中,有⼀个指向该进程的mm_struct结构体指 针。
我们Linux对应的源码:
struct task_struct
{
/*…*/
struct mm_struct *mm; //对于普通的用户进程来说该字段指向他的虚拟地址空间的用户空间部分,对于内核线程来说这部分为NULL。
struct mm_struct *active_mm; // 该字段是内核线程使用的。当该进程是内核线程时,它的mm字段为NULL,表示没有内存地址空间,可也并不是真正的没有,这是因为所有进程关于内核的映射都是一样的,内核线程可以使用任意进程的地址空间。
/*…*/
}
可以说, mm_struct 结构是对整个用户空间的描述。每⼀个进程都会有⾃⼰独⽴的 mm_struct , 这样每⼀个进程都会有⾃⼰独⽴的地址空间才能互不⼲扰。先来看看由 task_struct 到mm_struct ,进程的地址空间的分布情况:

我们查看一下mm_struct在Linux中的源码:
struct mm_struct
{
/*…*/
struct vm_area_struct *mmap;
struct rb_root mm_rb;
unsigned long task_size;
/*…*/
/*
指向虚拟区间
(VMA)
链表
*/
/* red_black
树
*/
/*
具有该结构体的进程的虚拟地址空间的⼤⼩
*/
//
代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
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;
/*…*/
}
看下面这一块,start_code,代码段的起始地址,end_code:结束地址。ok。这一块不就是区域划分吗。划分的地址。

下面这一块主要是由两个结构, 一个是mmap:是一个链表一个是mm_rb:是一颗红黑树。
用来链接每个被使用的区域链接其进行统一的管理。

那既然每⼀个进程都会有⾃⼰独⽴的 mm_struct ,操作系统肯定是要将这么多进程的 mm_struct 组织起来的!
虚拟空间的组织⽅式有两种: 1.当虚拟区较少时采取单链表,由mmap指针指向这个链表;
2.当虚拟区间多时采取红⿊树进⾏管理,由mm_rb指向这棵树。
linux内核使⽤ vm_area_struct 结构来表⽰⼀个独⽴的虚拟内存区域(VMA),由于每个不同质的虚 拟内存区域功能和内部机制都不同,因此⼀个进程使⽤多个vm_area_struct结构来分别表⽰不同类型 的虚拟内存区域。上⾯提到的两种组织⽅式使⽤的就是vm_area_struct结构来连接各个VMA,⽅便进 程快速访问。
我们简单查看这个源码:

图解:
这个链表会创建对应的链表对这些空间进行管理起来。

当然这里的堆不知一个,为什么堆的空间不是连续的, 大概是因为扩容吧。

为什么要有虚拟地址空间
这个问题其实可以转化为:如果程序直接可以操作物理内存会造成什么问题?
在早期的计算机中,要运⾏⼀个程序,会把这些程序全都装⼊内存,程序都是直接运⾏在内存上的, 也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运⾏多个程序时,必须保证 这些程序⽤到的内存总量要⼩于计算机实际物理内存的⼤⼩。 那当程序同时运⾏多个程序时,操作系统是如何为这些程序分配内存的呢?例如某台计算机总的内存 ⼤⼩是128M,现在同时运⾏两个程序A和B,A需占⽤内存10M,B需占⽤内存110。计算机在给程序分 配内存时会采取这样的⽅法:先将内存中的前10M分配给程序A,接着再从内存中剩余的118M中划分 出110M分配给程序B。

这种分配方法可以保证程序A和程序B都能运行,但是这种简单的内存分配策略问题很多。
– 安全风险 – 每个进程都可以访问任意的内存空间,这也就意味着任意一个进程都能够去读写系统相关内存区域,如果是一个木马病毒,那么他就能随意的修改内存空间,让设备直接瘫痪。
– 地址不确定 – 众所周知,编译完成后的程序是存放在硬盘上的,当运行的时候,需要将程序搬到内存当中去运行,如果直接使用物理地址的话,我们无法确定内存现在使用到哪里了,也就是说拷贝的实际内存地址每一次运行都是不确定的,比如:第一次执行a.out时候,内存当中一个进程都没有运行,所以搬移到内存地址是0x00000000,但是第二次的时候,内存已经有10个进程在运行了,那执行a.out的时候,内存地址就不一定了
– 效率低下 – 如果直接使用物理内存的话,一个进程就是作为一个整体(内存块)操作的,如果出现物理内存不够用的时候,我们一般的办法是将不常用的进程拷贝到磁盘的交换分区中,好腾出内存,但是如果是物理地址的话,就需要将整个进程一起拷走,这样,在内存和磁盘之间拷贝时间太长,效率较低。
存在这么多问题,有了虚拟地址空间和分页机制就能解决了吗?
当然!
– 地址空间和页表是OS创建并维护的!是不是也就意味着,凡是想使用地址空间和页表进行映射,也一定要在OS的监管之下进行访问!!也顺便保护了物理内存中的所有的合法数据,包括各个进程以及内核的相关有效数据!
– 因为有地址空间的存在和页表的映射的存在,我们的物理内存中可以对未来的数据进行任意位置的加载!物理内存的分配 和 进程的管理就可以做到没有关系,进程管理模块和内存管理模块就完全成了解耦合。
– 因为有地址空间的存在,所以我们在C、C++语言上new, malloc空间的时候,其实是在地址空间上申请的,物理内存可以甚至一个字节都不给你。而当你真正进行对物理地址空间访问的时候,才执行内存的相关管理算法,帮你申请内存,构建页表映射关系(延迟分配),这是由操作系统自动完成,用户包括进程完全0感知!!
– 因为页表的映射的存在,程序在物理内存中理论上就可以任意位置加载。它可以将地址空间上的虚拟地址和物理地址进行映射,在进程视角所有的内存分布都可以是有序的。
虚拟空间的意义:
有了虚拟空间我们的进程管理和内存的管理能一定程度的进行解耦合。
缺页中断:
当我们运行一个程序的时候我们, 要加载2个GB但是,我们现在物理内存中先加载1GB,然后在虚拟内存中进行加载, 通过页表找到对应的关系, 然后, 当我们的虚拟地址空间运行了一个GB的时候通过页表发现对应的空间没有对应的数据了,那我们就要发生缺页中断, 暂停进程的运行。然后我们在将剩余的1GB加载内存中。然后填充页表继续运行。
深度了解进程挂起:
有了上面的认识我们的一个进程,当我们挂起时, 我们将对应的页表删除, 然后将物理内存的数据进行拷贝到磁盘的swap分区中。 然后等到下次运行时使用调度算法,暂停进程,将对应的swap分区的数据进行加载到内存, 填充页表,继续运行。





