计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 物联网工程
学 号 2021113459
班 级 2237301
学 生 沈通
指 导 教 师 吴锐
计算机科学与技术学院
2024年5月
摘 要
本文从hello.c为例,论述hello.c从被程序员编写后到预处理,编译,汇编,链接的全过程以及在hello程序执行时对进程和内存的管理。通过了解hello.c文件的一生,加上《深入理解计算机系统》书中的内容与课上老师的讲授,我将计算机系统这一体系串联起来,相信会对日后的程序编写、管理有所帮助。
关键词:计算机系统;hello.c;程序一生;Linux
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 6 -
5.3 可执行目标文件hello的格式...................................................................... - 22 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 31 -
6.3 Hello的fork进程创建过程......................................................................... - 32 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................... - 38 -
7.3 Hello的线性地址到物理地址的变换-页式管理......................................... - 39 -
7.4 TLB与四级页表支持下的VA到PA的变换................................................ - 40 -
7.5 三级Cache支持下的物理内存访问............................................................. - 41 -
7.6 hello进程fork时的内存映射..................................................................... - 41 -
7.7 hello进程execve时的内存映射................................................................. - 42 -
7.8 缺页故障与缺页中断处理.............................................................................. - 42 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
第一步,使用使用编程软件(如Codeblocks)编写hello.c 源程序文本文件;第二步对.c文件进行预处理,得到hello.i源程序文本文件;第三步进行对.i文件编译,得到hello.s汇编语言程序文本文件;第三步进行汇编,转化为hello.o可重定位目标程序;最后将此程序同所需的函数库文件进行链接,最终生成可执行目标程序hello。此时在shell中调用相关命令即可创建进程,执行程序。这便是P2P(From Program to Process)。
在shell中调用命令时,shell调用fork函数创建进程,之后通过exceve在进程的上下文中加载hello,进程在虚拟内存空间中进行映射,加载所需物理内存。执行过程中,指令进入CPU流水线,执行结束后,父进程回收子进程,内核清除相关信息。这是O2O(From Zero-0 to Zero-0)。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:AMD Ryzen 7 5800H;16GB内存;
软件环境:Windows 11 64位;VMware Workstation 14;Ubuntu 20.04;
开发与调试工具:gcc;as;ld;vim;edb;readelf;gedit;gdb
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.i 预处理后的文本文件
hello.s 编译后的文本文件
hello.o 汇编后的可重定位目标文件
hello 链接后的可执行目标文件
1.4 本章小结
这一章节是对整个流程的一个概括,编写、预处理、编译、汇编、链接、执行,简单介绍了实验环境和基本流程,从下一章节开始会对流程的每一步骤进行更详细的讲解。
第2章 预处理
2.1 预处理的概念与作用
概念:
预编译又称为预处理,就是为编译做的预备工作的阶段。做些代码文本的替换工作,处理以#开头的指令,比如拷贝#include包含的文件代码,#define宏定义的替换,条件编译等。
作用:处理关于“#”的指令
1.删除“#define”,展开所有宏定义。
2.处理条件预编译。
3.处理“#include”预编译指令,将包含的“.h”文件插入对应位置。这可是递归进行的,文件内可能包含其他“.h”文件。
4.删除所有注释。
5.添加行号和文件标识符。用于显示调试信息:错误或警告的位置。
6.保留“#pragma”编译器指令。(1)设定编译器状态,(2)指示编译器完成一些特定的动作。
2.2在Ubuntu下预处理的命令
指令为cpp hello.c>hello.i。
图 1 预处理指令
2.3 Hello的预处理结果解析
文件的内容增加,变为3117行,其中放在文件最后的是原代码。原文件中宏进行宏展开,头文件被包含进文件,放在文件的前面。文件的格式并没有发生太大变化,仍为容易阅读的C语言文本文件。
可以看到文件前面不止是我们在代码中引用的三个头文件,还有其他在头文件中被引用的,他们都在.i文件中有所说明。
图 2 hello.i文件
图 3 hello.i文件
将.i文件同原.c文件对比,可以看到注释部分被删除,添加了行号和文件标识符。
图 4 hello.c文件
2.4 本章小结
这一章节介绍了程序从编写完成后经历的第一步——预处理,简单介绍了预处理器的工作并通过命令行进行hello.i文件生成,分析其结果,为下一步进行编译打下基础。
第3章 编译
3.1 编译的概念与作用
概念:编译程序,也称为编译器,是指把用高级程序设计语言书写的源程序,翻译成等价的机器语言格式目标程序的翻译程序。
作用:
- 扫描(词法分析)
将源代码程序输入扫描器,将源代码的字符序列分割成一系列记号。
- 语法分析
基于词法分析得到的一系列记号,生成语法树。
- 语义分析
由语义分析器完成,指示判断是否合法,并不判断对错。又分静态语义:隐含浮点型到整形的转换,会报warning;动态语义:在运行时才能确定。
- 源代码优化(中间语言生成)
中间代码(语言)使得编译器分为前端和后端,前端产生与机器(或环境)无关的中间代码,编译器的后端将中间代码转换为目标机器代码。
- 代码生成,目标代码优化
代码生成器:依赖于目标机器,依赖目标机器的不同字长,寄存器,数据类型等;目标代码优化器:选择合适的寻址方式,左移右移代替乘除,删除多余指令。
3.2 在Ubuntu下编译的命令
指令为gcc -S hello.i -o hello.s
图 5 编译指令
3.3 Hello的编译结果解析
3.3.1数据
- 常量
数字常量:通过“$”字符进行数字常量的标注。看到判断条件时以及循环次数的常数储存在.text节中。
图 6 数字常量示例
图 7 数字常量示例
字符串常量:printf使用的字符串常量储存在.rotate段中,用.string进行标注。
图 8 字符串常量示例
- 变量
这一程序中变量有三个,循环变量i,用于存储输入字符个数的argc,用于存储输入字符的数组argv。存放位置如下:
图 9 变量示例
图 10 变量示例
图 11 变量示例
由加一操作可以看出i储存在栈中地址为-4(%rbp);由与4进行比较看出argc储存在栈中地址-20(%rbp)的位置;由移入、指针加看出argv数组储存在栈中,地址为-32(%rbp)。
3.3.2赋值
变量的赋值在程序中出现一次,对循环变量i进行赋值,设置为0。
图 12 变量赋值示例
3.3.3类型转换
程序中调用atoi函数将等待时间从字符串形式转换到整型数。
图 13 类型转换示例
3.3.4算术操作
每一轮循环中,循环变量i都会加1。
图 14 算术操作示例
3.3.5关系操作
将argc与4进行比较,相同则跳转。
图 15 关系操作示例
将i与7进行比较,大于等于则跳转
图 16 关系操作示例
3.3.6数组/指针/结构操作
本程序定义argv数组,首先取argv[2],位于-16(%rbp);然后取argv[1],位于-24(%rbp);最后取等待时间argv[3],位于-8(%rbp),在下一步的调用sleep函数中使用。
图 17 数组/指针/结构操作示例
3.3.7函数调用
Main函数:
传入参数argc、argv,其中argc存放在%rdi中,argv存放在栈中。作为主函数对后续函数进行调用。
图 18 函数调用示例
Puts函数和Printf函数:
两者都是用于输出,输入为要输出对象的地址,第一次在满足if时调用,输出字符串的开头;第二次在满足for循环条件时调用输出argv[1]、argv[2]。
图 19 函数调用示例
图 20 函数调用示例
Exit函数:
传入参数为1,当if条件满足时,用于退出程序。
图 21 函数调用示例
Atoi函数:
将argv[3]传入,将字符串形式转变为整型数,为Sleep函数提供参数。
图 22 函数调用示例
Sleep函数:
将转变为整型数的argv[3]传入,进入等待状态。
图 23 函数调用示例
Getchar函数:
等待用户键入后,再将返回值设为0,主函数正常返回。
图 24 函数调用示例
3.4 本章小结
这一章节进行了对hello.i文件的编译工作,生成了hello.s汇编语言程序文本文件。作业中通过将源文件和生成文件对比,解析了汇编代码,加强了对编译过程的理解,将在下一步进行汇编处理。
第4章 汇编
4.1 汇编的概念与作用
概念:
把汇编语言翻译成机器语言的过程称为汇编。在汇编语言中,用助记符代替操作码,用地址符号或标号代替地址码,这种程序机器不能直接识别,要由一种程序翻译成机器语言,这种起翻译作用的程序叫汇编程序。
作用:
将汇编语言根据特定的转换规则转换为机器语言,机器语言是计算机真正能够 理解的代码格式,生成的目标文件由可执行的指令构成。
4.2 在Ubuntu下汇编的命令
指令为as hello.s -o hello.o。
图 25 汇编指令
4.3 可重定位目标elf格式
首先使用readelf -h hello.o查看头信息,ELF头以一个16字节的序列开始,用于描述这一文件的信息。头部剩下部分则包含目标文件类型、数据大小、版本、字序、头部大小以及节头部表中项的大小和数量。
图 26 ELF头
再使用readelf –S hello.o查看节头表,表中包含节的名称、地址、大小、类型、偏移量。
图 27 节头表
之后使用readelf –s hello.o可以查看符号表,表中包含程序中的函数和全局变量的信息,如大小、类型等。
图 28 符号表
最后使用readelf –s hello.o这一命令查看重定位节。发现本程序需要进行重定位的信息有:.rodata中模式串,puts,exit,printf,atoi,sleep,getchar。在后续的链接操作中会根据重定位节的信息对这些变量符号进行修改,通过偏移量计算出正确的地址。
图 29 重定位节
4.4 Hello.o的结果解析
使用objdump -d -r hello.o,观察hello.o的反汇编结果,并与hello.s进行对比。
图 30 反汇编结果
通过比较发现有三点不同。
首先操作数的进制不同,其中hello.s是十进制的,而hello.o反汇编结果为十六进制。其次分支转移方式不同,其中hello.s中跳转指令后跟着的是要跳转到的段落名,而hello.o反汇编结果跟着的是要跳转到的地址。最后是函数调用方式不同,与分支转移的不同类似,其中hello.s中函数调用指令后跟着函数名,而hello.o反汇编结果跟的是相对于main函数的偏移地址。
4.5 本章小结
这一章节中,hello.s文件经过汇编器后生成一个可重定位的、机器语言构成的文件,简要分析了汇编的生成结果并在反汇编后与.s文件进行比较,为下一章节的链接操作做准备。
第5章 链接
5.1 链接的概念与作用
概念:
链接就是将不同部分的代码和数据收集和组合成为一个单一文件的过程,这个 文件可被加载或拷贝到内存并执行。
作用:
通过编译器的5个步骤后,我们获得目标代码,但是里面的各个地址还没有确定,空间还没有分配。链接过程主要包括:地址和空间的分配,符号决议和重定位。
5.2 在Ubuntu下链接的命令
命令:ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-[1]
gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
图 31 链接指令
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
使用readelf -h hello查看头信息。通过ELF头的信息,我们可以得知共25个节,文件为可执行文件。
图 32 ELF头
每个节的详细信息通过节头表进行分析,使用readelf –S hello查看节头表。这里详细列出了各个节的大小、偏移量、起始地址等。
图 33 节头表
图 34 节头表
接下来用readelf –I hello查看elf格式的程序性头表信息:
图 35 程序性头表
最后用readelf –s hello看elf格式的符号表信息:
图 36 符号表
图 37 符号表
5.4 hello的虚拟地址空间
在这里我的虚拟机出现了问题,无法安装edb,只能使用同学的截图进行分析。
使用edb加载hello,查看Data Dump一栏:
图 38 edb下Data Dump
图 39 edb下Data Dump
可以看到虚拟地址空间始于0x401000,终于0x401ff0。
elf头的程序入口地址时0x4010f0,利用5.3中的节头部表可以找到,这恰好是.text段地址。因此根据节头部表,我们就能找到每个节的信息。
图 40 edb下入口地址
图 41 节头部表中相应地址
5.5 链接的重定位过程分析
使用指令objdump -d -r hello将重定位项目的内容输出到文件中,如下图所示:
图 42 重定位项目
与下图的hello.o相关信息进行比较,
图 43 hello.o相关信息
区别有两处:第一,以0开头的虚拟地址变成了具体的内存地址;第二,函数的调用也变成内存地址,增加了库函数、.init和.plt节。
由此可以看出,链接的过程实际上是将多个.o文件拼接在一起,其中的重定位过程中,链接器整理符号表中的条目,并为它们分配内存地址。具体过程是:先将同类节合并为一个节,然后赋予它们内存地址,使指令和变量有唯一的内存地址,最后将重定位节中的符号引用改为内存地址。
图 44 链接过程示意图
5.6 hello的执行流程
各个子程序名或程序地址如下表:
表 1 子程序名与程序地址
5.7 Hello的动态链接分析
由于编译器无法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略,将过程地址的绑定推迟到第一次调用该过程。动态链接器使用过程链接表PLT和全局偏移量表GOT实现动态链接,在GOT中存放函数目标地址,在PLT中使用GOT中地址跳转到目标函数。
在被dl_init调用前,每一条PIC函数调用的目标地址都实际指向PLT中的代码逻辑,初始时每个GOT条目都指向对应的PLT条目的第二条指令。
图 45 调用前
图 46 调用后结果
GOT表的起始位置为0x404000。通过调用dl_init前后比较可知,0x404008~0x404015之间的内容,即GOT[1]和GOT[2]的内容发生了变化。GOT[1]保存的是指向已经加载的共享库的链表地址,GOT[2]是动态链接器在ld-linux.so模块中的入口。在接下来执行程序的过程中,便可以使用过程链接表PLT和全局偏移量表GOT进行动态链接。
5.8 本章小结
这一章节主要介绍了链接的过程,使用edb分析了链接前后的变化,经过这一过程,hello.c正式从程序员所写的源代码程序变成可执行文件,hello程序就此诞生。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:
一个正在运行的程序或者软件就是一个进程,它是操作系统进行资源分配的基本单位,也就是说每启动一个进程,操作系统都会给其分配一定的运行资源(内存资源)保证进程的运行。
作用:
图 47 多进程示意图
进程给予应用程序一些关键抽象:第一,一个独立的逻辑控制流,进程使得我们感觉在独占处理器;第二,一个私有地址空间,进程使得我们感觉在独占内存系统。
6.2 简述壳Shell-bash的作用与处理流程
Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。
处理流程:
其基本功能是解释并运行用户的指令,重复如下处理过程:
(1)终端进程读取用户由键盘输入的命令行;
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量;
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令;
(4)如果不是内部命令,调用fork( )创建新进程/子进程;
(5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序;
(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait...)等待作业终止后返回;
(7)如果用户要求后台运行(如果命令末尾有&号),则shell返回。
6.3 Hello的fork进程创建过程
首先进行进程创建,在shell中输入./hello 20211111523宋子奕 3
由于hello不是内部命令,所以shell会fork一个子进程并进行后续操作。新建的子进程几乎和父进程相同。子进程拥有与父进程用户级虚拟地址空间相同且独立的一份副本,与父进程任何打开的文件描述符相同的副本。使用fork()函数来创建一个子进程,fork函数的原型为:pid_t fork(void)。
图 48 hello进程基本过程
6.4 Hello的execve过程
调用fork之后,子进程使用execve函数加载并运行hello程序,其中参数列表为argv,环境变量为envp。下图为示例:
图 49 execve参数示意图
参数列表argv指向一个指针数组,这一指针数组每一个指针都指向一个参数字符串,其中第一个指针argv[0]便是程序的名称;环境变量envp同样指向指针数组,每个指针指向一个环境变量的字符串,格式为”name = value”。
execve会启动加载器,加载器会删除子进程现有的虚拟内存段,创建一组新的代码、数据、堆、栈,新的堆栈都被初始化为0,通过页映射到可执行文件的页大小的片,新的代码数据初始化。最后跳转到_start地址,调用main函数。
6.5 Hello的进程执行
上下文使系统正确运行所需的状态,由系统内核维持,系统中的每个程序都运行在进程的上下文中。并发流是一个逻辑流在时间上与另一个逻辑流的重叠。一个进程执行它的控制流的一部分时间叫做时间片。
控制寄存器使用模式位描述当前进程享有的特权:当设置了模式位时,进程运行在内核模式中;当没有设置模式位时,进程运行在用户模式中。内核模式中,可以执行任何指令(包括特权指令),访问任何内存;用户模式中,则不能执行特权指令,访问内核区的代码数据。在执行中,内核可以抢占当前进程,而后又恢复先前被抢占的进程,这种方式叫做调度,而调度过程是由上下文切换来实现的。
图 50 上下文切换
上下文切换步骤为:首先保存当前进程的上下文,然后恢复先前被抢占后保存的上下文,最后将控制传递给新进程。
hello程序中执行了sleep系统调用,会将用户模式切换为内核模式,令程序进行休眠,将控制转给其他进程。当休眠结束时,会发送信号给内核,hello程序回到用户模式。getchar函数执行read系统调用过程与上文类似。
6.6 hello的异常与信号处理
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
异常总共分为4类:中断、陷阱、故障、终止。如下表所示:
表 2 异常的类型
在发生异常时会发出信号,常见的信号种类如下表所示:
表 3 信号的种类
对于hello程序,我们尝试在执行过程中从键盘输入。
1)不断乱按,程序会正常执行,而乱按的内容会被一同输出。
图 51 不断乱按结果
2)输入回车,输出会换行,程序正常执行。
图 52 输入回车结果
3)输入Ctrl-C后,内核会发送一个SIGINT信号到前台进程组的每一个进程,默认终止前台作业。
图 53 输入Ctrl-C结果
前台作业立即终止,用ps查看前台进程组。
图 54 ps后结果
发现已经没有了hello进程。
- 输入Ctrl-Z后,内核将前台作业挂起。
图 55 输入Ctrl-Z结果
使用ps命令,便可在后台看到hello进程。
图 56 ps后结果
使用jobs命令同理。
图 57 jobs后结果
使用pstree指令,可以打印进程树,其中包含着自开机以来各个进程的父子关系,下图为进程树的一部分。
图 58 pstree结果
由先前的jobs指令结果可知,hello的后台job号为1,使用fg 1指令将其调回前台,hello进程便继续执行输出指令。
图 59 fg 1指令结果
使用kill的相关指令也可以对进程进行控制,
-1为重新加载进程,
图 60 kill -1指令结果
-9为终止进程,
图 61 kill -9指令结果
-15为正常停止进程。
图 62 kill -15指令结果
6.7本章小结
在这一章节,hello程序已经诞生,而在其运行过程中需要经历fork创建子进程、execve加载并运行、内核调度上下文切换等过程,本章都有详细的介绍与分析。最后着重分析了异常与信号的相关知识以及hello程序中对这两项的处理过程。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:又称为绝对地址,是由程序产生的与段相关的偏移地址部分,由选择符和偏移量组成。在hello反汇编代码中看到的就是逻辑地址。
线性地址:逻辑地址到物理地址变换的中间层。CPU在保护模式下,线性地址为段基址和段内偏移地址;如果CPU在保护模式下没有开启分页功能,则线性地址被当作物理地址;如果开启了,则线性地址被当作虚拟地址。
虚拟地址:逻辑地址计算后的结果。在hello反汇编代码中经过计算便是虚拟地址。
物理地址:用于内存芯片级的单元寻址,与地址总线相对应,是出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址。如果没有启用分页机制,那么hello的线性地址就是物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
将程序按内容或过程函数关系分成段,例如程序段、数据段。以段为单位分配内存,通过地址映射机制,将段式虚拟地址转换成实际内存物理地址。段的长度由相应的逻辑信息组的长度决定,因而各段长度不等。
逻辑地址由段标识符和段内偏移量组成。其中段标识符由一个16位的字段组成,称为段选择符,结构如下:
图 63 段选择符结构
地址变换的过程如下图所示:
图 64 段式管理流程
首先根据TI字符是0还是1判断段描述符在GDT还是在LDT中,然后根据GDTR或者LDTR找到对应GDT或LDT在对应内存中的位置,再根据段选择符的前13位,在相应描述符表中查找到对应的段描述符,得到基地址,再结合段内偏移量获得线性地址。
优点:可以分别编写和编译,可以针对不同类型的段采取不同的保护,可以按段为单位进行共享,包括通过动态链接进行代码共享,每次交换的是一组相对完整的逻辑信息。
缺点:会产生外部碎片,进程必须全部装入内存。
7.3 Hello的线性地址到物理地址的变换-页式管理
将各进程的虚拟空间划分为若干长度相等的页。内存空间按页的大小建立页表。
图 65 页式管理流程
从线性地址到物理地址需要进行地址翻译,线性地址地址可以分为VPN和VPO两部分,系统根据CR3得到页表基址。MMU通过VPN到页表中选择适当的页表项,得到相应地物理页号,因为VPO和PPO是相同的,因此将取得地PPN和VPO串联起来就能得到对应地物理地址。
优点:没有外碎片,每个内碎片不超过页的大小。
缺点:程序全部装入内存,要求有相应的硬件支持(硬件产生缺页中断)。
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7 MMU使用四级页表进行地址翻译,如下图所示:
图 66 四级页表下地址变换
36位VPN划分为4个9位VPN,分别用于一级页表的偏移量,最终由L4页表得到的PPN与PPO(VPO)结合,便是物理地址。
7.5 三级Cache支持下的物理内存访问
同样在Core i7 CPU中使用,
图 67 三级Cache下物理内存访问
将虚拟地址在TLB中寻找,若命中,则将PTE发送给L1Cache,否则在页表中更新PTE,然后在L1Cache中寻找物理地址,检测是否命中,若不命中则发送给下一级Cache(L2、L3),直到命中为止。
7.6 hello进程fork时的内存映射
fork函数创建hello进程时步骤如下:
(1)创建hello的mm_struct、vm_area_struct链表和页表的原样副本。
(2)两个进程中的每个页面都标记为只读
(3)两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行hello的步骤如下:
(1)删除已有页表和结构体vm_area_struct链表。
(2)创建新的页表和结构体vm_area_struct链表。代码和初始化的数据映射到.text和.data区(目标文件提供),.bss和栈映射到匿名文件。
(3)设置PC,指向代码区域的入口点,Linux根据需要换入代码和数据页面。
进程中内存映射情况如下图所示:
图 68 进程中内存映射情况
7.8 缺页故障与缺页中断处理
RAM缓存不命中称为缺页,此时虚拟内存中的字不在物理内存中。PTE中有一个有效位,若定位后发现有效位为0,则未命中,缺页;反之,则命中。若发生缺页故障,因为从磁盘获取资源消耗较长时间,内核会将当前进程挂起。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,把要缓存的页缓存到牺牲页的位置。如果这个牺牲页被修改过,就把它交换出去。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这次不会再度引起缺页。
下图以VP3未命中为例:
图 69 VP3第一次未命中
缺页处理程序选择VP4作为牺牲页,再度执行这一指令时情况如下图:
图 70 VP3第二次命中
可见没有再产生异常。
7.9动态存储分配管理
7.10本章小结
这一章节主要是hello进程执行时的内存分配、管理,由很多内存管理相关的概念。比较了页式管理和段式管理的优缺点,介绍了Core i7 CPU内存管理上的方法,在进程的不同阶段的内存映射等。
结论
hello.c的一生都经历了以下几个阶段:
- 预处理:
通过预处理器,将hello.c中include的外部的头文件头文件插入程序文本中,完成字符串的替换,生成hello.i。
- 编译
通过编译器,对词法分析和语法进行分析,将合法指令翻译成等价汇编代码,将hello.i 翻译成汇编语言文件 hello.s。
- 汇编
通过将汇编器,将文本翻译成机器语言指令,把指令打包成可重定位目标程序格式,生成hello.o 文件。
- 链接
通过链接器,将hello.o的程序编码与动态链接库等收集整理成为一个单一文件,生成完全链接的可执行的目标文件hello。
- 加载运行
打开Shell,在其中键入 ./hello,终端使用fork函数创建进程,使用execve函数进行代码和数据的加载。
- 访存
内存管理单元MMU将逻辑地址,一步步映射成物理地址,通过多级缓存来访问物理内存、磁盘中的数据。
- 信号处理
进程无时无刻不在等待着信号,针对各种信号有不同的应对策略。
- 回收
shell父进程等待并回收hello子进程,内核将为hello进程消耗的资源释放。
附件
列出所有的中间产物的文件名,并予以说明起作用。
文件名称 | 文件作用 |
hello.i | hello.c预处理后的文本文件 |
hello.s | hello.i编译后的文本文件 |
hello.o | hello.s汇编后的可重定位目标文件 |
hello | hello.o链接后的可执行目标文件 |
参考文献
[1] https://blog.csdn.net/weixin_44799217/article/details/115007536
[2] https://blog.csdn.net/yilongdashi/article/details/82707022
[3] https://blog.csdn.net/weixin_46199479/article/details/123438544
[4] https://blog.csdn.net/qq_52019899/article/details/124673687
[5] https://zhuanlan.zhihu.com/p/95015248
[6] https://blog.csdn.net/weixin_42237429/article/details/120347689