【Linux深入剖析】再续环境变量 | 进程地址空间


📙 作者简介 :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的速度

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/708768.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

FX110网:外汇交易中的隔夜利息该如何计算?

在交易过程中&#xff0c;我们经常能够听到 “隔夜利息”这个词&#xff0c;但不少新手依然不是很明白这个专业名词的意思。今天小编帮助大家理解这个概念。“隔夜利息”的含义 顾名思义&#xff0c;是根据持仓总数计算的每日可赚取或需支付的利息。每个货币都有他们自己的基准…

蓝桥杯备赛第三篇(图论)

1.邻接表 static class Edge {int next;int value;public Edge(int next, int value) {this.next next;this.value value;}}static HashMap<Integer, LinkedList<Edge>> graph new HashMap<>();public static void addEgde(int from, int to, int value) …

贝叶斯优化双向门控循环单元BO-BIGRU时序预测的matlab实现【源代码】

贝叶斯优化双向门控循环单元简介&#xff1a; 贝叶斯优化双向门控循环单元&#xff08;BO-BIGRU&#xff09;是一种结合了贝叶斯优化和双向门控循环单元&#xff08;BIGRU&#xff09;的神经网络模型。BIGRU是一种改进的循环神经网络&#xff08;RNN&#xff09;&#xff0c;它…

go语言通过切片实现先进后出逻辑

目录 一、go语言的通道: 二、go语言实现先进后出: 一、go语言的通道: Go的通道(Channel)是先进先出(FIFO)的数据结构,它保持了发送数据和接收数据的顺序。当你向通道发送数据时,数据会被放入通道的尾部;而从通道接收数据时,会从通道的头部取出数据。这确保了数据的…

现代信号处理学习笔记(二)参数估计理论

参数估计理论为我们提供了一套系统性的工具和方法&#xff0c;使我们能够从样本数据中推断总体参数&#xff0c;并评估估计的准确性和可靠性。这些概念在统计学和数据分析中起着关键的作用。 目录 前言 一、估计子的性能 1、无偏估计与渐近无偏估计 2、估计子的有效性 两个…

Python入门到精通(九)——Python数据可视化

Python数据可视化 一、JSON数据格式 1、定义 2、python数据和JSON数据转换 二、pyecharts 三、折线图 四、地图 五、动态柱状图 一、JSON数据格式 1、定义 JSON是一种轻量级的数据交互格式。可以按照JSON指定的格式去组织和封装数据JSON本质上是一个带有特定格式的字符…

小程序 API 能力汇总——TYML NodesRef API

NodesRef 用于获取 TYML 节点信息的对象 方法 SelectorQuery NodesRef.fields(Object fields, NodesRef.FieldsCallback callback) 获取节点的相关信息。需要获取的字段在 fields 中指定。返回值是 nodesRef 对应的 selectorQuery SelectorQuery NodesRef.boundingClientR…

嘴尚绝卤味传统与创新的完美结合

在当下这个美食文化丰富多彩的时代&#xff0c;卤味作为一种深受大众喜爱的食品&#xff0c;不仅承载着传统的烹饪智慧&#xff0c;更在不断创新中展现出新的魅力。嘴尚绝卤味&#xff0c;作为卤味市场中的佼佼者&#xff0c;凭借其独特的优势&#xff0c;正逐渐成为消费者心中…

java高级——动态代理

目录 动态代理介绍明星代理案例实现案例分析动态代理应用场景 动态代理介绍 用一个明星的案例来解释动态代理的流程。 假设现在有一个明星坤坤&#xff0c;它有唱歌和跳舞的本领&#xff0c;作为明星是要用唱歌和跳舞来赚钱的。但是每次做节目&#xff0c;唱歌的时候要准备话…

阿里云2024年服务器2核4G配置评测_CPU内存带宽_优惠价格

阿里云2核4G服务器多少钱一年&#xff1f;2核4G服务器1个月费用多少&#xff1f;2核4G服务器30元3个月、85元一年&#xff0c;轻量应用服务器2核4G4M带宽165元一年&#xff0c;企业用户2核4G5M带宽199元一年。本文阿里云服务器网整理的2核4G参加活动的主机是ECS经济型e实例和u1…

卢森堡比利时土耳其媒体宣发稿助力跨境出海推广新闻营销

【本篇由言同数字科技有限公司原创】随着全球化进程的加速&#xff0c;越来越多的品牌开始考虑在海外市场扩展业务。对于品牌来说&#xff0c;跨境海外推广是必要的&#xff0c;因为它可以帮助品牌打开更大的市场、吸引更多的消费者、提高品牌知名度和形象&#xff0c;并在全球…

Linux磁盘性能方法以及磁盘io性能分析

Linux磁盘性能方法以及磁盘io性能分析 1. fio压测1.1. 安装fio1.2. bs 4k iodepth 1&#xff1a;随机读/写测试&#xff0c;能反映硬盘的时延性能1.3. bs 128k iodepth 32&#xff1a;顺序读/写测试&#xff0c;能反映硬盘的吞吐性能 2. dd压测2.1. 测试纯写入性能2.2. 测试…

MurmurHash算法

MurmurHash&#xff1a;(multiply and rotate) and (multiply and rotate) Hash&#xff0c;乘法和旋转的hash 算法。 一、哈希函数 散列函数&#xff08;英语&#xff1a;Hash function&#xff09;又称散列算法、哈希函数&#xff0c;是一种从任何一种数据中创建小的数字“…

抖音小店新店没有体验分怎么办?怎么从零做体验分?新手商家速看

大家好&#xff0c;我是电商花花。 新手开店的体验分都不是很高&#xff0c;我们想要做店铺体验分都要从零开始做。 如果新手开店不需要怎么出体验分&#xff0c;不知道怎么提高店铺体验分的&#xff0c;都可以看一下今天的文章&#xff0c;教大家怎么做店铺的体验分。 首先&…

基于springboot + vue实现的前后端分离-汽车票网上预定系统(项目 + 论文)

项目介绍 系统是一个B/S模式系统&#xff0c;采用Spring Boot框架&#xff0c;MySQL 数据库设计开发&#xff0c;充分保证系统的稳定性。系统具有界面清晰、操作简单&#xff0c;功能齐全的特点&#xff0c;使得汽车票网上预订系统管理工作系统化、规范化。本系统的使用使管理人…

JVM——JVM与Java体系结构

文章目录 1、Java及JVM简介1.1、Java是跨平台的语言1.2、JVM是跨语言的平台 2、Java发展里程碑3、Open JDK和Oracle JDK4、虚拟机与JVM4.1、虚拟机4.2、JVM 5、JVM整体结构6、Java代码执行流程7、JVM的架构模型7.1、基于栈式架构的特点7.2、基于寄存器架构的特点 8、JVM的生命周…

React.FC详细说明以及案例

React.FC是React中用于定义函数式组件的一种类型。它是React.FunctionComponent的缩写&#xff0c;表示一个接收props作为输入并返回JSX元素的函数组件。React.FC提供了一种在TypeScript中使用的方式&#xff0c;允许我们为组件提供props的类型定义&#xff0c;并且可以利用Typ…

Unity3D 兰伯特漫反射光照模型详解

前言 Unity3D 提供了丰富的功能和工具&#xff0c;让开发者可以轻松创建出高质量的游戏。其中&#xff0c;光照模型是游戏中非常重要的一部分&#xff0c;它可以让游戏场景看起来更加真实和生动。在 Unity3D 中&#xff0c;我们可以使用不同的光照模型来实现不同的效果&#x…

网络基本类型

机器之间的通信是一个复杂的过程&#xff0c;它体现了大问题的复杂性。本章主要从“模型和结构”的计算思维概念&#xff0c;介绍网络通信的方法&#xff1b;并且用“安全”的概念&#xff0c;介绍网络攻击的防护方法&#xff0c;以及信息的加密和解密。 ▶1.互联网的发展 19…

嵌入式驱动学习第一周——定时器与延时函数

前言 这篇博客一起学习定时器&#xff0c;定时器是最常用到的功能之一&#xff0c;其最大的作用之一就是提供了延时函数。 嵌入式驱动学习专栏将详细记录博主学习驱动的详细过程&#xff0c;未来预计四个月将高强度更新本专栏&#xff0c;喜欢的可以关注本博主并订阅本专栏&…