目录
1. 程序地址空间分布
2. 两个问题
3. 虚拟地址和物理地址
4. 页表
5. 解决问题
6. 为什么要有地址空间
1. 程序地址空间分布
测试一下:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
int gal_init = 1;
int gal_uninit;
int main(int argc, char* argv[], char* env[])
{
printf("code add:%p\n",main);
const char* str = "hello";
printf("str add:%p\n",str);
printf("init global add:%p\n",&gal_init);
printf("uninit global add:%p\n",&gal_uninit);
char* heap1 = (char*)malloc(10);
char* heap2 = (char*)malloc(10);
char* heap3 = (char*)malloc(10);
char* heap4 = (char*)malloc(10);
printf("heap1 add:%p\n",heap1);
printf("heap2 add:%p\n",heap2);
printf("heap3 add:%p\n",heap3);
printf("heap4 add:%p\n",heap4);
printf("stack1 add:%p\n",&heap1);
printf("stack2 add:%p\n",&heap2);
printf("stack3 add:%p\n",&heap3);
printf("stack4 add:%p\n",&heap4);
int i = 0;
for(;i < argc; ++i)
{
printf("%s:%p\n",argv[i],&argv[i]);
}
printf("env add:%p\n",env);
return 0;
}
运行结果:
code add | 0x40057d |
int a add | 0x7ffc2b16df7c |
static int b add | 0x601044 |
str add | 0x40081d |
init global add | 0x60103c |
uninit global add | 0x601048 |
heap1 add | 0x2301010 |
heap2 add | 0x2301030 |
heap3 add | 0x2301050 |
heap4 add | 0x2301070 |
stack1 add | 0x7ffc2b16df70 |
stack2 add | 0x7ffc2b16df68 |
stack3 add | 0x7ffc2b16df60 |
stack4 add | 0x7ffc2b16df58 |
./myproc | 0x7ffc2b16e078 |
env add | 0x7ffc2b16e088 |
这些都是之前就了解过的内容,今天详细聊聊地址空间
2. 两个问题
1. 安全问题:
这些程序都在同一个地址空间中,如果发生了越界访问,野指针问题,这些问题该怎么办?
2. 一个特殊现象问题:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
int num = 0;
int ret = fork();
if(ret == 0)
{
int i = 3;
while(i--)
{
printf("I am child,num = %d,&num = %p\n",num,&num);
sleep(1);
}
num = 1;
while(1)
{
printf("I am child,num = %d,&num = %p\n",num,&num);
sleep(1);
}
}
else
{
while(1)
{
printf("I am father,num = %d,&num = %p\n",num,&num);
sleep(1);
}
}
return 0;
}
为什么父进程和子进程中的num同一个地址,但是却有两个值?
3. 虚拟地址和物理地址
在Linux地址下,这种地址叫做 虚拟地址
我们在用C/C++语言所看到的地址,全部都是虚拟地址!
物理地址,用户一概看不到,由OS统一管理
由OS负责将 虚拟地址 转化成 物理地址
上面的地址空间分布就是虚拟地址,每个进程被创建,就会有对应的虚拟地址表
虚拟地址表在linux下就是由mm_struct结构体来描述的
Linux下的进程管理PCB:task_struct就有一个指针指向mm_struct,程序和虚拟地址空间联系起来了
4. 页表
OS是如何把虚拟地址转换成物理地址的呢?
页表
页表是一种key-value的数据结构,记录虚拟地址和物理地址一一映射关系
task_struck有一个指针指向页表
页表里的值(虚拟地址,物理地址)是哪里来的呢?
代码在被编译器编译后的每一条语句每一个函数都会有对应的地址,这个地址是逻辑地址,和虚拟地址一样,作为页表的虚拟地址。当程序被加载到物理内存中时,就会有对应的物理地址,然后在对应到页表里面。
程序被执行时,使用的地址是虚拟地址,需要用页表映射到物理地址
5. 解决问题
到这里,我们就可以解决第一个问题,每个进程都有一个虚拟地址和页表,这些都由OS来维护,在页表对应的每一个物理地址后面,都有一个像文件访问权限一样的标志,如果这个物理地址没有访问权限,就会直接报错终止。有效的保护了物理内存。
第二问题的答案就在图中,fork函数创建子进程时,其实就是拷贝了大部分对应的task_struct,mm_struct和页表,因为父子进程之间大部分属性都一样,但当需要改变num的值时,子进程就在物理空间上重新开了一块空间,拷贝父进程,OS也会更新对应的页表映射关系。这个叫做写时拷贝。但是虚拟地址还是一样的,只是映射关系发生了变化。所以num相同的虚拟地址,不同的值。
6. 为什么要有地址空间
安全性,有效的保护了物理内存
因为地址空间和页表是OS创建并维护的,凡是想用地址空间和页表进行映射,都需要在OS的监管下来进行访问。
内存管理模块和进程管理模块完成了解耦
提高内存的利用率
用户申请的物理空间,malloc和new其实是在虚拟地址上申请的,OS通过延迟分配,提高物理内存的利用率
地址空间和页表的存在可以将内存分布有序化(按照地址空间分布)和进程的独立性(不同的进程映射到不同的物理空间)每一个进程不知道其他进程的存在
对于程序的分批加载,当程序刚刚新建的状态下,进程就只创建内核结构,程序和数据还没有加载到内存中。
程序的分批换出,当进程短时间内不会再被执行了,比如阻塞了,进程的数据和代码被换出到磁盘中的swap区,页表的映射关系也改为磁盘地址。这个过程就叫挂起。
完,写的不好的地方多有体谅,还在学