当前位置: 首页 > article >正文

【Linux篇】环境变量与地址空间

📌 个人主页: 孙同学_
🔧 文章专栏:Liunx
💡 关注我,分享经验,助你少走弯路!
在这里插入图片描述

一.环境变量

1. 环境变量

1.1基本概念

  • 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
  • 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
  • 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
  1. 命令行参数
    在这里插入图片描述
    在这里插入图片描述
    我们会发现argv是一个变长数组,会把我们输入的内容呈现出来。实际上argv是一个指针数组。当我们在命令行中输入一个./code以空格作为分隔符,其实我们输入的时一个长字符串,我们把它叫做命令行或者命令行命令,其实就是一个字符串。当我们执行c语言程序时,这个字符串就会被切分成以空格作为分隔符,切成好几份,所以它把第一个字符串的地址填在argv[0]里面,依次类推。其中数组的有效元素个数就是argc
    在这里插入图片描述

所以有人帮我们把命令行当中我们输入的字符串打散成这种以空格作为分隔符的上图这个样子,这个样子就叫做命令行参数,命令行参数依次变成一个字串,放到一个叫argv的数组里,一共有argc个有效元素,最后这个argv把有效元素放完之后,必须以NULL结尾。
指令选项实现原理:main函数的命令行参数,是实现程序不同子功能的方法。
进程有一张argv表,用来支持实现选项功能!

  1. 我们会发现当执行系统命令比如ls时不需要带./,而执行我们自己的程序时就需要带./,这是为什么呢?
  • 要执行程序我们先得找到它,./表示在当前路径下,而系统命令不需要是因为存在环境变量,来帮助我们找到目标二进制文件
  • ls是在/usr/bin/路径下的,我们会发现当前路径下我们输入code是不会运行的,而当我们把code拷贝到/usr/bin路径下就会执行了
  • 执行命令时系统为什么会在/usr/bin路径下去查呢?答案是系统中存在环境变量(PATH)来帮助系统找到目标二进制文件
  • 环境变量(PATH):系统中搜索指令的默认搜索路径

1.2 常见环境变量

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

1.3 查看环境变量方法

  • env:查看所有环境变量
    在这里插入图片描述
    环境变量的构成:名字+内容
  • echo $NAME : //NAME(你的环境变量名称)查看单个环境变量

🌵1.如何理解环境变量?存储的角度

  • bash内部有两张表,一个是环境变量表,另一个是命令行参数表。
    在这里插入图片描述

🌵2. 环境变量最开始从哪里来的呢?

  • 系统相关的配置文件中来的
  • 在每个用户的家目录里都会有.bash_profile.bash_rc这两个配置文件
    在这里插入图片描述

1.4 和环境变量相关的命令

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

1.5 环境变量的组织方式

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

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

  1. 方法一:main()函数
    在这里插入图片描述
    在这里插入图片描述
    上面获取的环境变量是父进程(bash),环境变量可以被子进程继承
  2. 方法二:getenv(),它会根据环境变量的名字来获取指定环境变量的内容
    在这里插入图片描述
    在这里插入图片描述

✏️ 如果我们想写一个程序,只能我执行,其他人一律不执行,该如何设计呢?根据我们刚刚对环境变量的认识,现在只有一个人知道登陆用户是谁,那就是bash.
➀我们写一个只有sp能运行的程序
在这里插入图片描述
运行
在这里插入图片描述
可以正常运行
➁我们拿root账号来运行一下
在这里插入图片描述
不能运行
所以这个程序只能由sp运行
所以获取环境变量的第二种做法叫做getenv ,环境变量可以被子进程继承是因为我们可以把环境变量相关的信息让子进程继承下去,子进程就可以和环境变量来做个性化操作,比如定制一个只能自己执行的程序

  1. 方法三:使用全局变量environ
    在这里插入图片描述
    我们可以看到它的参数类型为char **,因为环境变量表是一个char * 的,所以char **environ应该指向第一个元素
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

2. 环境变量的特性

2.1 环境变量具有全局特性

  • 环境变量通常具有全局属性,可以被子进程继承下去

2.2 补充两个概念

  1. bash会记录两套变量:一个是环境变量,一个是本地变量在这里插入图片描述
    可以通过set命令查到所有的本地变量,本地变量不会被子进程继承,只在bash内部使用
  2. 我们的环境变量是在谁的上下文里面呢?bash
    export命令是一个内建命令built-in command,不需要创建子进程,而让bash自己亲自执行,或者系统调度完成。

二.程序地址空间

1. 程序地址空间回顾

我们在以前学习c/c++的时候,就听说过c/c++程序默认内存地址空间是代码区,字符常量区,初始化数据区,未初始化数据区,堆区,栈区,共享区等。

  1. 下面我们把整个程序的内存空间布局以代码的方式打出来
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  2. 证明这个地址是虚拟地址
    在这里插入图片描述

在这里插入图片描述
结论:

  • 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
  • 但地址值是一样的,说明,该地址绝对不是物理地址!
  • 在Linux地址下,这种地址叫做 虚拟地址
  • 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理

OS必须负责将 虚拟地址 转化成 物理地址

2. 进程地址空间

一个进程一个虚拟地址空间,虚拟地址空间的宽度是一字节,32位下是2^32个地址 * 1字节 = 4GB(0-3GB用户空间[拿到地址就能直接访问],3-4GB内核空间),64位下是2^64个地址。

一个进程一套页表,页表是用来做虚拟地址和物理地址映射的。
在这里插入图片描述
一个int类型有4个字节,但我们的虚拟进程空间的宽度位1个字节,如何处理呢?因为我们有类型,实际上我们在访问任何一个变量时,只要知道起始地址+偏移量就访问到了,系统访问的是最小的那个地址。

有父进程,也就会有子进程,子进程的很多东西都是拷贝父进程的,他把父进程task_struct里面的属性给自己拷贝一份,把个别的属性自己一更改,一个进程,一个虚拟地址空间,一个进程,一套页表,所以我们的子进程也有自己的虚拟地址空间和页表
在这里插入图片描述
以前我们说过,子进程的PCB和一些物理属性都是拷贝自父进程的,同样的,页表也是拷贝自父进程的,相当于发生了浅拷贝,所以子进程和父进程就有相同的虚拟地址空间,我们也就理解了为什么全局变量为什么默认地被父子进程共享,因为他们的虚拟地址空间到物理空间的映射关系是一样的,相当于它们指向同一块物理内存。变量如此,代码也是如此。

上面我们演示的子进程的gval++,父进程的gval 不变是怎么回事呢?
原因是子进程的gval++的时候,操作系统会在物理内存空间上重新开辟一块空间,把老变量gval的内容拷贝到新空间,此时就得到了一个新的变量或者物理地址,然后操作会把这个新的物理空间地址给给子进程的页表,构建全新的映射关系,这种机制称为写实拷贝
在这里插入图片描述

  • 上面的图就足矣说明问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!

3. 虚拟内存管理 - 第一讲

描述linux下每一个虚拟地址空间的所有信息的结构体是mm_struct,每一个进程都只有一个mm_struct结构体,每个进程的task_struct结构体中都只一个指针指向mm_struct

struct task_struct
{
 /*...*/
 struct mm_struct *mm; //对于普通的用户进程来说该字段指向他的虚拟地址空间的用户空间部分,对于内核线程来说这部分为NULL。 
 struct mm_struct *active_mm; // 该字段是内核线程使用的。当该进程是内核线程时,它的mm字段为NULL,表示没有内存地址空间,可也并不是真正的没有,这是因为所有进程关于内核的映射都是⼀样的,内核线程可以使用任意进程的地址空间。 
 /*...*/
}

mm_struct结构是对整个用户空间的描述。每一个进程都会有自己独立的mm_struct,这样每一个进程都会有自己独立的地址空间才能互不干扰。下面是task_structmm_struct,进程的地址空间的分布情况:
在这里插入图片描述
定位mm_struct文件所在位置和task_struct所在路径是一样的,不过他们所在文件是不一样的,mm_struct所在的文件是mm_types.h

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,操作系统肯定是要将这么多进程的mm_struct组织起来的!虚拟空间的组织方式有两种:

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

linux内核使用 vm_area_struct 结构来表示一个独立的虚拟内存区(VMA),由于每个不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多vm_area_struct结构来分别表示不同类型的虚拟内存区域。上面提到的两种组织方式使用的就是vm_area_struct结构来连接各个VMA,方便进程快速访问。

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查找页表),也可以对你的地址和操作进行合法判定,进而保护物理内存

📌:页表项里面除了虚拟地址和物理地址外,还有一个条目,这个条目里面包含着r,w,x权限,实现对物理内存的保护

✏️ 再谈野指针问题

当这个指针指向的对应区域被释放了,即物理内存释放了,所以映射关系要去掉,当对一个已经释放了的内存访问时,页表中就不存在对应的虚拟物理映射关系,查页表时会失败,操作系统会知道,就把进程干掉了,所以有了野指针之后,进程有可能会崩溃。

✏️ char *str = “hello world” ; *s = ‘H’; 这段代码能编过吗?答案使能编过

上面的char *str = "hello world" ;我们都知道叫做字符串常量,我们用指针指向字符串常量时,我们在c语言中已经学过,字符串常量不能被修改。字符串是被编译到字符常量区的,也就是和正文部分是编到一块的,所以它是只读的,所以想把这个字符串常量修改成 *s = 'H';时,查页表时就会发现是只读的,而要时页表会转化失败,所以操作系统不让我们转。

✏️ 再谈为什么要有虚拟地址空间
让进程管理和内存管理进行一定程度的解耦合

🎯 澄清一些问题!

  1. 我们可以不加载代码和数据,只有task_struct,mm_struct,页表,程序也能运行,因为存在缺页中断
  2. 创建进程,先有task_struct,mm_struct等,还是先加载代码和数据?
    答案是先要有内核数据结构,然后才加载代码和数据
  3. 如何理解进程挂起(阻塞挂起)??
    先找到对应的进程,然后将页表清空,将物理内存里对应的代码和数据和换出到swap分区里。只保留页表中的左半部分,而把右半部分换出。
  4. 堆区细节性话题:堆区有自己的开始与结束,我们平时用堆区时可能malloc了好多次,申请了不同的堆空间,而每个堆都有起始地址,而定义的堆空间上只有一个起始和结束,那么如何确定其他的地址开始和结束呢?
    在这里插入图片描述vm_area_struct里面就有vm_startvm_end,会记录下你所需要的vm_startvm_end,一份堆区对应一个vm_area_struct
    在这里插入图片描述

👍 如果对你有帮助,欢迎:

  • 点赞 ⭐️
  • 收藏 📌
  • 关注 🔔

http://www.kler.cn/a/589331.html

相关文章:

  • C++ 类和对象----构造函数
  • 一个简单的RPC示例:服务端和客户端
  • 【算法day13】最长公共前缀
  • AI时代的软件测试该如何“破局”?
  • 【Qt】QWidget属性2
  • 【项目合集】基于ESP32的智能化妆柜
  • 【Azure 架构师学习笔记】- Azure Databricks (22) --Autoloader
  • 随笔小记-本人常用桌面应用(流程图-boardmix,截图-snipaste,文件比较-beyond compare,远程控制-向日葵,解压-360压缩)
  • 机试准备第18天
  • Python的类和对象(4)
  • 在Django模型中的Mysql安装
  • oracle 基础知识之 多表查询
  • JVM---Java 类生命周期与类加载机制
  • 电子电气架构 --- 智能电动汽车的品牌竞争转变
  • 【失败了】LazyGraphRAG利用本地ollama提供Embedding model服务和火山引擎的deepseek API构建本地知识库
  • 面试系列|蚂蚁金服技术面【3】
  • C语言内存函数讲解
  • 10-SDRAM控制器的设计—— signaltap 调试
  • iptables与firewall的区别,从不同的角度讲解
  • 基于金融产品深度学习推荐算法详解【附源码】