当前位置: 首页 > news >正文

【Linux系统编程】进程概念(四)进程优先级、进程切换、环境变量和程序地址空间

【Linux系统编程】进程概念(四)进程优先级、进程切换、环境变量和程序地址空间

  • 1. 进程优先级
    • 1.1 基本概念
    • 1.2 优先级VS权限
    • 1.3 如何查看或修改进程优先级
      • 1.3.1 查看
      • 1.3.2 修改
  • 2. 竞争、独立、并行、并发
  • 3. 进程切换
    • 3.1 进程切换的核心机制:上下文保存与恢复
  • 4. 环境变量
    • 4.1 命令行参数
    • 4.2 环境变量基本概念以及常见环境变量
    • 4.3 环境变量和进程之间的关系
    • 4.4 和环境变量相关的命令
    • 4.5 环境变量和本地变量
    • 4.6 OS如何把环境变量给bash
  • 5. 程序地址空间
    • 5.1 程序地址空间回顾
    • 5.2 进程地址空间
    • 5.3 地址空间
      • 5.3.1 地址空间的基本概念
      • 5.3.2 地址空间的区域划分
      • 5.3.3 深入理解地址空间的本质
    • 5.4 进程与进程地址空间
      • 5.4.1 进程的完整构成
      • 5.4.2 进程地址空间的存在意义
    • 5.5 页表机制详解
      • 5.5.1 页表地址与进程上下文
      • 5.5.2 内存区域权限控制机制
      • 5.5.3 内存页面状态与惰性加载
      • 5.5.4 页表在模块解耦中的作用
    • 5.6 验证命令行参数和环境变量的地址是在栈的地址之上
  • 6. 进程概念相关例题
    • 6.1 题目
    • 6.2 答案

1. 进程优先级

1.1 基本概念

  • cpu资源分配的先后顺序,就是指进程的优先权(priority)。
  • 优先权高的进程有优先执行权力。配置进程优先权对多任务环境的Linux很有用,可以改善系统性能。
  • 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。

1.2 优先级VS权限

  • 优先级:已经能了,先后的问题(进程在已经能得到某种资源的前提下,得到某种资源的先后顺序)。
  • 权限:能不能的问题。

为什么要有优先级?

如果资源不足了,对于如何分配资源,需要设置优先级,决定进程获得某种资源的先后顺序。

1.3 如何查看或修改进程优先级

1.3.1 查看

ps -l命令,会输出以下几个内容

在这里插入图片描述
我们介绍以下圈住的信息:

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

1.3.2 修改

PRI 和 NI

PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是进程被CPU执行的先后顺序,此值越小进程的优先级越高。

那NI呢?就是我们所说的nice值了,其表示进程可被执行的优先级的修正数值。

PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new) = PRI(old)+ nice,但是其实PRI(old)是一个定值也就是80,也就是说PRI(new)的大小是由nice决定的。

nice值得取值范围是-20~19,一共40个级别。

PRI vs NI

需要强调得一点是,进程得nice值不是进程得优先级,他们不是一个概念,但是进程的nice值会影响到进程优先级的变化。

可以理解nice值是进程优先级的修正数值。

用top命令可以更改已存在进程的nice

具体步骤:
进入top后按 r -> 输入进程的PID -> 输入nice值

我们以下面的代码为例,了解修改操作

#include <stdio.h>    
#include <unistd.h>                                                           
#include <sys/types.h>    int main()    
{    printf("I am process, pid: %d\n", getpid());    while(1)    {}                                                                                                          return 0;                                                   
}  

我们要想演示,就要开多个SSH渠道。

在该SSH渠道中,test程序已运行
在这里插入图片描述
因为该SSH渠道已经用了,所以要新开一个SSH渠道,去查看test进程的PRI和NI值。

ps -l 只能查看当前SSH渠道的进程,所以要用ps -al 查看所有SSH渠道的进程。

可以看到此时test进程的PRI是80,NI是0,接下来我们要对nice值进行修改。

在这里插入图片描述

将nice值修改为10后,ps -al查询该进程的PRI和NI值,发现跟我们想的是一样的。

如果OS不让你改nice值的话,可以使用sudo提权修改,因为OS已经将所有进程的优先级安排好了,OS不允许进程的优先级频繁改变,实践中也很少有改优先级的。

在这里插入图片描述

优先级的变化范围是有限的,为什么?

这就要说到分时操作系统实时操作系统了,Linux操作系统就是分时操作系统,OS给进程分配时间片,用相对公平公正的调度策略,较为均衡的让不同的进程都能在一段时间内得到CPU的资源,所以优先级的变化范围是有限的;而实时操作系统,例如汽车的操作系统就是实时操作系统,如果OS检测要有紧急情况需要刹车,那OS会立即把刹车的优先级拉满,不能说执行一下刹车就又执行别的操作了,那样就太挫了。

2. 竞争、独立、并行、并发

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

3. 进程切换

3.1 进程切换的核心机制:上下文保存与恢复

进程切换的本质是CPU寄存器内容的切换。为了实现进程“暂停后能继续执行”,关键在于保存和恢复进程的运行现场,即进程上下文

寄存器是共享的,但是寄存器里面的数据,本质是进程私有的,叫做进程上下文。

1. 理解进程上下文

  • 寄存器的作用:CPU寄存器(如通用寄存器 eax, ebx;栈指针 esp;程序计数器 eip 等)存放着进程运行时的临时数据,例如变量、函数返回地址、下一条要执行的指令地址等。
  • 上下文定义:这些寄存器中的临时数据共同构成了进程的上下文。它代表了进程在某个精确时刻的运行状态。

2. 进程为何要保存上下文?
当进程的时间片用完或被更高优先级进程中断时,它必须让出CPU。为了将来能无缝衔接地继续运行,进程在离开CPU前,必须将其当前的上下文完整地保存下来。

3. 上下文保存在哪里?
操作系统在每个进程的进程控制块(PCB) 中专门设计了一个数据结构( reg_info 结构体)。当进程被切换出去时,其当前的寄存器值(eip, eax, esp等)会被立刻保存到PCB的这个结构体中。

4. 如何恢复运行?
当该进程再次被调度获得CPU时,操作系统会从其PCB中取出之前保存的上下文数据,重新加载到对应的CPU寄存器中。特别是程序计数器(eip) 的恢复,使得CPU能够从进程上次被中断的指令处继续执行。

总结:进程切换的两大关键步骤

  1. 保存上下文:将当前进程的寄存器状态保存到其PCB中。
  2. 恢复上下文:将下一个要运行进程的寄存器状态从其PCB加载到CPU寄存器中。

4. 环境变量

4.1 命令行参数

我们以前写的main函数基本都是无参的,即int main(),那么main函数可以有参数吗?

答案是有的,我们以下面的代码为例,介绍一下main函数的参数。

argc -> argument count 参数数量,就是命令行参数数量
argv -> argument vector 参数表

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

细节一:命令行参数至少是1,argc >= 1,argv[0]一定有元素,指向的就是程序名。

在这里插入图片描述

为什么要有命令行参数?

命令行参数的本质应用,是为了实现一个命令,可以根据不同的选项,实现不同的子功能,也是LInux中所有命令选项功能的实现方式。

例如:

在这里插入图片描述

那么我们写的test程序的命令行参数,也可以通过这样的方式,实现不同的功能。

细节二:选项,是以空格分隔的字符串,一个字符,也是字符串。

在这里插入图片描述

细节三:一共有argc个,argv[argc-1]是最后一个,argv[argc] == NULL

我们用下面的代码演示。

#include <stdio.h>   int main(int argc, char* argv[])                                                                            
{                                                                                                           int i = 0;                                                                                              for(; argv[i]; ++i) // argv[i]是否为空是for循环结束条件   {    printf("argc[%d]->%s\n", i, argv[i]);    }                                                                                                           if(argv[argc] == NULL)      {      printf("NULL\n");      }                                return 0;      
}   

在这里插入图片描述

下面我们给test程序设置几个选项,演示一下。

#include <stdio.h>
#include <string.h>int main(int argc, char* argv[])
{int i = 0;for(; i < argc; ++i){if(strcmp(argv[i], "-a") == 0){printf("执行-a选项的功能\n");}if(strcmp(argv[i], "-b") == 0){printf("执行-b选项的功能\n");}if(strcmp(argv[i], "-c") == 0){printf("执行-c选项的功能\n");                                                                                                                                           }}
}

在这里插入图片描述

那么VS2022有没有命令行参数?有的。

4.2 环境变量基本概念以及常见环境变量

  • 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
  • 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量在帮助编译器进行查找。
  • 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。

常见的环境变量

  • PATH:指定命令的搜索路径
  • HOME:指定用户的主工作目录(即用户登录到LInux系统中时,默认的目录)

对于我们平常用的ls、whoami、cd等命令,Linux怎么知道要去/usr/bin/路径下找可执行程序呢?它还会不会去其他路径下也找呢?

环境变量PATH中就保存着,这些命令的路径,Linux默认就会去PATH中保存的路径找。

如何查看环境变量

echo $环境变量名

我们发现PTH中确实保存着usr/bin路径,但也有别的路径,说明Linux还会去其他路径找。
在这里插入图片描述

我们以下面的代码测试一下PATH是否如我们所说

#include <stdio.h>int main()
{printf("hello world\n");return 0;
}

我们知道我们要想执行该代码编译的二进制程序,必须要指明路径。

在这里插入图片描述

所以我们可以通过PATH=$PATH:当前路径,将当前路径保存在PATH中,那么我们就可以不指明当前路径去执行code了

在这里插入图片描述
注意:不要写成:PATH=当前路径,这样PATH中保存的路径就只有当前路径了,如果执行了该操作也不用担心,退出该用户,再重新登录就可以了。

我们发现执行PATH=当前路径后,PATH中保存的路径确实只有当前路径了,且ls、cat等指令也不能运行了,因为OS找不到。

那为什么echo、pwd、cd等命令却能执行呢?

echo、pwd、cd等命令是内建命令,可以理解为shell内部自己定义的,bash自己内部的一次函数调用,不依赖第三方路径。
普通命令:存在的二进制文件级别的命令。

在这里插入图片描述
那Windows中有没有环境变量这个概念呢?

答案是有的,我们在装Python、Java或其他软件时,它就要求你配置环境变量Path。

在这里插入图片描述
在这里插入图片描述

我们发现我们安装的Xshell就装了环境变量。

在这里插入图片描述

那么我们就可以打开cmd,直接输入Xshell,它就会通过Path环境变量中保存的路径帮我们打开Xshell了。

在这里插入图片描述
它也确实帮我打开Xshell了。

在这里插入图片描述

4.3 环境变量和进程之间的关系

获取环境变量的三种方法

1、如何用代码获取环境变量?

下面我们就要通过代码介绍一下main函数的第三个参数:

env -> environ vector 环境变量表

#include <stdio.h>int main(int argc, char* argv[], char* env[])
{// 加了argc和argv但是不用,有的编译器可能会报错// 所以可以这样做,避免报错。                                                  (void)argc;                                                                                                                                                                   (void)argv;    int i = 0; for(; env[i]; ++i) // env也是以NULL结尾的{                             printf("env[%d]: %s\n", i, env[i]);}                                                  return 0;
}

系统级的变量,变量名和变量内容,往往具有全局属性

用代码获取环境变量本质是把环境变量表传递给进程

默认是bash内部的环境变量传递给进程,这些进程都是bash的子进程,子进程和父进程的代码和数据可以共享!

而bash中的环境变量是来自Linux系统的配置文件,我们登录用户时,OS将环境变量加载到内存中,然后给bash进程。所以我们刚刚改变PATH是改变的bash的PATH,退出再重新登录,OS就会再次加载到内存,再给bash

在这里插入图片描述

2、用全局变量environ(系统级变量)

在这里插入图片描述

在这里插入图片描述

我们用下面的代码解释environ

#include <stdio.h>    
#include <unistd.h>                                                              int main()                                                                                                                                                                        
{                                                                                     extern char** environ; // 前置声明    int i = 0;                                                                for(; environ[i]; ++i)                                                    {                                                                         printf("environ[%d]: %s\n", i, environ[i]);                           }       return 0;
}                 

在这里插入图片描述

3、命令env

在这里插入图片描述

不同的环境变量,会有不同的应用场景

用环境变量让不同的用户有无权限看到一些内容

getenv是获得该环境变量的内容,如果不存在就返回NULL

在这里插入图片描述

#include <stdio.h>                                                          
#include <stdlib.h>
#include <string.h>int main()                                                
{char* user = getenv("USER");                                                                                                                                                  if(user == NULL){                                        printf("该环境变量不存在\n");return 1;                                                         }else if(strcmp(user, "lsb") == 0){printf("合法用户,执行\n");printf("USER=%s\n", user);                                             }                                                                         else      {printf("不合法用户,不能执行\n");                                     }return 0;
}             

只用lsb用户才是合法用户,才能看到USER环境变量的内容

在这里插入图片描述

其他用户都是不合法用户,但是root是特权阶级,可以变成lsb,然后拿到内容。

在这里插入图片描述

4.4 和环境变量相关的命令

  • echo: 显示某个环境变量值
  • export:设置一个新的环境变量
  • env:显示所有环境变量
  • unset:清除环境变量
  • set:显示本地定义的shell变量和环境变量

4.5 环境变量和本地变量

环境变量具有全局属性,子进程能获取父进程的环境变量,环境变量是全局的本质是环境变量可以被子进程继承。

本地变量具有局部属性,只能在当前进程中看到,不能被子进程继承。

在这里插入图片描述

下面我们验证一下本地变量确实不能被子进程继承

#include <stdio.h>      
#include <stdlib.h>    int main()    
{    printf("TEST_ENV: %s\n", getenv("TEST_ENV"));    printf("TEST1_ENV: %s\n", getenv("TEST1_ENV"));    return 0;
}

已知code进程是bash进程的子进程

在这里插入图片描述

我们用export就可以设置新的环境变量

这也就说明了环境变量具有全局属性的本质是环境变量可以被子进程继承。

在这里插入图片描述

用unset清除环境变量

在这里插入图片描述
用set显示本地定义的shell变量和环境变量

在这里插入图片描述

4.6 OS如何把环境变量给bash

OS是通过该用户家目录中的.bash_profile文件和.bashrc文件,将环境变量给bash的

在这里插入图片描述

每个用户都有自己的.bash_profile文件和.bashrc文件

在这里插入图片描述
我们可以在.bash_profile中打印几段话来验证

在这里插入图片描述

这是原来的登录界面

在这里插入图片描述

这是修改后的登录界面

在这里插入图片描述

5. 程序地址空间

5.1 程序地址空间回顾

在我们学C/C++时,想必对这个空间布局图有一定的了解。

在这里插入图片描述

下面我们先通过代码来验证图中的划分方式是否正确。

#include <stdio.h>         int g_unval;      
int g_val = 100;      int main()                                                                                                                                                                        
{                                   const char *str = "helloworld";       printf("code addr: %p\n", main);                 printf("read only string addr: %p\n", str);      printf("init global addr: %p\n", &g_val);          printf("uninit global addr: %p\n", &g_unval);      static int test = 10;                    char *heap_mem = (char*)malloc(10);       char *heap_mem1 = (char*)malloc(10);      char *heap_mem2 = (char*)malloc(10);      char *heap_mem3 = (char*)malloc(10);                                  printf("heap addr: %p\n", heap_mem); printf("heap addr: %p\n", heap_mem1); printf("heap addr: %p\n", heap_mem2); printf("heap addr: %p\n", heap_mem3); printf("test static addr: %p\n", &test); printf("stack addr: %p\n", &heap_mem);  printf("stack addr: %p\n", &heap_mem1);      printf("stack addr: %p\n", &heap_mem2);   printf("stack addr: %p\n", &heap_mem3);                                                                                                                     return 0;                                                                                                                 
}  

我们发现打印的结果确实跟图中画的一样。

另外,static修饰的局部变量实际上是具有全局变量的属性的,通过打印的结果可以看到,其地址跟初始化数据和未初始化数据的地址很近。

在这里插入图片描述

其实我们看到的这些地址都是虚拟的,而不是真正的物理地址。

下面我们通过代码验证一下。

#include <stdio.h>    
#include <unistd.h>    int g_val = 100;    int main()    
{    printf("g_val: %d, &g_val: %p\n", g_val, &g_val);    pid_t id = fork();    if(id == 0)    {    while(1)    {    printf("I am child process, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);    sleep(1);                                                                                                                                                             }                                }                                    else                                 {                                    while(1)                         {                                printf("I am parent process, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);    sleep(1);                    }                                }                                    
}  

这样的运行结果很好理解,子进程共享父进程的代码和数据,且子进程没有对数据进行修改,所以全局变量g_val的值和地址是相同的。

在这里插入图片描述

下面我们对该代码稍微改变一下

#include <stdio.h>    
#include <unistd.h>    int g_val = 100;    int main()    
{    printf("g_val: %d, &g_val: %p\n", g_val, &g_val);    pid_t id = fork();    if(id == 0)    {    while(1)    {    printf("I am child process, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);    sleep(1); ++g_val; // 让g_val的值一直在变化                                                                                                                                                            }                                }                                    else                                 {                                    while(1)                         {                                printf("I am parent process, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);    sleep(1);                    }                                }                                    
}  

在这里插入图片描述

当我们观察到子进程将全局变量 g_val 的值修改时,这背后正是 “写时拷贝” 机制在发挥作用。该机制的核心目的是维护进程的独立性。在子进程尝试写入数据前,父子进程共享同一份物理内存中的数据(值为100)。一旦子进程需要修改,操作系统便会为其创建该数据的独立副本。此后,子进程在其私有副本上修改g_val,而父进程的数据保持不变。因此,子进程和父进程打印的g_val不同的现象便得到了合理解释。归根结底,由于数据内容已不同,父子进程访问的已不再是同一个物理变量。

然而,一个令人困惑的现象随之出现:当我们分别打印父子进程中这个全局变量的地址时,显示的地址值竟然是相同的。这与我们之前的结论似乎矛盾。要理解这一点,关键在于区分虚拟地址物理地址

首先可以排除这是物理地址。因为如果双方访问的是同一物理地址,那么从该地址读出的数据必然相同。但事实是,父子进程读出的值不同,这有力地证明了程序中直接获取的地址绝非物理地址。

因此,结论是:这个相同的地址是虚拟地址。操作系统的内存管理机制使得父子进程的相同虚拟地址,被映射到了不同的物理地址上。这就是为什么值可以不同,但“地址”却看起来一样的根本原因。

总结:

  1. 在C/C++程序中,我们通过指针直接看到的所有地址,都是虚拟地址
  2. 虚拟地址到真实物理地址的转换过程,对用户来说是透明的,完全由操作系统通过页表等机制进行管理。
  3. “写时拷贝”是实现这一现象的关键技术。它在保持进程间共享数据以节省内存的同时,又在需要修改时赋予它们独立的数据空间,完美兼顾了效率与隔离性。

其实我们原来所说的“程序地址空间”是不准确的,应该叫做“进程地址空间/虚拟地址空间”。

5.2 进程地址空间

将上面发现的现象以图画的形式展现出来,加深对进程地址空间的理解。

在这里插入图片描述

深入理解进程

一个进程的本质,远不止是“内核数据结构 + 代码和数据”的简单组合。我们需要对其中的“内核数据结构”进行更深入的剖析。在先前的内容中,我们常将其简化为进程控制块(PCB,即 task_struct),但这并不完整。一个更完善的表述应该是:内核数据结构 = PCB + 进程地址空间 + 页表。这三者共同构成了操作系统管理和调度一个进程所需的完整元数据。

虚拟地址与物理内存的映射机制

操作系统为每个进程创建了独立的进程地址空间页表。当进程访问其地址空间中的各个区域(如堆、栈、全局数据区、代码区等)时,所获取的变量地址均为虚拟地址

这些变量的实际数据存储在物理内存中。每个进程独有的页表,核心功能就是建立虚拟地址到物理地址的映射关系,这一转换过程由内存管理单元(MMU)硬件辅助完成。

因此,访问一个变量(例如全局变量 g_val)的完整路径是:

  1. 在进程的虚拟地址空间中找到 g_val 的虚拟地址。
  2. 通过MMU查询页表,将此虚拟地址转换为对应的物理地址。
  3. 最终通过该物理地址访问物理内存,完成数据的读写。

父子进程的创建与写时拷贝(Copy-on-Write)

  1. 继承与共享:父进程创建子进程时,子进程并非立即拥有完全独立的资源。初始状态下,子进程会“继承”父进程的进程地址空间和页表的一份副本。这意味着,在此时,父子进程的页表映射关系相同,它们看到的虚拟地址相同,并且这些虚拟地址指向物理内存中的同一份代码和数据。这是一种高效的资源共享机制。

  2. 修改触发分离:当子进程试图修改数据(如将 g_val 修改)时,关键的写时拷贝机制被触发。此过程由操作系统自动完成,对进程透明:

    • 物理内存分离:操作系统会在物理内存中为新数据分配新空间,将原始数据复制过去,然后子进程在新副本上进行修改。
    • 虚拟地址不变:整个过程对进程的虚拟地址空间是“零感知”的。变量 g_val虚拟地址始终保持不变
    • 页表更新:操作系统会更新子进程的页表,将原来的虚拟地址条目指向新的物理地址。而父进程的页表保持不变,其虚拟地址依然映射到原始的物理地址。

5.3 地址空间

5.3.1 地址空间的基本概念

  1. 地址空间的来源:在32位体系结构中,地址总线为32位,可产生 2^32 个不同的地址。由于内存访问的最小单位是字节(Byte),因此可寻址的内存总大小为:2 ^ 32 * 1byte = 2^30byte * 4 = 4GB
    地址空间即为所有可访问地址的集合,范围为 [0, 2^32)。

  2. 进程与地址空间的关系:在Linux系统中,每个进程都拥有独立的地址空间,以实现内存隔离与管理。为有效管理众多进程的地址空间,内核采用“先描述,再组织”的方式,使用结构体(在Linux中为 mm_struct)来描述每一个进程的地址空间。

5.3.2 地址空间的区域划分

  1. 划分内容:进程地址空间在逻辑上被划分为多个区域,包括(从高地址到低地址):
    • 命令行参数与环境变量
    • 共享库映射区
    • 未初始化数据段(.bss)
    • 已初始化数据段(.data)
    • 代码段(.text)

在这里插入图片描述

尽管每个进程的虚拟地址空间默认为4GB(在32位系统中),但进程实际使用的物理内存通常远小于此。

  1. 划分方法:在Linux内核中,通过对 mm_struct 结构体中各区域的起始与结束地址进行定义,即可实现地址空间的划分。

  2. 区域调整:若需调整某一区域的大小,仅需修改其对应的起始或结束地址变量即可,具有很高的灵活性。

5.3.3 深入理解地址空间的本质

  1. 地址空间的本质:进程地址空间是内核为每个进程维护的一个虚拟内存视图,它定义了进程可以“看到”的内存范围。通过对线性地址进行区域划分(定义各区域的 startend),内核为进程提供了结构化的内存布局。

  2. 内核管理方式:与进程控制块(PCB)类似,地址空间(mm_struct)也是内核的一个数据结构对象。操作系统通过“先描述,再组织”的方式,统一管理所有进程的地址空间。

  3. 地址空间的连续性:在划分的每个区域内,地址是连续的。每个字节都有其唯一的虚拟地址,进程可以通过这些地址直接访问内存。

  4. 与进程的关联:在进程的PCB(例如 task_struct)中,包含一个指向其地址空间对象的指针(struct mm_struct *mm),从而将进程与其地址空间关联起来。

// 描述进程地址空间的核心结构体(简化版)
struct mm_struct {unsigned long start_code;  // 代码段起始地址unsigned long end_code;    // 代码段结束地址unsigned long start_data;  // 数据段起始地址unsigned long end_data;    // 数据段结束地址unsigned long start_brk;   // 堆起始地址unsigned long brk;         // 堆当前结束地址unsigned long start_stack; // 栈起始地址// ... 其他区域边界定义
};

5.4 进程与进程地址空间

5.4.1 进程的完整构成

在深入理解进程地址空间(由 mm_struct 结构体描述)之后,我们可以对“进程”这一概念给出更精确的定义:

进程 = 内核数据结构 + 程序的代码和数据

其中,内核数据结构 主要包括:

  • 进程控制块(task_struct:描述进程的基本属性、状态和资源。
  • 进程地址空间(mm_struct:描述进程的虚拟内存布局。
  • 页表:实现虚拟地址到物理地址的映射机制。

这三者共同构成了操作系统管理和调度进程的完整元数据体系。

5.4.2 进程地址空间的存在意义

进程地址空间是操作系统内存管理的核心抽象,其主要作用体现在以下三个方面:

  1. 提供统一的内存视角
    通过页表映射,地址空间为进程呈现了一个连续、线性的虚拟内存视图。进程无需关心物理内存的实际布局(可能碎片化),可以像操作一个连续的大数组一样访问内存,从而将物理上的“无序”转换为逻辑上的“有序”。

  2. 实现内存访问安全保护
    地址空间在CPU执行内存访问指令时增加了一层转换检查。每次通过虚拟地址访问内存前,都需要经过页表的审查。如果检测到异常访问(如越界、权限不符),操作系统会直接拦截该请求,防止其到达物理内存。这有效保护了物理内存和其他进程的数据安全。

  3. 实现进程管理与内存管理的解耦
    地址空间作为进程管理模块和内存管理模块之间的中间层,有效地将两者解耦:

    • 进程管理模块只需关注进程的调度、上下文切换等,通过操作 task_structmm_struct 来管理进程的虚拟内存视图。
    • 内存管理模块则专注于物理内存的分配、回收、页面置换等,通过操作页表来维护虚拟地址到物理地址的映射关系。
      这种职责分离极大地简化了系统设计,提高了操作系统的可维护性和稳定性。

5.5 页表机制详解

页表是操作系统实现虚拟内存管理的核心数据结构,用于建立虚拟地址与物理地址之间的映射关系,并实现内存访问控制与状态跟踪。下图展示了页表的基本结构及其关键功能:

在这里插入图片描述

5.5.1 页表地址与进程上下文

  1. 页表地址的存储与切换
    在进程被CPU调度执行时,CPU的cr3寄存器用于存放当前进程页表的物理地址。该地址属于进程上下文的一部分,当进程切换时,cr3寄存器的内容会随之更新,从而实现页表的切换。

  2. 进程地址空间与页表的关联
    进程的PCB(task_struct)中包含一个指向进程地址空间结构体(mm_struct)的指针。当进程被切换时,PCB连同其关联的进程地址空间和页表一并被换出,从而保证每个进程享有独立的虚拟内存视图。

5.5.2 内存区域权限控制机制

页表除记录地址映射外,还包含权限控制位(如r/w),用于标识内存区域的可访问性。以下示例说明了对只读区域的非法写入行为:

#include <stdio.h>
int main() 
{char *p = "hello";  // 字符串常量存储在只读区*p = 'x';           // 尝试修改常量区内容 → 触发段错误return 0;
}

执行结果:

Segmentation fault (core dumped)

权限控制原理

  • 若访问的数据位于可读可写区域(如.data段),页表权限位为rw,操作系统允许读写操作。
  • 若访问只读区域(如代码段.text或字符串常量区),页表权限位为r,任何写入尝试将被CPU的内存管理单元(MMU)拦截,并触发段错误。

注意:物理内存本身不具备权限控制能力。权限限制是通过页表这一软件层结合硬件机制实现的保护屏障。

5.5.3 内存页面状态与惰性加载

页表中设有“存在位”(Present Bit),用于标识对应页面是否已加载至物理内存:

  • 1:页面已加载,可正常访问
  • 0:页面未加载,访问将触发缺页异常

应用场景:惰性加载(Lazy Loading)
当运行远大于物理内存的程序(如10GB的游戏)时,操作系统采用分批加载策略:

  1. 仅加载程序启动所必需的少量代码和数据(如500MB)。
  2. 当程序访问未加载的页面时,触发缺页异常,操作系统再按需加载对应页面。
  3. 使用完毕的页面可被换出,腾出空间供其他页面使用。

该机制有效避免了早期加载全部内容导致的内存浪费,显著提升了大型应用的执行效率与系统资源利用率。

5.5.4 页表在模块解耦中的作用

页表作为进程管理模块与内存管理模块之间的中间层,实现了两者的解耦:

  • 进程管理模块只需关注进程的调度、上下文切换等,通过操作 task_structmm_struct 来管理进程的虚拟内存视图。
    • 内存管理模块则专注于物理内存的分配、回收、页面置换等,通过操作页表来维护虚拟地址到物理地址的映射关系。

这种设计增强了系统的可维护性与扩展性,符合操作系统分层设计原则。

5.6 验证命令行参数和环境变量的地址是在栈的地址之上

在这里插入图片描述

以下面的代码为例

#include <stdio.h>      
#include <unistd.h>      
#include <stdlib.h>      int g_unval;      
int g_val = 100;      int main(int argc, char *argv[], char *env[])                                                                                                                                     
{                                 const char *str = "helloworld";      printf("code addr: %p\n", main);      printf("read only string addr: %p\n", str);      printf("init global addr: %p\n", &g_val);        printf("uninit global addr: %p\n", &g_unval);      static int test = 10;                              char *heap_mem = (char*)malloc(10);      char *heap_mem1 = (char*)malloc(10);      char *heap_mem2 = (char*)malloc(10);      char *heap_mem3 = (char*)malloc(10);      printf("heap addr: %p\n", heap_mem); printf("heap addr: %p\n", heap_mem1); printf("heap addr: %p\n", heap_mem2); printf("heap addr: %p\n", heap_mem3); printf("test static addr: %p\n", &test); printf("stack addr: %p\n", &heap_mem); printf("stack addr: %p\n", &heap_mem1); printf("stack addr: %p\n", &heap_mem2); printf("stack addr: %p\n", &heap_mem3); for(int i = 0 ;i < argc; i++)                                            {                                  printf("argv[%d]: %p\n", i, argv[i]);      }                                              for(int i = 0; env[i]; i++)      {                                printf("env[%d]: %p\n", i, env[i]);      }                                               return 0;      
}    

在这里插入图片描述

6. 进程概念相关例题

6.1 题目

1、关于进程退出返回值的说法中,正确的有
A.进程退出的返回值可以随便设置
B.进程的退出返回值可以在父进程中通过wait/waitpid接口获取
C.程序异常退出时,进程返回值为-1
D.进程的退出返回值可以在任意进程中通过wait/waitpid接口获取

2、以下关于进程退出描述正确的有: [多选]
A.exit函数退出一个进程时会刷新文件缓冲区
B.exit函数退出一个进程时不会刷新文件缓冲区
C._exit函数退出一个进程时会刷新文件缓冲区
D._exit函数退出一个进程时不会刷新文件缓冲区

3、如何使一个进程退出,以下错误的是
A.在程序的任意位置调用return
B.在main函数中调用return
C.在程序的任意位置调用exit接口
D.在程序的任意位置调用_exit接口

4、关于waitpid函数WNOHANG参数的描述正确的是:()[多选]
A.若选项参数被设置为WNOHANG则waitpid为一直阻塞
B.若选项参数被设置为WNOHANG则waitpid为非阻塞
C.若waitpid设置WNOHANG后,没有子进程退出则返回值为-1
D.若waitpid设置WNOHANG后,没有子进程退出则返回值为0

5、关于pid_t waitpid(pid_t pid,int *status,int options);函数,以下描述错误的有()
A.若pid大于0,则表示等待指定的子进程退出
B.若pid等于-1,则表示等待任意一个子进程退出
C.status参数用于获取退出子进程的退出码
D.若options选项参数被设置为WNOHANG则waitpid为一直阻塞

6、以下不是进程等待功能的是()
A.获取子进程的退出码
B.释放僵尸子进程资源
C.等待子进程退出
D.退出指定子进程

7、下面哪些属于,fork后子进程保留了父进程的什么?[多选]
A.环境变量
B.父进程的文件锁,pending alarms和pending signals
C.当前工作目录
D.进程号

8、在CPU和物理内存之间进行地址转换时,( ) 将地址从虚拟(逻辑)地址空间映射到物理地址空间
A.TCB
B.MMU
C.CACHE
D.DMA

9、以下哪些命令可以查看环境变量 [多选]
A.echo
B.env
C.set
D.export

10、请问孤儿进程会被以下哪一个系统进程接管?
A.syslogd
B.init
C.sshd
D.vhand

11、关于僵尸进程,以下描述正确的有?
A.僵尸进程必须使用waitpid/wait接口进行等待
B.僵尸进程最终会自动退出
C.僵尸进程可以被kill命令杀死
D.僵尸进程是因为父进程先于子进程退出而产生的

12、下面有关孤儿进程和僵尸进程的描述,说法错误的是?
A.孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。
B.僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
C.孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
D.孤儿进程和僵尸进程都可能使系统不能产生新的进程,都应该避免

13、以下描述错误的有
A.守护进程:运行在后台的一种特殊进程,独立于控制终端并周期性地执行某些任务。
B.僵尸进程:一个进程 fork 子进程,子进程退出,而父进程没有 wait/waitpid子进程,那么子进程的进程描述符仍保存在系统中,这样的进程称为僵尸进程。
C.孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,这些子进程称为孤儿进程。(孤儿进程将由 init 进程收养并对它们完成状态收集工作)
D.精灵进程:精灵进程退出后会成为僵尸进程

14、通过fork和exec系统调用可以产生新进程,下列有关fork和exec系统调用说法正确的是? [多选]
A.fork生成的进程是当前进程的一个相同副本
B.fork系统调用与clone系统调用的工作原理基本相同
C.exec生成的进程是当前进程的一个相同副本
D.exec系统调用与clone系统调用的工作原理基本相同

6.2 答案

1题答案:B
进程的退出返回值不能随意设置,因为进程的退出返回值实际上只用了一个字节进行存储,因此随意设置可能会导致实际保存的数据与设置的数据不同的情况,因为过大会导致数据截断存储。
pid_t waitpid(pid_t pid, int *status, int options);函数中 status参数 用于父进程获取退出子进程的返回值。
程序异常退出时,意味着程序并没有运行到return/exit去设置返回值,则返回值不做评判标准,因为返回值的存储位置的数据是一个未知随机值。
根据以上理解,B选项正确。 其中D选项错误是因为并不能由任意进程获取子进程退出返回值

2题答案:A、D
库函数 exit 可以在任意位置调用,用于退出进程, 并且退出前会刷新文件缓冲区中的数据到文件中
系统调用 _exit 可以在任意位置调用,用于退出进程,但是退出时直接释放所有资源,并不会刷新缓冲区
根据以上理解,A和D选项正确。

3题答案:A
退出进程的方式有三种,
在main函数中return
在任意位置调用库函数 exit
在任意位置调用系统调用 _exit
根据以上理解,A选项错误,因为在普通函数中return退出的只是对应函数,而不是进程

4题答案:B、D
waitpid默认阻塞等待任意一个或指定子进程退出,当options被设置为WNOHANG则函数非阻塞,且当没有子进程退出时,waitpid返回0,并不会阻塞。
因此根据对于waitpid函数的参数认识理解分析,正确选项为B和D选项

5题答案:D
根据正确选项理解函数参数功能即可
waitpid默认阻塞等待任意一个或指定子进程退出,当options被设置为WNOHANG则函数非阻塞,且当没有子进程退出时,waitpid返回0

6题答案:D
进程等待:等待子进程退出,获取子进程返回值,释放子进程资源,避免出现僵尸进程
因此根据以上理解,不属于进程等待功能的只有D选项。

7题答案:A、C
fork函数功能是通过复制父进程,创建一个新的子进程。
A选项正确:环境变量默认会继承于父进程,与父进程相同
B选项错误:信号相关信息各自独立,并不会复制
C选项正确:工作路径也相同
D选项错误:每个进程都有自己独立的标识符
根据理解分析,正确选项为A和C选项

8题答案:B
A TCB 线程控制块
B 内存管理单元,一种负责处理中央处理器(CPU)的内存访问请求,功能包括虚拟地址到物理地址的转换(即虚拟内存管理)、内存保护、中央处理器高速缓存的控制
C CACHE 高速缓存
D DMA 直接内存存取

9题答案:A、B、C
echo 用于输出打印一个变量的内容,包括环境变量
env 用于打印所有环境变量信息
set 用于输出打印所有环境配置以及变量信息,不限于环境变量
export用于设置环境变量
根据题意,选择D,因为D并不是用于查看环境变量的操作。

10题答案:B
孤儿进程:父进程先于子进程退出,运行在后台,父进程成为1号init进程(在centos7中1号进程改名为systemd进程),退出后由1号进程回收资源
syslogd:系统中的日志服务进程
init:init进程是内核启动的第一个用户级进程,用于完成处理孤儿进程以及其他的一些重要任务。
sshd:远程登录服务进程
vhand:内存置换服务进程

11题答案:A
僵尸进程是指先于父进程退出的子进程程序已经不再运行,但是因为需要保存退出原因,因此资源没有完全释放的进程,它不会自动退出释放所有资源,也不会被kill命令再次杀死,僵尸进程会产生资源泄露,需要避免,避免僵尸进程的产生采用进程等待(wait/waitpid)方式完成
根据以上理解分析:
A选项正确,僵尸进程会造成资源泄露,必须使用wait/waitpid接口进行等待处理
B选项错误,僵尸进程不会完全释放资源退出
C选项错误,僵尸进程是已经退出运行的进程,无法被杀死
D选项错误,僵尸进程是子进程先于父进程退出。

12题答案:D
根据答案选项理解正确描述
僵尸进程:子进程先于父进程退出,父进程没有对子进程的退出进行处理,因此子进程会保存自己的退出信息而无法释放所有资源成为僵尸进程,导致资源泄露。
孤儿进程:父进程先于子进程退出,子进程成为孤儿进程,运行在后台,父进程成为1号进程(而孤儿进程的退出,会被1号进程负责任的进行处理,因此不会成为僵尸进程)
根据以上对两种特殊进程的理解分析选项:
A选项正确,父进程退出后,所有子进程都会成为孤儿进程;
B选项正确,僵尸进程的产生就是因为父进程没有对子进程的退出进行处理,因此子进程无法完全释放资源
C选项正确,子进程成为孤儿进程后被1号进程收养,并且他们的退出状态由1号进程完成处理
D选项错误,僵尸进程的产生会造成资源泄露需要避免,但是孤儿进程的产生一般都是具有目的性的,并且退出后并不会成为僵尸进程,因此无需特殊处理。

13题答案:D
僵尸进程:子进程先于父进程退出,父进程没有对子进程的退出进行处理,因此子进程会保存自己的退出信息而无法释放所有资源成为僵尸进程,导致资源泄露。
孤儿进程:父进程先于子进程退出,子进程成为孤儿进程,运行在后台,父进程成为1号进程(而孤儿进程的退出,会被1号进程负责任的进行处理,因此不会成为僵尸进程)
守护进程&精灵进程:这两种是同一种进程的不同翻译,是特殊的孤儿进程,不但运行在后台,最主要的是脱离了与终端和登录会话的所有联系,也就是默默的运行在后台不想受到任何影响
根据以上理解分析:
D错误:精灵进程其实和守护进程是一样的,不同的翻译叫法而已,它的父进程是1号进程,退出后不会成为僵尸进程

14题答案:A、B
A fork调用通过复制父进程创建子进程,子进程与父进程运行的代码和数据完全一样
B fork创建子进程就是在内核中通过调用clone实现
C exec是程序替换函数,本身并不创建进程
D clone函数的功能是创建一个pcb,fork创建进程以及后边的创建线程本质内部调用的clone函数实现,而exec函数中本身并不创建进程,而是程序替换,因此工作机理并不相同
基于以上理解,正确选项是A和B选项

http://www.dtcms.com/a/592964.html

相关文章:

  • 数据结构(18)
  • 网站页面制作自己做的网站能卖么
  • 大型省级政务平台采用金仓数据库(KingbaseES)
  • K8s 中创建一个 Deployment 的完整流程
  • 想做网站制作运营注册什么公司核实国内网站建设推荐
  • 操作系统?进程地址空间!!!
  • 进阶指南:API 批量调用优化方案(并发控制 + 重试机制 + 限流策略)
  • C++---强类型枚举(scoped enumeration)enum class
  • FFmepg--20-合成H.264视频和AAC音频和时间基转化
  • 深入理解MQTT内核和实现实时通信实战:物联网消息推送的秘密武器
  • 乐云seo模板网站建设主流网站开发技术
  • 第二届数证杯物联网取证+网络流量取证
  • 宁夏住房和城乡建设厅网站首页公司主页和公司网站
  • 如何在 macOS 上安装和配置 Redis ?
  • 【Linux网络】Socket编程实战,基于UDP协议的Dict Server
  • web京东商城前端项目4页面
  • 15、Linux 打包压缩命令
  • 网站后台源代码更改营销广告策划方案
  • 数据库迁移革命:金仓KReplay如何用真实负载回放技术缩短3周测试周期
  • 网站开发搭建合同范本企业软件解决方案
  • Java 中 Arrays.sort() 的底层实现
  • MPAndroidChart 双柱分组图:解决 X 轴标签无法居中问题及 UI 宽度计算指南
  • 政务外网终端一机两用安全管控解决方案
  • 数字华容道游戏
  • M4-R1 开源鸿蒙(OpenHarmory)开发板丨串口调试助手实战案例
  • 建材做网站好吗破解插件有后门wordpress
  • 旅游网站建设流程步骤怎么自己做礼品网站
  • C语言--文件读写函数的使用,对文件读写知识有了更深的了解。C语言--文件读写函数的使用,对文件读写知识有了更深的了解。
  • 数据结构示例代码
  • 数字化工厂:基于层级模型的智能制造新范式