推荐书籍,《深入理解Linux内核》。鸠摩搜书 | 全网电子书搜索引擎,小说人必备 | Tbox导航 (tboxn.com)
寄存器
你应该知道,代码是被加载到内存当中,cpu才能进行运算的,那么,我们在写函数返回值的时候,或者是这个程序的返回值我们是怎么拿到的呢?
其实很简单,就是靠 cpu 当中寄存器。cpu 当中寄存器有很多个,根据使用的编译器不同,各种方式功能,使用的 寄存器都有所区别。
当函数要返回一个值之时,会先用move 指令把这个函数的返回值存储到一个寄存器当中(其中的 eax 就是一种寄存器,当然这里只是举例子,不同编译器可能使用不同的 寄存器):
return -> mov eax 10
然后,如果在函数外部有 对应变量来接收函数的返回值的话,就会 再次使用 move 指令把寄存器当中的数据,拷贝到 对应变量当中:
int a = add(a,b);
mov eax -> a
所以,当你的 函数返回值 的数据很多时,比如返回的是一个非常大的结构体对象,那么我们一般是不使用 传值返回,因为传值返回就会占用多个寄存器,因为寄存器的大小不大。我们一般是在堆上开辟空间,因为堆上的空间不会随着函数栈帧 的销毁(栈的销毁)而销毁,在返回只是也不是返回 一个堆上的空间大小,而是直接返回这个堆空间的首地址。
系统如何得知,当前进程运行到哪一行代码的?
cpu 还使用 寄存器来记录当前执行到代码的那一行了。
在cpu 当中有一个寄存器,叫做 程序计数器。在很多教材当中 喜欢把它叫做 PC指针,eip。说白了这个就是一个计数器,记录当前进程正在执行的指令的下一行指令的地址。
比如,当前cpu正在执行的是 50 行代码,那么 程序计数器 记录的就是 第 51 行地址。
所以,我们在写高级语言时,会遇到顺序语句,逻辑语句,循环语句。也就是程序计数器在计算下一行要执行的语句的行数 的 算法不同而已。
例如顺序语句就是顺序往下依次递增;循环语句就是,在循环体当中按照 各行代码 语句执行,当执行到 循环体最后一行,重复循环体第一行进行执行。
cpu 当中的寄存器有很多,比如:
- 通用寄存器:eax,ebx,ecx,edx····· 这种寄存器只要 有人想用,就能用。
- 栈帧:ebp,esp,eip
- 状态寄存器:staus···· 可以用于 实现 进程 调度算法。
- ··························
那么寄存器扮演者什么角色呢?
首先,寄存器也具有,对数据进行临时保存的能力。计算机在运行时,一些重要的数据,必须保存到cpu 当中。因为 放在 cpu 内部,这些数据才会离 cpu 更近,而且,寄存器取放数据的效率高。
所以,cpu 为了提高效率,就会把 处理频率高的数据(进程的高频数据),放到寄存器当中。也就是说,cpu 内的寄存器,存储的是进程相关的数据,这部分数据是会随时被cpu 访问和修改的。
而且,这部分数据都是进程的临时数据 -- 进程的上下文。
当某一个进程的要离开之时,要把自己的进程上下文数据保存好,甚至带走。保存带走的目的就是为了,当这个进程在进程切换回到cpu当中执行之时,进程可以恢复到之前的运行状态,也就是上次运行是运行到哪一步了。让 cpu 知道加下来该如何运行这个进程。(这个过程被称之为 -- 保存进程上下文的操作)
如果只是把进程的临时数据都在cpu 当中保存的话,当新进程想进入的时候,只是简单粗暴的把进程的数据和代码拷贝到 cpu 当中,那么,cpu 当中 老进程的数据不就被覆盖了吗?
所以,进程再被切换的时候,会做两件事情(这个过程称之为进程切换的操作):
- 保存上下文
- 恢复上下文
上述 恢复进程上下文 的操作,可以理解为把进程曾经保存的 进程数据都重新放到cpu当中,cpu在按照这些上下文数据,在去继续执行这个进程。
所以,如果是一个要执行很长时间的进程,不能一直在cpu上运行,当这个进程在当前时间片的执行时间到了,就要进行进程切换。因为这个进程要进行很长时间的执行,所以,这个进程注定是要被进行高频的进程切换的。那么该进程在运行期间,随时可能被中断,进行进程切换,在切换期间,就会有这个进程上下文 打包保存带走,和 恢复上下文的操作。
进程的上下文保存在哪?
寄存器当中能存储的数据肯定是不多的,所以,保存的进程的临时数据也是不多的,所以,结合这种情况和上述 例子,那么,在执行进程切换的时候,进程的上下文数据就不能保存在 cpu 的寄存器当中。
那么,进程的上下文保存在哪?进程的 PCB 对象当中吗?
你可能会认为:是定义一个专门用于存储当前进程的 上下文文件的 结构体对象,把这个结构体对象放到 PCB 当中。
其实这个做法是不太对的,因为这个做法太慢了。
cpu 在保存进程的上下文时,有自己的硬件方法:
x86保护模式——全局描述符表GDT详解_gdt全局描述符表 作用-CSDN博客
第十四课 局部段描述符的使用-CSDN博客
等等,关于操作系统硬件。
环境变量
环境变量介绍
环境变量是有系统提供的一组 name = value (键值对)形式的变量,不同的环境变量有不同的用户,通常具有全局属性。
环境变量 一般是指在操作系统中用来指定操作系统运行环境的一些参数
PATH环境变量
我们在 配置 Java 环境,可能会遇到过配置环境变量的问题。
我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但
是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
就像你在 开头 #include<> 引入的头文件,编译器是在哪里找到 头文件的位置的,靠的就是 环境变量当中对这个头文件 路径存储。
还有,在Linux 当中,你可能会有疑问,系统当中的命令,本质上也就是一个一个的软件,执行这个命令,也就是借助 bash 帮助我们解析这个命令,然后运行这个命令的程序。
但是,为什么我们输入指令,直接输入指令的名称就可以运行了;但是,我们运行自己写的程序还是需要 带上绝对目录,或者是 相对目录呢?
其实,这些系统当中的命令是在 /usr/bin 这个目录下的,这个目录是 系统默认的指令搜索的路径,所以,不需要带上 绝对路径 和 相对路径,让操作系统找到这个文件。
但是,我们自己写的 可执行文件,是在工作目录下存储的,需要带上 路径。
产生上述的原因就是,关于Linux在指令的搜索上,会帮我们 配置一个环境变量 --- PATH。这个环境变量是自动配置好的, 这个 PATH 当中包含了 一些 路径:
他是以 ":" 作为分隔符,来定义了多个路径:
这上面的每一条路径,就是系统在执行指令之时,在查找指令的可执行文件的默认路径。所以,在执行执行之时,就不会再 带上路径了。
我们可以使用 env 这个命令来查看到 系统当中所有的环境变量:
在 当中还可以 通过 getenv()函数,来获取到 传入的 字符串表示的 环境变量:
如在例子,利用 C 程序打印 PATH 环境变量的内容:
输出 PATH 内容:
因为获取到是 PATH 环境变量的内容,所以,不同的用户调用这个函数,得到的结果是不一样的,比如,下述登录 root 账户来运行这个程序:
这样的话,我们在 代码当中就可以判断,当前是哪一个用户(利用返回的字符串来控制),更具不同的用户就可以,实现不同的功能:
除了使用 getenv()函数之外,还可以使用 putenv()将环境变量拼接为字符串,然后将其替换原来的环境变量:
C语言putenv()函数:用于改变或增加环境变量的内容 - C语言网 (dotcpp.com)
在 PATH 当中添加 和 删除路径
PATH=/xxx/xxx
使用上述方式,可以直接覆盖掉 PATH 当中的路径,以 上述我们输入的 /xxx/xxx 路径。
PATH=$PATH:/xxx/xxx
上述方式是在 PATH 原有的基础之上来,追加 一个 路径。
如下例子所示,就追加了一个 路径 :
此时,在我们刚刚配置的目录下的text 可执行文件,不需要路径都可以执行了:
注意:PATH是一个内存级的环境变量,当你启动shell 之时,才会创建,如果你关机了,或者是退出 shell 了,那么这个环境变量就没了。当后序再次启动之时,这个环境变量其实是在 系统当中有配置文件的,启动就会按照配置文件来配置PATH。
像上述是在 Xshell 当中运行的,不需要Linux 操作系统重启,直接重启 Xshell 即可。
HOME环境变量
如果是以 root 用户登录的话,使用 pwd 和 echHOME 看到的路径是这样的:
如果是以 普通用户登录的话,看到是如下路径:
也就是说,root用户刚登录的话,默认是在 .home 这个目录下的;而普通用户 刚登录的话,默认是在 自己的家目录下的。
当你在使用 shell 登录账户时候,shell 会识别 你当前登录的是什么用户,然后 填充 HOME 环境变量,当登录成功后,就默认帮你放到这个用户的 HOME 路径下了,因为有 HOME 的存在,shell 根据 HOME 给我们分配命令行解释器(bash)。
SHELL环境变量
这个环境变量当中存储的是,我们当前使用的是哪一个 shell,对应的可执行程序:
环境变量的组织方式
环境变量的组织方式,是以一个 environ 指针管理的 一个 char* [] 保存很多个 字符串 的指针数组存储的:
命令行参数
main函数是有参数的,如下所示:
int main(int argc , char* argv[])
{
return 0;
}
argv是一个字符串数组,前面的 argc 表示这个 argv 这个字符串数组存储的是什么内容:
我们发现,这个存储的是,我们运行这个 可执行程序,所使用 命令和 选项。是以空格来分隔 各个选项的:
main()函数不是第一个被调用的函数,main函数是被 Statup()这个函数所调用的。main 函数也是被地调用的函数。所以他才能接收的到我们传入的 命令。其实本质上,我们传入的命令(输入的命令)其实就是一个 字符串。比如 ./text -a -b 本质上就是 输入了 "./text -a -b"这个字符串。然后 bash 命令行解释器就帮我们,按照空格分隔的方式,分隔(解释)了这个 命令。
而,我们使用的命令后面有不同的选项,这个选项是可以在 main 函数当中用 argv 这个字符串数组,采用 下标的方式访问到的,就和上述的例子是一样的。
其实,我们使用命令,看上去有很多个选项,可以实现不同的功能,本质上那个就是用传入的不同的 字符串,分别判断这些字符串,然后实现不同功能:
示例输出:
所以,为了支持在命令行当中也可以更具我们自己的需要,来输入不同的参数,所以在main函数当中就可以 带 参数传入。这种方式是为指令,工具,软件等等提供命令行选项的支持。
main当中的 env 参数(理解 环境变量的 全局属性)
main函数当中有两个 核心向量表,一个是命令行参数表,另一个是环境变量表。
main()函数除了上述说的两个参数之外,还有参数, env 字符串数组:
int main(int argc , char* argv[], char* env[])
{
return 0;
}
这个 env 字符串数组 的结构 是和 argv 一样的。分隔方式也是一样,用空格来分隔,分隔的是一个一个的环境变量。
首先我们还是利用循环来打印一下 这个 环境变量表:
输出:
上述截图没有截完,其实这里的输出和 外部使用 env 命令是差不多的。把系统的环变量打印了。
所以,在理解上述两个 main 函数的 核心向量表之后,其实,当我们写了一个可执行程序,执行这个可执行程序时,不是简简单单的 把这个程序加载到内存当中,然后运行;
而是,一定有一个函数,去调用了 main()函数,把 main()函数最核心的两个向量表传入到main()函数的当中了。
有上述打印的 和 env 指令一样的环境变量信息,那么为什么 text 这个程序能拿到 环境变量信息呢?
别忘了,text 是 bash 命令行解释器所创建的子进程,而上述打印的 环境变量,不是 运行了 text 可执行程序之后才有的,而是当我们打开我们 shell ,也就是打开终端之后,这些个 环境变量就被创建了,所以,这些环境是来自 bash 的。
所以,我们就有了一个结论,子进程是可以继承父进程的全部环境变量信息的。
而,我们所运行的进程,都是子进程,bash 本身在启动的时候,会从操作系统 的配置文件当中,读取环境变量信息,bash 会在自己的上下文当中就会创建自己的 环境变量表。如果你需要,创建子进程,bash 就可以把这个 环境变量表给 创建的子进程来一份。子进程会继承父进程给的所有环境变量信息。
所以这也就意味着,从bash 运行开始,往后我们在终端上写的各种命令,执行的各种进程,都是bash 帮助我们创建的子进程,都会认识到 这些环境变量。
因为都是 bash 这个命令行解释器,解释出,然后创建了 bash 的子进程,这个进程之间的关系,不管是 父子关系,还是兄弟进程之间都是有 bash 进程上下文当中的 环境变量信息的。
所以,这就是为什么 环境变量具有全局属性。
假设,现在你想制定一些新的规则的话,就可以利用 环境变量的方式来制定 规则,这样的话,后续只用命令的方式运行的进程,就都会遵循这个 环境变量的 指定的规则了。
如果,不想在main()函数当中传入参数,又想获得到 所以的环境变量的话;可以使用 C语言 在 unistd.h 这个头文件当中定义的 environ 这个第三方变来获取:
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
// 声明 这个 变量
extern char **environ;
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}
创建/取消 一个 环境变量
使用 export 命令就可以创建一个 环境变量,如下例子所示:
上述,使用 env 也查到新创建的 环境变量的 存在。
那么,当前就把 上述新创建的 环境变量,加入到了bash的上下文当中了。
此时我们在下述可执行文件当中,就可以在main()函数当中,打印出 我们上述所添加的环境变量了:
输出:
我们可以使用 unset 这个命令来删除一个 环境变量。
我们上述所创建的 MY_VALUE 这个环境变量,在使用 unset 删除之后就不在了:
此时,env 也查不出 MY_VALUE 这个环境变量了。
本地变量(shell变量)和 常规命令 , 内建命令
当我们在 终端上直接随便输入一个 全大写的 变量名(这个变量名是原本没有的),然后去赋值的话,能不能 创建一个新的环境变量呢?答案是不行的。
如上所示,我们使用 env 和 grep 过滤,不能查看到 Y_VALUE 的信息,那么这个 MY_VALUE变量是 不存在的吗?
这个 MY_VALUE 又是存在的,我们可以 使用 echo 查看到这个 变量的信息:
其实这里的 MY_VALEU 不是环境变量,他是 本地变量。
可以使用 set 命令来查看到 当前进程的所以变量(包括 环境变量 和 本地变量):
本地变量是不会被 子进程继承的,只会在本BASH当中有效。
像是在 BASH 当中还是有一些 自己定义的 本地变量的:
那么,现在有一个问题,我们上述说过了,我们在命令行当中运行的都是 BASH的 子进程,那么子进程是拿不到 BASH 当中的本地变量的。
但是,我们上述在打印 本地变量的值的时候,使用的是 echo 这个命令来查看到的 本地变量的信息;这个 echo 不也是 BASH 所创建的 子进程吗?echo 为什么可以拿到 BASH当中的 本地变量的信息呢?
其实,不是所以的命令都是要由 bash 来创建子进程的。
LINUX 当中的命令其实分两批命令:
- 常规命令 -- 由创建子进程完成的。(这是我们所使用的很多命令)
- 内建命令 -- 不创建子进程,由bash 亲自执行的命令。类似于bash 调用了自己实现的,或者是系统提供的函数。
像上述的 echo 这个命令就是 一个 内建命令,所以,是在bash 当中自己执行,不需要创建子进程。所以,可以直接访问到 bash 上下文当中存储的本地变量的信息。
其实,cd 命令也是一个内建指令。cd 移动目录,其实本质上是bash 的执行目录发生了改变,所以,cd 如果只是一个 子进程的话,就不能更改的到 父进程 bash 的工作目录。只能是 cd 是内建命令,他更改的 工作目录其实和 bash 是一个工作目录;更改的其实是 bash 当中的 保存 工作目录的 本地变量。
有一个函数 chdir()这个函数可以帮助你修改路径:
谁调用这个接口,谁就会 把自己的工作路径改为 chdir ()这个函数传入的字符串对应的路径。
像上述的例子,就可以实现 比如 :./text / 修改工作路径到 更目录的类似效果。