[Linux]:环境变量与进程地址空间

news2024/11/15 13:55:59

img

✨✨ 欢迎大家来到贝蒂大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:Linux学习
贝蒂的主页:Betty’s blog

1. 环境变量

1.1 概念

**环境变量(environment variables)**一般是指在操作系统中用来指定操作系统运行环境的一些参数,具有全局属性,可以被子继承继承下去。

如:我们在编写C/C++代码的时,在链接的时候,我们并不知道所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。

1.2 查看环境变量

我们可以通过指令echo $NAME //NAME为待查看的环境变量名称,以下是常见的环境变量

环境变量名称表示内容
PATH命令的搜索路径
HOME用户的主工作目录
SHELL当前Shell
HOSTNAME主机名
TERM终端类型
HISTSIZE记录历史命令的条数
SSH_TTY当前终端文件
USER当前用户
MAIL邮箱
PWD当前所处路径
LANG编码格式
LOGNAME登录用户名

比如我们查看环境变量PATH

那这一连串的地址究竟是指什么呢?在回答这个问题之前,我们首先要思考为什么输入指令时,直接输入指令名称即可如ls,而执行我们自己的可执行程序必须在前面加./表示当前路径呢?如./a.out

其实答案很简单,系统能够通过指令名称找到其对应的位置,但是我们自己的可执行程序却不可以,必须指明在当前路径下。

现在我们就知道环境变量PATH中的地址具体代表什么了,代表的就是默认查找的路径。

如果我们想让我们的可执行程序也像系统指令一样使用,一种方法就是:将可执行程序拷贝到环境变量PATH的某一路径下。还要一种方法就是:将可执行程序所在的目录导入到环境变量PATH当中。

1.3 相关指令

  1. 指令env:显示所有的环境变量。

  1. 指令<font style="color:rgb(77, 77, 77);">export</font>:设置一个新的环境变量。

  1. 指令set:显示本地定义的shell变量和环境变量。

  1. 指令unset:取消本地变量与环境变量。

1.4 环境变量的组织方式

每个程序都会有一张环境变量表,环境变量表是一个字符指针数组,每个指针指向一个以\0结尾的环境字符串,最后一个字符指针为空。

画板

2. main函数的三个参数

main函数其实是有参数的,但是我们一般并不是经常使用。以下是main函数的原型:

int main(int argc,char* argv[],char* env[])
  • argc:代表命令行有效参数的个数。
  • argv : 指向命令行参数。
  • env: 指向环境变量。

首先我们通过下面代码来观察一下前两个参数的效果:

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

一共有三个有效参数,第一个有效参数为./a.out,第二个有效参数为-a,第三个参数为-b。这三个有效参数都被argv这个指针数组所指向,并且argv最后一个参数指向NULL

同样我们通过以下代码来探究一下第三个参数:

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

运行程序将显示所有的环境变量。

并且我们还可以通过第三方变量envison获取环境变量:

#include <stdio.h>
int main(int argc, char* argv[])
{
    extern char **environ;//先声明外部变量
    int i = 0;
    for(i = 0; environ[i]; i++)
    {
        printf("%s\n", environ[i]);
    }
    return 0;
}

并且我们最后还有介绍一个接口getenv,它能根据名称获取对应的环境变量

#include <stdio.h>
#include<stdlib.h>
int main(int argc, char *argv[], char *env[])
{
    printf("%s\n",getenv("PATH"));//获取对应的环境变量
    return 0;
}

3. 程序地址空间

相信大家对这幅图并不陌生了,这是我们的常说的内存布局分布图:

画板

其中堆栈相对而生,栈向下生长(在栈上的变量先定义的地址更大),堆向上生长(在堆上的变量先定义的地址更小)。

我们可以通过以下代码来验证:

#include<stdio.h>
#include<stdlib.h>
int g_unval;
int g_val = 100;
int main(int argc,char* argv[],char* env[])
{
    const char* str = "hello world";
    printf("code addr:%p\n",main);//main函数就在代码区
    printf("string rdonly addr:%p\n",str);//字符常量区
    printf("init addr:%p\n",&g_val);//已初始化全局数据区
    printf("uninit addr:%p\n",&g_unval);//未初始化全局数据
	
    //堆区
    char* heap1 = (char*)malloc(10);
    char* heap2 = (char*)malloc(10);                                                                                                               
    char* heap3 = (char*)malloc(10);    
    char* heap4 = (char*)malloc(10);    
    printf("heap1 addr:%p\n",heap1);    
    printf("heap2 addr:%p\n",heap2);    
    printf("heap3 addr:%p\n",heap3);    
    printf("heap4 addr:%p\n",heap4);    
	//栈区
    int a = 10;                    
    int b = 20;                    
    printf("stack addr:%p\n",&a);    
    printf("stack addr:%p\n",&b);    
	//命令行参数
    int i = 0;
    for( i = 0; argv[i]; i++)    
    {                              
        printf("argv[%d]:%p\n", i, argv[i]);    
    }
	//环境变量
    for(i = 0; env[i]; i++)
    {
        printf("env[%d]:%p\n", i, env[i]);
    }
    return 0;
}

最后让我们再来看看这一段代码:

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int g_val=100;
int main()
{
    pid_t id=fork();//创建子进程
    if(id==0)
    {
        //child
        g_val=200;
        printf("child PID:%d,PPID:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
    }
    else if(id>0)
    {
        //father
        printf("father PID:%d,PPID:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
    }
    else
    {
        // fork error
    }
    return 0;
}

我们惊讶地发现,当数据发生修改的时,在父子进程当中的同一个变量,地址是相同的,但是值却是不同的,这明显不符合我们的认知,因为同一个地址的值怎么可能不同呢。

前面我们学习进度时已经知道,当fork创建子进程时,父子默认情况共享数据。然而修改数据时,为了维护进程独立性,会发生写时拷贝,所以父子进程的值不同,但是地址为什么会不变呢?

如果我们是在同一个物理地址处获取的值,那必定值是相同的,而现在在同一个地址处获取到的值却不同,这只能证明我们打印出的地址并不是物理地址。

实际上,我们在语言层面上打印出来的地址都不是物理地址,而是一种虚拟地址。物理地址用户一概是看不到的,是由操作系统统一进行管理的,所以即使父子进程打印的地址相同,但是物理地址可能是不同的,这也就解释了为什么地址相同,而值却不同的问题。

4. 进程地址空间

我们之前将那张布局图称为程序地址空间实际上是不准确的,那张布局图实际上应该叫做进程地址空间,进程地址空间本质是内存中的一种内核数据结构,在Linux当中进程地址空间具体由结构体mm_struct实现,其一般包含以下这些信息:

struct mm_struct
{
    //代码区
    unsigned int code_start;  
    unsigned int code_end;  
    //已初始化全局数据区
    unsigned int init_data_start;
    unsigned int init_data_end;
    //未初始化全局数据区
    unsigned int uninit_data_start;
    unsigned int uninit_data_end;

    //....栈区
    unsigned int stack_start;
    unsigned int stack_end;
};

在结构体mm_struct当中,每一个的区域都代表一个虚拟地址,这些虚拟地址通过页表映射与物理内存建立联系。由于虚拟地址大小一般为4G,是由0x000000000xffffffff线性增长的,所以虚拟地址又叫做线性地址

每个进程被创建时,其对应的进程控制块task_struct和进程地址空间mm_struct也随之被创建。而操作系统就可以通过进程的task_struct找到对应的mm_struct(因为task_struct有一个结构体指针指向的是mm_struct)。

画板

然后我们就可以更加深入解释上面地址相同,值却不同的现象:首先父进程有自己的task_structmm_struct,该父进程创建的子进程也会有属于其自己的task_structmm_struct,父子进程的进程地址空间当中的各个虚拟地址分别通过页表映射到对应的物理内存,如下图:

画板

此时若是子进程要将g_val改为200,此时为了维护进程的独立性,不影响父进程的数据,子进程就会发生写实拷贝。

画板

  1. 问题一:为什么数据要进行写时拷贝?

进程间具有独立性。多进程运行,需要独享各种资源,运行期间互不干扰,不能让子进程的修改影响到父进程。

  1. 问题二:为什么不在创建子进程的时候就进行数据的拷贝?

子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延时分配),提高空间利用率。

  1. 问题三:代码会不会进行写时拷贝?

绝大数情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝。

  1. 问题四:为什么要有进程地址空间?
  • 通过添加一层软件层,实现对进程操作内存的风险管理(权限管理),本质是保护物理内存中各个进程的数据安全。
  • 将内存申请和使用在时间上解耦,利用虚拟地址空间屏蔽底层申请内存过程,实现进程读写内存操作与操作系统内存管理在软件层面分离。
    • 例如在堆上申请空间可能暂不全部使用甚至不用,从操作系统角度可在实际使用时再开辟空间建立映射关系,即基于缺页中断进行物理内存申请。
    • 若物理内存已满而仍需申请,操作系统可执行内存管理算法,将某些进程闲置空间置换到磁盘,使进程仍能申请到内存,且用户在应用层无感知。
  • 站在CPU和应用层角度,进程统一使用4GB空间且各空间区域相对位置较确定。有了虚拟地址空间,CPU能以统一视角看待物理内存,不同进程通过各自页表映射到不同物理内存,同时程序代码和数据可加载到内存任意位置,大大减少内存管理负担。

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

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

相关文章

在Unity环境中使用UTF-8编码

为什么要讨论这个问题 为了避免乱码和更好的跨平台 我刚开始开发时是使用VS开发,Unity自身默认使用UTF-8 without BOM格式,但是在Unity中创建一个脚本,使用VS打开,VS自身默认使用GB2312(它应该是对应了你电脑的window版本默认选取了国标编码,或者是因为一些其他的原因)读取脚本…

自己部门日均1000+告警?如何减少90%无效告警?

目录标题 一、告警的类别1.技术告警1.1基础设施告警1.2基本服务告警 2.业务告警3.监控大盘告警 二、为何需要告警治理&#xff1f;三、治理迫在眉睫1.1告警治理策略1.2核心监控告警点1.3避免告警反模式1.4告警规约制定1.5自动化处理 一、告警的类别 一般的告警分为以下几点&am…

ISP面试准备2

系列文章目录 文章目录 系列文章目录前言一.如何评价图像质量&#xff1f;二.引起图像噪声的原因三. ISP3.1 ISP Pipeline主要模块3.1.1坏点校正&#xff08;Defect Pixel Correction, DPC&#xff09;3.1.2黑电平校正&#xff08;Black Level Correction, BLC&#xff09;3.1.…

面试官:synchronized的锁升级过程是怎样的?

大家好&#xff0c;我是大明哥&#xff0c;一个专注「死磕 Java」系列创作的硬核程序员。 回答 在 JDK 1.6之前&#xff0c;synchronized 是一个重量级、效率比较低下的锁&#xff0c;但是在JDK 1.6后&#xff0c;JVM 为了提高锁的获取与释放效&#xff0c;,对 synchronized 进…

基于JSP的实验室管理系统

你好呀&#xff0c;我是计算机学姐码农小野&#xff01;如果有相关需求&#xff0c;可以私信联系我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;JSP技术 Spring Boot框架 工具&#xff1a;IDEA/Eclipse、Navicat、Tomcat 系统展示 首页 用户个…

自然语言处理系列六十二》神经网络算法》MLP多层感知机算法

注&#xff1a;此文章内容均节选自充电了么创始人&#xff0c;CEO兼CTO陈敬雷老师的新书《自然语言处理原理与实战》&#xff08;人工智能科学与技术丛书&#xff09;【陈敬雷编著】【清华大学出版社】 文章目录 自然语言处理系列六十二神经网络算法》MLP多层感知机算法CNN卷积…

【Python篇】PyQt5 超详细教程——由入门到精通(序篇)

文章目录 PyQt5 超详细入门级教程前言序篇&#xff1a;1-3部分&#xff1a;PyQt5基础与常用控件第1部分&#xff1a;初识 PyQt5 和安装1.1 什么是 PyQt5&#xff1f;1.2 在 PyCharm 中安装 PyQt51.3 在 PyCharm 中编写第一个 PyQt5 应用程序1.4 代码详细解释1.5 在 PyCharm 中运…

电子电气架构---私有总线通信和诊断规则

电子电气架构—私有总线通信和诊断规则 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明自…

最新版 | SpringBoot3如何自定义starter(面试常考)

文章目录 一、自定义starter介绍二、自定义Starter的好处及优势三、自定义starter应用场景四、自定义starter1、创建autoconfigure的maven工程2、创建starter的maven工程3、在autoconfigure的pom文件中引入MyBatis的所需依赖4、编写自动配置类MyBatisAutoConfiguration5、编写i…

红旗EQM换电连接器哪家生产

红旗EQM换电连接器概述 红旗EQM换电连接器是针对红旗品牌电动汽车设计的一种快速更换电池的装置。它允许用户在短时间内完成电池的更换&#xff0c;从而提高电动车的使用效率和便捷性。接下来&#xff0c;我们将详细探讨红旗EQM换电连接器的相关操作步骤、所需工具以及最新的相…

[Git使用] 实战技巧

文章目录 1. 理解分叉点2. Rebase3. FixUp4. Revert1. 理解分叉点 合并分支的时候会产生分叉点 比如: 仓库有dev和feature两个分支; 操作1:dev远程新建一个文件操作2:feature提交第一次操作3:远程执行把feture合并到dev分支在可视化界面可以看到 远程Dev分支的可视化: …

MySQL 锁分类有哪些?一文带你详解!!

MySQL 锁 全局锁全局锁的应用场景全局锁的缺点 表级锁表锁元数据&#xff08;MDL&#xff09;锁MDL 锁的问题 意向锁AUTO-INC 锁 行级锁记录锁&#xff08;Record Lock&#xff09;间隙锁&#xff08;Gap Lock&#xff09;临键锁&#xff08;Next-Key Lock&#xff09;插入意向…

安卓开发板_联发科MTK开发评估套件串口调试

串口调试 如果正在进行lk(little kernel ) 或内核开发&#xff0c;USB 串口适配器&#xff08; USB 转串口 TTL 适配器的简称&#xff09;对于检查系统启动日志非常有用&#xff0c;特别是在没有图形桌面显示的情况下。 1.选购适配器 常用的许多 USB 转串口的适配器&#xf…

宝塔部署Vue项目解决跨域问题

一、前言 使用宝塔面板部署前端后端项目相比用命令行进行部署要简单许多&#xff0c;宝塔的可视化操作对那些对Linux不熟悉的人很友好。使用宝塔部署SpringBoot后端项目和Vue前端项目的方法如下&#xff1a; 1、视频教程 2、文字教程1 3、文字教程2 以上的教程完全可以按照步骤…

以太网交换机工作原理学习笔记

在网络中传输数据时需要遵循一些标准&#xff0c;以太网协议定义了数据帧在以太网上的传输标准&#xff0c;了解以太网协议是充分理解数据链路层通信的基础。以太网交换机是实现数据链路层通信的主要设备&#xff0c;了解以太网交换机的工作原理也是十分必要的。 1、以太网协议…

SQLException: No Suitable Driver Found - 完美解决方法详解

&#x1f6a8; SQLException: No Suitable Driver Found - 完美解决方法详解 &#x1f6a8; **&#x1f6a8; SQLException: No Suitable Driver Found - 完美解决方法详解 &#x1f6a8;****摘要 &#x1f4dd;****引言 &#x1f3af;****正文 &#x1f4da;****1. 问题概述 ❗…

网络层 VII(IP多播、移动IP)【★★★★★★】

一、IP 多播 1. 多播的概念 多播是让源主机一次发送的单个分组可以抵达用一个组地址标识的若干目的主机&#xff0c;即一对多的通信。在互联网上进行的多播&#xff0c;称为 IP 多播&#xff08;multicast , 以前曾译为组播&#xff09;。 与单播相比&#xff0c;在一对多的…

【go】内存分配模型

内存是怎么分配给对象的&#xff1f; 内存分配优化的地方是&#xff1f; 讲讲golang内存分配模型&#xff1f; ans: 1.按照对象的大小分配&#xff1a;先算出对象的大小如果是tiny对象&#xff0c;就从tiny block中获取地址和偏移量&#xff0c;将对象打包到mcache;如果是16B以…

Xilinx系FPGA学习笔记(五)ROM的IP核学习

系列文章目录 文章目录 系列文章目录前言ROM IP分布式ROM生成ROM配置创建COE文件 块ROM生成如何快速生成Example Design 两种ROM对比 前言 最近在学习小梅哥的xilinx型FPGA开发板&#xff0c;一边学习一边记录&#xff0c;简化整理一下笔记 ROM IP 在 Memories &Storage …

JVM、JRE和 JDK:理解Java开发的三大核心组件

Java是一门跨平台的编程语言&#xff0c;它的成功离不开背后强大的运行环境与开发工具的支持。在Java的生态中&#xff0c;JVM&#xff08;Java虚拟机&#xff09;、JRE&#xff08;Java运行时环境&#xff09;和JDK&#xff08;Java开发工具包&#xff09;是三个至关重要的核心…