【解锁创意之门:环境变量引领你的编程奇思妙想】(中):https://developer.aliyun.com/article/1425753
此时我们就看不到我们刚刚设置的环境变量myenv和本地变量hello了。还有一个问题,我们之前提到了如果我们修改了系统的某一个环境变量,我们只需要关掉我们的Xshell,然后再次启动,此时环境变量就被恢复了目前我们见到的环境变量都是内存级别的,所以bash进程退出了,那么这个环境变量也就没有了,那么下次启动bash环境变量是从哪里来的呢?答案是磁盘里文件当中。当你启动bash时,它会按照特定的顺序读取和执行这些文件,从而设置相应的环境变量。这确保了在每个用户登录时和每个Shell启动时都能够获得正确的环境变量。那我们来看看这个文件
bash_profile:这是一个Bash shell的配置文件,它在用户登录时执行一次。如果存在,通常位于用户的主目录下,文件名为.bash_profile。
那我们想自己导入一个环境变量呢?export MYVAL=youcanseeme
然后我们退出我们的Xshell,看看再次启动的时候是否有刚刚设置的环境变量。
此时我们就可以刚刚设置的环境变量。除了从bash_profile中导入我们的环境变量,其实更多的是从basrc中导入的。
而里面又是etc/bashrc中导入的,我们可以打开观看一下。由于里面都是脚本文件,我们这里就不多多介绍了。
总结:是什么为什么怎么办?
- 1.环境变量是有系统提供的一组变量,每一个环境变量都有它的用途
- 2.在不同的场景下,执行某些对应的工作或者任务时,是需要知道它的更多属性的,比如平时创建的文件的时候,它就知道此时文件的拥有者是谁。
- 3.指令操作、代码操作、三种获取环境变量的方式和环境变量的特性。
4.程序地址空间
由于64位下的程序地址空间比较复杂,我们下面展示的都是kernel 2.6.32内核32位平台下
4.1.程序地址空间图
上面的图我们以及不陌生了,在前面的c语言专栏这张图就已经出现过了,今天我们再来看这张图,来验证一下程序地址空间的特性。
运行结果:
从上面的结果我们可以发现地址是和我们上面的程序地址空间图变化一致的。我们发现栈地址和堆地址之间相差非常大,可以确定它们之间出现非常大的镂空。下面我再来验证其他的特性:堆区向上(高地址)增长,而栈区(低地址)增长。我们来看一段代码看一下他们的增长方向。
我们来看一下运行结果:
此时就能验证上面的结果,即堆栈相对而生。我们看到程序地址空间图还要环境变量和命令行参数,我们再来看一下他们
这里我们给命令行带上-a -b -c参数,然后再来看一下结果:
可以得出命令行参数表(每个表表向的地址)和环境变量表(每个表表向的地址)都比栈大,且环境变量表最大。那如果我们这样写呢?
运行结果:
这里我们就有一个结论了,无论是表,还是表指向的项目,都在栈上。
同时根据上面的程序地址空间图,已初始化和未初始化是我们的全局变量,会在我们的进程运行期间,一直运行!如果我们再定义一个变量,然后将其用static修饰会怎样呢?
我们发现被static修饰的变量的地址会居于已初始化和未初始化之间,为什么呢?因为static修饰的变量会自动初始化成0,这也就是为什么static修饰的变量不会随着局部函数调用而销毁,因为此时在语言上已经变成全局的了。问题又来了,上面的程序地址空间图是我们的内存吗?我们来看一下代码
运行结果:
我们发现,输出出来的变量值和地址是一模一样的,很好理解呀,因为子进程按照父进程为模版,数据本来就是被父子进程所共享的,父子并没有对变量进行进行任何修改。可是将代码稍加改动:
运行结果:
我们子进程将我们的数据修改之后,父子进程输出不同的值,这也很合理,因为此时发生了写时拷贝,父子进程是两个进程,两个进程之间相互独立,他们之间不能发生数据干扰,对于父子进程而言他们应该是访问的是不同的变量,但是此时变量的地址确实一样的。我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
- 但地址值是一样的,说明,该地址绝对不是物理地址!
- 在Linux地址下,这种地址叫做 虚拟地址
- 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理
- OS必须负责将 虚拟地址 转化成 物理地址 。
所以就可以得出结论:上面的程序地址空间图不是我们的物理内存,而是我们的进程地址空间。
5.进程地址空间
5.1.什么是进程地址空间
所以之前说‘程序的地址空间’是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?看图:
在多进程编程中,当创建子进程时,操作系统会使用写时复制(Copy-on-Write,COW)技术来优化内存的使用。写时复制是一种延迟内存拷贝的策略,它允许父子进程共享相同的物理内存页,直到其中一个进程尝试修改该页的内容时,才会真正进行拷贝。
当在子进程中修改数据时,由于发生了写操作,操作系统会复制相应的内存页,确保父子进程不再共享相同的物理内存页。因此,尽管父子进程最初访问相同的变量地址,但在写操作之后,它们实际上拥有各自的拷贝,此时只有相同的虚拟地址,而真正的物理地址是不相同的。
这就是为什么观察到父子进程输出的地址相同但变量内容不同的原因。这种行为确保了父子进程在修改数据时互不干扰,因为它们实际上操作的是各自私有的内存拷贝。
上面的图就足矣说名问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!
现在我们再来看看什么是进程地址空间?
进程地址空间是指操作系统为每个运行中的进程分配的内存空间。每个进程都有自己独立的地址空间,这使得不同进程能够独立运行,而不会相互干扰。这也就是上面的富豪给自己每个孩子独立画出的大饼,使得每个孩子都认真学习,每个孩子将来都会继承亿万遗产。这样每个进程有自己的地址空间,这样做的好处是,每个进程都认为它是系统中唯一的运行程序,都能享有4G的大空间,它可以独立地访问和修改自己的内存空间,而不会影响其他进程的内存。
进程地址空间本质其实就是pcb中的数据结构,那如何理解上面进程地址空间的各个区域呢?
在操作系统中,进程地址空间的属性通常会通过一些整型变量描述。这些信息可以存储在进程控制块(PCB)数据结构中,被一个struct mmstruct所管理。我们来看一下源码。
区域划分的本质就是区域内的各个地址都可以使用!!!但是我们的地址空间,不具备对我们的代码和数据的保存能力!代码和数据没有存放在地址空间,而是在物理内存中存放的!此时就需要将地址空间的虚拟地址转化到物理内存中!此时操作系统就给我们提供了一张表 - 页表!虚拟地址是给进程的,进程是你的,所以这个虚拟地址是给用户的。
存放页表的地址通常是物理地址,而不是虚拟地址。页表是操作系统用来进行虚拟地址到物理地址映射的关键数据结构之一。在典型的虚拟内存系统中,每个进程都有自己的页表,这个页表负责将进程中的虚拟地址映射到物理地址。
因为页表本身是由操作系统管理的数据结构,需要在物理内存中保留,以确保对它的快速访问。如果页表本身存放在虚拟地址空间中,那么就需要在访问页表时进行额外的虚拟地址到物理地址的映射,这会导致循环依赖问题,因为访问页表的过程本身需要页表。
在多进程的环境中,不同进程的地址空间是相互独立的,互不干扰。这也是为什么在多进程编程中,父子进程可以拥有相同的变量地址,但在写时复制的过程中,它们的地址空间会逐渐分离,以确保彼此之间的数据不会相互影响。
5.2.为什么要有进程地址空间 + 页表
1.将物理内存从无序变成有序:
- 虚拟内存映射到物理内存: 进程通常拥有一个虚拟地址空间,该空间被划分为若干段,如代码段、数据段等。虚拟地址并不直接对应物理内存,而是通过页表进行映射关系的管理。
- 分页技术: 将虚拟地址空间和物理内存划分为固定大小的页,以便更灵活地管理内存。这样,操作系统可以按需将虚拟页面映射到物理内存中,使得物理内存的使用变得有序,而非零散的分布。
2.解耦合进程管理和内存管理:
- 独立性:进程地址空间的引入使得进程的创建、撤销和切换变得相对独立于底层物理内存的管理。进程管理的任务(例如上下文切换、进程调度)与内存管理的任务(例如分配、回收内存)可以相对独立地进行。这使得操作系统更加模块化,易于扩展和维护。
- 更好的资源隔离:每个进程都有自己的地址空间,一个进程的错误不会直接影响其他进程的内存。这提高了系统的稳定性和安全性。
3.保护内存的重要手段:
- 地址空间隔离:不同的进程有不同的页表,因此它们的虚拟地址空间是相互隔离的。这种隔离可以防止一个进程直接访问另一个进程的数据,提高了系统的安全性。
5.3.malloc申请的内存会立即使用吗,本质是在虚拟地址申请的还是物理地址申请的呢
malloc函数是用于在C语言中动态分配内存的函数。当调用 malloc 时,它会返回一个指向分配的内存块的指针。这个内存块在逻辑上被视为可用的,但在物理上可能并没有被实际分配。
具体来说,malloc 会在进程的虚拟地址空间中分配一块指定大小的内存,并返回这块内存的起始地址。但是,这个时候并没有真正分配物理内存。物理内存的分配通常是在程序试图访问这块内存时进行的,这就是所谓的"延迟分配"。
当程序开始使用 malloc 返回的指针来写入数据时,操作系统会在需要的时候分配物理内存,并将虚拟地址空间中的页面与实际的物理内存页关联起来。这个过程被称为""缺页中断"。操作系统会负责将相应的页面加载到物理内存中,并将适当的页表项设置为指向这个物理内存页。
所以,malloc 返回的内存块在逻辑上是立即可用的,但在物理上并不一定立即分配。分配的物理内存是在程序首次访问这块内存时动态完成的。这种延迟分配的机制可以帮助操作系统更高效地管理内存,只为程序实际需要的部分分配物理内存,充分提高内存的使用率。