Linux - 环境变量、程序地址空间、进程地址空间及Linux2.6内核进程调度队列

目录

环境变量

    基本概念

    常见环境变量

    查看环境变量的方法

    测试PATH

    测试HOME

    测试SHELL

    和环境变量相关的命令

    环境变量的组织方式

    通过代码获取环境变量

    通过系统调用获取环境变量

程序地址空间

进程地址空间

Linux2.6内核进程调度队列

    一个CPU拥有一个runqueue

    优先级

    活动队列

    过期队列

    active指针和expired指针


环境变量

    基本概念

        环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。

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

        环境变量通常具有某些特殊用途,并且在系统当中通常具有全局特性。 

    常见环境变量

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

    查看环境变量的方法

        我们可以通过echo命令来查看环境变量,方式如下:

echo $NAME //NAME为待查看的环境变量名称

 例如:查看环境变量PATH。

russleo@VM-0-2-ubuntu:~$ echo $PATH

    测试PATH

        当你在计算机的终端界面输入ls命令时,瞬间就能看到当前目录下的文件和子目录列表,整个过程流畅且高效,仿佛魔法一般。但这个“魔法”背后的原理却并非神秘莫测,而是遵循了一套清晰的规则——这就是Linux和Unix系统中命令执行机制的奥秘所在。

        在深入探讨之前,让我们先来了解一下为何像ls这样的常用命令可以不加任何前缀就直接执行。这是因为这些命令通常会被预装在系统的特定目录中,比如/bin/usr/bin等,而这些目录被包含在一个环境变量PATH中。每当你在终端输入一个命令时,Shell(即命令解释器)就会在PATH所列出的目录里寻找与之匹配的可执行文件。由于ls等基础命令已经被放置在这些目录下,所以Shell可以轻易地找到它们并执行。

        相比之下,当你尝试运行一个你自己编译或创建的可执行程序时,情况就大不相同了。假设你的自定义程序存放在当前的工作目录中,Shell并不会自动在这里搜索可执行文件,因为这样做会带来安全风险,同时也可能引起命名冲突。为了确保每次执行的程序都是用户有意为之,系统设计了一个额外的步骤:你需要在命令前加上./。这个小小的点斜杠组合实际上是在告诉Shell:“请在当前目录下查找并执行接下来指定的程序。”      

  

        系统就是通过环境变量PATH来找到ls命令的,查看环境变量PATH我们可以看到如下内容:

         可以看到环境变量PATH当中有多条路径,这些路径由冒号隔开,当你使用ls命令时,系统就会查看环境变量PATH,然后默认从左到右依次在各个路径当中进行查找。

        而ls命令实际就位于PATH当中的某一个路径下,所以就算ls命令不带路径执行,系统也是能够找到的。

那么我们怎么样能实现自己的可执行程序也不用带路径就可以执行呢?

方式一:将可执行程序拷贝到环境变量PATH的某一路径下

        在操作系统中,可执行程序的执行是通过查找环境变量PATH指定的路径来完成的。因此,如果希望在不指定路径的情况下执行程序,可以将该程序拷贝到环境变量PATH包含的某个路径下,系统就能自动找到并执行该程序。

        举例:如果我们有一个名为myprogram的可执行程序,并希望在任何路径下都能直接执行它,可以按照以下步骤操作:

  1. 找到系统环境变量PATH包含的路径之一,比如/usr/local/bin(在Linux和macOS系统中常见)。
  2. myprogram程序拷贝到该路径下:
    cp myprogram /usr/local/bin/

    此后,无论当前位于哪个目录,只需输入myprogram即可执行该程序,系统会自动在环境变量PATH指定的路径中查找并执行它。

方式二:将可执行程序所在的目录导入到环境变量PATH中

        如果希望多个可执行程序所在的目录都能被系统自动找到,可以将这些目录添加到环境变量PATH中。这种方式适用于同时管理多个程序的情况。

  1. 找到你想要添加的目录路径,假设为/path/to/your/executables

  2. 将该路径添加到环境变量PATH中,可以通过以下命令实现(假设使用bash shell):

    export PATH="/path/to/your/executables:$PATH"
    

    这样做会将/path/to/your/executables目录添加到PATH的最前面,使得系统优先在该目录查找可执行程序。

  3. 确认修改生效,可以在当前终端中输入echo $PATH查看PATH变量是否包含了新增的目录。

    测试HOME

        任何一个用户在运行系统登录时都有自己的主工作目录(家目录),环境变量HOME当中即保存的该用户的主工作目录。

普通用户示例:

超级用户示例:

    测试SHELL

        我们在Linux操作系统当中所敲的各种命令,实际上需要由命令行解释器进行解释,而在Linux当中有许多种命令行解释器(例如bash、sh),我们可以通过查看环境变量SHELL来知道自己当前所用的命令行解释器的种类。

        而该命令行解释器实际上是系统当中的一条命令,当这个命令运行起来变成进程后就可以为我们进行命令行解释。 

    和环境变量相关的命令

  • echo:显示某个环境变量的值。

  • export:设置一个新的环境变量。 

  • env:显示所有的环境变量。 

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

  • unset:清除环境变量。 

    环境变量的组织方式

在系统当中,环境变量的组织方式如下:

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

    通过代码获取环境变量

你知道main函数其实是有参数的吗?
        main函数其实有三个参数,只是我们平时基本不用它们,所以一般情况下都没有写出来。
        我们可以在Windows下的编译器进行验证,当我们调试代码的时候,若是一直使用逐步调试,那么最终会来到调用main函数的地方。        

        在这里我们可以看到,调用main函数时给main函数传递了三个参数。

下面我们先通过代码解释一下前两个参数:

        在Linux操作系统下,编写以下代码,生成可执行程序并运行。

        main函数的参数解释如下:第一个参数表示字符指针数组中有效元素的数量,而第二个参数是一个字符指针数组。该数组的第一个元素存储可执行程序的位置,其余元素存储给定的选项,而数组的最后一个元素为NULL。 

下面我们可以尝试编写一个简单的代码:

        该代码运行起来后会根据你所给选项给出不同的提示语句。

#include <stdio.h>                                                                                                                         
#include <string.h>
int main(int argc, char *argv[], char* envp[])
{if(argc > 1){if(strcmp(argv[1], "-a") == 0){printf("you used -a option...\n");}else if(strcmp(argv[1], "-b") == 0){printf("you used -b option...\n");}else{printf("you used unrecognizable option...\n");}}else{printf("you did not use any option...\n");}return 0;
}

下面我们再了解一下第三个参数:

main函数的第三个参数实际上是环境变量表,通过这个参数可以访问系统的环境变量。 

例如,编写以下代码:

        运行结果就是各个环境变量的值:

        除了使用main函数的第三个参数来获取环境变量以外,我们还可以通过第三方变量environ来获取。  

        运行该代码生成的可执行程序,我们同样可以获得环境变量的值: 

注意: libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern进行声明。 

    通过系统调用获取环境变量

        除了通过main函数的第三个参数和全局变量environ来获取环境变量,我们还可以使用系统调用的getenv函数。该函数接受环境变量的名称作为参数,在环境变量表中进行搜索,并返回指向对应值的字符串指针。

例如,使用getenv函数获取环境变量PATH的值。

程序地址空间

在我们之前的学习过程中,对于下面这张空间布局图都有所了解

在Linux操作系统中,我们可以通过以下代码对该布局图进行验证: 

代码结果:

 

下面我们来再看一段代码

        在代码中,我们使用 fork 函数创建了一个子进程。子进程将全局变量 g_val 从 100 修改为 200 后进行打印。而父进程则先休眠 3 秒钟,然后再打印全局变量的值。

        理论上来说,子进程打印的全局变量值应该是 200,由于父进程是在子进程修改全局变量之后才进行打印,所以父进程打印的全局变量值也应该是 200。然而,实际运行结果如下所示: 

        可以看到,父进程打印的全局变量 g_val 的值仍然是之前的 100。更奇怪的是,父进程和子进程中打印的全局变量 g_val 的地址是相同的,这意味着父子进程在同一个地址读取到了不同的值。

        如果我们在同一个物理地址获取的值,那这些值应该是相同的。然而,现在在同一个地址获取到的值却不同,这只能说明我们打印出来的地址并不是物理地址!

        实际上,在编程语言层面打印出来的地址都是虚拟地址。物理地址是用户无法看到的,由操作系统统一管理。因此,即使父子进程打印出来的全局变量地址(虚拟地址)相同,两个进程中的全局变量值仍然是不同的。

 

注意: 虚拟地址和物理地址之间的转化由操作系统完成。 

进程地址空间

        我们之前将那张布局图称为程序地址空间实际上是不准确的,实际上那张布局图应该叫做进程地址空间。进程地址空间本质上是内存中的一种内核数据结构,在 Linux 系统中,进程地址空间具体由结构体 mm_struct 来实现。

        进程地址空间类似于一把尺子,这把尺子的刻度范围从 0x00000000 到 0xffffffff。尺子按照刻度被划分为不同的区域,例如代码区、堆区、栈区等。在 mm_struct 结构体中,记录了这些区域的边界刻度,例如代码区的开始刻度和结束刻度。如下图所示:

        在 mm_struct 结构体中,各个边界刻度之间的每一个刻度都代表一个虚拟地址,这些虚拟地址通过页表映射物理内存建立联系。由于虚拟地址是从 0x00000000 到 0xffffffff 线性增长的,所以虚拟地址也被称为线性地址。 

扩展:

  1. 堆向上增长和栈向下增长实际上是通过改变 mm_struct 中堆和栈的边界刻度来实现的。
  2. 我们生成的可执行程序实际上也被分为了多个区域,例如初始化区、未初始化区等。当该可执行程序运行时,操作系统将对应的数据加载到相应的内存区域中,这样可以大大提高操作系统的工作效率。实际上,执行程序的“分区”操作是由编译器完成的,因此代码的优化级别实际上是由编译器决定的

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

        例如,父进程有自己的 task_structmm_struct,而该父进程创建的子进程也有其自己的 task_structmm_struct。父子进程的进程地址空间中的各个虚拟地址分别通过页表映射到物理内存的某个位置,如下图所示:

        当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才会在内存中拷贝一份父进程的数据,然后再进行修改。

        例如,当子进程需要将全局变量 g_val 改为 200 时,此时会在内存中的某个位置存储 g_val 的新值,并改变子进程中 g_val 的虚拟地址,使其通过页表映射后得到新的物理地址。

        这种在需要进行数据修改时再进行拷贝的技术,称为写时拷贝技术。

1. 为什么数据要进行写时拷贝?

:因为进程具有独立性。多进程运行时需要独享各种资源,并且在运行期间互不干扰。写时拷贝可以确保子进程的修改不会影响到父进程,从而保持进程的独立性。

2. 为什么不在创建子进程的时候就进行数据的拷贝?

:因为子进程不一定会使用父进程的所有数据。如果子进程不对数据进行写入,没有必要对数据进行拷贝。通过按需分配(延时分配),在需要修改数据的时候再进行分配,可以更加高效地使用内存空间。

3. 代码会不会进行写时拷贝?

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

为什么要有进程地址空间?

:进程地址空间有以下几个重要作用:

  1. 避免系统级别的越界问题:例如,进程1不会错误地访问到进程2的物理地址空间,因为每次对某一地址空间进行操作之前,需要先通过页表映射到物理内存,而页表只会映射属于进程自己的物理内存。虚拟地址和页表的配合使用,本质上是为了包含内存。
  2. 统一的地址空间视图:每个进程都认为自己拥有相同的空间范围,包括进程地址空间的构成和内部区域的划分顺序。这样在编写程序时,只需关注虚拟地址,而无需关心数据在物理内存中的实际存储位置。
  3. 实现进程的独立性和内存空间的合理使用:每个进程都认为自己在独占内存,从而更好地完成进程的独立性。同时,合理使用内存空间,即在实际需要使用内存时再进行分配,可以将进程调度与内存管理解耦或分离。

对于创建进程的现阶段理解

:一个进程的创建实际上伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)以及页表的创建。

Linux2.6内核进程调度队列

    一个CPU拥有一个runqueue

        一个 CPU 拥有一个 runqueue,用于管理进程的调度。如果系统中有多个 CPU,就需要考虑进程的负载均衡问题,以确保每个 CPU 的负载相对均匀。

    优先级

优先级队列的下标说明如下:

  • 普通优先级:100~139
  • 实时优先级:0~99

        我们讨论的进程大多数都是普通优先级的。之前提到的 nice 值范围是 -20 到 19,共有 40 个级别,依次对应 queue 中普通优先级的下标 100~139。

        需要注意的是,实时优先级用于实时进程,即在执行一个进程之前必须完成另一个进程。现代计算机系统中几乎不再使用这种模型,因此对于 queue 中下标为 0~99 的元素,我们不需要关注。

    活动队列

        活动队列中包含时间片还没有结束的所有进程,这些进程根据优先级排列,其中 nr_active 代表当前运行状态的进程总数。queue[140] 数组中的每个元素都是一个进程队列,具有相同优先级的进程按 FIFO 规则排队调度。

调度过程如下:

  1. queue[140] 的下标 0 开始遍历。
  2. 找到第一个非空队列,该队列必定是优先级最高的队列。
  3. 选择该队列中的第一个进程进行调度。
  4. 调度完第一个进程后,继续调度该队列中的其他进程,直到该队列的所有进程都被调度完。
  5. 继续向后遍历 queue[140],寻找下一个非空队列。

        为了提高查找非空队列的效率,可以使用 5 × 32 个比特位的 bitmap[5] 来表示 queue 数组中的 140 个元素(即 140 个优先级),这大大提高了查找效率。

        总结来说,在系统中查找最合适的调度进程的时间复杂度是常数级别的,不会因为进程数量的增加而增加时间成本,这就是所谓的 O(1) 调度算法。

    过期队列

  • 过期队列和活动队列的结构相同。
  • 过期队列上放置的进程都是时间片耗尽的进程。
  • 当活动队列上的进程被处理完毕之后,对过期队列的进程进行时间片重新计算。

    active指针和expired指针

  • active指针永远指向活动队列。
  • expired指针永远指向过期队列。

        由于活动队列中的时间片未到期的进程会越来越少,而过期队列中的进程数量会越来越多(新创建的进程会放到过期队列中),所以会出现活动队列中的所有进程时间片都到期的情况。此时,将 active 指针和 expired 指针的内容交换,就相当于将过期队列变成活动队列,活动队列变成过期队列,从而重新拥有一批新的活动进程。这样循环进行即可。

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

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

相关文章

谈一谈爬虫开发工程师

爬虫就只是抓数据的吗&#xff1f;并不是&#xff0c;爬虫工程师的工作不再仅仅是抓取数据&#xff0c;还需要处理其他各种复杂问题&#xff0c;今天我们就来聊聊爬虫开发工程师。 一、 爬虫开发工程师工作内容 爬虫开发工程师是负责编写和维护网络爬虫程序的专业人员。他们的…

【多模态大模型】 ALBEF in NeurIPS 2021

一、引言 论文&#xff1a; Align before Fuse: Vision and Language Representation Learning with Momentum Distillation 作者&#xff1a; Salesforce Research 代码&#xff1a; ALBEF 特点&#xff1a; 该方法使用ViT进行图像特征提取&#xff0c;提出将BERT分两部分&am…

Cocos Creator2D游戏开发(3)-飞机大战(1)-背景动起来

资源见: https://pan.baidu.com/s/1cryYNdBOry5A4YEEcLwhDQ?pwdzual 步骤 1, 让背景动起来 2, 玩家飞机显现,能操控,能发射子弹 3.敌机出现 4. 碰撞效果(子弹和敌机,敌机和玩家) 5. 积分和游戏结束 6. 游戏存档,对接微信小游戏,保存历史最高分 7. cocos发布到微信小游戏 资源…

探索Python的进度条神器:tqdm

文章目录 探索Python的进度条神器&#xff1a;tqdm一、背二、tqdm简介三、安装tqdm四、tqdm的五个简单使用示例五、tqdm在不同场景下的应用六、常见问题及解决方案七、总结 探索Python的进度条神器&#xff1a;tqdm 一、背 景&#xff1a;为什么选择tqdm&#xff1f; 在Python…

苦学Opencv的第十四天:人脸检测和人脸识别

Python OpenCV入门到精通学习日记&#xff1a;人脸检测和人脸识别 前言 经过了十三天的不懈努力&#xff0c;我们终于也是来到了人脸检测和人脸识别啦&#xff01;相信大家也很激动吧。接下来我们开始吧&#xff01; 人脸识别是基于人的脸部特征信息进行身份识别的一种生物识…

Spring 常用的三种拦截器详解

前言 在开发过程中&#xff0c;我们常常使用到拦截器来处理一些逻辑。最常用的三种拦截器分别是 AOP、 Interceptor 、 Filter&#xff0c;但其实很多人并不知道什么时候用AOP&#xff0c;什么时候用Interceptor&#xff0c;什么时候用Filter&#xff0c;也不知道其拦截顺序&am…

spring —— 事务管理器

事务管理主要针对数据源进行操作&#xff1a;在数据库方面&#xff0c;通过 TransactionManager 事务管理器进行管理&#xff0c;表明一旦出现错误&#xff0c;该数据源的所有数据全部复原。那么数据库如何判断是否发生了错误呢&#xff1f;这就需要在代码方面&#xff0c;通过…

抖音直播弹幕数据逆向:websocket和JS注入

&#x1f50d; 思路与步骤详解 &#x1f575;️‍♂️ 思路介绍 首先&#xff0c;我们通过抓包工具进入的直播间&#xff0c;捕获其网络通信数据&#xff0c;重点关注WebSocket连接。发现直播弹幕数据通过WebSocket传输&#xff0c;这种方式比传统的HTTP更适合实时数据的传输。…

前端基于 axios 实现批量任务调度管理器 demo

一、背景介绍 这是一个基于 axios 实现的批量任务调度管理器的 demo。它使用了axios、promise 等多种技术和原理来实现批量处理多个异步请求&#xff0c;并确保所有请求都能正确处理并报告其状态。 假设有一个场景&#xff1a;有一个任务列表&#xff0c;有单个任务的处理功能…

【Qt】QLCDNumberQProgressBarQCalendarWidget

目录 QLCDNumber 倒计时小程序 相关属性 QProgressBar 进度条小程序 相关设置 QLCDNumber QLCDNumber是Qt框架中用于显示数字或计数值的小部件。通常用于显示整数值&#xff0c;例如时钟、计时器、计数器等 常用属性 属性说明intValueQLCDNumber显示的初始值(int类型)va…

企业版邮箱适用哪些企业

企业邮箱适合哪些企业呢&#xff1f;企业版邮箱为企业提供安全、稳定、集成的邮件服务&#xff0c;支持初创、中小、大型企业及特定行业需求。ZohoMail作为优质提供商&#xff0c;提供多层安全措施、移动访问、集成能力及定制化服务&#xff0c;满足不同规模企业需求。 一、企…

2023年系统架构设计师考试总结

原文链接&#xff1a;https://www.cnblogs.com/zhaotianff/p/17812187.html 上周六参加了2023年系统架构设计师考试&#xff0c;这次考试与以前有点区别&#xff0c;是第一次采用电子化考试&#xff0c;也是教材改版后的第一次考试。 说说考前准备&#xff1a;为了准备这次考试…

基于微信小程序的校园警务系统/校园安全管理系统/校园出入管理系统

摘要 伴随着社会以及科学技术的发展&#xff0c;小程序已经渗透在人们的身边&#xff0c;小程序慢慢的变成了人们的生活必不可少的一部分&#xff0c;紧接着网络飞速的发展&#xff0c;小程序这一名词已不陌生&#xff0c;越来越多的学校机构等都会定制一款属于自己个性化的小程…

《通讯世界》是什么级别的期刊?是正规期刊吗?能评职称吗?

问题解答 问&#xff1a;《通讯世界》是不是核心期刊&#xff1f; 答&#xff1a;不是&#xff0c;是知网收录的第一批认定学术期刊。 问&#xff1a;《通讯世界》级别&#xff1f; 答&#xff1a;国家级。主管单位&#xff1a;科学技术部 主办单位&#xff1a;中国科学技…

关于虚拟机在桥接模式下连接网络问题的记录

2024年7月28日03:49:19 环境&#xff1a;ubuntu22.04 desktop 虚拟机 问题&#xff1a;使用wget下载nginx安装包时出现问题&#xff0c;443端口持续无连接成功回复。 随后在确定配置ip无问题&#xff0c;检查了其正常访问互联网&#xff0c;随后试图ping niginx网站&#xff…

基于OSS前端直传的分片上传以及断点续传

一、大文件分片上传 原型 大文件如果直接上传的话由于nginx的限制会导致响应500报错&#xff0c;或者响应时间过长导致响应超时 并且大文件上传有如下缺点 上传时间长: 对于大文件&#xff0c;直接上传可能需要较长时间&#xff0c;特别是在网络速度较慢或不稳定的情况下。这…

ChatGPT秘籍:如何用AI阅读文献,提升你的学术效率

在当今信息泛滥的时代&#xff0c;迅速高效地搜集与处理信息显得尤为关键。本文将聚焦于如何利用ChatGPT高效阅读文献与文档&#xff0c;并提供详尽的技巧、心得以及实用的指令和插件解析&#xff0c;助你充分发挥ChatGPT的潜能。无论你是学生、科研人员还是行业从业者&#xf…

雪花算法的一些问题解析

前言 最近做项目&#xff0c;有些老旧项目&#xff0c;需要生成分布式唯一ID&#xff0c;不允许重复&#xff0c;此时如果要对其他中间件和数据库依赖小&#xff0c;那么就需要一套固定的ID生成规则&#xff0c;雪花算法就正当合适&#xff0c;当时Twitter就是用来存储数据库I…

JSP基础语法与指令

任何语言都有自己的语法&#xff0c;在java中有&#xff0c;JSP作为java技术的一种应用&#xff0c;它拥有一些自己扩充的语法(了解知道即可&#xff01;&#xff01;&#xff01;)&#xff0c; Java所有语法都支持&#xff01; JSP表达式 <html><head><title…

【Redis 初阶】初识 Redis

一、了解 Redis Redis 官网&#xff1a;Redis - The Real-time Data Platform Redis 是一种基于键值对&#xff08;key-value&#xff09;的 NoSQL 数据库。与很多键值对数据库不同的是&#xff0c;Redis 中的 key 都是 string&#xff08;字符串&#xff09;&#xff0c;值&a…