🧸🧸🧸各位大佬大家好,我是猪皮兄弟🧸🧸🧸
文章目录
以下结论只在Linux有效,其他系统类似
一、验证地址空间
这里的地址空间,指的就是进程地址空间
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int g_val=100;
int main()
{
pid_t id = fork()
if(id==0)//这里就不做对进程创建失败的判断了
{
int cnt=0;
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;//修改两个进程中其中一个的g_val来验证地址空间
printf("child change g_val 100->200 success\n";
}
}
}
else
{
while(1)
{
printf("i am parent,pid:%d ,ppid:%d,g_val:%d,&g_val:%p\n",
getpid(),getppid(),g_val,&g_val);
sleep(1);
}
}
return 0;
}
我们发现,同一个地址,同时读取的时候出现了不同的值,这就告诉了我们,这里的地址绝对不是物理内存的地址,而是虚拟地址,所以,我们所说的地址,其实是虚拟地址
特定的硬件和地址空间就是这种对应关系:
如果在访问虚拟内存的时候,如果访问的那个空间对应的是内存,就访问内存,对应的是外设,就访问外设(以统一的视角看待不同的设备)
二、进程地址空间的分布
命令行参数环境变量之下是用户空间,大约3G,之上是内核空间,大约1G
其实在正文代码和初始化数据的中间还有一个区,是字符串常量区。
三、验证地址
#include <stdio.h>
int g_val=100;//已初始化
int g_unval;//未初始化
int main(int argc,char* argv[],char* env[])
{
printf("code addr: %p\n",main);//函数是存在代码段的
printf("Init global addr: %p\n",&g_val);
printf("Uninit global addr: %p\n",&g_unval);
char *heap_mem =(char*)malloc(10);//堆
printf("heap addr: %p\n",heap_mem);
printf("stack addr: %p\n",&heap_mem);//指针变量是存在栈区的
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;
}
从上可以看出
1.正文代码并不是从000…000开始的
2.初始化数据和未初始化数据是在一起的
3.栈区和命令行参数环境变量大概是一起创建的,并且在栈区上面
在malloc(10)的时候 ,C标准库不仅仅是生成了10个字节,其实生成了更多,多出来的通常用来 描述这个堆的属性信息(被称作cookie数据),有了这些数据,free才知道我要free多少。
四、历史vs现代 计算机访问物理内存
1.历史
历史是直接访问物理内存的,内存是个硬件,只负责存和取,内存本身是可以被随时读写的,所以,如果内存中加载了多个进程,进程万一弄错了,就可能直接改掉其他进程的东西,而且,如果进程一是输入账号和密码的操作,那么进程二直接对那块内存进行读取就可获得,所以,这样访问物理内存是特别不安全的。(根本原因就是直接使用物理内存,没有软件来设置权限)
2.现代
1.每一个进程有一个PCB(task_struct)
2.操作系统给每一个进程创建了一个进程地址空间。(虚拟内存)
3.这个地址空间的编制认为是0x000…0 到 0xFF…FF的
虚拟地址空间(进程地址空间)上的地址称为虚拟地址,最终是会存到物理内存上的,系统存在一种映射机制,映射机制的核心工作就是把用户的代码,数据经过映射机制映射到物理内存当中,总之,凡是要访问物理内存,需要先进行映射,这就是把虚拟内存映射为物理地址的过程
我们就在CPU和物理内存之间引入了虚拟地址和映射机制,非法的行为都被拦截,非法就禁止映射,变相的保护了内存
五、其它问题
1.什么是进程地址空间
每个进程都有地址空间,地址空间的本质在内核中是一种数据结构 ,里面至少含有各个数据的划分
struct addr_room
{
int code_start;
int code_end;
int init_start;
int init_end;
int unInit_start;
int unInit_end;
int heap_start;
int heap_end();
int stack_start;
int stack_end;
.....
其他的属性
}
这个结构体叫做mm_struct
在PCB当中,就有一个struct mm_struct*mm来指向它自己的进程地址空间
2.映射关系谁来维护
1.映射关系是操作系统自己维护的
2.映射关系是一种表结构,称为(用户级)页表
3.地址空间和页表是每一个进程独一份的
4.只要保证,每一个进程的页表,映射的是物理内存的不同区域,就能做到不互相干扰,进而保证进程的独立性(就是拿一张表找到位置,存数据)
所以,子进程继承父进程,可以做到同一个地址,值不同,就是因为有地址空间的存在,子进程是会继承父进程的大部分属性的,其中就包括进程地址空间
3.写时拷贝
如果父子进程的g_val值是一样的话,在物理内存当中只会开辟一块空间,而当修改某一进程的g_val的时候,才会重新开辟另一块空间,这就叫做写时拷贝
4.编译的时候就已经有地址
其实可执行程序编译的时候就已经有地址了,地址空间不仅仅是OS要遵守,编译器也是要遵守的,编译器在编译代码的时候,已经形成了各个区域,并且采用和Linux内核当中一样的编址方式,给每一个 变量,每一行代码都进行了编址,所以程序在编译的时候,每一个字段早已具有了一个虚拟地址(虚拟地址也是数据,加载的时候也要加载)。比如执行过程中的函数调用,就会根据这个地址去调用相应的函数(通过地址来标定逻辑)
链接动态库就是再链接的时候,把程序当中调用库中的函数的地址拷贝到可执行程序中。
当程序加载到内存的时候,每行代码,每个变量也就通过编译器形成的虚拟地址来映射到物理内存。
六、为什么要存在地址空间?
1.凡是非法的访问或者映射,操作系统都会识别到并终止这个进程
因为地址空间和页表是OS创建和维护的,也就意味着一定要在OS的监管之下进行访问,也便保护了物理内存中的合法数据,包括各个进程,以及内核的相关有效数据
进程崩溃的本质就是进程退出,是OS杀掉了这个进程
2.因为有地址空间的存在,有页表的映射的存在,所以我们可以对数据进行任意位置的加载(如果不是这样,而是直接访问内存,首先不考虑安全问题的情况下,我们还需要对内存碎片进行管理,这其实是特别不好弄的)
物理内存的分配就可以和进程的管理,做到没有关系(进程通过地址空间管理)
内存管理模块 vs 进程管理模块 ,做到解耦合,减少关联性,降低维护成本。
物理地址的申请内存,是由操作系统自动完成的,用户,进程,完全0感知
比如我们在new malloc的时候,本质上也是在虚拟内存上申请的空间
如果我申请了物理空间,不立马使用,就是浪费了空间
所以,因为有地址空间的存在,上层申请空间,其实是在地址空间上申请的。物理内存甚至可以一个字节都不给,当你真正对物理 地址空间进行访问的时候,才执行内存的相关管理算法,帮我申请内存,构建页表 映射关系,然后再让我进行内存的访问。这种虚拟内存分配空间,而物理内存没有分配的技术叫做缺页中断
其实就是延迟分配,用这种策略来提高整机效率
3.直接访问物理内存的话是乱序的,地址空间+页表的存在,可以将内存分布有序化!(通过映射,虚拟内存有序,物理内存依旧乱序),有了映射,保证映射到不同的地址,就可以很容易的做到进程独立性的实现