Linux进程概念-详细版(二)

目录

前言:

进程优先级

基本概念

查看系统进程

PRI与NI

查看进程优先级信息

通过top命令更改进程的nice值

通过renice命令更改进程的nice值

四个重要概念

环境变量

基本概念

常见环境变量

查看环境变量的方法

测试PATH

测试HOME

测试SHELL

​编辑 和环境变量相关的命令

环境变量的组织方式 

通过代码获取环境变量

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

如何理解本地变量只会在本bash内部有效,不会被子进程继承访问

程序地址空间

 进程地址空间


前言:

在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。

进程优先级

基本概念

什么是优先级?

优先级实际上就是获取某种资源的先后顺序,而进程优先级实际上就是进程获取CPU资源分配的先后顺序,就是指进程的优先权(priority),优先权高的进程有优先执行的权力。

优先级存在的原因?

优先级存在的主要原因就是资源是有限的,目前大部分的电脑是只有一个CPU的,所有单个电脑的CPU资源是有限。但的在规定上,一个CPU在同一时刻只能执行一个进程的指令,(但这并不意味着它只能运行一个进程)而进程是可以有多个的,所以需要存在进程优先级,来确定进程获取CPU资源的先后顺序。

查看系统进程

在Linux或者Unix操作系统中,用ps -l命令会类似输出以下几个内容,这在我们一中已经详细介绍了:

ps -l

列出的信息当中有几个重要的信息,如下:

  • UID:代表执行者的身份。
  • PID:代表这个进程的代号。
  • PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号。
  • PRI:代表这个进程可被执行的优先级,其值越小越早被执行。
  • NI:代表这个进程的nice值。(后面会介绍什么是nice值)

PRI与NI

  • PRI代表进程的优先级(priority),通俗点说就是进程被CPU执行的先后顺序,该值越小进程的优先级别越高。
  • NI代表的是nice值,其表示进程可被执行的优先级的修正数值。
  • PRI值越小越快被执行,当加入nice值后,将会使得PRI变为:PRI(new) = PRI(old) + NI。
  • 若NI值为负值,那么该进程的PRI将变小,即其优先级会变高。
  • 调整进程优先级,在Linux下,就是调整进程的nice值。
  • NI的取值范围是-20至19,一共40个级别。

 注意: 在Linux操作系统当中,PRI(old)默认为80,即PRI(new) = 80(old) + NI。

查看进程优先级信息

当我们创建一个进程后,我们可以使用ps -al命令查看该进程优先级的信息。

ps -al

通过top命令更改进程的nice值

top命令就相当于Windows操作系统中的任务管理器,它能够动态实时的显示系统当中进程的资源占用情况。 

top

输入top指令后会看到如下: 

然后按“r”键,会要求你输入待调整nice值的进程的PID。 

 输入进程PID并回车后,会要求你输入调整后的nice值。

输入nice值后按“q”即可退出,如果我们这里输入的nice值为10,那么此时我们再用ps命令查看进程的优先级信息,即可发现进程的NI变成了10,PRI变成了90(80+NI)。

注意: 若是想将NI值调为负值,也就是将进程的优先级调高,需要使用sudo命令提升权限。 

通过renice命令更改进程的nice值

使用renice命令,后面跟上更改后的nice值和进程的PID即可。

 

 注意: 若是想使用renice命令将NI值调为负值,也需要使用sudo命令提升权限。

四个重要概念

  • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便有了优先级。
  • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
  • 并行: 多个进程在多个CPU下分别同时进行运行,这称之为并行。
  • 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。

环境变量

基本概念

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

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

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

其实在一开始我听到环境变量时,也是很懵懵的,但是后面也是听了两三遍才反应过来,这里为了大家更好理解,就拿我们Windows下的环境进行举一个游戏例子说明一下。

首先我们查看我们Windows下已经下载的环境变量

 打开设置中的系统,点开高级系统设置

点击环境变量 

 就可以看到你的电脑的环境变量了。

我本人的电脑是安装了c++/python/java/c#的环境变量的。 

这里就拿我的世界举例,说明是环境变量。

使用 PCL 启动器(如 TLauncherMultiMC 等)玩 Minecraft Java 版 时,通常需要确保你的电脑上已经安装了适当的 Java 环境。Java 环境变量的设置在大多数情况下是必需的,尤其是在使用 PCL 启动器 时。

为什么需要配置 Java 环境变量?

Minecraft Java 版需要 Java Runtime Environment (JRE)Java Development Kit (JDK) 来运行。通常,Minecraft 会自动识别和配置 Java,但如果你的系统上没有正确设置环境变量,或者如果你安装了多个 Java 版本,可能会导致 Minecraft 无法找到 Java,或者使用错误的 Java 版本。

又比如说,我们使用vs编译器适配c++的编译环境的时候,其实在vs上下载的时候就适配好了c++的环境变量。

所以说,环境变量其实就是为了让在各个目标文件进行链接的时候,告诉电脑所链接的动静态库在哪里,使得生成可执行程序。

在Linux下

常见环境变量

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

查看环境变量的方法

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

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

例如,查看环境变量PATH。

测试PATH

在学习Linux下,你是否有这样的疑惑,就是ls与我们自己生成的c语言的可执行程序,二者都是一个待执行的,想要执行起来,应该都是用相同的方法,但是ls不像我们自己生成的c语言可执行程序一样,不需要带 ./  ,只需要ls就可以。而我们自己生成的可执行程序必须要在前面带上 ./ 才可以执行?

容易理解的是,./ 中的 . 表示当前路径。要执行一个可执行程序必须要先找到它在哪里,既然不带./就可以执行ls命令,说明系统能够通过ls名称找到ls的位置,而系统是无法找到我们自己的可执行程序的,所以我们必须带上./,以此告诉系统该可执行程序位于当前目录下。

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

可以看到环境变量PATH当中有多条路径,这些路径由冒号隔开,当你使用ls命令时,系统就会查看环境变量PATH,然后默认从左到右依次在各个路径当中进行查找。
而ls命令实际就位于PATH当中的某一个路径下,所以就算ls命令不带路径执行,系统也是能够找到的。

这时候就会有人有疑问了:那可不可以让我们自己的可执行程序也不用带路径就可以执行呢?

答案是可以的

当然可以,下面给出两种方式:

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

既然在未指定路径的情况下系统会根据环境变量PATH当中的路径进行查找,那我们就可以将我们的可执行程序拷贝到PATH的某一路径下,此后我们的可执行程序不带路径系统也可以找到了。

sudo cp proc /usr/bin

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

将可执行程序所在的目录导入到环境变量PATH当中,这样一来,没有指定路径时系统就会来到该目录下进行查找了。

export PATH=$PATH:/home/haha/code/12_11 // pwd 显示的路径

可以不带 ./ 直接运行

测试HOME

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

普通用户展示:

超级用户展示:

 

测试SHELL

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

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

 和环境变量相关的命令

1>、echo:显示某个环境变量的值。

注意:echo $ 是显示本地变量 

2>、export:设置一个新的环境变量。 

注意:如果将本部分代码去掉export是添加本地变量添加 

3>、env:显示所有的环境变量。 

部分环境变量说明:

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

4>、set:显示本地定义的shell变量和环境变量。 

5>、unset:清除环境变量。 

环境变量的组织方式 

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

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

通过代码获取环境变量

我们在一开始也说了,一个文件想要生成可执行程序,就需要通过链接,找到静态库与动态库,那么在c语言中,我们使用main函数的时候是怎么样找到的呢?这里就需要提一个疑问,你知道main函数其实是有参数的吗?

在 C 语言中,main 函数不仅可以有参数,而且它的确是通过链接器找到的。这些都涉及到程序的启动过程、链接过程以及 main 函数的定义和调用。

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

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

我们先来说说main函数的前两个参数。

首先先提一点,第一个参数argc其实就是count,第二个数向量表(也就是value)

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

运行结果如下:

现在我们来说说main函数的前两个参数,main函数的第二个参数是一个字符指针数组,数组当中的第一个字符指针存储的是可执行程序的位置,其余字符指针存储的是所给的若干选项,最后一个字符指针为空,而main函数的第一个参数代表的就是字符指针数组当中的有效元素个数。

下面我们可以尝试使用刚才讲到的知识点,编写一个简单的代码,该代码运行起来后会根据你所给选项给出不同的提示语句。 

#include <stdio.h>                                                                                                                         
#include <string.h>
int main(int argc, char* argv[])
{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函数的第三个参数来获取系统的环境变量。
例如,编写以下代码,生成可执行程序并运行打印所有的环境变量。 

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

注意:我们这里使用到的第三方变量environ来获取每一个第三个参数来获取环境变量。

同样也可以不使用直接访问env也可以。

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

扩展:

在规定上一个程序要运行起来,是至少需要两个表的

  1. 命令行参数表(argv)
  2. 环境变量表(env)

但是我们在一开始将前两个参数的时候是没有传第三个参数env的但是在实际上,并没有妨碍我们运行,甚至来说,我们在平时写c语言代码的时候一个都没有传,它还可以运行。

这是因为:

  • 虽然 envpmain 函数的一个可选参数,环境变量 对程序来说是至关重要的,但操作系统负责提供这些环境变量并将其与当前进程关联。即使你在程序中不显式地使用 envp 参数,你仍然可以通过库函数(如 getenv)访问这些环境变量。操作系统会自动为你管理并提供这些信息,保证程序能够正常运行。这也在证明我们写的c语言代码其实最外层不是main函数,还有别的函数在调用main函数。
  • 对于大多数常见的程序,它们可以不显式地接收 envp,操作系统会自动将环境变量加载到进程中,且程序可以在需要时通过标准库函数(如 getenv)访问这些环境变量。
  • 当你在 main 函数中只定义了 argcargv 时,操作系统仍然会设置并提供环境变量。这些环境变量会自动关联到当前进程的地址空间。当你使用诸如 getenv("HOME") 等标准库函数时,它们会在内存中查找环境变量表。

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

除了通过 main 函数的第三个参数(envp)和第三方变量 environ 来获取环境变量外,我们还可以通过系统调用 getenv 函数来获取环境变量。getenv 函数根据所给定的环境变量名,在环境变量表中进行查找,并返回一个指向相应值的字符串指针。

例如,使用 getenv 函数获取环境变量 PATH 的值,可以如下所示:

运行结果:

如何理解本地变量只会在本bash内部有效,不会被子进程继承访问

比如说我直接在Linux下输入以下指令

my_var="I am a local variable"

他的意思就是在bash进程中添加本地变量my_var。

然后我运行指令

echo $my_var

 他就会 输出: I am a local variable。

以上操作就是在bash内部添加了本地变量,然后再打印本地变量my_var的值,但是我们再创建一个进程。

我们已经知道所有的进程的父进程(祖宗进程)全都是bash,bash进程的PID是1,是所有进程的父进程(祖宗进程)。但是我们在创建的进程中,是无法访问/继承父进程bash内部定义的/已有的本地变量,只可以继承访问环境变量。

同样一个道理,如果我们在刚创建的进程中fork一个子进程,那么改子进程也不会继承其父进程的本地变量。

所以结论:

  • 本地变量 是仅在当前 shell 会话或进程内有效的变量。它们不会被子进程继承,也不会出现在 env 或其他子进程的环境中。
  • 环境变量 是通过 export 设置的,它们可以在父进程和所有子进程之间共享。

所以,本地变量只会在当前 shell(或进程)内有效,而 子进程 只能继承 环境变量,无法继承父进程的 本地变量

程序地址空间

32位的计算机系统理论上最大的内存为4G。

其推导过程为:32位计算机理论上的最大可寻址内存量为:2^32字节

1字节(Byte)= 8位(bit),所以可以寻址的最大内存为 2^32 字节,即:

2^32字节=4,294,967,296 字节=4 GB。

所以你一定看过这个图,就算没见过原图,也一定见过类似的图

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


#include <stdio.h>
#include <stdlib.h>int g_unval;  // 未初始化的全局变量
int g_val = 100;  // 已初始化的全局变量int main(int argc, char* argv[], char* envp[]) {int i;// 打印代码段地址printf("code addr: %p\n", main);  // main函数的地址// 打印只读常量区地址char* str = "hello world";  // 字符串字面量,存储在只读数据区printf("read only addr: %p\n", str);  // 字符串常量的地址// 打印已初始化数据段的地址printf("init addr: %p\n", &g_val);  // 已初始化全局变量g_val的地址// 打印未初始化数据段的地址printf("uninit addr: %p\n", &g_unval);  // 未初始化全局变量g_unval的地址// 打印堆区地址int* p = (int*)malloc(10 * sizeof(int));  // 动态分配10个int类型的内存if (p == NULL) {printf("Memory allocation failed!\n");return 1;  // 如果内存分配失败,则退出}printf("heap addr: %p\n", p);  // 堆区的地址// 打印栈区地址printf("stack addr: %p\n", &str);  // 栈上局部变量str的地址printf("stack addr: %p\n", &p);    // 栈上局部变量p的地址// 打印命令行参数的地址for (i = 0; i < argc; i++) {printf("args addr: %p\n", argv[i]);  // 打印每个命令行参数的地址}// 打印环境变量的地址i = 0;while (envp[i]) {printf("env addr: %p\n", envp[i]);  // 打印每个环境变量的地址i++;}// 释放动态分配的内存free(p);return 0;
}

运行结果:

可以观察到栈空间的开辟是从高地址向低地址的顺序开辟空间

堆是由低地址向高地址的顺序开辟空间

各个空间的地址大小顺序也符合上图的展示  



 下面我们来看一段奇怪的代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>int g_val = 100;int main() {pid_t id = fork();  // 创建一个新的进程if (id == 0) {  // 子进程printf("child: PID: %d, PPID: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);}else if (id > 0) {  // 父进程printf("father: PID: %d, PPID: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);}else {  // fork失败perror("fork failed");exit(1);}return 0;
}

在Linux下运行的效果如下:

可以看到父进程与子进程的全局变量g_val的地址空间其实是一样的,那么按照我们c语言学的知识,那么父进程与子进程其实是公用一个地址的数据的,也就代表着如果父进程修改了其值,按道理,子进程的值也会跟着修改。

我们对原有的代码上进行修改,使得子进行先修改其g_val的值然后打印,子进程执行完后,在让父进程执行,打印对应的数据。

按照上面的解释,按道理父进程的 g_val 的值应该会由原来的100变为200.

 那么我们看看实际运行效果吧

 运行结果:

但实际上只有子进程的进行了修改,这就显得矛盾了。

毕竟我们观察到现在 g_val 的父子进程的地址还是相同啊,这是为什么?

 那么我们只能总结出来:

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

实际上,我们在语言层面上打印出来的地址都不是物理地址,而是虚拟地址。物理地址用户一概是看不到的,是由操作系统统一进行管理的。

所以就算父子进程当中打印出来的全局变量的地址(虚拟地址)相同,但是两个进程当中全局变量的值却是不同的。

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

此回答来自chatGTP

扩展:

为什么要设置虚拟内存,直接存物理内存不行么?

设置虚拟内存的主要目的是为了简化计算机系统的内存管理,增强系统的灵活性、安全性和可扩展性。虽然直接使用物理内存似乎是一个直观的选择,但实际操作系统和硬件环境中使用虚拟内存有以下几个关键的理由:

. 隔离与保护 (Memory Protection)

  • 虚拟内存允许每个进程都有独立的虚拟地址空间,避免不同进程之间直接访问彼此的内存。这样,一个进程无法直接修改或影响其他进程的内存,从而增强了系统的安全性。
  • 如果没有虚拟内存,多个进程可能会使用相同的物理地址空间,这样一个进程的错误(如越界写入)可能会破坏其他进程的数据,甚至导致系统崩溃。

2. 简化编程模型

  • 对程序员来说,虚拟内存简化了内存管理,程序员无需关心物理内存的实际布局。每个程序都假设自己拥有一个连续的内存空间(虚拟地址空间),而不需要了解背后复杂的物理内存细节。
  • 这样,程序可以像运行在独立的机器上一样运行,不用担心内存碎片、物理内存的分配和释放等复杂问题。

3. 内存共享 (Memory Sharing)

  • 虚拟内存允许多个进程共享同一块物理内存。例如,多个进程可以共享只读的库文件,如 C 标准库,或共享某些数据结构。每个进程在虚拟地址空间中有独立的地址映射,但是它们可以映射到相同的物理内存区域。
  • 如果直接使用物理内存,进程之间的共享将更加复杂,且不易实现高效的共享机制。

4. 地址空间分离

  • 使用虚拟内存,操作系统可以给每个进程分配一个完整的虚拟地址空间,即使多个进程的虚拟地址空间重叠,它们也不会干扰彼此。比如,进程A和进程B可能都有虚拟地址 0x4000,但它们实际上指向的是不同的物理地址。
  • 这与直接使用物理内存不同,直接物理内存的管理会导致地址空间的重叠问题。

5. 程序加载和动态链接

  • 在没有虚拟内存的情况下,程序加载可能受限于物理内存的大小和布局,程序需要在启动时被完全加载到物理内存中。虚拟内存通过分页机制允许程序在运行时动态加载,并按需加载和卸载代码和数据。
  • 这种按需分页机制意味着一个大程序不必一次性加载到内存中,而是可以分段加载,只加载当前需要执行的部分。

6. 交换空间和页面交换 (Swapping and Paging)

  • 虚拟内存使得操作系统可以利用硬盘(或其他存储设备)作为扩展内存的“交换空间”。
  • 当物理内存不足时,操作系统可以将不活跃的内存页面写入硬盘中的交换文件或交换分区,然后将需要的页面从硬盘加载到物理内存中。这使得系统可以运行比实际物理内存更大的程序或处理更多的任务。
  • 没有虚拟内存,操作系统就无法实现这一点,因为程序的物理地址和物理内存是绑定的。

7. 内存分配和回收的灵活性

  • 操作系统可以根据需求动态地管理物理内存。它可以将物理内存分配给正在运行的进程,并根据需要回收不再使用的内存(例如,通过内存交换机制)。虚拟内存的使用使得这种动态管理变得更加简单和高效。
  • 如果没有虚拟内存,操作系统需要复杂地跟踪物理内存的每一部分,并直接分配和回收,这样会增加管理的难度。

8. 支持更大内存空间

  • 虚拟内存允许程序访问比物理内存更大的地址空间。例如,现代计算机系统中的 64 位操作系统可以支持数 TB 的虚拟内存地址空间(理论上最大可寻址 16 EB),即使物理内存只有几 GB 或几十 GB。这使得开发人员可以编写支持大量数据集和复杂计算的程序,而不必担心物理内存的限制。
  • 如果没有虚拟内存,程序只能访问有限的物理内存,这对需要大量内存的应用程序(如数据库、大型数据分析)来说是一个严重的限制。

 进程地址空间

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

进程地址空间就类似于一把尺子,尺子的刻度由0x00000000到0xffffffff,尺子按照刻度被划分为各个区域,例如代码区、堆区、栈区等。而在结构体mm_struct当中,便记录了各个边界刻度,例如代码区的开始刻度与结束刻度,如下图所示:

划分完区域后,比如 brk_start 的值为 1000 , brk_end 的值为 5000 。那么 [1000,5000] 之间的区域就叫做虚拟地址线性地址对于区域的扩大或缩小操作,只需要改变 start 与 end 的数值就可以了

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


每个进程被创建时,其对应的进程控制块(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的虚拟地址通过页表映射后得到的物理地址即可。


此时可以理解为什么会发生同一块空间能读取到不同值的现象了

  • 父子进程有着各自的 mm_struct,其成员起始值一致
  • 对于同一个变量,如果未改写,则两者的虚拟地址通过 页表 + MMU 转换后指向同一块空间
  • 发生改写行为,此时会在真实空间中再开辟一块空间,拷贝变量值,让其中一个进程的虚拟地址空间映射改变,这种行为称为 写时拷贝

当我们创建子进程后,OS以父进程的PCB为模板创建了子进程的PCB结构。所以子进程也拥有自己的 mm_struct,且在同样的位置存在与父进程相同的虚拟地址:

当我们对子进程或父进程的数据不做修改的时候,父子进程读取的变量数据与变量地址都是相同的。而当我们修改父进程或子进程的数据时,先修改哪一个,哪一个就会发生写时拷贝,即在物理内存中另外再开辟一块空间,并修改页表的映射关系,使之映射到新空间:

 总结:同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!

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

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

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

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

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

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


所以到这里,我们对进程又有了更深了理解

  1. 对于一个进程想要运行起来至少要有两表:命令行参数表,环境变量表
  2. 对于一个进程的创建实际上伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)以及页表的创建。

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

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

相关文章

前端监控方案sentry整体概览

查看PDF 目****录 1. Sentry介绍 1.1. 编写目的 1.2. 名词定义 2. Sentry监控原理概述 2.1. 常见的性能优化指标及获取方式 2.2. 常见的前端异常及其捕获方式 3. Sentry?整体架构 4. Sentry安装部署 4.1. 前提条件 4.2. 安装 5. Sentry环境配置 5.1. 初始化配置 …

C++11相关知识点

auto、decltype自动类型推导 auto并不代表实际的数据类型&#xff0c;且auto并不是在任何场合下都能推导出变量的实际类型&#xff0c;使用auto必须初始化。 auto 变量名 变量值&#xff1b; 当变量不是指针或引用类型时&#xff0c;推导的结果中不会保留const、volatile关键…

【面试】分布式锁专题

1.你说一下什么是分布式锁 分布式锁是一种在分布式系统环境下实现的锁机制&#xff0c;它主要用于解决&#xff0c;多个分布式节点之间对共享资源的互斥访问问题&#xff0c;确保在分布式系统中&#xff0c;即使存在有多个不同节点上的进程或线程&#xff0c;同一时刻也只有一…

Error relaunching VirtualBox VM process: 5 启动虚拟机时发生了错误

出现错误 一大早起来发现虚拟机打不开&#xff0c;看了虚拟机日志是正常的&#xff0c;还回了个档都不行。 最后我突然想起之前在哪看到过&#xff1a;“完美游戏平台会导致虚拟机的问题。” 解决方法 于是我把完美游戏卸载了&#xff0c;发现&#xff0c;真的&#xf…

MCU、ARM体系结构,单片机基础,单片机操作

计算机基础 计算机的组成 输入设备、输出设备、存储器、运算器、控制器 输入设备&#xff1a;将其他信号转换为计算机可以识别的信号&#xff08;电信号&#xff09;。输出设备&#xff1a;将电信号&#xff08;&#xff10;、&#xff11;&#xff09;转为人或其他设备能理解的…

从零开始:如何在.NET Core Web API中完美配置Swagger文档

目录 新建项目 RestFul Swagger配置 注释展示 版本控制 Token传值 方法封装 新建项目 打开visual studio创建新项目&#xff0c;这里我们选择.net core web api模板&#xff0c;然后输入项目名称及其解决方案创建新项目 这里使用配置一些其他信息&#xff0c;根据自己情…

百度搜索应适用中文域名国家标准,修复中文网址展示BUG

12月1日中文域名国家标准正式实施。该标准“明确了中文域名在编码、解析、注册、字表等方面的技术要求&#xff0c;适用于中文域名注册管理机构、注册服务机构、网络软硬件服务商及终端用户”。 00:23 显然&#xff0c;百度作为网络软硬件服务商&#xff0c;是包括在国家标准的…

Windows安装elasticsearch、Kibana以及IK分词器

一、下载 1.下载elasticsearch 访问官网Download Elasticsearch | Elastic&#xff0c;下载elasticsearch 2.下载 Kibana 访问Download Kibana Free | Get Started Now | Elastic &#xff0c;下载 Kibana 3. IK分词器下载 访问Gitee 极速下载/elasticsearch-analysis-ik选…

第一个C++程序--(蓝桥杯备考版)

第一个C程序 基础程序 #include <iostream>//头⽂件 using namespace std;//使⽤std的名字空间 int main()//main函数 {cout << "hello world!" << endl; //输出&#xff1a;在屏幕打印"hello world!" return 0;}main函数 main 函数是…

Elasticsearch Serverless 中的数据流自动分片

作者&#xff1a;来自 Elastic Andrei Dan 在 Elastic Cloud Serverless 中&#xff0c;我们根据索引负载自动为数据流配置最佳分片数量&#xff0c;从而使用户无需摆弄分片。 传统上&#xff0c;用户会更改数据流的分片配置&#xff0c;以处理各种工作负载并充分利用可用资源。…

TcpServer 服务器优化之后,加了多线程,对心跳包进行优化

TcpServer 服务器优化之后&#xff0c;加了多线程&#xff0c;对心跳包进行优化 TcpServer.h #ifndef TCPSERVER_H #define TCPSERVER_H#include <iostream> #include <winsock2.h> #include <ws2tcpip.h> #include <vector> #include <map> #…

python进阶-05-利用Selenium来实现动态爬虫

python进阶-05-利用Selenium来实现动态爬虫 一.说明 这是python进阶部分05&#xff0c;我们上一篇文章学习了Scrapy来爬取网站&#xff0c;但是很多网站需要登录才能爬取有用的信息&#xff0c;或者网站的静态部分是一个空壳&#xff0c;内容是js动态加载的,或者人机验证&…

Linux —— vim 编辑器

一、什么是vim vim是一个功能强大、高度可定制的文本编辑器。以下是对vim编辑器的具体介绍&#xff1a; 历史背景&#xff1a;vim最初由Bram Moolenaar在1991年开发&#xff0c;作为vi编辑器的增强版&#xff0c;增加了许多新的特性和改进。它继承了vi的基本编辑功能和键盘快捷…

高效率同步降压转换器 - YB2416D: 实现快速充电和高效能供电的利器

概述: YB2416是一款输入耐压超过40V&#xff0c;在4.5V~30V输入电压条件下正常工作&#xff0c;并且能够实现精确恒压以及恒流的同步降压型DC-DC转换器。 内部集成80m2的上管和40m2的下管&#xff0c;无需外部肖特基二极管&#xff0c;可连续输出3A电流。输出3A电流时系统转换…

Repo管理

文章目录 前言Repo介绍清单仓库清单仓库的组成 初始化Repo同步远程仓库Repo实际应用 前言 我们知道&#xff0c;Git是用来管理某一个仓库&#xff0c;那当一个项目用到了多个仓库时&#xff0c;怎么来同步管理这些仓库呢&#xff1f;这个时候就可以引入Repo管理。 Repo介绍 …

神经网络的起源与工作原理

神经网络起源&#xff1a;一个生物神经网络是由一组化学上相连或功能上相关的神经元组成。一个神经元可能与许多其他神经元相连&#xff0c;网络中的神经元和连接的总数可能很广泛。连接&#xff0c;称为突触&#xff0c;通常是从轴突到树突形成的&#xff0c;尽管树突和其他连…

Qwen2.5-7B-Instruct vLLM 部署调用

Qwen2.5-7B-Instruct vLLM 部署调用 vLLM 简介 vLLM 框架是一个高效的大语言模型推理和部署服务系统&#xff0c;具备以下特性&#xff1a; 高效的内存管理&#xff1a;通过 PagedAttention 算法&#xff0c;vLLM 实现了对 KV 缓存的高效管理&#xff0c;减少了内存浪费&…

解决 Mac(M1/M2)芯片,使用node 14版本

前言 nvm 在安装 Node.js v14.21.3 时&#xff0c;报错&#xff1a; nvm install 14 Downloading and installing node v14.21.3... Downloading https://nodejs.org/dist/v14.21.3/node-v14.21.3-darwin-arm64.tar.xz... curl: (56) The requested URL returned error: 404Bin…

TesseractOCR-GUI:基于WPF/C#构建TesseractOCR简单易用的用户界面

前言 前篇文章使用Tesseract进行图片文字识别介绍了如何安装TesseractOCR与TesseractOCR的命令行使用。但在日常使用过程中&#xff0c;命令行使用还是不太方便的&#xff0c;因此今天介绍一下如何使用WPF/C#构建TesseractOCR简单易用的用户界面。 普通用户使用 参照上一篇教…

【ETCD】【源码阅读】configurePeerListeners() 函数解析

configurePeerListeners 是 ETCD 的一个核心函数&#xff0c;用于为集群中节点之间的通信配置监听器&#xff08;Peer Listener&#xff09;。这些监听器主要负责 Raft 协议的消息传递、日志复制等功能。函数返回一个包含所有监听器的列表。 函数签名 func configurePeerList…