🌏博客主页:PH_modest的博客主页
🚩当前专栏:Linux跬步积累
💌其他专栏:
🔴 每日一题
🟡 C++跬步积累
🟢 C语言跬步积累
🌈座右铭:广积粮,缓称王!
文章目录
- 环境变量
- 基本概念
- 常见环境变量
- 查看环境变量的方法
- 测试PATH
- 测试HOME
- 测试SHELL
- 和环境变量有关的命令
- 环境变量的组织方式
- 通过代码获取环境变量
- 通过系统调用获取环境变量
- 环境变量通常是具有全局属性的
- 程序地址空间
- 进程地址空间
- Linux2.6内核进程调度队列
- 一个CPU拥有一个runqueue
- 优先级
- 活动队列
- 过期队列
- active指针和expired指针
环境变量
基本概念
- 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。
- 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
- 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
- 环境变量,不是一个,而是一堆,彼此其实没有关系
- 环境变量,一般是系统内置的具有特殊用途的变量。定义变量的本质,其实就是开辟空间,在运行期间我们的程序也能开辟空间。
操作系统/bash是用C语言写的程序,它能在运行中开辟空间吗?
答:可以,系统的环境变量,本质就是系统自己开辟空间,给他名字和内容即可!
常见环境变量
- PATH: 指定命令的搜索路径。
- HOME: 指定用户的主工作目录(即用户登录到Linux系统中的默认所处目录)。
- SHELL: 当前Shell,它的值通常是/bin/bash。
查看环境变量的方法
echo $NAME
//NAME:你的环境变量名称
测试PATH
创建test.c文件
对比./test执行和直接test执行
直接执行test没有显示任何东西,而加了./之后就可以顺利执行代码,最终打印出hello world!
这是为什么呢?
因为我们执行一个可执行程序之前必须先找到它在哪里,./就是告诉系统我们要执行的程序就位于当前目录下。
那为什么ls不用加./也能直接执行呢?
因为ls的路径已经被配置到环境变量当中了,系统可以直接通过环境变量PATH来找到ls命令,查看环境变量PATH我们可以看到如下内容:
可以看到环境变量PATH当中有多条路径,这些路径由冒号隔开,当你使用ls命令的时候,系统就会查看环境变量PATH,然后默认从左到右依次在各个路径下查找。
而ls命令就处于PATH当中的某一路径下,所以就算ls命令不带路径执行,系统也是能够找到的。
那可不可以让我们自己的可执行程序也不用带路径就可以执行呢?
当然可以,有两种方法:
方法一:直接将可执行程序拷贝到环境变量PATH的某一路径下。
既然在未指定路径的情况下,系统会根据环境变量PATH当中的路径进行查找,那我们就可以将我们的可执行程序拷贝到PATH的某一路径下,此后我们的可执行程序不带路径系统也可以找到了。
[_HPH@iZbp1ezziqb3x7ubzpkn9wZ demo-8-15]$ sudo cp test /usr/bin
方法二: 将可执行程序所在的目录导入到环境变量PATH中。
将可执行程序所在的目录导入到环境变量PATH当中,这样一来,没有指定路径时,系统就会来到该目录下进行查找了。
[_HPH@iZbp1ezziqb3x7ubzpkn9wZ demo-8-15]$ export PATH=$PATH:/home/_HPH/dirforproc/ENV
测试HOME
任何一个用户在运行系统登录时都有自己的主工作目录(家目录),环境变量HOME当中即保存的改用户的主工作目录。
普通用户示例:
超级用户示例:
~和HOME的关系
由此可知,使用cd ~
命令可以直接进入家目录
测试SHELL
我们在Linux操作系统中所敲的各种命令,实际上需要由命令行解释器进行解释,而在Linux当中有许多种命令行解释器(例如bash、sh),我们可以通过查看环境变量SHELL来知道自己当前所用的命令行解释器的种类。
而该命令行解释器实际上是系统当中的一条命令,当这个命令运行起来变成进程后就可以为我们进行命令行解释。
和环境变量有关的命令
1、echo:显示某个环境变量的值。
2、export:设置一个新的环境变量
3、env:显示所有的环境变量
环境变量名称 | 表示内容 |
---|---|
PATH | 命令的搜索路径 |
HOME | 用户的主工作目录 |
SHELL | 当前Shell |
HOSTNAME | 主机名 |
TERM | 终端类型 |
HISTSIZE | 记录历史命令的条数 |
SSH_TTY | 当前终端文件 |
USER | 当前用户 |
邮箱 | |
PWD | 当前所处路径 |
LANG | 编码格式 |
LOGNAME | 登录用户名 |
4、set:显示本地定义的shell变量和环境变量。
5、unset:清除环境变量
环境变量的组织方式
在系统中,环境变量的组织方式如下:
每个程序都会收到一张环境变量表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串,最后一个字符指针为空。
通过代码获取环境变量
main函数其实是有三个参数的,但是平时基本不用它们,所以一般情况都没写出来。
main(int argc, char *argv[ ],char *env[ ])
我们先聊一聊前面两个参数。
运行结果如下:
现在我们来说说main函数的前两个参数,main函数的第二个参数是一个字符指针数组,数组当中的第一个字符指针存储的是可执行程序的位置,其余字符指针存储的是所给的若干选项,最后一个字符指针为空,而main函数的第一个参数代表的就是字符指针数组当中的有效元素个数。
我们可以尝试编写一个简单的代码,再来感受一下:
#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函数的第三个参数接受的实际就是环境变量表,我们可以通过main函数的第三个参数来获取系统的环境变量。
例如如下代码:
运行结果就是各个环境变量的值:
除了使用main函数第三个参数来获取环境变量之外,还可以使用第三方变量environ来获取。
我们同样可以获取环境变量的值:
注意:
- libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern进行声明。
- 本地变量不是环境变量,本地变量只在bash内部有效。
通过系统调用获取环境变量
我们还可以使用getenv函数来获取环境变量,getenv函数可以根据所给环境变量名,在环境变量表中进行搜索,并返回一个指向相应值的字符串指针。
例如,使用getenv函数获取环境变量PATH的值。
运行结果:
环境变量通常是具有全局属性的
- 环境变量通常具有全局属性,可以被子进程继承下去。
直接查看,发现没有结果,说明该环境变量根本不存在。
但是当我们添加环境变量export MYENV="hello world"
之后再次运行,发现有结果了!
说明: 环境变量是可以被子进程继承下去的!
实验:
- 如果只进行
MYENV="hello world"
,不调用export导出,在用我们的程序查看,会有什么结果?
不会打印出任何东西,因为是普通变量。
程序地址空间
这张空间布局图大家应该都见过:
下面是部分讲解:
在Linux操作系统中,我们可以通过一下代码对该布局图进行验证:
运行结果如下,与布局图所示是吻合的:
下面我们来看一段奇怪的代码:
代码当中用fork函数创建了一个子进程,其中让子进程先将全局变量g_val从100改为200后打印,而父进程先休眠3秒,然后再打印全局变量的值。
按道理来说子进程打印的全局变量的值为200,而父进程是在子进程将全局变量改后再打印的全局变量,那么也应该是200,但是结果却是100。
我们发现,父子进程,输出地址是一样的,但是变量内容不一样!能得出如下结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
- 但地址值是一样的,说明,该地址绝对不是物理地址!
- 在Linux系统下,这种地址叫做
虚拟地址/线性地址
- 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,有OS统一管理
注意:虚拟地址
和物理地址
之间的转化是由OS负责的。虚拟地址是给进程的——你的——用户的!
进程地址空间
我们之前将那张布局图成为进程地址空间实际上是不准确的,那张布局图实际上应该叫做进程地址空间,进程地址空间本质上是内存中的一种内核数据结构,在Linux中进程地址空间具体由结构体mm_struct实现。
进程地址空间就类似于一把尺子,尺子的刻度由0x00000000到0xffffffff,尺子按照刻度被划分为各个区域,例如代码区,堆区,栈区等。而在结构体mm_struct当中,便记录了各个边界刻度,例如代码区的开始刻度与结束刻度,如下图所示:
在结构体mm_struct当中,各个边界刻度之间的每一个刻度都代表一个虚拟地址,这些虚拟地址通过页表映射与物理内存建立联系。由于虚拟地址是由0x00000000到0xffffffff线性增长的,所以虚拟地址又叫做线性地址。
补充:
- 堆向上增长以及栈向下增长实际就是改变mm_struct当中堆和栈的边界刻度。
- 我们生成的可执行程序实际上也被分为了各个区域,例如初始化区,未初始化区等。当可执行程序运行起来的时候,操作系统将对应的数据加载到对应内存当中即可,大大提高了操作系统的工作效率。而进行可执行程序的“分区”操作的实际上是编译器,所以说代码的优化级别实际上是编译器说了算。
每个进程被创建时,其对应的进程控制块(task_struct)和进程地址空间(mm_struct)也会随之被创建。而操作系统可以通过进程的task_struct找到其mm_struct,因为task_struct当中有一个结构体指针存储的是mm_struct的地址。
例如,父进程有自己的task_struct和mm_struct,该父进程创建的子进程也有属于其自己的task_struct和mm_struct,父子进程的进程地址空间当中的各个虚拟地址分别通过页表映射到物理内存的某个位置,如下图:
说明: 上面的图就足以说明问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!
而当子进程刚刚被创建的时候,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。
例如,子进程需要将全局变量g_val改为200,那么此时就在内存的某处存储g_val的新值,并且改变子进程当中g_val的虚拟地址通过页表映射后得到的物理地址即可。
这种在需要进行数据修改时再进行拷贝的技术,叫做写时拷贝技术。
- 为什么数据要进行写实拷贝?
进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程。
- 为什么不在创建子进程的时候就进行数据的拷贝?
子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再进行分配(延时分配),这样可以高效的使用内存空间。
- 代码会不会进行写时拷贝?
90%的情况下是不会的,但并不能代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝。
- 什么是地址空间?
进程地址空间,每一个进程,都会存在一个进程地址空间,32[0,4GB];
进程地址空间是数据结构,具体到进程中,就是特定数据结构的对象!
- 为什么要有进程地址空间?
- 有了进程地址空间后,就不会有任何系统级别的越界和野指针等得发访问的问题存在了(可以进行拦截)。例如进程1不会错误的访问到进程2的物理地址空间,因为你对某一地址空间进行操作之前需要先通过页表映射到物理内存,而页表只会映射属于你自己的物理内存。总的来说,虚拟地址和页表的配合使用,本质功能就是包含内存。
- 有了进程地址空间后,每个进程都认为时相同的空间范围,包括进程地址空间的构成和内部区域的划分顺序等都是相同的,这样一来我们在编写程序的时候只需要关注虚拟地址,无需关注数据在物理内存当中实际的存储位置。
- 有了进程地址空间后,每个进程都会认为自己在独占内存,这样能更好的完成进程的独立性以及合理使用内存空间(当实际需要使用内存空间的时候再在内存进行开辟),并能将进程调度与内存管理进行解耦与分离。
- 对于创建进程的现阶段理解:
一个进程的创建实际上伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)以及页表的创建。
- malloc/new 申请内存,申请的内存,你会直接使用吗?本质是在哪里申请的?
申请的内存,我们不一定直接使用。申请内存,本质是在进程的虚拟地址空间内申请的。操作系统需要为效率和资源的使用率负责,这样可以保证内存的使用率,不会空转;可以提升new或者malloc的速度。
Linux2.6内核进程调度队列
一个CPU拥有一个runqueue
如果有多个CPU就要考虑进程个数的父子均衡问题。
优先级
queue下标说明:
- 普通优先级:100~139。
- 实时优先级:0~99。
我们进程的都是普通的优先级,前面说到nice值的取值范围是 -20 ~ 19,共40个级别,一次对用queue当中普通优先级的下标100 ~ 139。
注意: 实时优先级对应实时进程,实时进程是指先将一个进程执行完毕再执行下一个进程,现在基本不存在这种机器了,所以对于queue当中下标为 0 ~ 99的元素我们不关心。
活动队列
时间片还没有结束的所有进程都按照优先级放在活动队列当中,其中nr_active代表总共有多少个运行状态的进程,而queue[140]数组当中的一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度。
调度过程如下:
- 从0下标开始遍历queue[140]。
- 找到第一个非空队列,该队列必定为优先级最高的队列。
- 拿到选中队列的第一个进程,开始运行,调度完成。
- 接着拿到选中队列的第二个进程进行调度,直到选中进程队列当中的所有进程都被调度。
- 继续向后遍历queue[140],寻找下一个非空队列。
bitmap[5]:queue数组当中一个有140个元素,即140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5 × 32个比特位表示队列是否为空,这样一来便可以大大提高查找效率。
总结: 在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不会随着进程增多而导致时间成本增加,我们称之为进程调度的O(1)算法。
过期队列
- 过期队列和活动队列的结构相同。
- 过期队列上放置的进程都是时间片耗尽的进程。
- 当活动队列上的进程被处理完毕之后,对过期队列的进程进行时间片重新计算。
active指针和expired指针
- active指针永远指向活动队列。
- expired指针永远指向过期队列。
由于活动队列上时间片未到期的进程会越来越少,而过期队列上的进程数量越来越多(新创建的进程都会被放到过期队列上),那么总会出现活动队列上的全部进程的时间片都到期的情况,这时将active指针和expired指针的内容交换,就相当于让过期队列变成活动队列,活动队列变成过期队列,就相当于又具有了一批新的活动进程,如此循环进行即可。