【Linux】浅谈环境变量和进程地址空间

news/2025/3/22 23:51:27/

一、环境变量

基本概念

环境变量(Environment Variables)是操作系统提供的一种机制,用于存储和传递配置信息、系统参数、用户偏好设置等。

环境变量的作用
  1. 配置程序行为:
    程序可以通过环境变量获取配置信息,例如日志级别、数据库连接信息、API 密钥等。
    例如,LOG_LEVEL=DEBUG 可以设置程序的日志级别为调试模式。
  2. 指定系统路径:
    环境变量可以指定程序或库的路径,例如 PATH 环境变量用于指定系统可执行文件的搜索路径。
    例如,PATH=/usr/bin:/bin:/usr/local/bin 表示系统会在这些目录中查找可执行文件。
  3. 传递用户偏好设置:
    环境变量可以存储用户的偏好设置,例如语言环境(LANG)、时区(TZ)等。
    例如,LANG=en_US.UTF-8 设置了系统的语言环境为美国英语,使用 UTF-8 编码。
  4. 动态配置:
    环境变量可以在运行时动态设置和修改,而无需重新编译程序。
    例如,可以在启动脚本中设置环境变量,以控制程序的行为。

常见的环境变量

  1. PATH
    用途:指定系统可执行文件的搜索路径。
    示例:PATH=/usr/bin:/bin:/usr/local/bin
    说明:当用户在终端中输入命令时,系统会在 PATH 指定的目录中查找可执行文件。
  2. HOME
    用途:指定当前用户的主目录路径。
    示例:HOME=/home/user
    说明:程序可以使用 HOME 环境变量来访问用户的主目录,例如保存配置文件。
  3. LANG
    用途:指定系统的语言环境。
    示例:LANG=en_US.UTF-8
    说明:设置系统的语言环境和字符编码,影响程序的输出和国际化行为。
  4. TZ
    用途:指定系统的时区。
    示例:TZ=America/New_York
    说明:设置系统的时区,影响程序中日期和时间的显示。
  5. LD_LIBRARY_PATH
    用途:指定动态链接器搜索动态库的路径。
    示例:LD_LIBRARY_PATH=/usr/local/lib:/opt/lib
    说明:动态链接器会在 LD_LIBRARY_PATH 指定的目录中查找动态库。
  6. USER
    用途:指定当前登录的用户名。
    示例:USER=zzx
    说明:程序可以使用 USER 环境变量获取当前用户的名称。

查看环境变量

echo $NAME 其中NAME为你要查找的环境变量名称,比如PATH,USER等
在这里插入图片描述

可执行文件的默认执行路径

我们先写一个简单程序

#include <stdio.h>
int main()
{printf("hello world");return 0;
}

对上面的源代码编译形成一个可执行文件 hello ,当我们在命令行输入 hello 来执行这个可执行文件时候发现执行不了,报错 Command 'hello' not found 这是为什么呢?
其实在 Linux 和类 Unix 系统中,可执行文件的默认执行路径是由环境变量 PATH 决定的。PATH 环境变量是一个以冒号(:)分隔的目录列表,操作系统会在这些目录中按顺序查找可执行文件,而 hello 这个命令我们默认执行路径也就是PATH指定的路径,而不是当前路径。
解决方法:

  1. 指定当前路径,执行命令 ./hello
  2. 临时修改 PATH 环境变量。将当前路径添加到 PATH 环境变量中,expot PATH=$PATH:hello
  3. 永久修改 PATH 环境变量,通过修改 shell 配置文件,我们可以在配置文件~/.bashrc 中添加下面这段代码
export PATH=$PATH:你的可执行文件所处路径

然后再执行 source ~/.bashrc 是配置文件生效

  1. 创建符号链接到已有的 PATH 目录。
ln -s /path/to/your/program/your_executable /usr/local/bin/your_executable

如果在 /usr/local/bin 中找到了符号链接 your_executable,操作系统会解析这个符号链接,找到它指向的实际文件 /path/to/your/program/your_executable,并运行该文件。

  1. 使用 alias 为程序创建一个简化的命令。
alias myprogram='/path/to/your/program/your_executable'

将上述命令添加到你的 shell 配置文件中(如 ~/.bashrc 或 ~/.zshrc),以永久生效。

环境变量相关命令

  • export : 在全局环境变量表中设置一个全新的环境变量
  • env : 显示所有环境变量
  • unset : 清楚环境变量
  • set : 显示本地定义的shell变量和环境变量

环境变量的组织方式

在这里插入图片描述
每个程序都有一个环境变量表,环境变量表是一个字符指针数组,每个指针指向一个以‘ \0 ’ 结尾的环境字符串

代码获取环境变量

  • 命令⾏第三个参数
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{int i = 0;for(; env[i]; i++){printf("%s\n", env[i]);}return 0;
}
  • 通过 extern 第三方变量 environ 获取
#include <stdio.h>
int main(int argc, char *argv[])
{extern char **environ;int i = 0;for(; environ[i]; i++){printf("%s\n", environ[i]);}return 0;
}

通过系统调用获取

通过系统调用 getenv(name) 来获取环境变量,其实现我们可以通过 man 手册来查找(man getenv)
在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
int main()
{printf("%s\n", getenv("PATH"));return 0;
}

子进程继承环境变量

  • 环境变量通常具有全局属性,可以被子进程继承下去。
代码演示
#include <stdio.h>
#include <stdlib.h>
int main()
{printf("%s\n", getenv("PATH"));return 0;
}

由我们在编译上面代码形成可执行程序,在运行时就会帮我们打印 PATH 环境变量可知,环境变量是会被子进程继承的(注意: 任何在命令行执行的可执行文件都是由 shell 进行 fork() 创建的子进程,在通过 进程切换 执行的命令,所以上面可进程就是 shell 的子进程)

程序地址空间

程序地址空间布局:
在这里插入图片描述
但上面的地址空间布局就是物理地址空间布局吗,答案是不是的。
我们可以通过一段程序验证

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{pid_t id = fork();if(id < 0){perror("fork");return 0;}else if(id == 0){ //childprintf("child pid %d : %d : %p\n", getpid(), g_val, &g_val);}else{ //parentprintf("parent pid %d : %d : %p\n", getpid(), g_val, &g_val);}sleep(1);return 0;
}

输出结果:

parent pid 4635 : 0 : 0x56436823f014
child pid 4636 : 0 : 0x56436823f014

我们可以看见父子进程的 gval 的值是不一样的,但地址确实一样的,我们就可以得出一下结论:

  • 我们看到的地址值绝对不是物理地址,而是虚拟地址,即我们在C/C++语言中看到的地址都是虚拟地址!而不是物理地址,物理地址用户是看不到的,统一由操作系统管理
  • OS必须负责虚拟地址带物理地址的转换

进程地址空间

由上面可知 程序地址空间 的说法不准确,那应该为什么呢,准确来说应该为 进程地址空间

虚拟地址转换为物理地址

在这里插入图片描述
通过这个我们就可以看出,同一个变量地址相同,内容不同,其实是虚拟地址空间相同,但是物理内存上的地址其实是不同的。
也就是相当于操作系统给编译器画了一张大饼,告诉编译器这里的空间你全部可以用,但其是并不是全部可以用,你申请空间时 操作系统 就会看你申请的空间物理内存是否足够容纳的下,如果容纳不下,操作系统就不会帮你申请,直接打回你的申请,就像你兄弟告诉你我有100块,你说给我200块,你兄弟说不行一样

浅谈虚拟内存管理

那么既然有虚拟内存,那么操作系统就需要对虚拟内存管理,那操作系统是怎么对虚拟内存管理的呢?
和进程一样,也是先描述,再组织

描述

每一个进程都有一个 task_struct ,在 task_struct 中就有一个描述进程地址空间的结构体 mm_struct (内存描述符)

struct task_struct
{/*...*/struct mm_struct *mm; //对于普通的⽤⼾进程来说该字段指向他的//虚拟地址空间的⽤⼾空间部分,对于内核线程来说这部分为NULL。struct mm_struct *active_mm; // 该字段是内核线程使⽤的。当该进程是内核线程时,//它的mm字段为NULL,表⽰没有内存地址空间,可也并不是真正的没有,这是因为所有进程关//于内核的映射都是⼀样的,内核线程可以使⽤任意进程的地址空间。/*...*/
}
struct mm_struct
{/*...*/struct vm_area_struct *mmap;/* 指向虚拟区间(VMA)链表 */struct rb_root mm_rb;/* red_black树 */unsigned long task_size;/*具有该结构体的进程的虚拟地址空间的⼤⼩*//*...*/// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。unsigned long start_code, end_code, start_data, end_data;unsigned long start_brk, brk, start_stack;unsigned long arg_start, arg_end, env_start, env_end;/*...*/
}

每一个进程都有独立的 mm_struct ,这样保证了进程之间的独立性。

组织

虚拟地址组织方式有两种:

  1. 当虚拟区较少时采取单链表,由 mmap 指针指向这个链表
  2. 当虚拟区间多时采取红黑树进行管理,由 mm_rb 指向这棵树

linux内核使用 vm_area_struct 结构来表示一个独立的虚拟内存区域(VMA),一个进程使用多个 vm_area_struct 来表示不同位置的虚拟内存区域。

struct vm_area_struct {unsigned long vm_start; //虚存区起始unsigned long vm_end;//虚存区结束struct vm_area_struct* vm_next, * vm_prev;//前后指针struct rb_node vm_rb;//红⿊树中的位置unsigned long rb_subtree_gap;struct mm_struct* vm_mm;//所属的 mm_structpgprot_t vm_page_prot;unsigned long vm_flags;//标志位struct {struct rb_node rb;unsigned long rb_subtree_last;} shared;struct list_head anon_vma_chain;struct anon_vma* anon_vma;const struct vm_operations_struct* vm_ops; //vma对应的实际操作unsigned long vm_pgoff;//⽂件映射偏移量struct file* vm_file;//映射的⽂件void* vm_private_data;//私有数据atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMUstruct vm_region* vm_region;/* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMAstruct mempolicy* vm_policy;/* NUMA policy for the VMA */
#endifstruct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;

通过上面的描述,我们可以用图来表示以上结构:
在这里插入图片描述
在这里插入图片描述

为什么要有虚拟地址空间

  1. 安全风险,如果每个进程都可以访问任意内存空间,那么就意味着任意一个进程都能够读写系统先关的内存区域,如果是一个木马病毒,就意味着可以随意修改内存空间,导致很多不缺定因素
  2. 地址不确定,如果使用的是物理地址,每次运行的程序的地址是不确定的,因为每次运行的程序个数都不一样
  3. 效率低,如果直接使用物理内存的话,一个进程就是作为一个整体(内存块)操作的,如果出现内存不够用,就只能将不常用的进程拷贝到磁盘的交换分区中,等下次内存够了,在加载回来,如果是虚拟地址空间,就可以通过分页管理和页表映射分批加载进程,大大减小了内存的压力,使内存能够以更小的空间做更多的事
  4. 地址空间和页表都是OS来维护的,也就意味着想要使用地址空间和页表映射,就需要OS来监督,这也就保护了物理内存中的合法数据,包括各个进程以及内核的有效数据
  5. 由页表和地址空间的存在,也就意味着物理内存的分配和进程的管理没有关系了,进程管理模块和内存管理模块就完成了解耦
  6. 有了页表,程序在物理内存中理论上就可以在任意位置上加载。但它可以将地址空间上的虚拟地址空间和物理地址空间进程映射,所以在进程视角所有的内存分布都是有序的

http://www.ppmy.cn/news/1581254.html

相关文章

vue3父子组件传值

在 Vue 3 中&#xff0c;Composition API 是一种新的编写组件逻辑的方式&#xff0c;它通过 setup 函数提供了一种更灵活的方式来组织和复用代码。与传统的 Options API 相比&#xff0c;Composition API 更适合处理复杂的逻辑场景&#xff0c;尤其是在需要跨组件复用逻辑时。 …

如何理解std::promise和std::future

std::promise 是 C11 引入的一个类&#xff0c;用于在线程之间传递异步结果&#xff08;值或异常&#xff09;。它通常与 std::future 配合使用&#xff0c;std::promise 用于设置值或异常&#xff0c;而 std::future 用于获取这些值或异常。 下面通过一个更直观的生产者-消费者…

红数码影视(RED Digital Cinema)存储卡格式化后的恢复方法

红数码影视(RED Digital Cinema)的摄像机可以生成两种RAW级高清视频文件&#xff0c;一种是R3D&#xff0c;一种是MOV。其中MOV属于苹果(apple)公司的QT视频封装结构&#xff0c;使用的视频编码是Apple ProRes;而R3D则是RED公司自创的RAW视频文件&#xff0c;这种文件解码需要使…

Excel(进阶篇):powerquery详解、PowerQuery的各种用法,逆透视表格、双行表头如何制作透视表、不规则数据如何制作数据透视表

目录 PowerQuery工具基础修改现有数据理规则PowerQuery抓取数据的两种方式多文件合并透视不同表结构多表追加数据透视追加与合并整理横向表格:逆透视 数据用拆分工具整理数据算账龄 不等步长值组合合并文件夹中所有文件PowerQuery处理CSV文件双行表头、带合并单元格如何做数据…

在 Spring Boot 结合 MyBatis 的项目中,实现字段脱敏(如手机号、身份证号、银行卡号等敏感信息的部分隐藏)可以通过以下方案实现

在 Spring Boot 结合 MyBatis 的项目中&#xff0c;实现字段脱敏&#xff08;如手机号、身份证号、银行卡号等敏感信息的部分隐藏&#xff09;可以通过以下方案实现。以下是分步说明和完整代码示例&#xff1a; 一、实现方案选择 1. 方案一&#xff1a;自定义注解 Jackson 序…

迷你主机与普通台式电脑区别

一、【两者区别】 1、迷你电脑主机大部分都是采用带U的芯片&#xff0c;也就是低功耗芯片&#xff0c;整机功耗在10W-25W左右&#xff1b;普通台式机自然就是用台式机芯片&#xff0c;整机功耗约150W-300W&#xff1b; 2、迷你电脑主机大部分采用低压内存&#xff0c;也就是笔…

MSE分类时梯度消失的问题详解和交叉熵损失的梯度推导

下面是MSE不适合分类任务的解释&#xff0c;包含梯度推导。以及交叉熵的梯度推导。 前文请移步笔者的另一篇博客&#xff1a;大模型训练为什么选择交叉熵损失&#xff08;Cross-Entropy Loss&#xff09;&#xff1a;均方误差&#xff08;MSE&#xff09;和交叉熵损失的深入对比…

简述一下Unity中的碰撞检测

碰撞检测的基本工作原理 Unity中的碰撞检测是游戏开发中的核心功能&#xff0c;依赖于NVIDIA的PhysX物理引擎实现。它通过碰撞体&#xff08;Collider&#xff09;和刚体&#xff08;Rigidbody&#xff09;两个主要组件来检测和响应游戏对象之间的物理交互&#xff0c;遵循以下…