【Linux】进程地址空间、环境变量:从理论到实践(三)

🌈 个人主页:Zfox_
🔥 系列专栏:Linux

目录

  • 🚀 前言
  • 一:🔥 环境变量
    • 🥝 基本概念
    • 🥝 常见环境变量
    • 🥝 查看环境变量方法
  • 二:🔥 测试
    • 🥝 PATH
    • 🥝 修改PATH
  • 三:🔥 与环境变量相关的命令
  • 四:🔥 获取环境变量
    • 通过代码如何获取环境变量
  • 五:🔥 进程地址空间
    • 🥝 定义与本质
    • 🥝 虚拟地址与页表
    • 🥝 进程描述符mm_struct
    • 🥝 struct_task_struct,struct_mm_struct和页表的关系
  • 六:🔥 共勉

🚀 前言

  • 🐲 接着上一篇博客我们继续往下学习,点击跳转上一篇博客 【Linux】进程优先级、调度、命令行参数:从理论到实践(二)

一:🔥 环境变量

🥝 基本概念

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

🥝 常见环境变量

  1. PATH : 指定命令的搜索路径,PATH中存放的环境变量是为了在执行命令的时候,可以在PATH中找到对应的路径,这样就可以不用写出命令绝对路径了。
  2. HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)。
  3. SHELL : 当前 Shell ,它的值通常是 /bin/bash

🥝 查看环境变量方法

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

例如:查看PATH环境变量:

echo $PATH

在这里插入图片描述

二:🔥 测试

🥝 PATH

💦 PATH 的原理解释

PATH存放的环境变量是为了在执行命令的时候,可以在PATH中找到对应的路径,这样就可以不用写出命令绝对路径了。

  • 举一个例子:ls的执行过程

💦 ls 命令的最常用的一个命令,但是其实 ls 也只不过是在系统中的一个封装的可执行程序而已,可以使用 which 命令查看 ls 的路径,可以看到 ls 的路径为/usr/bin/ls

🍊 所以我们可以这样使用ls命令:

/usr/bin/ls 		# 查看当前目录下的文件

但是平时我们却通常是直接使用ls命令的,这是为什么呢?这就是因为在 PATH 路径下有 /usr/bin
在这里插入图片描述

因为在使用ls命令的时候,系统就会首先去PATH中的环境变量从左向右地寻找ls的工作路径。如果发现了ls的工作路径,这时直接使用ls命令就不会报错了

🥝 修改PATH

🍊 通常我们自己在写完一个代码后,形成了一个可执行文件,需要通过 ./ 这样的方式才可以运行,这是因为环境变量 PATH 中没有当前可执行程序的工作目录,所以我们只能通过 ./ 这样的方式,自己手动的通过相对路径的方式运行可执行程序。

假设我们在/home/lisi/test目录下,有一个hello的可执行程序。运行之后可以打印出hello world

💦 如果想要让我们的可执行程序可以直接像ls命令那样直接运行,我们可以用两种方法:

  1. 在PATH中用别人的工作目录。

可以将hello这个可执行程序,拷贝一份放入PATH中已经存在的工作目录下(比如说是/usr/bin/,这样在运行可执行程序的时候,其实运行的是在 /usr/bin/ 下的hello。

sudo cp hello /usr/bin/

但是强烈地不推荐使用这种方法,因为这样就会污染了其他工作目录。不利于整个系统的发展。

  1. 自己在PATH创建一个新目录。

第二种方法就是将当前的工作目录添加到PATH中即可。

需要使用export命令,在PATH中添加新的工作目录。

export PATH=$PATH:/home/lisi/test/

在执行完上述的命令之后,就可以直接使用hello指令了。

💦 注意:这样操作的话,其实在下一次重新开使用Linux的时候,原来的环境变量就会被重新的覆盖,导致 hello 又不可以直接被使用了。如果想要永久的使得命令生效,就必须要修改~/.bash_profile 文件才可以。

vim ~/.bash_profile 		# 将创建工作目录的指令写在.bash_profile中
source .bash_profile 		# 使得.bash_profile中的内容生效

三:🔥 与环境变量相关的命令

命令说明
set显示当前 Shell 所有变量,/etc/bashrcs shell变量,/etc/profile 环境变量,用户环境变量,自定义变量
env显示当前用户的环境变量 ~/.bashrcs及~/.profile
export显示从 Shell 中导出成环境变量的变量,也能通过它将自定义变量导出为环境变量。但只是临时生效,shell关闭后,变量就会释放。
unset删除环境变量,执行 unset PATH ,再执行 ls 将提示找不到 ls 命令

四:🔥 获取环境变量

✨ extern char ** environ \colorbox{pink}{✨ extern char ** environ} ✨ extern char ** environ

💦 environ 是一个二级字符指针,相当于一个字符串数组,是程序运行的环境变量,当程序启动时,会复制,父进程的环境变量。程序在 shell 中运行,父进程就是当前 shell 。若当前 shell 使用了 export a=123 ,程序运行后 environ 也会存在 a=123

✨ getenv、setenv、unsetenv函数 \colorbox{pink}{✨ getenv、setenv、unsetenv函数} ✨ getenvsetenvunsetenv函数
getenvsetenvunsetenv 三个函数存在 stdlib.h 中。

  1. getenv
char *getenv (const char *__name)	   

根据环境变量名获取环境变量

  1. setenv
int setenv (const char *__name, const char *__value, int __replace)

设置环境变量,replace=0表示若存在不进行替换,replace=1表示若存在也会进行替换。

  1. unsetenv
int unsetenv (const char *__name)

根据环境变量名删除环境变量

#include <stdio.h>
#include <stdlib.h>int  main(int argc, char * argv[])
{char * lang = getenv("LANG"); // 获取本程序运行的语言环境if (NULL == lang){return -1;}puts(lang);char * a = getenv("a");if (a == NULL){puts("不存在");setenv("a", "345", 0);}else{puts(a);setenv("a", "345", 1); // 第三个参数为1表示存在就替换, 为0表示,存在就算了}a = getenv("a");puts(a);unsetenv("a"); // 删除本程序的a的环境变量, 对父进程没有影响a = getenv("a");if (a == NULL){puts("不存在");}return 0;
}

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

💦 每个程序中的main函数中都要参数,分别为 int argcchar* argv[]char* envp[]

💦 其中arge表示argv中有效数据的个数,而argv是存放指向命令参数的指针数组,envp是存放指向环境变量的指针数组。

用代码演示获取环境变量:

#include <stdio.h>
#include <unistd.h>int main(int argc ,char* argv[],char* envp[])
{int i=0;while(envp[i]){printf("envp[%d]:%s\n",i,envp[i]);i++;                                                                                                          }return 0}

f264766cb894987b4907d6b2042a654.png)

五:🔥 进程地址空间

🥝 定义与本质

  • 🍁 定义进程地址空间是用来描述操作系统中的进程所占的空间。 由于进程的独立性,每个进程都认为自己独占系统内存资源,因此通过让每个进程都看到完整的地址空间来实现这种独立性。

  • 🍁 本质进程地址空间本质上是虚拟地址空间,它通过虚拟地址与物理地址的映射来分配空间。 这些虚拟地址在进程运行时由操作系统通过页表等机制映射到实际的物理地址上。

来段代码感受一下

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

💦 输出

parent[17440]: 0 : 0x601058
child[17441]: 0 : 0x601058
  • 我们发现,输出出来的变量值和地址是一模一样的,很好理解呀,因为子进程按照父进程为模版,父子并没有对变量进行进行任何修改。可是将代码稍加改动:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>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 { //parentsleep(3);printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);}sleep(1);return 0;
}

💦 输出:

child[18300]: 100 : 0x601058
parent[18299]: 0 : 0x601058

🍊 我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:

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

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

🥝 虚拟地址与页表

💦 不管中间发生了什么最终会出现两个不同的数值,说明在实际存储的空间中 g_val 一定是被存放在了不同的区域当中了,这就是下面要介绍的一个新的概念: 「虚拟地址空间」

🦁 实际上,平时我们站在学习语言的层面上看的地址空间都是虚拟的地址空间,也就是说这个空间并不是实际存在的,并不是实际的物理上的内存地址空间。为了防止用户破坏系统中的空间,真正存储变量的空间由操作系统统一管理。
在这里插入图片描述
🦁 所以上面 &x 其实打印出来的是虚拟的地址空间,虚拟地址空间通过一系列的翻译转化可以通过「页表」映射到真正的物理地址空间,而子进程中的 g_val=100 和父进程中的 g_val 就存放在这两个真正的物理空间中。这就解释了为什么相同的虚拟地址空间可以有两个不同的数值。

🥝 进程描述符mm_struct

🦁 其实 「进程地址空间」 本质上也是一种在操作系统的一个内核数据结构,在Linux中进程地址空间称之为 struct mm_struct (内存描述符)的结构体Linux就是通过这个结构体来实现 「内存管理」 的。

  • 🎯 每个进程只有一个mm_struct结构,在每个进程的task_struct结构体中,有一个指向该进程的结构。可以说,mm_struct结构是对整个用户空间的描述。
struct mm_struct {//...unsingned long start_code,end_code,start_data,end_data;  //代码段的开始start_code ,结束end_code,数据段的开始start_data,结束end_dataunsigned long start_brk,brk,start_stack;       //start_brk和brk记录有关堆的信息,start_brk是用户虚拟地址空间初始化,brk是当前堆的结束地址,start_stack是栈的起始地址unsigned long arg_start,arg_end,env_start,env_end;     //参数段的开始arg_start,结束arg_end,环境段的开始env_start,结束env_end// ...
};

🍁 上面这个 Linux 内核的源代码,可以看到 struct mm_struct 中也是被划分成为了多个不同的区域的。这些虚拟地址通过页表和物理内存建立映射的联系。由于虚拟地址也是有 0x000000000xFFFFFFFF 线性增长的,所以虚拟地址也叫作「线性地址」

🦁 补充:

  1. 堆的向上增长和栈的向下增长是上都是在改变 truct mm_structend_brkend_stack 的位置而已。

  2. 我们生成的可执行程序实际上也被分为了各个区域,例如初始化区、未初始化区等。当该可执行程序运行起来时,操作系统则将对应的数据加载到对应内存当中即可,大大提高了操作系统的工作效率。

🥝 struct_task_struct,struct_mm_struct和页表的关系

💦 在最开始介绍进程在创建的时候,我们了解到每当起一个进程的时候都实际上是在内核中创建了一个 struct task_struct

学到这里,我们又可以重新的认知进程的创建过程:创建与进程对应的进程控制块 struct task_struct,进程描述符 struct mm_struct 和对应的页表。而 struct task_struct 中有指向 struct mm_struct 的指针,所以可以找到 struct mm_struct。然后 struct mm_struct 中的内容通过页表映射到物理内存中。

父子进程都有自己的 task_struct 和 mm_struct,父子进程的进程地址空间当中的各个虚拟地址分别通过页表映射到物理内存的某个位置。

  1. 一开始创建子进程的时候,子进程和父进程的代码和数据共享,即相同的虚拟地址会映射到相同的物理地址空间。
  1. 当在子进程要修改父进程中的数据的时候,父进程中的数据会重新的拷贝一份,然后子进程再对数据进行修改。这样父子进程中的数据就独立了。

这种只有在多个进程中其中一个进程对数据进行修改的时候再进行拷贝的行为称之为 「写时拷贝」

🦁 对于写时拷贝,有两个问题:

  1. 为什么要进行写时拷贝?
  • 🦁 进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程。
  1. 为什么不在创建子进程的时候就直接在子进程中拷贝一份父进程中的data和code?
  • 🦁 子进程不一定会修改父进程中的code或者data,只有当需要修改的时候,拷贝父进程中的数据才会有意义,这种按需分配的方式,也是一种延时分配,可以高效的时候使用内存空间和运行的效率。
  1. 进程地址空间的作用
  • 🦁 通过虚拟地址 + 页表的这种方式,可以使得用户不能接触到物理内存,这样就不会出现系统级别(访问物理内存)的越界问题了,因为虚拟内存的越界问题并不会影响到实际的物理内存。本质上说就是保护了内存。

  • 🐯 为每一个进程提供了一致的地址空间,从而简化了内存管理。

  • 🦊 更好的完成了进程的独立性以及合理使用内存空间,并将进程调度(task_struct管理)和内存管理(mm_struct管理)进行了解耦。

六:🔥 共勉

以上就是我对 【Linux】进程地址空间、环境变量:从理论到实践(三) 的理解,会立刻更新下一篇的,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/438643.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

前端算法合集-1(含面试题)

(这是我面试一家中厂公司的二面算法题) 数组去重并按出现次数排序 题目描述: 给定一个包含重复元素的数组&#xff0c;请你编写一个函数对数组进行去重&#xff0c;并按元素出现的次数从高到低排序。如果次数相同&#xff0c;则按元素值从小到大排序。 let arr [2, 11,10, 1…

GPTQ vs AWQ vs GGUF(GGML) 速览和 GGUF 文件命名规范

简单介绍一下四者的区别。 参考链接&#xff1a;GPTQ - 2210.17323 | AWQ - 2306.00978 | GGML | GGUF - docs | What is GGUF and GGML? 文章目录 GPTQ vs AWQ vs GGUF&#xff08;GGML&#xff09; 速览GGUF 文件命名GGUF 文件结构文件名解析答案 附录GGUF 文件命名GGUF 文件…

15分钟学 Python 第35天 :Python 爬虫入门(一)

Day 35 : Python 爬虫简介 1.1 什么是爬虫&#xff1f; 网页爬虫&#xff08;Web Crawler&#xff09;是自动访问互联网并提取所需信息的程序。爬虫的主要功能是模拟用户通过浏览器访问网页的操作&#xff0c;从而实现对网页内容的批量访问与信息提取。它们广泛应用于数据收集…

JAVA并发编程系列(13)Future、FutureTask异步小王子

美团本地生活面试&#xff1a;模拟外卖订单处理&#xff0c;客户支付提交订单后&#xff0c;查询订单详情&#xff0c;后台需要查询店铺备餐进度、以及外卖员目前位置信息后再返回。 时间好快&#xff0c;一转眼不到一个月时间&#xff0c;已经完成分享synchronized、volatile、…

【VUE】案例:商场会员管理系统

编写vuedfr实现对会员进行基本增删改查 1. drf项目初始化 请求&#xff1a; POST http://127/0.0.0.1:8000/api/auth/ {"username":"cqn", "password":"123"}返回&#xff1a; {"username":"cqn", "token&q…

读论文、学习时 零碎知识点记录01

1.入侵检测技术 2.深度学习、机器学习相关的概念 ❶注意力机制 ❷池化 ❸全连接层 ❹Dropout层 ❺全局平均池化 3.神经网络中常见的层

.NET Core 集成 MiniProfiler性能分析工具

前言&#xff1a; 在日常开发中&#xff0c;应用程序的性能是我们需要关注的一个重点问题。当然我们有很多工具来分析程序性能&#xff1a;如&#xff1a;Zipkin等&#xff1b;但这些过于复杂&#xff0c;需要单独搭建。 MiniProfiler就是一款简单&#xff0c;但功能强大的应用…

Unraid的cache使用btrfs或zfs?

Unraid的cache使用btrfs或zfs&#xff1f; 背景&#xff1a;由于在unraid中添加了多个docker和虚拟机&#xff0c;因此会一直访问硬盘。然而&#xff0c;单个硬盘实在难以让人放心。在阵列盘中&#xff0c;可以通过添加校验盘进行数据保护&#xff0c;在cache中无法使用xfs格式…

深入挖掘C++中的特性之一 — 继承

目录 1.继承的概念 2.举个继承的例子 3.继承基类成员访问方式的变化 1.父类成员的访问限定符对在子类中访问父类成员的影响 2.父类成员的访问限定符子类的继承方式对在两个类外访问子类中父类成员的影响 4.继承类模版&#xff08;注意事项&#xff09; 5.父类与子类间的转…

数据结构——计数、桶、基数排序

目录 引言 计数排序 1.算法思想 2.算法步骤 3.代码实现 4.复杂度分析 桶排序 1.算法思想 2.算法步骤 3.代码实现 4.复杂度分析 基数排序 1.算法思想 2.算法步骤 3.代码实现 4.复杂度分析 排序算法的稳定性 1.稳定性的概念 2.各个排序算法的稳定性 结束语 引…

C++(string类的实现)

1. 迭代器、返回capacity、返回size、判空、c_str、重载[]和clear的实现 string类的迭代器的功能就类似于一个指针&#xff0c;所以我们可以直接使用一个指针来实现迭代器&#xff0c;但如下图可见迭代器有两个&#xff0c;一个是指向的内容可以被修改&#xff0c;另一个则是指…

Pytorch最最适合研究生的入门教程,Q3 开始训练

文章目录 Pytorch最最适合研究生的入门教程Q3 开始训练3.1 训练的见解3.2 Pytorch基本训练框架work Pytorch最最适合研究生的入门教程 Q3 开始训练 3.1 训练的见解 如何理解深度学习能够完成任务&#xff1f; 考虑如下回归问题 由函数 y f ( x ) yf(x) yf(x)采样得到的100个…

【安当产品应用案例100集】018-Vmware Horizon如何通过安当ASP身份认证系统增强登录安全性

启用Radius认证是提高VMware Horizon环境安全性的有效方法&#xff0c;特别是在需要满足复杂安全要求的场景中。 启用Radius认证对于VMware Horizon具有以下几个关键优势&#xff1a; 增强安全性&#xff1a;Radius认证支持多种认证方法&#xff0c;包括PAP、CHAP、MS-CHAPv1…

web前端面试中拍摄的真实js面试题(真图)

web前端面试中拍摄的真实js面试题&#xff08;真图&#xff09; WechatIMG258.jpeg WechatIMG406.jpeg WechatIMG407.jpeg WechatIMG922.jpeg WechatIMG1063.jpeg © 著作权归作者所有,转载或内容合作请联系作者 喜欢的朋友记得点赞、收藏、关注哦&#xff01;&#xff01;…

TypeScript 算法手册 - 【冒泡排序】

文章目录 TypeScript 算法手册 - 冒泡排序1. 冒泡排序简介1.1 冒泡排序定义1.2 冒泡排序特点 2. 冒泡排序步骤过程拆解2.1 比较相邻元素2.2 交换元素2.3 重复过程 3. 冒泡排序的优化3.1 提前退出3.2 记录最后交换位置案例代码和动态图 4. 冒泡排序的优点5. 冒泡排序的缺点总结 …

【SpringBoot详细教程】-09-Redis详细教程以及SpringBoot整合Redis【持续更新】

🌲 Redis 简介 🌾 什么是Redis Redis 是C语言开发的一个开源高性能键值对的内存数据库,可以用来做数据库、缓存、消息中间件等场景,是一种NoSQL(not-only sql,非关系型数据库)的数据库 Redis是互联网技术领域使用最为广泛的存储中间件,它是「Remote DictionaryServic…

TARA分析方法论——威胁分析和风险评估方法

一、什么是TARA分析方法论 威胁分析和风险评估&#xff08;Threat Analysis and Risk Assessment&#xff09; 通过识别整车/项目的网络安全资产&#xff0c;分析其中的潜在的安全威胁&#xff0c;综合考虑威胁攻击可行性、危害影响等因素&#xff0c;识别出整车/项目可能存在…

Python并发编程(2)——初始Python多线程

左手编程&#xff0c;右手年华。大家好&#xff0c;我是一点&#xff0c;关注我&#xff0c;带你走入编程的世界。 公众号&#xff1a;一点sir&#xff0c;关注领取python编程资料 前言 什么是多线程&#xff1f; 为什么需要多线程&#xff1f; 多线程的优点和缺点&#xff1f…

前端规范工程-5:Git提交信息规范(commitlint + czg)

前面讲的都是在git提交之前的一些检查流程&#xff0c;然而我们git提交信息的时候&#xff0c;也应该是需要规范的。直接进入主题&#xff1a; 目录 需安装插件清单commitlint 介绍安装配置配置commit-msg钩子提交填写commit信息czg后续方式一&#xff1a;push触动build并上传…

Windows UAC权限详解以及因为权限不对等引发软件工具无法正常使用的实例分析

目录 ​1、什么是UAC&#xff1f; 2、微软为什么要设计UAC&#xff1f; 3、标准用户权限与管理员权限 4、程序到底以哪种权限运行&#xff1f;与哪些因素有关&#xff1f; 4.1、给程序设置以管理员权限运行的属性 4.2、当前登录用户的类型 4.3、如何通过代码判断某个进程…