云计算百科
云计算领域专业知识百科平台

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

一、环境变量

基本概念

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

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

  • PATH 用途:指定系统可执行文件的搜索路径。 示例:PATH=/usr/bin:/bin:/usr/local/bin 说明:当用户在终端中输入命令时,系统会在 PATH 指定的目录中查找可执行文件。
  • HOME 用途:指定当前用户的主目录路径。 示例:HOME=/home/user 说明:程序可以使用 HOME 环境变量来访问用户的主目录,例如保存配置文件。
  • LANG 用途:指定系统的语言环境。 示例:LANG=en_US.UTF-8 说明:设置系统的语言环境和字符编码,影响程序的输出和国际化行为。
  • TZ 用途:指定系统的时区。 示例:TZ=America/New_York 说明:设置系统的时区,影响程序中日期和时间的显示。
  • LD_LIBRARY_PATH 用途:指定动态链接器搜索动态库的路径。 示例:LD_LIBRARY_PATH=/usr/local/lib:/opt/lib 说明:动态链接器会在 LD_LIBRARY_PATH 指定的目录中查找动态库。
  • 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指定的路径,而不是当前路径。 解决方法:

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

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

  • 创建符号链接到已有的 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,并运行该文件。

  • 使用 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)
    { //child
    printf("child pid %d : %d : %p\\n", getpid(), g_val, &g_val);
    }
    else
    { //parent
    printf("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 ,这样保证了进程之间的独立性。

    组织

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

  • 当虚拟区较少时采取单链表,由 mmap 指针指向这个链表
  • 当虚拟区间多时采取红黑树进行管理,由 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_struct
    pgprot_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_MMU
    struct vm_region* vm_region;
    /* NOMMU mapping region */
    #endif
    #ifdef CONFIG_NUMA
    struct mempolicy* vm_policy;
    /* NUMA policy for the VMA */
    #endif
    struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
    } __randomize_layout;

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

    为什么要有虚拟地址空间

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

    评论 抢沙发

    评论前必须登录!