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

news2024/10/4 22:05:02

🌈 个人主页: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.coloradmin.cn/o/2188789.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Stable Diffusion绘画 | 插件-Deforum:动态视频生成(中篇)

本篇文章重点讲解参数最多的 关键帧 模块。 「动画模式」选择「3D」&#xff1a; 下方「运动」Tab 会有一系列参数&#xff1a; 以下4个参数&#xff0c;只有「动画模式」选择「2D」才会生效&#xff0c;可忽略&#xff1a; 运动 平移 X 让镜头左右移动&#xff1a; 大于0&a…

JavaSE——面向对象练习题

1.对象数组排序 定义一个Person类{name,age,job}&#xff0c;初始化Person对象数组&#xff0c;有3个person对象&#xff0c;并按照age从小到大进行冒泡排序&#xff1b;再按照name的长度从小到大进行选择排序。 public class HomeWork01 {public static void main(String[] a…

基于spring boot的篮球论坛系统

作者&#xff1a;计算机搬砖家 开发技术&#xff1a;SpringBoot、php、Python、小程序、SSM、Vue、MySQL、JSP、ElementUI等&#xff0c;“文末源码”。 专栏推荐&#xff1a;SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;Java精选实战项…

基于拥堵模型的轻量级平台公交室内情况监控系统

论文标题&#xff1a;Bus Indoor Situation Monitoring System Based on Congestion Model Using Lightweight Platform 作者信息&#xff1a;Dong Hyun Kim, Yun Seob Kim, 和 Jong Deok Kim* 所属机构&#xff1a;Pusan National University, Department of Computer Scienc…

Linux·环境变量与进程地址空间

1. 命令行参数 各位可能见过main函数也是有参数的&#xff0c;只是我们平时写的代码都比较简单&#xff0c;用不到main函数的参数&#xff0c;下面我们看一下main函数的参数是什么又是怎么用的 我们看这样一段代码 其编译运行后的效果是这样的 我们将main函数后面的那两个参数叫…

排序算法剖析

文章目录 排序算法浅谈参考资料评价指标可视化工具概览 插入排序折半插入排序希尔排序冒泡排序快速排序简单选择排序堆排序归并排序基数排序 排序算法浅谈 参考资料 数据结构与算法 评价指标 稳定性&#xff1a;两个相同的关键字排序过后相对位置不发生变化时间复杂度空间复…

MyBatisPlus——学习笔记

MyBatisPlus 一、导入依赖 <!-- MyBatisPlus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.2</version></dependency><!-- MySql --><de…

C++基础(7)——STL简介及string类

目录 1.STL简介 1.1什么是 1.2STL的历史版本 1.3STL的六大组件 ​编辑 1.4有用的网址 2.string类 2.1string的多种定义方式 2.2string的插入 2.2.1尾插&#xff08;push_back&#xff09; 2.2.2insert插入 2.3拼接&#xff08;append&#xff09; 2.4删除 2.4.1尾…

CoRL 2024 麻省理工学院提出T3触觉Transformer,打破触觉感知的壁垒,重塑未来机器人

在智能机器人领域&#xff0c;触觉感知的研究正逐渐成为关注的焦点。然而&#xff0c;如何让机器人通过触觉更智能地感知和操作&#xff0c;依然是一个未解决的挑战。基于相机的触觉感知是一种通过在软弹性体下嵌入相机来捕获与环境的细粒度交互的感知方法&#xff0c;是最流行…

Java报错输出的信息究竟是什么?

Java报错输出的信息究竟是什么&#xff1f; 本篇会带大家了解一下java运行时报错输出的信息内容&#xff0c;简单学习一下虚拟机内存中Java虚拟机栈的工作方式以及栈帧中所存储的信息内容 异常信息 当你的程序运行报错时&#xff0c;你是否会好奇打印出来的那一大坨红色的究竟…

搜索引擎相关的一段实习经历

0 前言 就是跟搜索相关的一段经历。主要工作就是建立倒排索引库相关的一些简单内容。 又翻到了以前的工作&#xff0c;权作纪念。 就是简单的封装cpp的库供python语言调用。 反正就是很多版本问题等等吧各种鬼问题。 我感觉这个思路可能还是待考证。 跨语言的调用我感觉还是不…

泛型编程--模板【C++提升】(特化、类属、参数包的展开、static、模板机制、重载......你想知道的全都有)

更多精彩内容..... &#x1f389;❤️播主の主页✨&#x1f618; Stark、-CSDN博客 本文所在专栏&#xff1a; C系列语法知识_Stark、的博客-CSDN博客 其它专栏&#xff1a; 数据结构与算法_Stark、的博客-CSDN博客 C系列项目实战_Stark、的博客-CSDN博客 座右铭&#xff1a;梦…

Java中的while和do...while循环

while和do...while循环 while循环基本语法执行流程注意事项练习 do...while循环基本语法说明流程图练习 while循环 基本语法 循环变量初始化; while(循环条件){循环体&#xff08;语句&#xff09;;循环变量迭代; }1&#xff09;while循环也有四要素&#xff1a;循环变量初始…

【JNI】普通类型的基本使用

简单使用 在上一期我们介绍了JNI的基本使用&#xff0c;这里简单介绍一下普通类型 HelloJNI.java&#xff1a;这里计算两个整型数的平均值&#xff0c;返回值类型为double public class HelloJNI { static {System.loadLibrary("hello"); }private native String …

electron-builder 首次执行报错问题解决

假日想研究一下 react electron 的使用&#xff0c;结果发现首次打包疯狂报错&#xff0c;研究了一下之后才发现是第一次的话 electron-builder 会从外面下载依赖包到我们系统中&#xff0c;由于某种力量导致压缩包无法下载或者是下载过慢导致失败&#xff0c;要解决其实也简单…

认知战认知作战:2024年9月30日中国股市大涨背景下的认知战分析报告

认知战认知作战&#xff1a;2024年9月30日中国股市大涨背景下的认知战分析报告 关键词&#xff1a;认知战, 中国股市, 信息操纵, 心理战, 技术战, 信息监管, 投资者素养, 国际合作, 法律法规, 协同作战, 谣言澄清, 市场情绪,认知作战,新质生产力,人类命运共同体,认知战,认知域…

《Linux从小白到高手》理论篇:深入理解Linux的计划任务/定时任务

值此国庆佳节&#xff0c;深宅家中&#xff0c;闲来无事&#xff0c;就多写几篇博文。本篇详细深入介绍Linux的计划任务/定时计划。 Linux的计划任务 在很多时候为了自动化管理系统&#xff0c;我们都会用到计划任务&#xff0c;比如关机&#xff0c;重启&#xff0c;备份之类…

二叉树--堆

1.二叉树的顺序结构 普通的二叉树是不适合用数组来存储的&#xff0c;因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结 构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储&#xff0c;需要注意的是这里的堆和操作系统 虚拟进程地址空间中的堆是两…

CSP-J Day 4 模拟赛补题报告

姓名&#xff1a;王胤皓&#xff0c;校区&#xff1a;和谐校区&#xff0c;考试时间&#xff1a; 2024 2024 2024 年 10 10 10 月 4 4 4 日 9 : 00 : 00 9:00:00 9:00:00~ 12 : 30 : 00 12:30:00 12:30:00&#xff0c;学号&#xff1a; S 07738 S07738 S07738 请关注作者的…

Windows应用开发-解析AVI视频文件

本Windows应用解析AVI视频文件&#xff0c;以表格的方式显示AVI文件结构。并可以将结果保存到bmp图片。下面是&#xff0c;使用该应用解析一部AVI电影获得的图片。 应用开发信息 定义一个INFO结构&#xff0c;包含两个字符串对象&#xff0c;一个ULONGLONG变量&#xff0c;和…