【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) { //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[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 { //parent
		sleep(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_data

    unsigned 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.mfbz.cn/a/888094.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Nat. Commun.:飞秒激光书写受蚂蚁启发的可重构微型机器人集体

背景介绍生物在各种环境中的集体行为十分普遍&#xff0c;它们能够自发有序地完成单个个体难以完成的任务。目前&#xff0c;生物集体的形成主要分为两大类。第一类生物个体之间没有直接接触&#xff0c;如蜜蜂、鱼和鸟类&#xff0c;这导致这些集体不稳定&#xff0c;容易受到…

Linux网络编程 -- 网络基础

本文主要介绍网络的一些基础概念&#xff0c;不涉及具体的操作原理&#xff0c;旨在构建对网络的基础认识。 1、网络的早期发展历程 20世纪50年代 在这一时期&#xff0c;计算机主机非常昂贵&#xff0c;而通信线路和设备相对便宜。为了共享计算机主机资源和进行信息的综合处…

基于图像的3D动物重建与生成

一、背景与目标 3D-Fauna 是一款用于基于图像和视频进行四足动物3D重建与生成的开源方案。自然界展示了复杂的相似性与多样性,该方法通过学习来自网上图片的四足动物的3D形态,能够从单张图片生成可动画化的带有纹理的3D网格模型。其最终目标是通过大量扩展现有的解决方案,实…

数据库(MySQL):使用命令从零开始在Navicat创建一个数据库及其数据表(一).创建基础表

一. 使用工具和命令 1.1 使用的工具 Navicat Premium 17 &#xff1a;“Navicat”是一套可创建多个连接的数据库管理工具。 MySQL版本8.0.39 。 1.2 使用的命令 Navicat中使用的命令 命令命令解释SHOW DATABASES&#xff1b;展示所有的数据库CREATE DATABASE 数据库名称; 创…

基于STM32的数字温度传感器设计与实现

引言 STM32 是由意法半导体&#xff08;STMicroelectronics&#xff09;开发的基于 ARM Cortex-M 内核的微控制器系列&#xff0c;以其强大的处理能力、丰富的外设接口和低功耗著称&#xff0c;广泛应用于嵌入式系统设计中。在这篇文章中&#xff0c;我们将介绍如何基于 STM32…

深度学习:基于MindSpore实现ResNet50中药分拣

ResNet基本介绍 ResNet&#xff08;Residual Network&#xff09;是一种深度神经网络架构&#xff0c;由微软研究院的Kaiming He等人在2015年提出&#xff0c;并且在ILSVRC 2015竞赛中取得了很好的成绩。ResNet主要解决了随着网络深度增加而出现的退化问题&#xff0c;即当网络…

数据结构与算法——动态规划算法简析

1.初步了解动态规划 由于本篇博客属于动态规划的初阶学习&#xff0c;所以大多都是简单的表示&#xff0c;更深层次的学术用语会在之后深度学习动态规划之后出现&#xff0c;本文主要是带各位了解一下动态规划的大致框架 1.1状态表示 通常的我们会开辟一个dp数组来存储需要表示…

015 品牌关联分类

文章目录 后端CategoryBrandEntity.javaCategoryBrandController.javaCategoryBrandServiceImpl.javaCategoryServiceImpl.javaBrandServiceImpl.java删除 npm install pubsub-jsnpm install --save pubsub-js这个错误是由于在尝试安装 pubsub-js 时&#xff0c;npm 发现了项目…

数据结构(栈和队列的实现)

1. 栈&#xff08;Stack&#xff09; 1.1 栈的概念与结构 栈是一种特殊的线性表&#xff0c;其只允许固定的一段插入和删除操作&#xff1b;进行数据插入和删除的一段叫做栈顶&#xff0c;另一端叫栈底&#xff1b;栈中的元素符合后进先出LIFO&#xff08;Last In First Out&…

C++——模拟实现vector

1.查看vector的源代码 2.模拟实现迭代器 #pragma oncenamespace jxy {//模板尽量不要分离编译template <class T>class vector{public:typedef T* iterator;//typedef会受到访问限定符的限制typedef const T* const_iterator;//const迭代器是指向的对象不能修改&#xf…

透明物体的投射和接收阴影

1、让透明度测试Shader投射阴影 &#xff08;1&#xff09;同样我们使用FallBack的形式投射阴影&#xff0c;但是需要注意的是&#xff0c;FallBack的内容为&#xff1a;Transparent / Cutout / VertexLit&#xff0c;该默认Shader中会把裁剪后的物体深度信息写入到 阴影映射纹…

毕业设计_基于springboot+ssm+bootstrap的旅游管理系统【源码+SQL+教程+可运行】【41001】.zip

毕业设计_基于springbootssmbootstrap的旅游管理系统【源码SQL教程可运行】【41001】.zip 下载地址&#xff1a; https://download.csdn.net/download/qq_24428851/89828190 管理系统 url: http://localhost:8080/managerLoginPageuser: admin password: 123 用户门户网站…

【设计模式-解释模式】

定义 解释器模式是一种行为设计模式&#xff0c;用于定义一种语言的文法&#xff0c;并提供一个解释器来处理该语言的句子。它通过为每个语法规则定义一个类&#xff0c;使得可以将复杂的表达式逐步解析和求值。这种模式适用于需要解析和执行语法规则的场景。 UML图 组成角色…

SPDK从安装到运行hello_world示例程序

SPDK从安装到运行示例程序 #mermaid-svg-dwdwvhrJiTcgTkVf {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-dwdwvhrJiTcgTkVf .error-icon{fill:#552222;}#mermaid-svg-dwdwvhrJiTcgTkVf .error-text{fill:#552222;s…

android compose ScrollableTabRow indicator 指示器设置宽度

.requiredWidth(30.dp) Box(modifier Modifier.background(Color.LightGray).fillMaxWidth()) {ScrollableTabRow(selectedTabIndex selectedTabIndex, // 默认选中第一个标签containerColor ColorPageBg,edgePadding 1.dp, // 内容与边缘的距离indicator { tabPositions…

【本地缓存】Java 中的 4 种本地缓存

目录 1、手写一个简单的本地缓存1.1、封装缓存实体类1.2、创建缓存工具类1.3、测试 2、Guava Cache2.1、Guava Cache 简介2.2、入门案例2.2.1、引入 POM 依赖2.2.2、创建 LoadingCache 缓存 2.3、Guava Cache 的优劣势和适用场景 3、Caffeine3.1、Caffeine 简介3.2、对比 Guava…

图的基本概念 - 离散数学系列(五)

目录 1. 图的定义 节点与边 2. 度与路径 节点的度 路径与圈 3. 图的连通性 连通图与非连通图 强连通与弱连通 连通分量 4. 实际应用场景 1. 社交网络 2. 城市交通系统 3. 网络结构 5. 例题与练习 例题1&#xff1a;节点的度 例题2&#xff1a;判断连通性 练习题…

linux基础 超级笔记

1.Linux系统的组成 Linux系统内核&#xff1a;提供系统最核心的功能&#xff0c;如软硬件和资源调度。 系统及应用程序&#xff1a;文件、任务管理器。 2.Linux发行版 通过修改内核代码自行集成系统程序&#xff0c;即封装。比如Ubuntu和centos这种。不过基础命令是完全相…

【瑞昱RTL8763E】刷屏

1 显示界面填充 用户创建的各个界面在 rtk_gui group 中。各界面中 icon[]表对界面进行描述&#xff0c;表中的每个元素代表一 个显示元素&#xff0c;可以是背景、小图标、字符等&#xff0c;UI_WidgetTypeDef 结构体含义如下&#xff1a; typedef struct _UI_WidgetTypeDef …

vite学习教程03、vite+vue2打包配置

文章目录 前言一、修改vite.config.js二、配置文件资源/路径提示三、测试打包参考文章资料获取 前言 博主介绍&#xff1a;✌目前全网粉丝3W&#xff0c;csdn博客专家、Java领域优质创作者&#xff0c;博客之星、阿里云平台优质作者、专注于Java后端技术领域。 涵盖技术内容&…