虚拟地址空间

13地址空间

其实我们学习语言前期,C/C++,我们通常的地址其实并不是物理地址,而是虚拟地址(线性地址)。

几乎所有的语言,如果他有“地址”的概念,这个地址一定是虚拟地址,而不是物理地址!!!

#include <stdio.h> #include <unistd.h> int g_val = 100; int main() { pid_t id = fork(); if(id == 0) { int cnt = 0; //child while(1) { printf("I am child, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",\ getpid(), getppid(), g_val, &g_val); sleep(1); cnt++; if(cnt == 5) { g_val = 200; printf("child chage g_val 100 -> 200 success\n"); } } } else { //father while(1) { printf("I am father, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",\ getpid(), getppid(), g_val, &g_val); sleep(1); } } }

运行结果

很显然,同一个地址,同时读取的时候,出现了不同的值!!!!、所以我们可以推断出这里的地址,绝对不是物理内存的地址!!!

其实我们的各种外设像磁盘、网卡、显卡址空间.....这类外设其实都是有寄存器的。由于硬件的不同,会导致它的寄存器的也会存在一些差异。为了统一处理,便统一映射了内存。确保从内存里看,看到的都是一段内存。

略有一点抽象

在语言层面,C/C++是编译型语言,在编译完成后,就会形成二进制可执行程序。当它双击或者 ./运行之后,就变成了一个进程了。

静态区在正文代码段中。

在32位下,一个进程的地址空间,取值范围是0x0000 0000 ~ 0x FFFF FFFF。

[0,3GB]:用户空间 [3GB,4GB]:内核空间

为了验证各个区域是否是这样排布的

通过下面的代码就行演示

#include <stdio.h> #include <unistd.h> #include <stdlib.h> int g_unval; int g_val = 100; int main(int argc, char *argv[], char *env[]) { // int a = 10; //字面常量 const char *str = "helloworld"; // 10; // 'a'; 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 stack 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; }

运行结果

在这个过程中,程序是早已经编译好的,所以打印地址,其实就是进程在打印地址。(地址是在程序运行之后打印的)

static修饰局部变量:本质是将该变量开辟在全局区域。

像这种(注释掉的)可以直接被编译,我们把它称为字面常量。

其实所有的字面常量硬编码进入了代码区。其实代码区细分一点,往一点就是字符常量区。

代码区都是拥有只读属性的。


关于地址空间的认识,大富豪和他三个互不认识的私生子

关于这个例子的认识


历史上,我们曾经直接访问过物理内存,但是这种方式(直接使用物理内存,物理地址),特别的不安全,因为内存本身是可以随时被读写的!!!!可能由于指针会导致一些问题。可能会存在一些恶意程序,利用指针得到账号和密码。

现代计算机,提出了虚拟地址空间的概念,要访问物理内存,需要先进行映射!!!通过一个映射机制将虚拟地址和物理地址的联系起来。如果虚拟地址是一个非法地址,则禁止映射!!

就像你拿到了压岁钱,但是你和商店老板之间就隔了你的妈妈。


由于我们的虚拟地址空间有不同的区域,就意味着我们进行划分。

划分区域 :小胖和小花的38线

所谓的区域划分,本质是在一个范围里定义start,和end

所谓的范围变化,本质就是对start或end的标记值 + /-特定的范围!!

由大富翁的例子,我们认识到:

地址空间是一种内核数据结构,它里面至少要有各个区域的划分。

这部分在Liunx中对应的就是struct mm_struct{ }。

PCB结构体(task_struct)中含有 mm_struct* 的指针。

同时也应该意识到 地址空间和页表(用户级)是每个进程都私有一份。

如何保证进程的独立性??

只要保证每个进程的页表映射的是物理内存的不同区域,就能做到,进程之间就不会互相影响,可以保证进程的独立性。

回答问题

fork函数为啥会有两个返回值?

简单来说就是发生了写时拷贝。

首先我们知道fork是有返回值(pid_t int )的,在执行return语句的时候子进程就已经创建好了,这时就会执行两次return语句,有两个返回值。而在这个过程中,return 的本质,可以理解为对id进行了写入!!! 这个时候就会进行写时拷贝,父进程和子进程的id在物理地址空间上,属于自己的变量空间。只不过是在用户层上用了 同一个变量(虚拟地址)进行标识。(下面是图片的解释)


扩展内容

当我们的程序编译形成可执行程序的时候,没有被加载到内存中的时候,我们的程序内部就存在地址了。

没错,可执行程序编译的时候,内部已经有地址了!!!!!

地址空间其实不能理解成OS内部要遵守,其实编译器也要遵守!!!即编译器编译代码的时候,就已经给我们形成了各个区域 代码区 ,数据区...........并且采用的是和Linux 内核中一样的编址方式,给每个变量,每个代码都进行了编址,故在程序编译的时候,每个字段已经具有了一个虚拟地址。

新命令 vma


为什么要有地址空间??

  1. 凡是非法的访问或者映射,OS都会识别到了,并终止该进程。------>>有效保护了物理内存----->>因为地址空间和页表是OS创建并维护的!!也就意味着凡是想使用地址空间和页表进行映射。也一定要在OS的监管之下来访问!!!----->>>也保护了物理内存中的所有合法数据,包括各个进程,以及内核的相关有效数据!
  2. 因为有地址空间的存在,因为也页表的映射的存在,我们的物理内存中,是可以对未来的数据进行任意位置的加载!! ---------------->>物理内存的分配 和进程的管理,可以做到没有关系!!

内存管理模块 vs 进程管理模块 完成了解耦合!!!

  1. 因为在物理内存中,理论上可以任何位置加载,这样就导致了在物理内存中的几乎所有的数据和代码在内存中都是乱序的!!!!-------------->>>>但是因为页表的存在,它可以将地址空间上的虚拟地址和物理地址进行映射,使得在进程的视角中所有的内存分布,都是有序!!--------------->>地址空间+页表的存在 使得内存分布有序化!!!

地址空间是OS给进程画的大饼。

因为有地址空间的存在,每个进程都认为自己拥有4GB(32位),并且每个空间是有序的,进而可以通过页表映射到不同的区域,来实现进程的独立性。(每个进程不知道,也不需要知道其他进程的存在!!!!)

让不同的进程映射到不同的物理内存中(通过进程地址空间+页表的方式实现),实现进程的独立性!!!

C/C++语言上new,malloc空间的时候,本质是在虚拟地址空间上申请的。

申请物理空间,而不立马使用?其实是空间的浪费!!------------>>操作系统通过延迟分配的策略,来提高整机效率。 (使得内存的有效使用几近是100%)

本质上,因为有地址空间的存在,所以上层申请空间,其实是在地址空间上申请的,物理内存甚至可以一个字节都不给你!!而当你真正进行对物理空间访问的时候,才执行内存的相关管理算法,(帮你申请内存,构建页表映射关系。),然后再让你进行内存的访问。

上面红字是由操作系统,自动完成的,用户包括进程是0感知的。

小明的玩具, 红包 500, 晚上不能玩,


面试题:请你描述一下,fork创建子进程,操作系统中都做了什么???

创建子进程,系统中多了一个进程。

  1. 分配新的内存块和内核数据结构给子进程
  2. 将父进程部分数据结构内容拷贝至子进程
  3. 添加子进程到系统进程列表当中
  4. fork返回,开始调度器调度

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值