【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.hqwc.cn/news/498258.html

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

相关文章

Linux Centos7配置SSH免密登录

Linux Centos7配置SSH免密登录 配置SSH免密登录说明&#xff1a; 分两步 第一步、给Server A生成密钥对 第二步、给Server B授权 生成密钥对之后&#xff0c;我们可以看看它保存的目录下的文件。 接下来我们就要把Server A&#xff08;10.1.1.74&#xff09;的公钥拷贝到Se…

Vuepress的使用

介绍 将markdown静态资源转换成html。 动态资源的转换还有很多&#xff0c;为什么要使用Vuepress&#xff1f; 目录分析 项目配置 详情 具体配置请看文档 插件配置 vuepress-theme-vdoing 主题插件 npm install vuepress-theme-vdoing -D先安装依赖配置主题 使用vuep…

八、ActiveMQ持久化

ActiveMQ持久化 一、MQ的高可用二、持久化介绍三、持久化存储方式1.AMQ Mesage Store(了解&#xff09;2.KahaDB消息存储(默认)2.1 存储原理 3.JDBC消息存储4.LevelDB消息存储(了解)5.JDBC Message Store with ActiveMQ Journal查询持久化存储方式 四、持久化存储使用1.JDBC消息…

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. 测试…

如何系统地学习Python:一份全面指南

在当前的技术环境中&#xff0c;Python已经成为了最受欢迎的编程语言之一。无论你是想进入数据科学领域&#xff0c;开发网站&#xff0c;还是简单地想提高工作效率&#xff0c;学习Python都是一个明智的选择。本文将为你提供一个系统学习Python的详细指南&#xff0c;包括理论…

Keepalived 双机热备基础知识

7.1 Keepalived 双机热备基础知识 Keepalived起初是专门针对LVS设计的一款强大的辅助工具&#xff0c;主要用来提供故障切换(Failover) 和健康检查査(Health Checking)功能一一判断LVS 负载调度器、节点服务器的可用性&#xff0c;及时隔离并替 换为新的服务器&#xff0c;当故…

【文生视频】Diffusion Transformer:OpenAI Sora 原理、Stable Diffusion 3 同源技术

文生视频 Diffusion Transformer&#xff1a;Sora 核心架构、Stable Diffusion 3 同源技术 Sora 网络结构提出背景输入输出生成流程变换器的引入Diffusion Transformer (DiT)架构Diffusion Transformer (DiT)总结 OpenAI Sora 设计思路阶段1: 数据准备和预处理阶段2: 架构设计阶…

从0到1实现五子棋游戏!!

Hello&#xff0c;好久不见宝子们&#xff0c;今天来给大家更一个五子棋的程序~ 我们今天要讲的内容如下&#xff1a; 文章目录 1.五子棋游戏介绍1.1 游戏玩法介绍&#xff1a; 2.准备工作2.1 具体操作流程 3.游戏程序主函数4.初始化棋盘4.1.定义宏变量4.2 初始化棋盘 5.打印…

从尺寸到实用性,教你如何设计理想电脑桌。福州中宅装饰,福州装修

设计卧室书桌&电脑桌时&#xff0c;需要考虑多个方面和注意事项&#xff0c;以确保书桌既实用又舒适。以下是一些关于卧室书桌&电脑桌设计的建议&#xff1a; ❶书桌&电脑桌的尺寸 根据卧室的空间大小和个人需求&#xff0c;确定合适的书桌尺寸。一般来说&#xf…

back-side illumination (BSI)

目录 原理学习资料 原理 这个术语有点误导作用&#xff0c;其实是把先前的在上面的层放在了后面&#xff0c;后面的层放在了前面。 以前的制造工艺是金属布线放在了前面&#xff0c;会挡住部分光线&#xff0c;以前的像素点比较大&#xff0c;没什么问题&#xff0c;后来像素…

JVM(3)

垃圾回收(GC)相关 在C/C中,当我们使用类似于malloc的内存开辟,还需要手动释放内存空间,这样的机制在使用时给我们造成了诸多不便,但在Java中,有垃圾回收这样的机制,这就是指:我们不再需要手动释放,程序会自动判定,某个内存空间是否可以继续使用,如果内存不使用了,就会自动释放…

疾控中心的污水采样瓶用的是什么材质

疾控中心的污水采样瓶采用的材质是聚乙烯或聚丙烯塑料。这种材质的污水采样瓶具有耐腐蚀、耐高压、无毒无味、重量轻、易于携带等优点。此外&#xff0c;这种材质的污水采样瓶还可以在高温下消毒&#xff0c;不会变形或破裂。 疾控中心的污水采样瓶通常有不同的容积和形状&…