在我们学习C语言和C++的时候经常会看到很多地址,这些地址通常都是被存放到对应的指针中的,那么这些地址到底指的是内存中的物理地址还是啥呢?显然今天这边文章就是来带大家解开这个谜底
从上面的图我们可以看出,程序地址空间中存在一些相关的区域:正文代码,初始化数据,未初始化数据,堆,共享区,栈,命令行和环境变量,内核空间,除了内核空间,其他空间都属于用户空间,所占的空间大小是3G,我们今天主要研究用户空间的各个区域,内核空间今天暂不讨论。
正文代码主要是指一些函数代码,比如常见的main函数就属于正文代码
我们在写代码的时候通常需要声明一些变量,当我们对一个变量声明之后并给予赋初始值,那么该变量就属于初始化数据,初始化数据又可分为初始化全局数据和初始化局部数据,其实就是全局变量和局部变量的差别
我们在写代码的时候通常需要声明一些变量,当我们对一个变量声明之后未给予赋初始值,那么该变量就属于未初始化数据,未初始化数据又可分为未初始化全局数据和未初始化局部数据,其实就是全局变量和局部变量的差别
在写代码的时候如果是通过动态内存开辟的空间一般都是存在于堆上的,常见的有C语言中的malloc,calloc,realloc函数
开辟的内存块和C++中new
开辟的内存块都是属于堆区上的内存
共享区这里暂不讨论,在后面进程间通信会展开说明
我们通常会在函数栈帧中创建很多变量,这个变量只能在函数栈帧中存在,出了函数栈帧,这个变量就会被销毁,我们称这个变量具有临时性,这个变量即为局部变量,存在于栈上
命令行参数指的是在命令行上敲入的一些参数,main函数的第二个参数就是负责获取命令行参数的,比如选项和命令等
环境变量:main函数的第三个函数就是负责接收环境变量的,在上篇文章(环境变量)中已经着重进行讲解
源文件addr.c
自动化构建项目makefile文件
我们一般在C语言函数中定义的变量都是属于局部变量,保存在栈上的,先定义的变量的地址会更高
源文件addr.c
自动化构建项目makefile文件
我们通过对比会发现,static修饰的静态局部变量存放的地址和全局变量存放的地址非常接近,其实static修饰局部变量时,该局部变量本质上已经变成了一个全局变量,当函数中的局部变量被
static
修饰时,编译器会将其编译进全局区
(1).物理地址:指在内存中实实在在存在的位置
(2).虚拟地址:并不是内存中存在的位置,而是通过一个叫页表的转换工具,将内存中的物理地址转化称虚拟地址
(3).物理地址和虚拟地址的关系
这里的虚拟地址就是在我们学习C语言中指针存放的地址,也就是说CPU拿到的是虚拟地址,然后可以通过页表转化成内存中的物理地址进而找到相关的代码
实验思路:利用fork()
创建子进程,从而系统中会多出一个进程,也就是现在有两个进程(父子进程),让父子进程同时访问已经设置好的全局数据,通过修改和不修改两种情况观察实验结果的差异
其实在每一个进程建立的时候,操作系统不仅会为进程创建一个PCB,同时还会为每一个进程创建一个进程地址空间,那么我们就可以知道,每一个进程都有自己独立的进程地址空间,那么这样系统中的进程地址空间就会非常多,操作系统就需要对这些进程地址空间进行管理和控制,而管理的本质就是先描述再组织,描述的意思就是为进程地址空间创建一个结构体,再Linux系统中,这个结构体叫做:mm_struct
,每一个进程都是相对独立,互不影响的,每一个进程中的PCB和mm_struct都是相互独立的,这就是进程的独立性
start
和end
来维护的,也就是说,如果我们向改变对应区域的大小,我们可以通过设置对应区域的start
和end
进行修改即可,在每一个区域的start和end中会包含很多的地址,这个地址就是所谓的虚拟地址,不是物理地址,物理地址是存在于内存中的,不是存在进程地址空间的代码被编译形成可执行程序之后是存在对应的地址的,也就是说程序中的每一段代码在程序中的位置已经确定,这个地址是代码在程序中的地址,与内存中的虚拟地址是没有任何关系的
代码被编译成可执行程序之后,在可执行程序中是存在相关区域的,存在的区域有:正文代码,初始化数据区,未初始化数据区,命令行参数和环境变量,这时需要注意:并不存在栈区和堆区,栈区和堆区是要等程序加载到内存中才存在的
物理地址是在代码在真正的内存中存在的地址(位置)
虚拟地址是指CPU直接能够访问到的地址,并不是相关代码在内存中的真实地址,这个虚拟地址的作用就是能够通过页表相关的映射关系转化成代码在内存中的物理地址
因此,我们可以知道,我们一旦有一个代码的虚拟地址还有页表的映射关系,其实就相当于我们有了代码在内存中的物理地址,虚拟地址和物理地址是通过页表建立联系的
进程地址空间是一个内核数据结构mm_struct
,它其实是操作系统给系统中的每一个进程画的一个大饼,就是给系统中的每一个进程一种错觉,让自己觉得自己能够占有整个系统中的所有资源(内存),当程序加载到内存形成进程的时候,操作系统不仅会给进程创建一个PCB,而且还会给每一个进程创建一个进程地址空间,其对应的内核数据机构叫:mm_struct
,每一个进程都独自拥有一个进程地址空间 ,每一个进程的进程地址空间中都包含有内存中的各个区域:正文代码,初始化数据,未初始化数据,堆区,共享区,栈区,命令行参数和环境变量,给对应的进程一种错觉就是拥有系统内存的所有资源,其实实际上并不是的,实际上每一个进程只能分到系统内存中的一部分,而不可能拥有系统内存的所有
页表是进程地址空间和物理内存之间存在的一个工具,其作用就是负责利用其中虚拟地址和物理地址的映射关系实现虚拟地址和物理地址之间的相互转化,也就是说有了虚拟地址和页表,我就可以找到对应的物理地址
磁盘是代码被编译形成可执行程序之后未加载到内存存在的地方,属于外设
写时拷贝是指当数据被修改的时候,系统会在内存中重新为该数据开辟一块新空间,将该数据原来的内存拷贝放到新空间,然后再在新空间对该数据进行修改
这个实验的思路很简单,就是利用fork()系统调用接口创建一个子进程,使原本只有一个进程变成了两个进程,然后让两个进程区访问程序中的同一个数据,当我们不对数据进行修改的时候,父子进程访问的数据的结果和数据的地址是一样的,当我们使用父子进程中的任何一个进程对该数据进行修改的话,那么后续继续让父子进程同时访问该数据就会出现不同的结果
fork()
调用之后出现两个返回值的问题pid_d id是属于父进程栈空间的变量,fork()函数内部return会被执行两次,return的本质就是将保存在寄存器上的值写入到接收返回值的变量中,当id = fork();的时候,谁先返回,谁就要发生写时拷贝,所以,同一个变量,会有不同的内容,本质是因为这个变量的虚拟地址是一样的,但是会有不同的物理地址,过程和上面的例子差不多
如果没有进程地址空间,那么就是task_struct直接对物理地址进行访问,那么如果有时出现代码写错,出现访问越界,或者野指针,或者指针指向操作系统的代码,那么当我们修改的时候,就会对其他代码造成影响,同时也会会导致物理内存的利用率低下,且访问控制薄弱
当我们向系统申请一块空间,比如使用malloc函数来申请空间的时候,系统不会马上去实际的物理内存中申请,只会在进程地址空间中的堆区上将对应的区域放大,当系统检测到此时需要访问到那块内存的时候,系统才会马上向内存申请对应空间,这样就大大地增大了内存资源的利用率
程序编译时确定的地址都是虚拟地址,但是访问的时候操作系统会将这个虚拟地址在页表中映射得到物理内存地址,进而访问物理内存区域,这样的话,每个进程都有自己独立的虚拟地址,跟其他进程互不影响,但是数据可以在物理内存任意位置存储,因为可以通过页表映射访问到实际物理存储的位置,实现了数据在物理内存上的离散存储,提高了内存利用率,并且可以在页表中对地址访问加以权限访问,提高了内存访问控制,程序运行时,其中中的数据和指令被打散在物理内存中存储,同时在页表中记录对应数据虚拟地址和物理地址的映射关系,以便于进程在虚拟地址访问的时候,操作系统能够通过映射找到物理地址进而访问物理内存