作者简介 :RO-BERRY
学习方向:致力于C、C++、数据结构、TCP/IP、数据库等等一系列知识
日后方向 : 偏向于CPP开发以及大数据方向,欢迎各位关注,谢谢各位的支持



目录

  • 1.环境变量再续
    • 1.1 和环境变量相关的命令
    • 1.2 环境变量的组织方式
    • 1.3 通过代码如何获取环境变量
    • 1.4 本地变量
    • 1.5 疑问
      • 查看环境变量配置文件
  • 2.进程地址空间
    • 2.1程序地址空间
      • 验证一
      • 验证二
      • 验证三
      • 验证四
      • 验证五
    • 2.2 奇怪的现象
    • 2.3 进程地址空间
    • 2.4 什么是地址空间
    • 2.5 为什么要有地址空间+页表

1.环境变量再续

1.1 和环境变量相关的命令

  1. echo: 显示某个环境变量值
  2. export: 设置一个新的环境变量
  3. env: 显示所有环境变量
  4. unset: 清除环境变量
  5. set: 显示本地定义的shell变量和环境变量

1.2 环境变量的组织方式

每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串

1.3 通过代码如何获取环境变量

  • 1.命令行第三个参数

前面讲述过main函数可以带两个参数,第一个参数是命令行参数的个数,第二个参数是存储命令行参数的指针数组
其实main函数还可以带第三个参数,那就是我们的环境变量
我们来打印一下试试看

#include<stdio.h>#include<string.h>#include<stdlib.h>int main(int argc,char* argv[],char *env[]){for(int i=0;env[i];i++){printf("---------------env[%d] -> %s\n",i,env[i]);}return 0;}

运行结果:

可以看到这里就是我们系统的所有环境变量

  • 2.函数getenv

getenv(环境变量名)—>得到一个环境变量,根据名字获得内容


测试代码:

#include<stdio.h>#include<string.h>#include<stdlib.h>int main(){const char *username=getenv("USER");if(username)printf("username: %s\n",username);elseprintf("None\n");return 0;}

测试结果:

这个函数可以用来实现限制权限,使用匹配函数将USER环境变量限定为等于某个用户,如果为其他用户访问此文件则输出你没有权限访问

  • 3.通过第三方变量environ获取

测试代码:

#include <stdio.h>int main(int argc, char *argv[]){ extern char **environ;//extern相当于声明,也可以在命令行使用可以设置自定义环境变量  int i = 0; for(; environ[i]; i++){ printf("%s\n", environ[i]); } return 0;}

测试结果:


1.4 本地变量

除了环境变量,还有本地变量,可以直接在命令行上输入变量名=内容,就可以得到一个本地变量
例如:
我们定义本地变量hello,使用echo指令查看其值

  • 本地变量无法使用env指令去查找到

  • 我们可以使用指令set进行查找

set:打印出本地变量以及环境变量

注:

环境变量具有全局性
本地变量不具有全局性,只在bash内部可用

1.5 疑问

  • 如何消除环境变量和本地变量?

unset

  • 我们用set打印环境变量以及本地变量的时候,每次都密密麻麻一片,环境变量是在bash的上下文里,bash是我们的命令行解释器,我们不启动Linux时,bash就不会存在,登录后,系统才会给我们分发bash进程,那么一开始bash进程从哪里获得的环境变量呢?

每次重启xshell的时候,环境变量就会更新,我们在这里要说明的是环境变量其实是内存级的变量,也就是说当我们启动xshell的时候,环境变量就会从我们的磁盘获取这些信息,会在其中的某种脚本或者配置文件中获取,也就是说环境变量会天然以文件的方式存储在磁盘。

查看环境变量配置文件

接下来让我们来见一见我们的环境变量配置文件
名为.bash_profile的文件就是我们的环境变量配置文件

vim .bash_profile

我们可以在其中自己创建变量,重新启动xshell即可

添加变量ABCD

  • 重启前
  • 重启后

2.进程地址空间

2.1程序地址空间

我们在学C语言的时候,画过这样的空间布局图:

可是我们对他并不理解
今天我们来进一步对其进行了解


验证一

首先来看各个部分的空间地址
代码:

#include<stdio.h>#include<stdlib.h>int g_unval; //未初始化数据int g_val= 100;//初始化数据int main(){printf("code addr: %p\n",main);printf("init data addr: %p\n",&g_val);printf("uninit data addr; %p\n",&g_unval);char *heap=(char*)malloc(20);printf("heap addr: %p\n",heap);printf("stack addr: %p\n",&heap);return 0;}

执行结果:

结论:低地址–>高地址
正文代码–>初始化数据–>未初始化数据–>堆–>栈


验证二

验证堆中数据存储是从低地址到高地址
栈中数据存储是从高地址到低地址

代码:

#include<stdio.h>#include<stdlib.h>int g_unval; //未初始化数据int g_val= 100;//初始化数据int main(){printf("code addr: %p\n",main);printf("init data addr: %p\n",&g_val);printf("uninit data addr; %p\n",&g_unval);char *heap=(char*)malloc(20);char *heap1=(char*)malloc(20);char *heap2=(char*)malloc(20);char *heap3=(char*)malloc(20);printf("heap addr: %p\n",heap);printf("heap addr: %p\n",heap1);printf("heap addr: %p\n",heap2);printf("heap addr: %p\n",heap3);printf("stack addr: %p\n",&heap);printf("stack addr: %p\n",&heap1);printf("stack addr: %p\n",&heap2);printf("stack addr: %p\n",&heap3);return 0;}

执行结果:

结论:堆栈相向而生
堆中数据存储是从低地址到高地址
栈中数据存储是从高地址到低地址


验证三

验证命令行与环境变量

代码:

#include<stdio.h>#include<stdlib.h>int g_unval; //未初始化数据int g_val= 100;//初始化数据int main(int argc,char *argv[],char *env[]){printf("code addr: %p\n",main);printf("init data addr: %p\n",&g_val);printf("uninit data addr; %p\n",&g_unval);char *heap=(char*)malloc(20);char *heap1=(char*)malloc(20);char *heap2=(char*)malloc(20);char *heap3=(char*)malloc(20);printf("heap addr: %p\n",heap);printf("heap addr: %p\n",heap1);printf("heap addr: %p\n",heap2);printf("heap addr: %p\n",heap3);printf("stack addr: %p\n",&heap);printf("stack addr: %p\n",&heap1);printf("stack addr: %p\n",&heap2);printf("stack addr: %p\n",&heap3);for(int i=0;argv[i];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;}

执行结果:

我们在这里打印的只是环境变量以及命令行参数的表的地址!!!

结论:命令行参数表整体地址比栈区大,并且地址由小到大增长
环境变量表比命令行参数整体地址更大,且地址也是由小到大增长

验证四

验证命令行与环境变量表内数据地址

代码: 输出argv[i]以及env[i]

#include<stdio.h>#include<stdlib.h>int g_unval; //未初始化数据int g_val= 100;//初始化数据int main(int argc,char *argv[],char *env[]){printf("code addr: %p\n",main);printf("init data addr: %p\n",&g_val);printf("uninit data addr; %p\n",&g_unval);char *heap=(char*)malloc(20);char *heap1=(char*)malloc(20);char *heap2=(char*)malloc(20);char *heap3=(char*)malloc(20);printf("heap addr: %p\n",heap);printf("heap addr: %p\n",heap1);printf("heap addr: %p\n",heap2);printf("heap addr: %p\n",heap3);printf("stack addr: %p\n",&heap);printf("stack addr: %p\n",&heap1);printf("stack addr: %p\n",&heap2);printf("stack addr: %p\n",&heap3);for(int i=0;argv[i];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;}

执行结果:

结论:无论是表,还是表指向的项目,都是在栈的地址的上部


验证五

验证未初始化数据以及初始化数据会在进程运行期间,一直都会存在

代码: 定义了一个变量C

#include<stdio.h>#include<stdlib.h>int g_unval; //未初始化数据int g_val= 100;//初始化数据int main(int argc,char *argv[],char *env[]){printf("code addr: %p\n",main);printf("init data addr: %p\n",&g_val);printf("uninit data addr; %p\n",&g_unval);char *heap=(char*)malloc(20);char *heap1=(char*)malloc(20);char *heap2=(char*)malloc(20);char *heap3=(char*)malloc(20);int c=0;printf("heap addr: %p\n",heap);printf("heap addr: %p\n",heap1);printf("heap addr: %p\n",heap2);printf("heap addr: %p\n",heap3);printf("stack addr: %p\n",&heap);printf("stack addr: %p\n",&heap1);printf("stack addr: %p\n",&heap2);printf("stack addr: %p\n",&heap3);printf("c addr: %p\n",&c);for(int i=0;argv[i];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;}

代码二: 修改变量C为static变量

#include<stdio.h>#include<stdlib.h>int g_unval; //未初始化数据int g_val= 100;//初始化数据int main(int argc,char *argv[],char *env[]){printf("code addr: %p\n",main);printf("init data addr: %p\n",&g_val);printf("uninit data addr; %p\n",&g_unval);char *heap=(char*)malloc(20);char *heap1=(char*)malloc(20);char *heap2=(char*)malloc(20);char *heap3=(char*)malloc(20);static int c=0;printf("heap addr: %p\n",heap);printf("heap addr: %p\n",heap1);printf("heap addr: %p\n",heap2);printf("heap addr: %p\n",heap3);printf("stack addr: %p\n",&heap);printf("stack addr: %p\n",&heap1);printf("stack addr: %p\n",&heap2);printf("stack addr: %p\n",&heap3);printf("c addr: %p\n",&c);for(int i=0;argv[i];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;}

执行结果:

代码一可以看到C变量在栈上保存

代码二可以看到变量C并不保存在栈上了

这里的原因是因为:
如果在将变量加static,那么此变量就已经默认为全局变量了


2.2 奇怪的现象

演示代码:

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

演示结果:

如上可以看到,我们父子进程互相具有独立性,子进程将值改为200,但是父进程依然只有100,但是我们可以发现一个现象,那就是g_val的地址是相同的,但是其值不同!!!同一个地址打印出不同的值

如果这个地址是内存里的地址,我们对同一个地址读取出两个不同的值,这是绝对不可能的,所以这里打出来的地址绝对不是物理地址!!!

引入一个概念:

这个地址叫做:虚拟地址/线性地址

结论:
能得出如下结论:
1.变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
2.但地址值是一样的,说明,该地址绝对不是物理地址!
3.在Linux地址下,这种地址叫做 虚拟地址
4.我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理


2.3 进程地址空间

有了上面的铺垫,我们来真正进入到进程地址空间
先直接说结论:

在Linux中是具有虚拟地址的,在Linux里使用的地址都是虚拟地址,虚拟地址空间和真实地址有一个映射关系,这个映射关系是由操作系统维护的一个表来记录的,子进程在继承父进程的时候同样会继承父进程的存储信息,映射关系表,对这些会重新进行拷贝操作, 所以会和父进程里的变量指向同一块地址空间,在这里注意,这里是两个虚拟地址空间指向的同一块内存地址,如上方,我们的子进程修改了g_val的值,按道理说应该改变的是物理地址的值,但是OS为了保证各个进程的独立性,所以OS会在物理空间重新给你开辟一个空间,修改子进程的映射关系表,看上去是指向同一块物理地址,实际上是两块物理地址。

虚拟地址空间以及映射关系表均在操作系统内部

上面的图就足矣说名问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了
不同的物理地址!

2.4 什么是地址空间

进程地址空间,每一个进程,都会存在一个进程空间,32【0,4GB】

进程地址空间的本质是数据结构,具体到进程中,就是特定的数据结构的对象,里面存储的是我们的虚拟地址,由操作系统提供。进程地址空间本质是进程看待内存的方式,抽象出来的一个概念,内核中用一个结构体mm_struct表示,这样每个进程都认为自己独占系统内存资源。

我们的地址空间,不具备对我们的代码和数据的保存能力!在物理内存中存放的!
将地址空间上的地址(虚拟/线性)转化到物理内存中,操作系统给我们的进程提供了一张映射表—页表

在进程控制块task_struct中有一个mm_struct结构体指针,指向一个mm_struct结构体,这个结构体里面完成对各个数据区域的划分,然后通过页表映射到物理内存上。

区域划分:将线性地址空间划分成为一个一个的area | [start, end]

struct area{int start;int end;}

在[start, end]之间的各个地址叫做虚拟地址。


2.5 为什么要有地址空间+页表

  • 将物理内存从无序变有序,让进程以统一的视角看待内存
  • 将内存管理和进程管理进行解耦合
  • 地址空间+页表是保护内存安全的重要手段

如果进程直接访问物理内存,那么我们看到的地址就是物理地址。c语言中可以用指针访问地址,如果指针越界了,有可能直接访问到另一个进程的代码和数据,这样的话进程的独立性无法保证。 因为物理内存暴漏,有可能有恶意程序直接通过物理地址进行内存数据的篡改。所以虚拟地址存在的第一个意义是保护物理内存,不受任何进程的直接访问,这样操作系统就可以在虚拟到物理之间转化的时候方便进行合法性校验。

【扩展】
malloc/new申请内存

1.申请的内存,你会直接在里面使用吗?
不一定
2.申请内存,本质在哪里申请?
进程的虚拟地址空间中申请

操作系统需要为效率和资源使用率负责
1.充分保证内存的使用率,不会空转
2.提升new或者malloc的速度