[Linux][环境变量][进程地址空间]详细解读

发布于:2024-04-17 ⋅ 阅读:(20) ⋅ 点赞:(0)

1.环境变量

1.基本概念

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

2.常见环境变量

  • PATH : 指定命令的搜索路径
  • HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
  • SHELL : 当前Shell,它的值通常是/bin/bash

3.查看环境变量的方法

echo $NAME // NAME:你的环境变量名称

4.测试PATH

  1. 创建hello.c文件
  2. 对比./hello执行和之间hello执行
  3. 为什么有些指令可以直接执行,不需要带路径,而我们的二进制程序需要带路径才能执行?
  4. 将我们的程序所在路径加入环境变量PATH当中, export PATH=$PATH:hello程序所在路径
    • 有效期限:临时改变,只能在当前的终端窗口中有效,当前窗口关闭后就会恢复原有的path配置

5.测试HOME

  • 用root和普通用户,分别执行 echo $HOME,对比差异
  • 执行 cd ~; pwd,对应 ~ 和 HOME 的关系

6.和环境变量相关的命令

  • echo:显示某个环境变量值
  • export:设置一个新的环境变量
  • env:显示所有环境变量
  • unset:清除环境变量
  • set:显示本地定义的shell变量和环境变量

7.环境变量的组织方式

  • 每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串
    请添加图片描述

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

  • 命令行第三个参数
int main(int argc, char *argv[], char *env[])
{
    int i = 0;
    for (; env[i]; i++)
    {
        printf("%s\n", env[i]);
    }
    return 0;
}
  • 通过第三方变量environ获取
    • libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明
int main(int argc, char *argv[])
{
    extern char **environ;
    int i = 0;
    for (; environ[i]; i++)
    {
        printf("%s\n", environ[i]);
    }
    return 0;
}

9.通过系统调用获取或设置环境变量

  • putenv
  • getenv
int main()
{
    printf("%s\n", getenv("PATH"));
    return 0;
}
  • 常用getenv和putenv函数来访问特定的环境变量

10.环境变量通常是具有全局属性

  • 子进程的环境变量是从父进程继承来的 --> 环境变量具有全局属性
    • 默认情况下,所有的环境变量都会被子进程继承
int main()
{
    char *env = getenv("MYENV");
    if (env)
    {
        printf("%s\n", env);
    }
    return 0;
}
  • 直接查看,发现没有结果,说明该环境变量根本不存在
    • 导出环境变量 – export MYENV=“hello world”
    • 再次运行程序,发现结果有了!
    • 说明:环境变量是可以被子进程继承下去的!
    • 如果只进行 MYENV=“helloworld”,不调用export导出,再用我们的程序查看,会有什么结果?为什么?
      • 此时MYENV只是一个普通变量

2.进程地址空间

请添加图片描述

0.这里的地址空间,是物理内存吗?

  • 不是物理内存!
int g_val = 0;
int main()
{
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork");
        return 0;
    }
    else if (id == 0)
    {
        // child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取
        g_val = 100;
        printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }
    else
    { 
        // parent
        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
  • 综上发现,父子进程,输出地址是一致的,但是变量内容不一样!可得出如下结论:
    • 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
    • 但地址值是一样的,说明,该地址绝对不是物理地址!
      • 在Linux地址下,这种地址叫做虚拟地址
    • 在用C/C++所看到的地址,全部都是虚拟地址!
      • 物理地址,用户一概看不到,由OS统一管理,OS必须负责将虚拟地址转化成物理地址
  • 发生了写时拷贝

1.为什么不能直接访问物理内存?

  • 内存本身是随时可以被读写的
    • 比如有一个指针,成了野指针,通过它,访问了别的进程的数据,并且篡改了其数据
    • 这样特别不安全!

2.分页&虚拟地址空间

请添加图片描述

  • 上图可看出,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址
  • 地址空间是一种内核数据结构(mm_struct),它里面至少要有:各个区域的划分
  • 地址空间和页表(用户级)是每一个进程都私有一份
    • 只要保证,每一个进程的页表,映射的是物理内存的不同区域,就能做到,进程之间不会互相干扰,保证进程的独立性

3.扩展内容1

  • 当程序在编译的时候,形成可执行程序的时候,没有被加载到内存中的时候,程序的内部,有地址吗?
    • 可执行程序其实在编译的时候,内部已经有地址了
  • 地址空间不要仅仅理解成为是OS内部要遵守的,其实编译器也要遵守
    • 即编译器编译代码的时候,就已经给用户形成了各个区域,如代码区,数据区等
    • 并且,采用和Linux内核中一样的编址方式,给每一个变量,每一行代码都进行了编址
    • 故,程序在编译的时候,每一个字段早已经具有了一个虚拟地址
    • 程序内部的地址,依旧用的是编译器编译好的虚拟地址,当程序加载到内存的时候,每行代码,每个变量便具有了一个外部的物理地址
    • 那么,当CPU读到指令的时候,指令内部也有地址,这个地址是物理地址还是虚拟地址?
      • 是虚拟地址!

4.为什么要有地址空间?

  • 凡是非法的访问或者映射,OS都会识别到,并且终止这个进程 --> 有效的保护了物理内存
    • 因为地址空间和页表是OS创建并维护的,也就意味着凡是想使用地址空间和页表进行映射,也一定要在OS的监管之下来进行访问
    • 也便保护了物理内存中的所有合法数据,包括各个进程以及内核的相关有效数据
  • 因为有地址空间&页表映射的存在,物理内存可以对未来的数据进行任意位置加载
    • 正因此,物理内存的分配就可以和进程的管理,做到没有关系
      • 即:(内存管理模块 vs 进程管理模块) --> 完成了解耦合
    • 所以,在C/C++中,new/malloc空间的时候,本质是在哪里申请的?
      • 实质是在虚拟空间中申请的
    • 但倘若申请了空间,如果不立马使用,是不是造成了空间的浪费?
      • 是的
    • 本质上,(因为有地址空间的存在,所以上层申请空间,其实是在地址空间上申请的,物理内存甚至可以一个字节都不给你!而当你真正进行对物理地址空间访问的时候,才执行内存的相关管理算法,帮你申请内存,构建页表映射关系),然后再让你进行内存的访问
      • 这一整套流程是操作系统自动完成的,用户/进程,都完全0感知
      • 此举为延迟分配内存的策略,提高了整机的效率
        • 使得几乎内存的有效使用是100%
  • 因为在物理内存中,理论上可以任意位置加载,所以物理内存中的几乎所有的数据和代码在内存中都是乱序的
    • 但是,因为页表的存在,它可以将地址空间上的虚拟地址和物理地址进行映射,所以在进程的视角,所有的内存分布都是有序的!
      • 地址空间+页表的存在,可以将内存分布有序化
    • 可以理解成,地址空间是OS给进程画的大饼
      • 进程要访问物理内存中的数据和代码,可能目前并没有在物理内存中,同样的,也可以让不同的进程映射到不同的物理内存,这样是不是便很容易做到,进程独立性的实现?
        • 进程的独立性,可以通过地址空间+页表的方式实现
      • 因为有地址空间的存在,每一个进程都认为自己拥有4GB空间(x32),并且各个区域是有序的,进而可以通过页表映射到不同的区域,来实现进程的独立性。每一个进程都不知道,也不需要知道其他进程的存在!

3.重新理解什么是挂起?

  • 加载本质就是创建进程,那么是不是必须非得立马把所有的程序的代码和数据加载的内存中,并创建内核数据结构建立映射关系?
    • 不是
    • 在最极端的情况下,甚至只有内核数据结构被创建出来
      • 即:新建状态
  • 页表映射的时候,映射的可不仅仅是内存,磁盘中的位置,也可以映射
  • 理论上,可以实现对程序的分批加载
  • 既然可以分批加载,可以分批换出吗?
    • 当然可以
  • 甚至,这个进程短时间不会再执行了,比如阻塞
  • 进程的数据和代码被换出了,就叫做挂起