进程概念

目录

进程概念

一、冯诺依曼体系结构

1.1.冯诺依曼体系架构

1.2.存储分级

1.3.数据流动

二、操作系统(Operator System)

2.1.操作系统的概念

2.2.设计OS的目的

2.3.管理机制解析

2.4.系统调用机制解析

三、进程

3.1.进程的基本概念

3.2.PCB进程描述

3.3.task_struct

3.4.getpid函数

3.5.ps指令

3.6.chdir函数

3.7.getppid函数

3.8.fork函数

四、进程状态

4.1.运行队列

4.2.运行状态

4.3.设备队列

4.4.阻塞状态

4.5.挂起状态

4.6.内核链表

4.7.Linux的进程状态

五、进程优先级

5.1.基本概念

5.2.PRI与NI

5.3.进程优先级命令

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

六、进程切换

6.1.CPU中的寄存器

6.2.进程切换

七、进程调度

7.1.O(1)调度队列

7.2.优先级

7.3.活动队列

7.4.过期队列

7.5.active指针与expired指针

八、命令行参数

九、环境变量

9.1.概念介绍

9.2.环境变量的相关命令

9.3.常见的环境变量

9.4.环境变量的组织方式

9.5.getenv函数

9.6.environ指针

9.7.环境变量的特性

9.8.本地变量

9.9.内建命令(built-in command)

十、程序地址空间

10.1.程序地址空间

10.2.虚拟地址

10.3.进程地址空间

10.4.虚拟内存管理


一、冯诺依曼体系结构

1.1.冯诺依曼体系架构

绝大多数计算机(比如:笔记本、服务器)都遵守冯诺依曼体系架构

中央处理器(CPU )由运算器(负责算术运算和逻辑运算)和控制器组成

存储器特指内存(主存储器)

外部设备包括输入设备和输出设备:

  • 输入设备:键盘、鼠标、话筒、摄像头、网卡、磁盘(外部存储)......
  • 输出设备:显示器、打印机、网卡、磁盘(外存存储)......

输入/输出(I/O)操作:

  • 将磁盘数据读取到内存(文件读取)
  • 将内存数据写入到磁盘(文件写入)

从内存的角度理解I/O:

  • 输入(Input):外部设备将数据交给内存
  • 输出(Output):内存将数据交给外部设备

冯诺依曼体系结构规定了程序执行前必须先将软件从磁盘加载到内存中

CPU只能通过内存来获取与写入数据

CPU执行代码和处理数据:

  • 调用printf时,数据首先存入缓冲区
  • 需要时才将缓冲区的数据刷新到外设
  • 数据通过“拷贝”方式在不同设备间传输

整个体系结构的效率取决于设备间的数据传输效率

关键特征:

  • CPU在数据层面只和内存交互
  • 外部设备在数据层面只和内存交互

1.2.存储分级

外部设备处理数据:毫秒级响应

中央处理器处理数据:纳秒级处理

由于CPU处理速度远快于外设,计算完后需要等待外部设备响应

根据木桶原理,整个体系结构的效率受限于最慢的外部设备环节

若将所有存储介质替换为寄存器,会导致成本过高

因此引入了内存作为适配方案,兼顾了成本与效率

冯诺依曼体系结构通过优化计算机的效率与成本,推动计算机的普及,为互联网的发展奠定了基础

加载到存储器中的操作系统通过预加载算法将外部设备的数据提前搬进内存,有效缓解了木桶效应

1.3.数据流动

  • QQ聊天

  • QQ发文件

QQ软件的作用:处理存储器与中央处理器之间的关系

二、操作系统(Operator System)

2.1.操作系统的概念

操作系统(OS):任何计算机系统中一个基本的程序集合

一款进行软硬件管理的软件

操作需要包括:

  • 内核:进程管理、内存管理、文件管理、驱动管理
  • 其他程序:函数库、shell程序......

2.2.设计OS的目的

底层硬件的访问需要安装对应的驱动程序

操作系统对下:与硬件交互,管理所有的软硬件资源(手段)

操作系统对上:为应用程序提供一个良好的执行环境(目的)

软硬件体系结构(层状结构):

高内聚:将逻辑相关与功能相似的代码和数据放在同一层

低耦合:层与层之间只通过接口交互,避免数据直接交换

示例1:面向过程的函数模块化

  • 独立封装各类功能函数
  • 主函数作为调用层,功能函数作为实现层
  • 通过标准接口实现层级解耦

示例2:C++面向对象继承分类

  • 利用继承机制构建语言级分层架构
  • 派生类作为调用层
  • 基类提供通用功能作为基础层

示例3:计算机硬件产业链分层

  • 芯片、内存等硬件由不同厂商生产
  • 厂商提供标准化接口作为调用层
  • 具体硬件生产作为底层功能层

访问操作系统,必须系统调用(系统提供的函数)

上层的代码是不可能绕过操作系统直接控制硬件

若程序访问硬件,必须贯彻整个软硬件体系结构

平时使用的各种标准库可能在底层封装系统调用

2.3.管理机制解析

管理流程:决策指定 → 任务执行

  • 校长:管理者(决策层)- 操作系统
  • 辅导员:执行者(执行层) - 驱动程序
  • 学生:被管理者(执行对象) - 底层硬件

管理特点:

管理者和被管理者可以不需要见面

管理者通过被管理者数据进行管理

通过执行者来获取被管理者的数据

管理者对被管理者的管理转化为对数据的管理

对数据的管理本质就是增删查改

数据结构实现:

用结构体将被管理者数据的属性进行集合

结构体内存放指针,指向下一个被管理者

形成链表结构,使管理操作转为链表操作

建模方法:

先描述,再组织,适用于任何的管理场景

比如:在C++语言中,类解决了描述问题,STL解决了组织问题

操作系统内一定会出现大量的数据结构以及与之匹配的算法来实现对底层硬件数据的高效管理

2.4.系统调用机制解析

操作系统不信任用户程序,但要为用户提供硬件访问服务

因此操作系统提供了专门的系统调用接口(C语言函数)

  • 输入参数:用户 → 操作系统
  • 返回值:操作系统 → 用户

用户与操作系统之间进行某种数据交互

示例:银行取款

客户不允许进入银行金库,但提供了许多服务的窗口

  • 大堂经理:外壳、库、指令、GUI界面
  • 银行窗口:系统调用接口
  • 银行系统:操作系统内核

库函数与系统调用是上下层的关系

三、进程

3.1.进程的基本概念

课本概念:程序的一个执行实例,正在执行的程序

内核观点:承担分配系统资源(CPU时间、内存)的实体

进程 == 内核数据结构对象 + 自己的代码和数据

进程 == PCB(task_struct)+ 自己的代码和数据

3.2.PCB进程描述

PCB(process control block):进程控制块

进程信息被放在叫做进程控制块的结构体类型中,是进程属性的集合

在Linux操作系统中,描述进程所有属性的结构体被称为task_struct

3.3.task_struct

内容分类

  • 标示符:描述本进程的唯一标示符,用来区别其他进程
  • 状态:任务状态、退出代码、退出信号
  • 优先级:相对于其他进程的优先级
  • 程序计数器:程序中即将被执行的下一条指令的地址
  • 内存指针:包括程序代码指针、进程相关数据指针、其他进程共享的内存块指针
  • 上下文数据: 进程执时处理器CPU的寄存器中的数据
  • I/O状态信息:显示的I/O请求,分配给进程的I/O设备,被进程使用的文件列表
  • 记账信息:处理器时间总和,使用的时钟数总和,时间限制,记账号

组织进程

所有运行在系统里的进程都以task_struct双链表的形式存在内核中

3.4.getpid函数

系统调用函数

功能:获取进程的PID(标识符)

原理:系统将当前运行进程的进程控制块中的PID信息进行拷贝

返回值:pid_t(整型)

实验:获取当前进程的PID

  • Makefile

  • myprocess.c

实验现象

3.5.ps指令

功能:查看系统当中的进程状态

(注:多条命令可以用;隔开,还可以用&&隔开)

常用选项

a:显示一个中终端所有的进程,包括其他用户的进程

x:显示没有控制终端的进程,如后台运行的守护进程

j:显示进程组ID,会话ID,父进程ID,与作业控制相关的信息

u:显示进程的详细信息,如用户、CPU和内存使用情况

grep指令在执行时也是一个进程,所以也会打印,-v选项可以避免

杀掉当前进程:ctrl+c

杀掉指定进程:kill -9 PID

(注:每一次启动同一个程序,进程的PID不同)

proc目录

通过文件的方式查看内存中的进程

proc目录中的数据都是内存中的数据,而非磁盘上的数据

显示某个进程的文件属性

显示某个进程的文件内容

cwd(current work dir):进程自己的当前路径

exe:进程对应的可执行文件

实验:通过文件操作在当前进程所在路径下新建文件

  • myprocess.c

实验现象

3.6.chdir函数

系统调用函数

功能:更改当前进程的路径

实验:修改进程所在路径,并且在修改的路径下新建文件

  • myprocess.c

实验现象

3.7.getppid函数

系统调用函数

功能:获取父进程的PID

实验:打印父进程的PID

  • myprocess.c

实验现象

bash:命令行解释器,本质是一个进程

操作系统会给每一个用户分配一个bash

3.8.fork函数

系统调用函数

功能:创建一个子进程

返回值:

  • 子进程创建成功

父进程:子进程的PID

子进程:0

  • 子进程创建失败

父进程:-1

实验:使用fork函数创建子进程

  • myprocess.c

实验:根据父子进程的不同返回值,使父进程打印一次,子进程循环打印

  • myprocess.c

实验现象

fork函数给父子返回各种不同的返回值的原因(父进程 : 子进程 = 1  : n)

  • 父进程需要获取子进程的PID
  • 子进程需要证明自己被创建

fork函数会返回两次的原因

  • 函数内部创建子进程,两个进程各自返回

变量id既等于0又大于0,并且导致两个条件同时成立的原因

  • 进程具有独立性,一个进程挂掉不会影响其他进程
  • 父进程与子进程在数据层面上默认是共享的

写时拷贝

父子进程任意一方修改共享的数据

OS会把修改的数据在底层拷贝一份

让目标进程可以修改这个拷贝

实验:子进程修改数据

  • myprocess.c

实验现象

四、进程状态

进程状态的本质:PCB内的一个整型变量

4.1.运行队列

一个CPU有一个调度队列(双端队列)

一个PCB既可以属于全局的双链表

又可以把相关PCB放在双端队列中

FIFO:先进先出,一种调度算法

4.2.运行状态

课本概念:一个进程在调度队列中,该进程的状态就是运行状态

4.3.设备队列

操作系统需要管理系统中的各种硬件资源(键盘、显示器、网卡、磁盘、摄像头......)

  • 先描述

  • 再组织

4.4.阻塞状态

课本概念:一个进程等待某种设备或者资源就绪

示例:C语言的scanf输入

当一个进程执行scanf函数时,需要读取键盘的数据

操作系统会检测键盘的状态,如果没有按键按下,说明键盘尚未就绪

操作系统会将该进程从运行队列中移走,将PCB链入键盘的阻塞队列

该进程永远不会被调度,一直处于阻塞状态,直到键盘处于就绪状态

按下按键后键盘就绪,硬件状态发生变化,进程不会知道,但操作系统会第一时间知道

操作系统会将该进程设为运行状态,并且重新链回到运行队列中,调度到该进程后运行

进程状态变化的表现:进程控制块在不同的队列中进行流动

(本质:对数据结构的增删查改)

4.5.挂起状态

  • 阻塞挂起状态

一个键盘可能同时有几个进程都处于阻塞状态,这些进程都在等待队列中

当内存严重不足的时候,会将等待队列中进程的代码与数据唤出到磁盘的

swap交换分区,此时这些进程的状态称为阻塞挂起状态,键盘一旦就绪,

就会把对应进程的代码和数据唤入进程控制块,将完整进程放入运行队列

  • 运行挂起状态

如果内存还是不够,只能将运行队列末端的部分进程的代码与数据

唤入磁盘的swap交换分区,此时这些进程的状态称为运行挂起状态

4.6.内核链表

传统链表

内核链表

计算links所在结构体的地址

(struct task_struct*)0:假设结构体的起始地址为0

&((struct task_struct*)0 → links):links相较于0号地址的偏移量

*list/*next:结构体中link成员的地址

(struct task_struct*)(*list/*next - &((struct task_struct*)0 → links)):结构体的首地址

每个进程控制块中可以有多个list_head,使它既可以属于全局链表,也可以属于运行队列

还可以把它放在任意结构,一个PCB在内核中只存在一份,但可以同时隶属多种数据结构

4.7.Linux的进程状态

kernel源代码定义

static const char *const task_state_array[] = {
    "R (running)",     /*0*/
    "S (sleeping)",    /*1*/
    "D (disk sleep)",  /*2*/
    "T (stopped)",     /*4*/
    "t (tracing stop)",/*8*/
    "X (dead)",        /*16*/
    "Z (zombie)",      /*32*/
};

R(Running)状态:运行状态

表明进程要么在运行中,要么在运行队列

  • myprocess.c

实验现象

R+:进程在前台运行

R:进程在后台运行(在文件名后加&)

后台的进程只有kill才能杀掉

S(sleeping)状态:睡眠状态(可中断睡眠 interruptible sleep)

进程在等待事件完成,处于浅睡眠状态时可以直接杀掉

  • myprocess.c

实验现象

T(stopped)状态:暂停状态

 用户通过ctrl+z,进程被暂停

t(tracing stop)状态:追踪状态

进程被调试到断点处,进程被追踪

D(Disk sleep)状态:磁盘休眠状态(不可中断休眠 uninterruptible sleep)

操作系统在内存空间不足时,无法杀掉处于磁盘睡眠状态的进程

访问磁盘这类关键存储设备,进程设为D状态,防止该进程丢失

X(dead)状态:死亡状态

结束状态,只是一个返回状态,不在任务列表中

Z状态:僵尸状态

子进程退出,父进程还在运行,但父进程没有读取子进程的状态

在Linux系统中,当前接触的所有进程一定是某一个进程的子进程

这个进程就是bash,创建子进程的目的是让子进程完成某种事情

在子进程退出之前,父进程需要获取子进程信息

在获取信息的过程中,子进程处于一种僵尸状态

  • myprocess.c

僵尸进程的危害

如果父进程不回收子进程的提出信息

那么会一直存在,导致内存泄漏问题

进程退出后,内存泄漏问题就不存在

常驻内存的进程具有内存泄漏问题时是比较麻烦的

操作系统是一个常驻内存的软件需要避免内存泄漏

slab机制

内核结构申请:申请空间+初始化

将不用的PCB中的数据删除,但不释放PCB内存空间

将该PCB存放在unuse列表,作为数据结构对象的缓存

孤儿进程

父进程先退出,子进程被1号进程领养,被领养的进程(子进程)称为孤儿进程

  • myprocess.c

实验现象

systemd进程:一号进程

孤儿进程会自动变成后台进程

(注:如果不领养,没有父进程,子进程进入僵尸状态就会造成无法解决的内存泄漏问题)

五、进程优先级

5.1.基本概念

目标资源稀缺时需要通过优先级确认谁先谁后问题

进程优先级可以衡量进程得到CPU资源的先后顺序

优先级:能得到资源,先后的问题

权限:是否能得到某种资源

进程优先级也是一种整型变量,存放在PCB中

  • 值越低,优先级越高
  • 值越高,优先级越低

Linux系统是基于时间片的分时操作系统,考虑公平性,优先级可能变化,但是变化幅度不能太大

UID:执行者身份

PID:进程的代号

PPID:父进程的代号

对比进程的UID与文件的UID

如果与第一个相等,就是拥有者

如果与第二个相等,就是所属组

如果前面的都不相等,就是other

Linux系统中,访问任何资源都是进程访问,进程就代表用户

5.2.PRI与NI

PRI:进程优先级,默认为80

NI:进程优先级的修正数据,nice值默认为0

进程真实的优先级 == PRI(默认) + NI

5.3.进程优先级命令

top命令

更改已经存在进程的nice值

  • top

  • r

  • 输入进程PID

  • 输入nice值

nice命令、renice命令

系统调用函数

#include<sys/time.h>
#include<sys/resource.h>

intgetpriority(int which,int who);
intsetpriority(int which,int who,int prio);

优先级的极值问题

nice值:[-20,19]

默认值:80

Linux进程的优先级范围:[60,99]

优先级设立不合理会导致优先级低的进程,长时间得不到CPU资源,进而导致该进程发生进程饥饿

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

竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,为了高效完成任务,就具有优先级

独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰

并行:多个进程在多个CPU下分别同时运行

并发:多个进程在一个CPU下采用进程切换的方式,在一段时间内让多个进程都得以推进

六、进程切换

一个进程占用CPU时,不可能会把自己的代码全部跑完,有对应的时间片

死循环的进程,不会打死操作系统的所有进程,因为它不会一直占有CPU

6.1.CPU中的寄存器

CPU内部的临时空间,存储进程中的代码和数据

寄存器(空间)不等于寄存器里面的数据(内容)

空间只有一份,但是内容可以是变化的,多份的

  • pc指针
  • 程序计数器:EIP
  • 栈底寄存器:ebp
  • 栈顶寄存器:esp
  • 通用寄存器:eax、ebx、ecx、edx
  • 段寄存器:cs、ds、es、fg、gs
  • 状态寄存器:eflags
  • 特权级寄存器:cr0~cr4
  • 指令寄存器、IR寄存器...

作用:存储正在运行的进程的临时数据

6.2.进程切换

CPU上下文切换:任务切换,CPU寄存器切换

一个进程的时间片到达后,该进程被操作系统从CPU中剥离下来

CPU内寄存器里面存放进程运行的临时数据(当前进程的上下文数据)

保存上下文数据:将CPU内寄存器里面的内容,保存起来

恢复上下文数据:将保存起来的数据恢复到CPU内寄存器

当前进程的上下文数据保存在该进程task_struct里的tss_struct结构体中

TSS:任务状态段

七、进程调度

调度与切换共同构成了调度器

分时操作系统:基于时间片公平调度进程

实时操作系统:新来的进程必须及时响应

7.1.O(1)调度队列

一个CPU,一个运行队列(runqueue)

7.2.优先级

实时优先级:[0,99](不考虑)

普通优先级:[100,139](与nice值的取值范围对应)

7.3.活动队列

时间片还没有结束的所有进程,按照优先级放在活动队列

nr_active:运行状态的进程总数

queue[140]:每个元素都是进程队列,元素下标表示优先级,相同优先级按照FIFO进行排队调度

bitmap[5]:140个优先级有140个进程队列,使用5 * 32个bit位表示队列是否为空,提高查找效率

无位图的查找流程(O(N)):

从0下标开始变量queue[140]

找到第一个非空队列,该队列必定为优先级最高的队列

拿到选中队列中的第一个进程,开始运行,调度该队列

有位图的查找流程(O(1)):

先在位图数组中找到非0的元素

再找该元素中第一个为1的bit位

7.4.过期队列

时间片耗尽的进程,按照先后放在过期队列

活动队列上的进程处理完毕后过期队列中进程的时间片重会新计算

7.5.active指针与expired指针

active指针:指向活动队列

expired指针:指向过期队列

当活动队列中的进程全都放入过期队列

就会交换两个指针中的内容,继续运行

新来的进程会默认插入在过期队列当中

八、命令行参数

功能:实现程序不同子功能的方法

实验:打印main函数的命令行参数

  • makefile

  • code.c

实验现象

指令选项的实现原理

命令行以空格作为分隔符,bash将命令行切分为各个子串

argc代表子串个数,argv是子串的数组名

进程拥有一张argv表,支持实现选项功能

实验:给程序设计指令选项

  • code.c

实验现象

九、环境变量

9.1.概念介绍

环境变量(environment variable):用来指定操作系统运行环境的一些参数

使用场景

在编写C/C++程序时,在链接的时候,从来不知道链接的动静态库来自哪里

但是照样可以链接成功,生成可执行程序,原因是有相关环境变量帮助查找

9.2.环境变量的相关命令

env:显示所有环境变量

echo:显示某个环境变量值

 export:导入环境变量

unset:清除环境变量

set:显示本地变量和环境变量

9.3.常见的环境变量

PATH变量:告诉操作系统去哪些路径下查找二进制文件

将路径添加到环境变量中,会覆盖原路径,导致系统命令丢失,重新登录后,就可以恢复

可以通过追加的方式新增路径,以防原路径覆盖,但重新登录后,会自动删除新增的路径

 HOME变量:当前用户的家目录

SHELL变量:命令行解释器版本

USER变量:当前用户

HISTSIZE变量:bash记录最多历史命令的条数

HOSTNAME变量:主机名

PWD变量 :当前shell所在路径

OLDPWD变量:上一次shell所在路径

9.4.环境变量的组织方式

登录后,系统会创建bash进程,从系统相关配置文件中读取环境变量信息

bash进程内部会构建一张环境变量表,拿到命令行后,将它拆分,构建命令行参数表

从环境变量表中找到PATH,查找命令行参数表的程序名字是否在PATH的路径下存在

系统相关配置文件

  • .bashrc

  • .profile

  • /etc/bash.bashrc

在配置文件.profile中修改PATH,退出登录后,新增的路径依旧存在

如果Linux系统中有10个用户登录,存在10个bash,每一个都要从配置文件中,将数据读取到表里

实验:通过命令行第三个参数获取父进程传递的环境变量

  • code.c

实验现象

9.5.getenv函数

系统调用函数

功能:获取指定环境变量的内容

实验:获取环境变量PATH的内容

  • code.c

实验现象

实验:设计一个程序只能登录用户运行

  • code.c

实验现象

9.6.environ指针

指向环境变量表的指针

实验:通过environ指针获取父进程传递的环境变量

  • code.c

实验现象

9.7.环境变量的特性

环境变量具有全局属性,可以被子进程继承

实验:打印新建子进程的环境变量

  • code.c

实验现象

9.8.本地变量

本地变量:在命令行中定义的变量

set查看本地变量和环境变量

本地变量不会被子进程继承,只能在bash内部被使用

9.9.内建命令(built-in command)

不需要创建子进程而是让bash自己亲自执行

bash自己调用函数,或者通过系统调用完成

(比如:echo、set、cd)

而普通命令需要bash来fork一个子进程执行

比如:ls、cat)

 >:续行提示符

十、程序地址空间

10.1.程序地址空间

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int g_unval;
int g_val = 100;

int main(int argc, char *argv[], char *env[])
{
    static int test = 10;
    const char *str = "helloworld";                        
    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("code addr: %p\n", main);             //main函数代码

    /*只读数据段*/
    printf("read only string addr: %p\n", str);  //字符串常量

    /*数据段*/
    printf("init global addr: %p\n", &g_val);    //初始化全局变量
    printf("test static addr: %p\n", &test);     //静态局部变量

    /*BSS段*/
    printf("uninit global addr: %p\n", &g_unval);//未初始化全局变量

    /*堆区*/
    //malloc申请的内存(从低地址向高地址增长)
    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("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;
}

实验现象

10.2.虚拟地址

实验:父子进程中的变量地址相同,但内容不同

  • code.c

实验现象

父子进程地址一样,如果该地址是内存地址,就会出现bug,所以不可能是内存地址

在Linux系统中,这种地址叫做虚拟地址,C/C++中指针用到的地址全部都是虚拟地址

10.3.进程地址空间

一个进程有一个虚拟地址空间

地址空间宽度:1字节

1个字节表示一个地址

32位机器:2^32个地址

64位机器:2^64个地址

拿到地址可以直接访问

一个整型变量有四个字节,会取地址数值最小的那个访问

一个进程有一套页表

页表:虚拟地址和物理地址的映射表

新建子进程的PCB、虚拟地址空间、页表都要从父进程拷贝

父子进程修改共享数据时会发生写时拷贝

10.4.虚拟内存管理

虚拟内存的本质:每个进程所创建的结构体对象

系统需要管理虚拟内存,需要先对虚拟内存描述

  • 先描述

虚拟内存结构体

虚拟地址空间是一个结构体,不是内存

里面是所有区域的起始地址和结束地址

功能:区域划分、VMA管理、页表管理、资源统计

struct mm struct 
{
    //...

    struct vm_area_struct *mmap; //指向虚拟区间链表(VMA)
    struct rb_root mm_rb;        //红黑树管理VMA
    unsigned long task_size;     //具有该结构体进程的虚拟地址空间大小

    //...

    /*代码段的起始与结束地址*/
    unsigned long start code, end code;

    /*数据段的起始与结束地址*/
    unsigned long start data, end data;

    /*堆的起始地址与当前堆顶指针的位置*/
    unsigned long start brk, brk;

    /*栈的起始地址*/
    unsigned long start stack;

    /*命令行参数/环境变量的起始地址*/
    unsigned long arg start, arg end, env start, env end;

    //...
}

虚拟内存区域结构体(VMA)

描述的是一段连续的虚拟地址区间

每一个区间都有独立的一个结构体

功能:区间管理、权限控制、映射管理、组织管理

struct vm_area_struct 
{
    unsigned long vm_start;                    //虚存区起始
    unsigned long vm_end;                      //虚存区结束
    struct vm_area_struct *vm_next, *vm_prev;  //前后指针
    struct rb_node vm_rb;                      //红黑树中的位置
    unsigned long rb_subtree_gap;
    struct mm_struct *vm_mm;                   //所属的虚拟内存结构体
    pgprot_t vm_page_prot;
    unsigned long vm_flags;                    //标志位
    struct 
    {
        struct rb_node rb;
        unsigned long rb_subtree_last;
    }shared;
    struct list_head anon_vma_chain;
    struct anon_vma *anon_vma;
    const struct vm_operations_struct *vm_ops; //VMA对应的实际操作
    unsigned long vm_pgoff;                    //文件映射偏移量
    struct file * vm_file;                     //映射的⽂件
    void * vm_private_data;                    //私有数据
    atomic_long_t swap_readahead_info;

#ifndef CONFIG_MMU
    struct vm_region *vm_region; 
#endif
#ifdef CONFIG_NUMA
    struct mempolicy *vm_policy;
#endif
    struct vm_userfaultfd_ctx vm_userfaultfd_ctx;

} __randomize_layout;
  • 再组织

当虚存区结构体较少时,采用单链表,由虚拟内存结构体中的mmap指针指向这个链表

当虚存区结构体较多时,采用红黑树,由虚拟内存结构体中的mm_rb指向这颗红黑树

页表

将虚拟地址空间与物理内存做映射

程序可以在物理内存任意位置加载

通过映射将地址从“无序”变为“有序”

进程视角所有内存分布都为有序

OS创建并维护虚拟地址和页表,监管地址空间与页表的映射

在地址空间转化的过程中,OS对地址和操作进行合法性判定

OS查询页表的权限位,进而保护物理内存中的所有合法数据

缺页中断

程序代码总大小:2GB

虚拟地址空间为代码段分配了2GB空间

物理内存为代码段分配了500MB物理页

当运行完500MB程序后,操作系统会暂停进程运行

从磁盘中再划500MB的物理地址到页表中继续运行

因为有地址空间与页表映射,物理内存可以对未来数据进行任意位置的加载

使内存分配与进程管理无关,让进程管理与内存管理进行一定程度的解耦合

创建进程后,先有进程控制块,虚拟地址空间,页表,再加载数据

挂起本质:保留页表的虚拟地址,清空物理地址,将物理内存中的数据放到磁盘上的swap分区

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐