摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息。摘要应包括本论文的目的、主要内容、方法、成果及其理论与实际意义。摘要中不宜使用公式、结构式、图表和非公知公用的符号与术语,不标注引用文献编号,同时避免将摘要写成目录式的内容介绍。
关键词:CSAPP;hello;编译;链接;进程;存储;……
Hello World作为每个程序员编程生涯朴实无华的开始,其背后却暗藏玄机。本篇大作业通过对hello的一生进行探讨,针对程序的编译过程、进程管理、存储管理、IO管理、p2p和o2o过程进行分析探究,进一步深入了解计算机系统。
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 5 -
3.2 在Ubuntu下编译的命令............................................................................. - 6 -
4.2 在Ubuntu下汇编的命令............................................................................. - 7 -
5.2 在Ubuntu下链接的命令............................................................................. - 8 -
5.3 可执行目标文件hello的格式.................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 10 -
6.3 Hello的fork进程创建过程..................................................................... - 10 -
6.6 hello的异常与信号处理............................................................................ - 10 -
7.1 hello的存储器地址空间............................................................................ - 11 -
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.1 Linux的IO设备管理方法.......................................................................... - 13 -
8.2 简述Unix IO接口及其函数....................................................................... - 13 -
第1章 概述
1.1 Hello简介
P2P过程:对hello.c的文本文件进行预处理生成hello.i,然后经过编译生成hello.s,然后经过汇编生成hello.o,最后经过链接生成可执行文件。
O2O过程:shell执行可执行目标文件,对进程进行管理,然后对储存进行管理,映射虚拟内存,分配物理内存,最后将hello的结果输出的显示器上,结束进程,释放内存空间。
1.2 环境与工具
硬件环境:Inter® Core™ i5-9300 CPU;1T SSD
软件环境:Windows10 64位;Ubuntu 20.04.2.0 64位
开发与调试工具:gcc;edb; readelf;objdump;gedit;hexedit;
1.3 中间结果
hello.c——原文件
hello.i——预处理之后文本文件
hello.s——编译之后的汇编文件
hello.o——汇编之后的可重定位目标执行
hello——链接之后的可执行目标文件
hello.elf——hello.o的elf格式,用来看hello.o的各节信息
obj——hello.o的反汇编文件,用来看汇编器翻译后的汇编代码
obj1——hello的反汇编文件,用来看链接器链接后的汇编代码
1.4 本章小结
本章主要简单介绍了hello的P2P,020过程,列出了本次实验基本信息。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。将所引用的所有库展开,处理所有的条件编译,并执行所有的宏定义,得到另一个通常是以.i作为文件扩展名的C程序。
预处理的作用:
1.将c程序中所有#include声明的头文件复制到新的程序中。比如hello.c中的#include 等命令告诉预处理器读取系统头文件stdio.h等的内容,并把它直接插入程序文本中;
2.条件编译。根据条件#if决定是否处理之后的代码;
3.执行宏替换。用实际值替换用#define定义的字符串。
2.2在Ubuntu下预处理的命令
cpp hello.c > hello.i
图2.2 预处理命令
2.3 Hello的预处理结果解析
打开hello.i,原来的helloc.c已经被拓展成了3060行,前面的内容是hello.c的三个#include指令包含的头文件的代码。
再看之前的头文件的处理,以第一条#include指令为例,cpp到默认的环境变量下搜索stdio.h头文件,打开/usr/include/stdio.h,发现其中仍有#include指令,于是再去搜索包含的头文件,直到最后的文件中没有#include指令,并把所有文件中的所有#define和#ifdef指令进行处理,执行宏替换和通过条件确定是否处理定义的指令。
图2.3 预处理结果
2.4 本章小结
本章主要介绍预处理的概念及作用,并结合hello.c处理后的hello.i对处理过程进行分析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
编译的概念:编译器将文本文件hello.i翻译成另一个文本文件hello.s,它包含一个汇编语言程序。
编译的作用:确认所有的指令都符合语法规则之后,将字符串转化成内部的表示结构,即翻译成等价的中间代码表示或汇编代码,便于后续生成机器代码。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1 数据
全局变量一般会在文件节中占相应类型的空间,本程序中不含全局变量,数据基本以局部变量和立即数方式出现。
1.字符串
程序中用到的字符串有:“用法: Hello 学号 姓名 秒数!\n”和“Hello %s %s\n”。编译器一般将字符串存放在.rodata节。如下图,可以看到第一个字符串中的汉字被编码成UTF-8格式,一个汉字占三个字节,每个字节用\分隔。第二个字符串中的两个%s为用户在终端运行hello时输入的两个参数。
2.整数
hello.c中的整型变量有argc和i。
其中argc是从终端传入的参数个数,也是main函数的第一个参数,所以由寄存器%edi进行保存。由图可知,argc又被存入了栈中-20(%rbp)的位置。i则是局部变量,用来控制循环次数的计数器,编译器会将局部变量保存在寄存器或者栈中,由图3.4的30行看出hello.s将i存储在栈中-4(%rbp)的位置。
3.数组
访问argv[]所指向的内容时,每次先获得数组起始地址,如图3.6的第34、37、44行,然后通过加8*i来访问之后的字符指针,再通过获得的字符指针寻找字符串
3.3.2 赋值
赋值操作只有i=0这一条,这条语句在汇编中用mov指令实现,由于int占4个字节,所以以‘l’作为后缀。
3.3.3 类型转换
类型转换只有一处,使用atoi函数将命令行的第三个字符串参数转换成了整型
3.3.4 算术操作
i++,使用addl实现
3.3.5 关系操作
C语言中的关系操作有==、!=、>、<、>=、<=,这些操作在汇编语言中主要依赖于cmp和test指令实现,它们只设置条件码而不改变目的寄存器的值。
在hello.c中有两处用到了关系操作,分别是argc!=4和i<8。这两句在hello.s中被cmp分别处理后设置条件码,为之后的je和jle提供判断依据。
3.3.6 数组/指针/结构操作
在hello.c中通过下标访问argv数组
在hello.s中取argv首地址,通过首地址加i*8字节找到argv[i]的地址,根据其中内容找到对应的字符串。
3.3.7 控制转移
第一处是判断argc是否与4相等,通过je判断条件码ZF位,如果为0,说明argc等于4,代码跳转到.L2。
第二处是判断循环变量i是否满足循环条件i<8。如果满足,跳转到.L4执行循环体,如果不满足,则退出循环。
3.3.8 函数操作
1.main函数
main函数被保存在.text节,程序运行时,由系统启动调用main函数,两个参数分别是由命令行传入的argc和argv[],分别被保存在%edi和%rsi中。
2.printf函数
第一个printf函数由于只有一个参数,所以被编译器优化为puts函数。参数被保存在寄存器%rdi中。第二个printf函数有三个参数,从内存中取出参数之后,第三、二、一个参数分别被保存在寄存器%rdx、%rsi、%rdi中。
3.exit函数
用户输入的不是四个参数时会调用exit函数结束程序
4.atoi函数
hello.c中通过atoi函数把用户输入的第四个参数从字符串转化成整型,对应的汇编代码如图3.18所示,第45行是取得用户输入的第四个参数,第46行把这个参数作为函数atoi的参数保存在%rdi中,然后调用atoi函数。
5.sleep函数
sleep函数的参数是atoi函数的返回值,该返回值被保存在%eax中,所以把%eax中的值传送给%rdi作为sleep函数的参数。
6.getchar函数
由于getchar函数没有参数,所以在退出循环之后直接调用。
3.4 本章小结
本章主要介绍了编译的概念及作用,并且结合hello.i编译生成的hello.s汇编代码分析了数据类型和各类操作在汇编语言中的存在和展示,对编译的过程有了更详尽的认识。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
汇编的概念:汇编器as将.s文件翻译成机器指令,把这些指令打包成可重定位目标程序格式,并将结果保存在目标文件中。
汇编的作用:将汇编语言进一步翻译为计算机可以理解的机器语言,生成.o文件。
4.2 在Ubuntu下汇编的命令
as hello.s -o hello.o
4.3 可重定位目标elf格式
使用readelf -a -W hello.o > hello.elf命令获得hello.o文件elf格式。
1.ELF头
开头的Magic序列描述了生成该文件的系统的字大小和字节顺序。剩下的部分包含帮助连接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。
2.节头
节头部表包含了文件中各个节的含义,包括节的地址、偏移量、大小等信息。如.text节,地址是从0x0开始,偏移量是0x40,大小是0x8e。
3. rela.text节
存放着代码的重定位条目。当链接器将这个目标文件和其他文件组合时,会结合这个节,修改.text节中相应位置的信息。如图中的重定位信息依次对应.L0、puts函数、exit函数、.L1、printf函数、atoi函数、sleep函数、getchar函数。
4. rela.eh_frame节
.eh_frame节的重定位信息。
5. symtab节
符号表,用来存放程序中的定义和引用函数的全局变量的信息。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编
图
与hello.s对比的差别:
(1)分支转移:汇编代码中直接以.L0等助记符表示,反汇编代码中表示为主函数+段内偏移量。
(2)函数调用:汇编代码中直接使用函数名称,反汇编的文件中call之后加main+偏移量。且在.rela.text节中为其添加重定位条目等待链接。
(3)访问全局变量:汇编代码中使用.LC0(%rip),反汇编代码中为0x0(%rip)。访问时需要重定位,所以初始化为0并添加重定位条目。
(4)进制:汇编代码中立即数使用十进制,反汇编代码中使用十六进制
4.5 本章小结
本章介绍了hello程序的汇编阶段,从汇编程序hello.s转化为可重定位目标程序hello.o。查看了hello.o 的ELF格式、对使用objdump 得到的反汇编代码与hello.s 进行比较,了解从汇编语言映射到机器语言需要实现的转换。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
注意:这儿的链接是指从 hello.o 到hello生成过程。
链接的概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。现代系统中链接是由链接器程序自动执行的。
链接的作用:链接器使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-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
图5.2 链接结果
5.3 可执行目标文件hello的格式
使用readelf -a -W hello命令查看hello的elf格式
图5.3 hello的elf
5.4 hello的虚拟地址空间
程序从0x400000开始加载,从上图的程序头中可以读出程序第一个LOAD(代码段)地址为0x400000。此区域包含ELF头、程序头部表。
图5.4.1数据段1
根据节头表查看.interp节地址0x4002e0,成功找到对应数据,即linux动态共享库的路径。
图5.4.2 数据段2
其他节类似
5.5 链接的重定位过程分析
使用命令objdump -d -r hello > obj1 生成hello的反汇编文件。
图5.5反汇编对比
不同点: hello.o的反汇编代码只有.text节,且只有main函数的具体实现,main函数也是从地址0开始的。而在hello的反汇编代码已经完成了重定位,增加了.plt,.init,.fini节,程序中使用的库函数的代码已经复制到程序中,各个节变得更加完整,跳转的地址也具有参考性。
5.6 hello的执行流程
Hello!_init
Hello!puts@plt
Hello!printf@plt
Hello!getchar@plt
Hello!atoi@plt
Hello!exit@plt
Hello!sleep@plt
Hello!.plt+0x70
Hello!.plt.got+0x10
Hello_start
Hello!main
Hello!__libc_csu_init
Hello!__libc_csu_fini
exit
5.7 Hello的动态链接分析
(分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。)
对于动态共享链接库中PIC函数,编译器无法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。
图5.7.1 执行前
调用dl_init后0x404008和0x404010处的两个8字节的数据发生改变,出现了两个地址,这就是GOT[1]和GOT[2]。
图5.7.2 执行后
5.8 本章小结
本章介绍了链接的概念和作用,对链接后生成的可执行文件hello的elf格式文件进行了分析,分析了hello的虚拟地址空间、重定位过程、执行过程的各种处理操作。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:一个执行中的程序的实例,同时也是系统进行资源分配和调度的基本单位。一般情况下,包括文本区域、数据区域和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
进程的作用:它提供一个假象,好像我们的程序独占地使用内存系统,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell-bash是一个C语言程序,它代表用户执行进程,它交互性地解释和执行用户输入的命令,能够通过调用系统级的函数或功能执行程序、建立文件、进行并行操作等。同时它也能够协调程序间的运行冲突,保证程序能够以并行形式高效执行。bash还提供了一个图形化界面,提升交互的速度。
处理流程:
(1) 终端进程读取用户由键盘输入的命令行。
(2) 分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量。
(3) 检查首个命令行参数是否是一个内置的shell命令。
(4) 若不是内部命令,调用fork()创建子进程。
(5) 在子进程中,用步骤2获取的参数,调用execve执行指定程序。
(6) 如果用户未要求后台运行,则shell使用waitpid等待作业终止后返回。
(7) 若要求后台运行,则shell返回。
6.3 Hello的fork进程创建过程
在终端输入:./hello 190110920 mch 1
接下来shell会分析这条命令,由于./hello不是一条内置的命令,于是判断./hello的语义是执行当前目录下的可执行目标文件hello,然后Terminal会调用fork创建一个新的运行的子进程,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着,当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间的区别在于它们拥有不同的PID。
6.4 Hello的execve过程
在fork之后,子进程调用execve函数,execve函数在新创建的子进程的上下文中加载并运行hello程序。execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有发生错误时execve才会返回到调用程序。所以,execve调用一次且从不返回。
加载并运行hello需要以下几个步骤:
删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
映射共享区域。如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
设置程序计数器。设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。
6.5 Hello的进程执行
上下文信息:上下文就是内核重新启动一个被抢占进程所需要的状态。它包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
逻辑控制流:一系列程序计数器PC 的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。
在hello运行过程中,若hello进程不被抢占且没有系统调用,则正常执行;若被抢占或进行了系统调用,那么内核会让当前进程休眠,切换到另一个进程。
进程的切换,是通过上下文切换的机制实现的,(1)保存当前进程的上下文;(2)恢复某个先前被抢占的进程的上下文(3)将控制转移给这个新恢复的程序。如图6.5-1,为进程上下文切换的示意图。
这里有个特殊的情况,当hello执行到系统调用函数sleep()的时候,它显式地请求让调用进程休眠。一般而言,即使系统调用没有阻塞,内核也可以决定执行上下文切换,而不是将控制返回给调用进程。
6.6 hello的异常与信号处理
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
1.hello执行过程中可能出现四类异常:中断、陷阱、故障和终止。
(1)中断是来自I/O设备的信号,异步发生,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码,就像没有发生过中断。
(2)陷阱是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。
(3)故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。
(4)终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。
2. hello对各种信号的处理
(1)正常运行
图6.6.1 正常
(2)不停乱按。除了第一次按回车之前的输入会被getchar()正常取走,剩余的内容保存在缓冲区,等进程结束后作为命令行的内容输入
图6.6.2 乱按
(3)ctrl+c。向进程发送SIGINT信号。信号处理程序终止并回收进程。
图6.6.3 ctrl+c
(4)ctrl+z。shell进程收到SIGSTP信号,信号处理函数的逻辑是打印屏幕回显、将hello进程挂起,通过ps命令我们可以看出hello进程没有被回收,其进程号时7308,用jobs命令看到job ID是1,状态是Stopped
图6.6.4 ctrl+z
输入pstree命令查看pstree:
图6.6.5 查看pstree
使用fg命令将其调到前台,此时shell程序首先打印hello的命令行命令,然后继续运行打印剩下的信息,之后再按下Ctrl-Z,将进程挂起。输入kill -9 16376发送SIGINT信号终止hello进程,再用jobs和ps命令查看发现hello进程已经被终止
图6.6.6 终止进程
6.7本章小结
本章介绍了进程的概念与作用,阐述了shell处理流程以及hello的fork进程创建和execve的过程,最后分析了hello的执行过程和过程中出现的异常的处理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
(1) 逻辑地址:由段选择符+偏移地址构成。其中段选择符位于段寄存器(16位,CS、SS等)中。而偏移地址即为汇编、c代码中显示的地址。常见段寄存器有CS(代码段)、DS(数据段)、SS(栈)等,不和绝对物理地址相关。
(2) 线性地址:是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。
(3) 虚拟地址:本书中与线性地址相同。
(4) 物理地址:实际物理内存对应地址。 CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
实模式下: 逻辑地址CS:EA =物理地址CS*16+EA
保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址, 段地址+偏移地址=线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
1.基本原理
将程序的逻辑地址空间划分为固定大小的页,而物理内存划分为同样大小的页框。程序加载时,可将任意一页放入内存中任意一个页框,这些页框不必连续,从而实现了离散分配。该方法需要CPU的硬件支持,来实现逻辑地址和物理地址之间的映射。在页式存储管理方式中地址结构由两部构成,前一部分是VPN(虚拟页号),后一部分是VPO(虚拟页偏移量)。
图7.3.1
优点:没有外碎片;一个程序不必连续存放;便于改变程序占用空间的大小(主要指随着程序运行,动态生成的数据增多,所要求的地址空间相应增长)。
缺点:要求程序全部装入内存,没有足够的内存,程序就不能执行。
2.页式管理的数据结构
在页式系统中进程建立时,操作系统为进程中所有的页分配页框。当进程撤销时收回所有分配给它的页框。在程序的运行期间,如果允许进程动态地申请空间,操作系统还要为进程申请的空间分配物理页框。操作系统为了完成这些功能,必须记录系统内存中实际的页框使用情况。还要在进程切换时,正确地切换两个不同的进程地址空间到物理内存空间的映射。这就要求操作系统要记录每个进程页表的相关信息。
页表:页表将虚拟内存映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。页表是一个页表条目(PTE)的数组。虚拟地址空间的每个页在页表中一个固定偏移量处都有一个PTE。假设每个PTE是由一个有效位和一个n位地址字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置,这个物理页中缓存了该虚拟页。如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配。否则,这个地址就指向该虚拟页在磁盘上的起始位置。
图7.3.2 页表结构
3.页式管理地址变换
MMU利用VPN来选择适当的PTE,将列表条目中PPN和虚拟地址中的VPO串联起来,就得到相应的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU生成一个虚拟地址,MMU就必须查阅一个PTE,为提升速度,在MMU中增加了一个PTE的缓存——TLB(翻译后备缓冲器)。
使用TLB进行地址翻译时,会将VPN解释为两部分:TLB标记,与TLB索引。
由TLBI,访问TLB中的某一组。遍历该组中的所有行,若找到一行的标记等于TLBT,且有效位valid为1,,则缓存命中,该行存储的即为PPN;若未找到一行的tag等于TLBT,或找到但该行的valid为0,则缓存不命中。进而需要到页表中找到被请求的块,用以替换原TLB表项中的数据。
四级页表缓存不命中后,VPN被解释成从低位到高位的4段,从高地址开始,第一段VPN作为第一级页表的索引,用以确定第二级页表的基址;第二段VPN作为第二级页表的索引,用以确定第三级页表的基址;第三段VPN作为第三级页表的索引,用以确定第四级页表的基址;第四段VPN作为第四级页表的索引,若该位置的有效位为1,则第四级页表中的该表项存储的是所需要的PPN的值。
在上述过程中,只要有一级页表条目的有效位为0,下一级页表就不存在,也就是产生缺页故障了,需要到内存中加载。从页表中取出的PPN加上与VPO相同的PPO就构成了物理地址PA。
图7.4 k级页表地址翻译
7.5 三级Cache支持下的物理内存访问
经过前面的过程,hello的物理地址已经得知,现在需要访问该物理地址。在现代计算机中,存储器被组织成层次结构,因为这样可以最大程度地平衡访存时间和存储器成本。所以在CPU在访存时并不是直接访问内存,而是访问内存之前的三级cache。已知Core i7的三级cache是物理寻址的,块大小为64字节。LI和L2是8路组相联的,而L3是16路组相联的。
得到了52位物理地址,接下来CPU把地址发送给L1,因为L1块大小为64字节,所以B=64,b=6。又L1是8路组相联的,所以S=8,s=3。标记位t有52-6-3=43位,即是得到的52位物理地址的前43位。首先,根据物理地址的s位组索引索引到L1 cache中的某个组,然后在该组中查找是否有某一行的标记等于物理地址的标记并且该行的有效位为1,若有,则说明命中,从这一行对应物理地址b位块偏移的位置取出一个字节,若不满足上面的条件,则说明不命中,需要继续访问下一级cache,访问的原理与L1相同,若是三级cache都没有要访问的数据,则需要访问内存,从内存中取出数据并放入cache。
图7.5 有关cache的内存访问
7.6 hello进程fork时的内存映射
fork函数被shell进程调用时,内核为新进程创建各种数据结构,并分配给他一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello 中的程序,用hello 程序有效地替代了当前程序。加载并运行hello 需要以下几个步骤:
(1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
(2)映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中,栈和堆地址也是请求二进制零的,初始长度为零。
(3)映射共享区域,hello 程序与共享对象libc.so 链接,libc.so 是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
首先,先判断这个缺页的虚拟地址是否合法,那么遍历所有的合法区域结构,如果这个虚拟地址对所有的区域结构都无法匹配,那么就返回一个段错误。接着查看这个地址的权限,判断一下进程是否有读写改这个地址的权限。
若以上都不是,那就是正常的缺页故障:当指令引用一个虚拟地址,而与该地址相对应的物理页面不在内存中,必须从磁盘中取时,就会发生缺页故障。
缺页中断处理:当发生缺页故障时,控制转移给处理程序,处理程序从磁盘加载适当的页面,然后将控制转移给引起故障的指令。接着指令再次执行,相应的物理页面已经驻留在内存中,指令就可以没有故障地运行完成了。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格,都要求显式分配块,不同之处在与怎样释放块。
1.显示分配器:要求显式地释放任何已分配的块。比如C标准库的malloc
2.隐式分配器:也叫垃圾收集器,要求分配器检测一个已分配块何时不再被程序使用再释放块。
动态内存分配的方法:
1. 隐式空闲链表:
图7.9.1 隐式
(1)找到一个空闲块
(2)分割空闲块:若找到一个空闲块,当分配块比空闲块小时,将空闲块分为两部分,剩余部分重新作为一个空闲块。
(3)获取额外的堆内存:当找不到合适的空闲块时,需要合并空闲块。若空闲块已经最大程度的合并了,分配器就需要调用sbrk函数,向内存申请额外的堆内存。
(4)合并空闲块:将相邻的空闲块合并。分为立即合并和推迟合并两种。
图7.9.1 合并
2.显式空闲链表
图7.9.2 显式
只记录空闲块链表, 而不是所有块
维护链表的顺序有:后进先出(LIFO),将新释放的块放置在链表的开始处,使用LIFO 的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比IFO 排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。
3.分离的空闲链表
图7.9.3 分离
7.10本章小结
本章主要介绍了hello的存储地址空间、intel的段式管理、hello的页式管理,以及TLB与四级页表支持下的VA到PA的变换过程和三级Cache支持下的物理内存访问。还阐述了hello进程fork和execve时的内存映射、缺页故障的处理流程和动态存储分配器的管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的I/O设备都被模型化为文件,而所有的输入和输出都被当作相应文件的读和写来完成,这种将设备优雅的映射为文件的方式,允许Linux内核引出一个简单的,低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix I/O 接口的几种操作:
(1)打开文件:程序要求内核打开文件,内核返回一个小的非负整数(描述符),用于标识这个文件。程序在只要记录这个描述符便能记录打开文件的所有信息。
(2)shell在进程的开始为其打开三个文件:标准输入、标准输出和标准错误。
(3)改变当前文件的位置:对于每个打开的文件,内核保存着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作显式地设置文件的当前位置为k。
(4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会出发一个称为EOF的条件,应用程序能检测到这个条件,在文件结尾处并没有明确的EOF符号。
(5)关闭文件:内核释放打开文件时创建的数据结构以及占用的内存资源,并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
UnixI/O函数:
(1)intopen(char* filename, int flags, mode_t mode);open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
(2)intclose(int fd);关闭一个打开的文件,返回操作结果。
(3)ssize_t read(int fd, void *buf, size_t n);read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
(4)ssize_t write(int fd, const void *buf,size_t);write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
8.3 printf的实现分析
图8.3 printf函数
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。
8.5本章小结
本章介绍了Linux中I/O设备的管理方法,UnixI/O接口和函数,并且具体分析了printf和getchar函数
(第8章1分)
结论
经过整个大作业的漫长征途,hello程序终于完成了它波澜壮阔而又精彩绝伦的一生。总结如下:
(1)编写:将C语言代码键入hello.c
(2)预处理:通过预处理器cpp,将调用的外部库展开合并得到hello.i
(3)编译:编译器gcc将得到的hello.i编译成汇编文件hello.s
(4)汇编:汇编器as又将hello.s翻译成机器语言指令得到可重定位目标文件hello.o
(5)链接:链接器ld将hello.o与动态链接库链接生成可执行目标文件hello
(6)运行:在shell中输入./hello 190110920 麦昌瀚 1
(7)创建子进程:shell进程调用fork为其创建子进程
(8)加载:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。
(9)执行:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流
(10)访问内存:当CPU访问hello时,请求一个虚拟地址,MMU把虚拟地址转换成物理地址并通过三级cache访存。
(11)动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
(12)信号:shell的信号处理函数可以接受程序的异常和用户的请求
(13)终止:执行完成后父进程回收子进程,内核删除为该进程创建的数据结构至此,hello运行结束
感悟:一个简单的hello.c也需要涉及计算机系统的方方面面,并且每一步凝聚了几代计算机设计者的心血,在有限的硬件水平下把程序的时空性能都做到了近乎完美的利用。在做本次大作业的过程中回顾了整个计算机系统的知识体系,对整个程序的运行过程有了一个新的认识,受益匪浅。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.c——原文件
hello.i——预处理之后文本文件
hello.s——编译之后的汇编文件
hello.o——汇编之后的可重定位目标执行
hello——链接之后的可执行目标文件
hello.elf——hello.o的elf格式,用来看hello.o的各节信息
obj——hello.o的反汇编文件,用来看汇编器翻译后的汇编代码
obj1——hello的反汇编文件,用来看链接器链接后的汇编代码
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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.
[7] [美]大卫R.奥哈拉伦,兰德尔·E.布莱恩特.深入理解计算机系统[M].龚奕利,贺莲译.北京:机械工业出版社,2016.7.
(参考文献0分,缺失 -1分)