
作者主页现有项目专栏算法篇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和BA需占⽤内存10MB需占⽤内存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分区的数据进行加载到内存 填充页表继续运行。