计算机系统
大作业
计算机科学与技术学院
2023年5月
本文运用了计算机系统的教学内容以及《深入理解计算机系统》一书中相关知识,对Hello.c文件的编译,执行过程进行了探讨,分析研究了其作为程序的生命周期,通过对其在linux环境下从编译到执行的全过程分析,对本学期的相关知识进行回顾与概括,对知识框架进行梳理,巩固本学期所学内容。
关键词:计算机系统;程序生命周期;程序人生
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 5 -
5.3 可执行目标文件hello的格式........................................................................ - 8 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 10 -
6.3 Hello的fork进程创建过程......................................................................... - 10 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................... - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理.......................................... - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换................................................ - 11 -
7.5 三级Cache支持下的物理内存访问............................................................. - 11 -
7.6 hello进程fork时的内存映射..................................................................... - 11 -
7.7 hello进程execve时的内存映射................................................................. - 11 -
7.8 缺页故障与缺页中断处理.............................................................................. - 11 -
8.2 简述Unix IO接口及其函数.......................................................................... - 13 -
第1章 概述
1.1 Hello简介
P2P指 From Program to Progress ,即从C程序源文件(Program)到可执行程序(Process)的过程,对一个C程序源文件使用编译器进行预处理,编译,链接后可以得到一个可执行格式的文件,即完成P2P过程。
020指From Zero to Zero,hello在被真正运行之前,在内存中不占据空间, 此为From Zero。程序执行结束之后,其由父进程进行回收,由内核清除其余信息,恢复到未执行之前的无占据空间状态,此为To Zero。
1.2 环境与工具
硬件环境:
处理器:AMD Ryzen 7 5800H 3.2GHz
内存:16GB(3200MHz)
软件环境:
Windows10;Ubantu
开发与调试工具:gcc,cpp,as,ld,edb,gdb,readelf
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
表格 1 中间结果
文件名 | 功能 |
hello.i | 预处理后得到的文本文件 |
hello.s | 编译后得到的汇编语言文件 |
hello.o | 汇编后得到的可重定位目标文件 |
hello.elf | 用readelf读取hello.o得到的ELF格式信息 |
hello.asm | 反汇编hello.o得到的反汇编文件 |
hello2.elf | 由hello可执行文件生成的.elf文件 |
hello2.asm | 反汇编hello可执行文件得到的反汇编文件 |
1.4 本章小结
本章主要介绍了P2P和020的概念,并对本文所用环境与中间文件做出了简要列举。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理是指在编译源代码之前对源代码进行的一系列操作,主要包括宏替换、文件包含、条件编译等。预处理器会将源代码中的预处理指令进行处理,生成新的源代码,然后再将新的源代码交给编译器进行编译。
预处理用于对代码进行初步的文本处理,实现一些在程序编译过程中无法实现的功能,如利用宏实现的代码生成。
2.2在Ubuntu下预处理的命令
在Ubuntu下,预处理的命令为:`gcc -E test.c -o test.i`。¹
这个命令会将test.c文件预处理后输出到test.i文件中。²
2.3 Hello的预处理结果解析
可以看到,原本24行的程序被扩展到了3000+行,编译器在此阶段将include的文件直接添加到处理后的预编译文件中,并且全部的#define定义的宏已经被递归展开完毕,相关代码已被生成。
2.4 本章小结
本章展示了对原始c文件进行预处理的方法,以及预处理过程中预处理器的工作。 同时结合Ubuntu系统展示了预处理过程和部分预处理文件。 预处理器在这个过程中的工作为后续的编译等过程提供了前提。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译是指编译器ccl对hello.i的内容检查通过后,将其翻译成汇编语言文件hello.s的过程
编译的作用可以体现在以下几个步骤中。
(1)词义分析,编译器将输入的字符一个一个地扫描,一个一个地生成单词,从而将源程序转化为由单词组成的中间程序。
(2)句法分析,编译器根据词义分析通过每个词生成句法分析树。 之后判断每条指令是否是符合c语言指令的合法指令。
(3) 代码优化。 编译器根据用户选择的不同优化级别,对代码进行不同程度的保守优化。 这种优化是等价且安全的。
(4) 生成代码,编译器可以将优化后的代码转换成汇编语言代码,生成hello.s文件
3.2 在Ubuntu下编译的命令
命令:gcc -m64 -no-pie -fno-PIC -S -o hello.s hello.i
运行结果如下:
3.3 Hello的编译结果解析
3.3.1数据
(1)常量
数字:通过观察我们可以得知,在程序中我们使用了许多的数字常量,这些常量都存储在程序的.text节中,直接体现在代码部分。部分截图如下:
图3-2 代码中的数字常量
我们可以看到,许多的运算都使用了数字常量。
字符串常量:程序中使用了printf()函数,这一输出中涉及了一些格式串,而格式串一般存储在.rodata节中,截图如下:
图3-3 代码中的字符串常量
(2)全局变量
程序中的全局变量只有main,我们知道这是一个函数型的全局变量,而且是一个强符号。
图3-4 全局变量main
(3)局部变量
程序中的局部变量都存储在堆栈段或者寄存器中,本程序中的局部变量有int argc,char *argv[],int i等等。
其中argc和argv都是运行时从寄存器传入,之后保存在运行时栈中。我们可以发现argc存储在%rbp-20的地方,它用来和4比较来决定程序的输入是否正确
图3-5 传入的argc与argv
而循环变量i保存在%rbp-4,它用来比较决定何时结束循环
图3-6 循环变量i
3.3.2 赋值
通过观察,我们可以知道程序中主要有两个赋值
(1)对循环变量i进行赋值,每次加1,截图如下:
图3-7 对循环变量i进行赋值
(2)在调用sleep函数之前,对传入的参数进行赋值,我们可以发现eax中的值是由atoi()函数得来,这就是我们所输入的argv[3]中的秒数,作为参数被从eax中复制到rdi中,截图如下:
图3-8 对sleep()传入的参数进行赋值
3.3.3 类型转换
(1)字符串变量转为整型数
程序中通过调用atoi()函数,来讲我们输入的字符串给转换为一个整型数字,截图如下:
图3-9 字符串转换为整型数字
3.3.4 算术操作
程序中最主要的算术操作就是计算循环变量i的值,每次循环递增,截图如下:
图3-10 循环变量递增
3.3.5 关系操作
程序中主要有两处关系操作,分别对应着两种条件下的跳转指令。
(1)argc!=4时的跳转,如果输入不符合要求,就打印正确做法并退出。
图3-11 第一次跳转
(2)循环变量达到9时结束循环
图3-12 第二次跳转
3.3.6 数组/指针/结构操作
程序中只有一处对于数组进行的操作,也就是传入的argv数组,通过观察我们可以知道,argv数组的4部分都存在栈中
图3-13 数组操作
3.3.7 控制转移
程序中控制转移相关操作与关系操作中相同
3.3.8 函数调用
程序主要存在三处函数调用,分别是main函数,atoi函数和 sleep函数
(1)main函数
参数传入:argc存放在rdi中,argv存放在rsi中,这两个参数通过寄存器传入后被压入运行时栈中
图3-14 main函数调用
(2)atoi函数
我们可以看到参数从rax中被复制入rdi,传入atoi函数调用
图3-15 atoi函数调用
(3)sleep函数
我们可以看到参数从eax中被复制入rdi,传入sleep函数调用
图3-16 sleep函数调用
3.4 本章小结
本章主要介绍了将预处理后的文件转化为汇编语言文件时发生的变化,并且展示了汇编代码中的一些主要操作,验证了这些操作的实现。
第4章 汇编
4.1 汇编的概念与作用
汇编是指汇编器将hello.s的汇编语言程序翻译为机器语言程序,并且将机器语言程序打包为可重定位文件hello.o的过程。
作用:
汇编器将汇编代码根据特定的转换规则转换为二进制代码,也就是机器码,机器只能理解机器代码。
4.2 在Ubuntu下汇编的命令
命令 gcc -m64 -no-pie -fno-PIC -c -o hello.o hello.s
汇编命令截图如下:
4.3 可重定位目标elf格式
(1)ELF头
以一个16字节的序列开始,这个序列描述了生成该文件系统下的字的大小以及一些其他信息。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息:包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。具体ELF头的代码如下:
图4-2 ELF头
(2)节头:
这里包含了文件中各个节的信息,包括类型、位置和大小等等
图4-3 节头
(3)重定位节.rela.text:
这个节中包含.text节中需要进行重定位的信息,在链接的时候,连接器会根据这些信息重定位节中的地址。节中包含这些内容:偏移量、信息、类型、 符号值、符号名称 + 加数。
图4-4 重定位节.rela.text
(4)符号表:
.symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。本程序中的getchar等函数名都需要在这一部分体现,具体信息如下图所示:
图4-5 符号表
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o >hello1_dis.txt
图4-6 反汇编命令
通过对比生成的文件和原文件,我们可以发现一些差异:
(1)分支转移:
在hello.s中,我们可以看到跳转的时候是直接指明段名称的,例如.L2,.L3等等,然而反汇编的得到的文件中,跳转主要是通过计算偏移量得到的
图4-7 跳转命令
(2)函数调用
在我们之前的汇编语言文件,call之后直接跟着函数名称,而在反汇编得到的文件,call 的目标地址是当前指令的下一条指令。这是因为 hello.c 中调用的函数都是共享库中的函数,最终需要通过链接器才能确定函数运行时的地址,在汇编的时候,对于这些不确定地址的函数调用,需要将其相对地址设置为全 0,然后在.rela.text节中为其添加重定位条目,等待链接时确定。
图4-8 call命令
(3)参数
在访问参数时,同样使用类似上述函数调用时的方式,将偏移地址暂时设为0,等待重定位时修改。
图4-9 访问参数
4.5 本章小结
本章主要介绍了汇编的过程,文件经过汇编器的转化,生成了一个可重定位的文件,为下一步的链接过程做准备。本章同时展示了可重定位文件的反汇编结果,,展示了暂时为0的偏移值,这些都是为了链接过程所做的准备。
(以下格式自行编排,编辑时删除)
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接是将各种不同文件的代码和数据片段收集并组合成一个单一文件的过 程,这个文件可被加载到内存并执行。
链接可以把预编译好了的若干目标文件合并成为一个可执行目标文件。使得分离编译成为可能,不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为可独立修改和编译的模块。当改变这些模块中的一个时,只需简单重新编译它并重新链接即可,不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
图5-1 链接
5.3 可执行目标文件hello的格式
命令:readelf -a hello > hello2_elf.txt
打开文件,内容如下:
(1)ELF头:
基本信息未发生改变,但是类型发生了变化并且添加了程序入口的地址。
图5-2 ELF头
(2)节头包含的信息更加丰富
图5-3 节头
(3)程序头中出现了偏移量、虚拟地址和物理地址
图5-4 程序头
(4)Dynamic section
图5-5 Dynamic section
(5)符号表
图5-6 符号表
5.4 hello的虚拟地址空间
使用edb打开hello,查看datadump窗口,我们可以得到虚拟地址空间结果如下所示:
我们可以发现这部分代码的地址是从0x401000开始的,地址从401000到401280
通过对照符号表,我们可以发现对应关系。
图5-7 data dump窗口和符号表
5.5 链接的重定位过程分析
命令objdump -d -r hello >hello2_dis.txt
图5-8 反汇编代码(部分)
不同之处有以下几点:
(1)hello中存在程序所调用的一些库函数的实现,例如getchar等
(2)链接后的函数都有了虚拟地址
图5-9 虚拟地址
(3)对于变量的引用也有了偏移量
图5-10 引用变量
链接的过程:
链接主要分为两个过程:符号解析和重定位。
(1)符号解析:目标文件定义和引用符号,符号解析将每个符号引用和一个符号定义关联起来。(2)重定位:编译器和汇编器生成从0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。
5.6 hello的执行流程
通过对照符号表,以及使用edb跟踪执行,我们可以得到执行流程如下
图5-11 符号表和datadump
子程序名 | 地址 |
hello! _start | 0x4010f0 |
Libc.so.6!__libc_start_main | 0x7ae62d588b0 |
hello!_init | 0x401000 |
hello!main | 0x401125 |
hello!printf@plt | 0x401040 |
hello!sleep@plt | 0x401080 |
hello!getchar@plt | 0x401050 |
Libc.so.6!exit | 0x7ae62d635f0 |
5.7 Hello的动态链接分析
当程序调用一个由共享库定义的函数时,由于编译器无法预测这时候函数的地址是什么,所以需要添加重定位记录,等待动态链接器处理。链接器采用延迟绑定的方法,将过程地址的绑定推迟到第一次调用该过程时。通过 GOT 和过程链接表 PLT 的协作来解析函数的地址。在加载时,动态链接器会重定位 GOT 中的每个条目,使它包含正确的地址。
首先查看elf中.got.plt节的内容
图5-12 .got.plt
图5-13 改变后的.got.plt
通过调用dl_init,我们可以发现.got.glt的内容发生了改变
5.8 本章小结
本章主要介绍了将可重定位目标文件链接生成可执行文件的过程。首先分析了链接的概念和作用,之后查看并验证了hello虚拟地址和节头部表的信息,最后分析了hello的执行流程并对动态链接进行了分析,加深了对于重定位和动态链接的理解。
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个正在运行的程序的实例,系统中的每一个程序都运行在某个进程的上下文中。
进程为应用程序提供两个关键抽象
(1)一个独立的逻辑控制流,提供一个假象,好像程序“独立”地使用处理器
(2)一个私有地址空间,提供一个假象,好像程序“独占”地使用内存
6.2 简述壳Shell-bash的作用与处理流程
shell是一个用C语言编写的交互型应用程序,代表用户运行其他程序。shell应用程序提供了一个界面,用户可以通过这个界面进行系统的基本操作,访问操 作系统内核的服务。shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行,求值步骤解析命令行,并根据解析结果运行程序。
处理流程:
(1)shell从终端读入命令
(2)shell将命令行拆分获得命令行参数,并且扫描识别所有参数
(3)如果是一个内置命令,就执行
(4)如果不是一个内置命令,那说明要执行一个程序,调用fork从init创建一个子进程
(5)通过命令结尾处是否有&决定是否切入后台执行
(6)执行任务
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行的子进程。调用fork函数后,新 创建的子进程几乎但不完全与父进程相同:子进程得到与父进程虚拟地址空间相同 的(但是独立的)一份副本,包括代码、数据段、堆、共享库以及用户栈,子进程获 得与父进程任何打开文件描述符相同的副本,这意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。fork被调用一次,却返回两次,子进程返 回0, 父进程返回子进程的PID。父进程和新创建的子进程之间最大的区别在于它 们有不同的 PID。
在本程序里,我们输入对应的指令调用hello,shell为为hello调用fork创建一个子进程。当子进程结束时,如果父进程尚未结束,就由父进程负责回收,否则由init回收。
6.4 Hello的execve过程
exceve 函数在当前进程的上下文中加载并运行一个新程序。exceve 函数加载 并运行可执行目标文件,并带参数列表和环境变量列表。只有当出现错误时,exceve 才会返回到调用程序。所以,与 fork 一次调用返回两次不同,在 exceve 调用一次 并从不返回。当加载可执行目标文件后,exceve 调用启动代码,启动代码设置栈, 将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的 第一条指令或入口点来运行该程序,由此将控制传递给新程序的主函数。
execve在加载了hello之后,会构造argc,argv和envp,并将控制传递给main函数。
6.5 Hello的进程执行
内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
进程时间片:
一个进程执行它的控制流的一部分的每一个时间段叫做时间片。
进程调度:
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这种决策称为调度,是由内核中的调度器代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
用户模式与核心模式的转换:
为了保证系统安全,需要限制应用程序所能访问的地址空间范围。因而存在用户态与核心态的划分,核心态拥有最高的访问权限,而用户态的访问权限会受到一些限制。处理器使用一个寄存器作为模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,一定程度上保证了系统的安全性。
图6-2 用户模式与核心模式的转换
6.6 hello的异常与信号处理
图6-3 正常执行
异常类型如下:
图6-4 异常类型
处理方法如下:
图6-5 异常的处理方法
异常:
(1)不停乱按:乱码会被认为是命令,不影响执行,但是会提示未找到命令
图6-6 不停乱按
(2)ctrl + z
图6-7 ctrl + z
通过使用ctrl + z命令,将进程挂起
(3)ps
图6-8 ps命令
在进程挂起后使用ps指令,打印pid
(3)jobs
图6-9 jobs命令
使用jobs命令,打印了被挂起的进程的jid
(4)pstree
图6-10 pstree命令
挂起后使用pstree命令
(5)fg
图6-11 fg命令
运行fg命令会把之前挂起在后台的hello重新调到前台并且执行,打印出剩余部分。
(6)kill
图6-12 kill命令
重新运行程序,我们可以找到pid为3350,通过使用kill -9 3350命令发送SIGKILL信号,可以杀死进程。
(7)ctrl + c
图6-13 ctrl + c命令
在程序运行时输入ctrl + c,向进程发送SIGINT信号,结束hello进程。这时再使用ps和jobs均查询不到hello。
6.7本章小结
本章主要介绍了 hello 可执行文件的执行过程,包括进程创建、加载和终止, 以及通过键盘输入等过程。之后介绍了异常以及异常的处理方法。 最后通过对hello进行操作直观地体现了进程的控制。在hello程序运行的过程中,内核对其进行进程管理,决定何时进行进程调度,在接收到不同信号时,还要进行对应的处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
- 逻辑地址
逻辑地址是指由程序产生的与段相关的偏移地址部分,逻辑地址由选择符和偏移量两部分组成。
- 线性地址
逻辑地址经过段机制转化后为线性地址,其为处理器可寻址空间的地址,用于描述程序分页信息的地址。
- 虚拟地址
虚拟地址即为线性地址。
- 物理地址
数据在真实内存中的实际存储地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel处理器从逻辑地址到线性地址的变换通过段式管理的方式实现。每个程序在系统中都注册有一个保存着该程序各段装入主存的状况信息,包括段号或段名、段起点、装入位、段的长度、主存占用区域表、主存可用区域表等的段表。
段选择符存放在段寄存器中,可以通过段选择符来得到对应段首地址。段选择符的结构如下:
图 48 段选择符的情况
它由三部分组成:索引,TI,RPL
索引:用来确定当前使用的段描述符在描述符表中的位置;
TI:根据TI的值判断选择全局描述符表(TI=0,GDT)或选择局部描述符表(TI=1,LDT);
RPL:判断重要等级。RPL=00,为第0级,位于最高级的内核,RPL=11,为第3级,位于最低级的用户状态;
通过一个索引,可以定位到段描述符,进而通过段描述符得到段基址。段基址与偏移量结合就得到了线性地址,虚拟地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(VA)到物理地址(PA)之间的转换通过对虚拟地址内存空间进行分页的分页机制完成。
通过7.2节中的段式管理过程,可以得到了线性地址/虚拟地址,记为VA。
虚拟地址可被分为两个部分:VPN(虚拟页号)和VPO(虚拟页偏移量),根据计算机系统的特性可以确定VPN与VPO的具体位数,由于虚拟内存与物理内存的页大小相同,因此VPO与PPO(物理页偏移量)一致。而PPN(物理页号)则需通过访问页表中的页表条目(PTE)获取,如下图所示。
图 49 Hello的线性地址到物理地址的变换-页式管理
若PTE的有效位为1,则发生页命中,可以直接获取到物理页号PPN,PPN与PPO共同组成物理地址。
若PTE的有效位为0,说明对应虚拟页没有缓存到物理内存中,产生页错误,调用操作系统的内核的缺页处理程序,确定牺牲页,并调入新的页面。再返回到原来的进程,再次调用导致缺页的指令。此时发生页命中,获取到PPN,与PPO共同组成物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
针对Intel Core i7 CPU研究VA到PA的变换。
Intel Core i7 CPU的基本参数如下:
- 虚拟地址空间48位(n=48)
- 物理地址空间52位(m=52)
- TLB四路十六组相连
- L1,L2,L3块大小为64字节
- L1,L2八路组相连
- L3十六路组相连
- 页表大小4KB(P=4x1024=2^12),四级页表,页表条目(PTE)大小8字节
由上述信息可以得知,VPO与PPO有p=12位,故VPN为36位,PPN为40位。单个页表大小4KB,PTE大小8字节,则单个页表有512个页表条目,需要9位二进制进行索引,而四级页表则需要36位二进制进行索引,对应着36位的VPN。TLB有16组,故TLBI有t=4位,TLBT有36-4=32位。
图 50 TLB与四级页表支持下的VA到PA的变换
如图所示, CPU产生虚拟地址VA,并将其传送至MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)在TLB中进行匹配,若命中,则得到PPN(40bit)与VPO(12bit)组合成物理地址PA(52bit)。若TLB没有命中,则MMU向页表中查询,由CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,则执行下一步确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并向TLB中添加条目。多级页表的工作原理展示如下:
s
若查询PTE的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
7.5 三级Cache支持下的物理内存访问
因为三级Cache的工作原理基本相同,所以在这里以L1 Cache为例,介绍三级Cache支持下的物理内存访问。
L1 Cache的基本参数如下:
- 8路64组相连
- 块大小64字节
由L1 Cache的基本参数,可以分析知:
块大小64字节→需要6位二进制索引→块偏移6位
共64组→需要6位二进制索引→组索引6位
余下标记位→需要PPN+PPO-6-6=40位
故L1 Cache可被划分如下(从左到右):
CT(40bit)CI(6bit)CO(6bit)
在7.4中我们已经由虚拟地址VA转换得到了物理地址PA,首先使用CI进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO取出相应的数据后返回。
若没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中请求数据(请求顺序为L2 Cache→L3 Cache→主存,若仍不命中才继续向下一级请求)。查询到数据之后,需要对数据进行读入,一种简单的放置策略如下:若映射到的组内有空闲块,则直接放置在空闲块中,若当前组内没有空闲块,则产生冲突(evict),采用LFU策略进行替换。
7.6 hello进程fork时的内存映射
当fork函数被当前进程hello调用时,内核为新进程hello创建各种数据结构,并分配给它一个唯一的PID。为了给这个新的hello创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当着两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数加载并运行hello需要以下几个步骤:
- 删除已存在的用户区域
删除当前进程hello虚拟地址的用户部分中的已存在的区域结构。
- 映射私有区域
为新程序的代码、数据、bss和栈区域创建新的私有的、写时复制的区域结构。其中,代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
- 映射共享区域
若hello程序与共享对象或目标(如标准C库libc.so)链接,则将这些对象动态链接到hello程序,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器
最后,execve设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
发生一个缺页异常后,控制会转移到内核的缺页处理程序。判断虚拟地址是否合法,若不合法,则产生一个段错误,然后终止这个进程。
若操作合法,则缺页处理程序从物理内存中确定一个牺牲页,若该牺牲页被修改过,则将它换出到磁盘,换入新的页面并更新页表。当缺页处理程序返回时,CPU 再次执行引起缺页的指令,将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,主存将所请求字返回给处理器。
7.9动态存储分配管理
动态内存管理的基本方法与策略介绍如下:
动态内存分配器维护着一个称为堆的进程的虚拟内存区域。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放可以由应用程序显式执行或内存分配器自身隐式执行。
具体而言,分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
下面介绍动态存储分配管理中较为重要的概念:
- 隐式链表
堆中的空闲块通过头部中的大小字段隐含地连接,分配器通过遍历堆中所有的块,从而间接遍历整个空闲块的集合。
对于隐式链表,其结构如下:
图 51 隐式链表的结构
- 显式链表
在每个空闲块中,都包含一个前驱(pred)与后继(succ)指针,从而减少了搜索与适配的时间。
显式链表的结构如下:
图 52 显式链表的结构
- 带边界标记的合并
采取使用边界标记的堆块的格式,在堆块的末尾为其添加一个脚部,其为头部的副本。添加脚部之后,分配器就可以通过检查前面一个块的脚部,判断前面一个块的起始位置和状态。从而实现快速合并,减小性能消耗。
- 分离存储
维护多个空闲链表,其中,每个链表的块具有相同的大小。将所有可能的块大小分成一些等价类,从而进行分离存储。
7.10本章小结
本章主要介绍了hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
结论
hello的一生:hello从一个c源文件变成一个可执行文件,最后变成一个进程,随后被执行并回收。 它的经历如下:
(1)预处理:hello.c源代码文件经过预处理程序cpp处理,插入头文件,调整结构,得到处理后的文本文件hello.i
(2)编译:hello.i由编译器编译得到汇编语言文件hello.s
(3)汇编:hello.s由汇编器编译,得到重定向文件hello.o
(4)链接:hello.o由链接器链接,生成可执行目标文件hello
图 7-1 编译系统
(5)加载:在shell中输入命令,使用shell调用fork函数,为shell生成进程
(6)运行:使用execve加载运行hello,将代码和数据加载到虚拟内存空间
(7)执行:当进程被调度时,CPU分配一个时间片给hello。 在一个时间片内,hello进程独占所有CPU资源,CPU不断取指令,顺序执行逻辑流程。 hello会处理这个过程中的各种异常。 此外,还有访问内存等行为。 该进程还调用了printf、getchar等函数,这些函数与Linux系统的I/O设备密切相关。
(8)回收:shell父程序等待hello结束并回收,之后内核清除所有hello信息。 如果shell提前结束,init作为hello这一孤儿进程的养父,负责将其回收。
图 7-2 运行时堆栈
回收后,hello就结束了它的一生。
我的体会与感悟:
(1) 写完这篇文章,我深刻体会到计算机的各个层次都是息息相关的。 即使是几十行的小hello程序,也要经过很多复杂的路径,经过几个应用程序的处理,逐步优化,最后生成一个可执行文件。 加载到内存后,与linux系统打交道,从加载、运行到最后被回收,完成hello的生命。
(2)hello一生经历过多次精巧的计算。 无论是内容的转换,还是链接时地址的计算,无一不透露出昔日计算机领域人才的思维火花,让人惊叹其中的奥妙。
(3)经过这一系列的操作和学习,对本学期所学的知识有了更深的理解,对csapp这本书从“程序员的角度”有了更深的理解。 我希望自己能利用本书所学的知识,在以后的学习和生活中进一步运用和创新。
附件
文件名 | 功能 |
hello.i | hello.c经过预处理得到的文本文件 |
hello.s | 编译后得到的汇编文件 |
hello.o | 汇编后得到的可重定向文件 |
hello | 链接得到的可执行文件 |
hello1_elf.txt | 用readelf读hello.o得到的elf文件 |
hello1_dis.txt | 反汇编hello.o得到的文件 |
hello2_elf.txt | hello的elf格式文件 |
hello2_dis.txt | hello的反汇编文件 |
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.