Linux学习笔记(八)--环境变量与进程地址空间

环境变量:

概念:指在操作系统中用来指定操作系统运行环境的一些参数,如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但 是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。

常见环境变量:

PATH: 定义了系统查找命令的目录列表。当你输入 ls时,系统会按照 PATH中的目录顺序去查找名为 ls的可执行文件。

HOME: 当前用户的家目录路径,例如 /home/username

USERUSERNAME: 当前登录的用户名。

SHELL: 当前用户使用的默认 shell 程序路径,例如 /bin/bash

PWD: 当前工作目录的路径。

查看环境变量的方法:

复制代码
echo $NAME //NAME:你的环境变量名称
printenv //显示所有环境变量

设置环境变量(临时):

在终端中直接使用 export命令设置的变量是临时的,只在​​当前 shell 会话​​及其​​子进程​​中有效。关闭终端或开启新窗口后就会失效。

语法:​export VARIABLE_NAME=value(注意:​​等号两边不能有空格​​。值如果包含空格,需要用单引号或双引号括起来。)

示例:

复制代码
# 设置一个变量 MY_VAR
export MY_VAR="Hello World"

# 查看是否设置成功
echo $MY_VAR

# 设置 PATH,注意通常是在原有 PATH 上追加
export PATH=$PATH:/my/custom/path

取消设置环境变量(临时):使用unset命令

语法:unset VARIABLE_NAME

示例:

复制代码
unset MY_VAR

测试HOME差异:在root用户和普通用户中分别使用echo $HOME;root用户和普通用户的 HOME变量值是不同的。区别如下

在root用户下执行echo $HOME命令时会显示/root,root用户的家目录​​是 /root,这是一个特殊目录,只有 root用户有权限访问。/root通常用于存放 root用户的个人配置文件。

而在普通用户(假设普通用户名为sanye)下执行echo $HOME命令时会显示/home/sanye。普通用户的家目录​​是 /home/用户名(如 /home/sanye)。普通用户只能访问自己的家目录,不能访问 /root或其他用户的家目录(除非有权限)。

和环境变量相关的命令:

  1. echo: 显示某个环境变量值

  2. export: 设置一个新的环境变量

  3. env: 显示所有环境变量

  4. unset: 清除环境变量 HOME 的关系

  5. set: 显示本地定义的shell变量和环境变量

本地变量:在 Linux Shell(如 Bash)中,​​本地变量​​是仅在当前 Shell 进程内部有效的变量,​​不会被继承到子进程​​。它们通常用于临时存储数据、控制脚本逻辑或函数内部操作。

本地变量与环境变量的区别:

定义方式:

复制代码
# 本地变量(仅当前 Shell 有效)
local_var="I am local"

# 环境变量(可被子进程继承)
export global_var="I am global"

常见用途:

(1)存储临时数据

复制代码
count=0  # 本地变量
for file in *.txt; do
    ((count++))
done
echo "找到 $count 个文本文件"

内建命令和常规命令:

在Linux中大部分指令是bash的子进程,但不是所有的指令都需要创建子进程(如echo),这一类命令我们称为内建命令。

常规命令:

(1)特点:

独立程序​​:存储在文件系统中的二进制可执行文件(如 /bin/ls)。

运行方式​​:Shell 通过创建子进程(fork()+ exec())执行。

​​继承关系​​:子进程继承父 Shell 的环境变量,但无法修改父 Shell 的状态(如工作目录、变量)。

(2)常见常规命令:

复制代码
ls, grep, cat, python, mkdir, cp

内建命令:

(1)特点:

Shell 自带功能​​:直接内置于 Shell 解释器中(如 cdexport)。

运行方式​​:​​不创建子进程​​,直接在当前 Shell 进程中执行。

用途​​:修改 Shell 自身状态(如变量、目录、环境)。

常见内建命令:

复制代码
cd, export, unset, echo, source, exit, alias

环境变量的组织方式:

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

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

(1)通过命令行的第三个参数:

复制代码
#include <stdio.h>

int main(int argc, char *argv[], char *env[]) {
    printf("=== 使用 main 参数 ===\n");
    for (int i = 0; envp[i] != NULL; i++) {
        printf("%d: %s\n", i, envp[i]);
    }
    return 0;
}

(2)通过第三方变量environ获取

复制代码
#include <stdio.h>
#include <unistd.h>

extern char **environ;  // 声明外部环境变量指针

int main() {
    printf("=== 使用 environ 指针 ===\n");
    for (char **env = environ; *env != NULL; env++) {
        printf("%s\n", *env);
    }
    return 0;
}

libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。

(3)通过getenv获取特定的环境变量

复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    // 获取单个环境变量的值
    char *home = getenv("HOME");
    char *user = getenv("USER");
    char *path = getenv("PATH");
    
    if (home) printf("HOME: %s\n", home);
    if (user) printf("USER: %s\n", user); 
    if (path) printf("PATH: %s\n", path);
    
    return 0;
}

环境变量通常具有全局属性,可以被子进程继承下去(环境变量的全局性是指 ​​对当前进程及其所有子进程可见​​,而非整个操作系统所有用户共享。)

复制代码
#include <stdio.h>  
#include <stdlib.h> 
int main()
{
    char *env = getenv("MYENV");  
    if(env){                      
        printf("%s\n", env);      
    }
    return 0;      
}

直接查看,发现没有结果,说明该环境变量根本不存在。导出环境变量 export MYENV="hello world" 再次运行程序,发现结果有了!说明:环境变量是可以被子进程继承下去的!

程序地址空间:

这是各个区的变量地址,可以自行去自己的编译器查看:

复制代码
#include <stdio.h>
#include <stdlib.h>

// 全局变量(数据段)
int global_init = 100;          
int global_uninit;            

void print_memory_layout() {
    int stack_var = 10;
    static int static_var = 200; 

    char *heap_var = (char*)malloc(100);
    
    char *env_var = getenv("MYENV");
    extern char **environ;    
    
    
    printf("【代码段】\n");
    printf("函数地址(main):       %p\n", main);
    printf("函数地址(print_memory_layout): %p\n\n", print_memory_layout);
    
    printf("【数据段】\n");
    printf("已初始化全局变量:       %p\n", &global_init);
    printf("静态变量:              %p\n", &static_var);
    printf("未初始化全局变量(BSS):  %p\n\n", &global_uninit);
    
    printf("【堆区】\n");
    printf("动态分配内存:          %p\n", heap_var);
    printf("堆变量指针:            %p\n\n", &heap_var);
    
    printf("【栈区】\n");
    printf("局部变量(stack_var):    %p\n", &stack_var);
    printf("环境变量指针(env_var):  %p\n\n", &env_var);
    
    printf("【环境变量区域】\n");
    if (env_var) {
        printf("MYENV值地址:          %p\n", env_var);
        printf("MYENV值内容:          %s\n", env_var);
    } else {
        printf("MYENV值地址:          (未设置)\n");
    }
    
    if (environ && *environ) {
        printf("环境变量表地址:        %p\n", environ);
        printf("第一个环境变量:        %p -> %s\n", *environ, *environ);
        
        printf("\n前5个环境变量地址:\n");
        int count = 0;
        for (char **env = environ; *env != NULL && count < 5; env++, count++) {
            printf("env[%d]: %p -> %s\n", count, *env, *env);
        }
    }
    
    free(heap_var);
}

int main() {
    if (getenv("MYENV") == NULL) {
        setenv("MYENV", "test_environment_value", 0);
    }
    
    print_memory_layout();
    
    return 0;
}

当我们在父进程中创建子进程时

复制代码
 #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)
    { //child
    printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }
    else
    { //parent
        printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }
    sleep(1);
    return 0;
 }

我们会发现输出的变量和地址是一摸一样的

复制代码
parent[2995]: 0 : 0x80497d8
 child[2996]: 0 : 0x80497d8

但是当我们对上述代码修改一部分时:

复制代码
#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)
    { 
        g_val=100;
        printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }else
    { 
        sleep(3);
        printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }
    sleep(1);
    return 0;
 }

child[3046]: 100 : 0x80497e8
 parent[3045]: 0 : 0x80497e8

我们会发现输出结果父子进程的地址是一样的,但是输出的内容却不一样,难道它们用同一份地址输出了两份内容?其实结果不是这样的,其实编译器显示的地址不是物理地址,而是虚拟地址。我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理。OS必须负责将 虚拟地址 转化成 进程地址空间 物理地址。这就是我们将要引入的进程地址空间:

在引入进程空间之前,我们需要先简单弄清楚硬件中的系统总线和I/O总线和高低电平的概念:

复制代码
高级语言 →   机器指令 →   CPU执行→总线传输 → 硬件操作
   ↓          ↓          ↓         ↓         ↓
  getenv() → mov指令 → 总线访问 → 内存读取 → 电平信号

在总线中将CPU与系统连接起来的叫做系统总线,内存与外设连接起来的叫做I/O总线。而在传输数据中也是通过这些总线中的高低电平来传输不同的二进制信号以此来输送数据。

总线与地址空间的硬件关联:

当程序访问进程地址空间中的某个地址(如 mov eax, [0x08049000])时:

地址总线​​:CPU 将虚拟地址 0x08049000发送到 ​​MMU(内存管理单元)​​

MMU 转换​​:通过页表将虚拟地址转为物理地址(如 0x2000F000

控制总线​​:发送读/写信号(高电平=读,低电平=写)

​​数据总线​​:传输实际数据(如从内存读取的值)

在32位系统中最大寻址空间为4GB

环境变量在地址空间中的硬件访问:环境变量位于​​栈区的高地址端​​(靠近内核空间),其物理内存通过总线访问

复制代码
物理内存:
CPU → 地址总线发送 environ 地址 → 内存控制器返回指针值 → 数据总线传输字符串

电平信号与进程内存:

(1)内存单元的电平表示:每个内存单元由电容存储电荷(高电平=1,低电平=0):

复制代码
地址 0x2000F000: [高][低][高][低]...  → 二进制 1010... → 数据 0xA...

(2)总线传输的电平序列​​

CPU 读取内存时:地址周期​​:地址总线发送 0x2000F000(32位电平信号,数据周期​​:数据总线返回 0xA5(8位电平信号:H L H L L H L H

虚拟地址空间:虚拟地址空间​​是操作系统为每个进程提供的"幻觉",让每个进程都认为自己独享整个内存空间。​​分页机制​​则是实现这种幻觉的关键技术。

那么页表是什么?接下来给大家解释一下页表的概念:页表​​是操作系统用于实现​​虚拟内存​​的关键数据结构,它建立了​​虚拟地址​​与​​物理地址​​之间的映射关系。(核心作用:1.地址转换​​:将进程看到的虚拟地址转换为实际的物理内存地址。2.​​内存保护​​:控制进程对内存的访问权限(读、写、执行)。3.共享内存​​:允许多个进程安全地共享同一物理内存页)

页表工作原理:

复制代码
虚拟地址 → [页表查询] → 物理地址
    ↓           ↓          ↓
进程虚拟空间     MMU       物理内存

所以这就是为什么上面父子进程地址相同但是内容不同的原因。

复制代码
父进程虚拟地址 --[页表]--> 物理内存的父进程g_val区域
子进程虚拟地址 --[页表]--> 物理内存的子进程g_val区域

惰性加载:​将资源分配推迟到实际需要使用时才进行​​,而不是在初始阶段就完成所有加载。

相关推荐
蒙奇D索大3 小时前
【数据结构】考研数据结构核心考点:平衡二叉树(AVL树)详解——平衡因子与4大旋转操作入门指南
数据结构·笔记·学习·考研·改行学it
jiunian_cn4 小时前
【Linux】高级IO
java·linux·服务器
☆璇4 小时前
【Linux】网络基础概念
linux·网络
andwhataboutit?4 小时前
Docker Compose学习
学习·docker·容器
郭庆汝4 小时前
自然语言处理笔记
笔记·自然语言处理·easyui
二进制怪兽4 小时前
[笔记] 驱动开发:Virtual-Display-Driver编译过程
笔记
ouliten4 小时前
cuda编程笔记(28)-- cudaMemcpyPeer 与 P2P 访问机制
笔记·cuda
poemyang4 小时前
“一切皆文件”:揭秘LINUX I/O与虚拟内存的底层设计哲学
linux·rpc·i/o 模式
im_AMBER4 小时前
数据结构 04 栈和队列
数据结构·笔记·学习