计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 120L021727
班 级 2003012
学 生 陈健坤
指 导 教 师 郑贵滨
计算机科学与技术学院
2021年5月
看似简单的一个hello程序的运行过程却十分复杂,包含了相当多的计算机系统底层知识。本篇报告通过在Ubuntu操作系统针对程序的编译过程、进程管理、存储管理、IO管理的探究,完整展现了hello的一生,也更深入地了解计算机系统。
关键词:计算机系统;Linux;程序的运行;进程管理;存储管理;IO管理……;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
P2P:即From Program to Process。用户用高级语言编写得到hello.c文件。但计算机不能直接执行hello.c。hello.c先通过一系列步骤:编译器预处理(cpp)得到hello.i,编译(cc1)得到hello.s,汇编(as)得到hello.o,链接(ld)生成可执行目标程序hello。接着执行此文件时,shell会先fork出一个子进程,然后execve函数在当前进程的上下文中加载并运行可执行目标文件。这就是P2P的整个过程。
020:即From Zero-0 to Zero-0。shell调用execve加载运行程序时,会将其映射到虚拟内存中,再分配物理地址并载入其中,接着执行主函数,输出结果到显示器上。最后hello运行结束后,shell回收子进程。
整个过程是计算机系统(硬件和操作系统)共同协作完成的,
1.2 环境与工具
硬件环境
软件环境
Windows10家庭中文版 64位
Ubuntu
开发与调试工具:
code blocks;gcc;readelf;edb;objdump;
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c:源文件
hello.i:编译器预处理(cpp)后得到的文本文件
hello.s:编译(cc1)后得到的汇编语言程序
hello.o:汇编(as)得到的可重定位目标程序
hello:链接(ld)生成的可执行目标程序
Hello.elf:elf格式 作用:查看hello.o信息
Obj:hello.o的反汇编文件 作用:查看汇编器翻译后的汇编代码
Obj2:hello的反汇编文件 作用:查看链接器链接后汇编代码
1.4 本章小结
本章简要地介绍了hello的生命历程,列出硬件设备,软件环境,生成的中间结果文件名。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理器(cpp)根据以字符#开头的命令,修改原始的c程序。比如hello.c中第一行的#include<stido.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个c程序,通常是以.i作为文件拓展名。
预处理的作用:处理#include,读取头文件插入。
展开所有的宏定义,即用实际值替换用#define定义的字符串。
删除所有注释。
2.2在Ubuntu下预处理的命令
cpp hello.c > hello.i
2.3 Hello的预处理结果解析
预处理后可以看到,生成了hello.i。Hello.i共3060行。原本的3条include已经不见了,说明预处理器已经将所要包含的库文件包含了进来。cpp到默认的环境变量下搜索stdio.h头文件,打开/usr/include/stdio.h,发现其中仍有#include指令,于是再去搜索包含的头文件,直到最后的文件中没有#include指令,并把所有文件中的所有#define和#ifdef指令进行处理,执行宏替换和通过条件确定是否处理定义的指令。
2.4 本章小结
本章主要介绍预处理的概念及作用,解析预处理后的hello.i。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。
编译的作用:进行词法分析,语法分析,生成目标代码,检查无误后生成汇编语言。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1数据
1.全局变量
全局变量(global):main
Type main,@function:main的类型为函数
2.局部变量
局部变量:i
类型:整形
可以看到函数首先压栈(%rbq),24行比较argc长度与4的关系后跳转 到L2,看L2:此时给i赋值。即编译器将局部变量i存放在栈的-4(%rbq) 中,占用4个字节的栈空间。
3.字符串
程序中的字符串有:“用法: Hello 学号 姓名 秒数!\n”和“Hello %s %s\n”。 编译器一般将字符串存放在.rodata节。两个字符串都是printf()函数参数。如下 图,可以看到第一个字符串中的汉字被编码成UTF-8格式,一个汉字占三个 字节,每个字节用\分隔。第二个字符串中的两个%s为用户在终端运行hello 时输入的两个参数。
4.整数
hello.c中的整型变量有argc和i。
argc是传入参数个数,也是main函数的第一个参数,所以由寄存器%edi 保存。观察22行,argc被存入了栈中-20(%rbp)的位置。
i上文已分析过。
3.3.2数组
char *argv[]是一个数组,数组的元素是指针,指针指向(地址)包含的值 是字符型数据,存放着用户在shell中输入的命令行数据,char *argv[]为函数 第二个参数,用寄存器%rsi传递。
访问argv[]所指向的内容时,看下图第34、37、44行,每次先获得数组 起始地址,每次通过加8*i访问之后的字符指针,再通过获得的字符指针获得 字符串。
3.3.3 赋值
i = 0,用mov指令实现
3.3.4关系操作
有argc的比较,循环变量i的比较。作用都是得到条件码供之后的条件判 断,选择跳转。
3.3.5算数操作
i++的算数操作,由add指令实现
3.3.6控制转移
当argc不等于4时,跳转到L2
当i<8时,跳转到L4,若大于等于8则退出循环
3.3.7类型转换
通过atoi函数把用户输入的第四个参数从字符串转化成整型,对应的汇编 代码如下图所示,第46行是取得用户输入的第四个参数,第47行把这个 参数作为函数atoi的参数保存在%rdi中,然后调用atoi函数。
3.3.8.函数操作
1.函数
main函数,atoi函数,sleep函数,printf函数,getchar函数,exit函数
main函数,上文已说明
atoi函数,上文已说明
sleep函数,参数是atoi函数的返回值,该返回值被保存在%eax中,所以 把%eax中的值传送给%rdi作为sleep函数的参数。
getchar函数,无参数,退出循环后调用
exit函数,argc!=4,即用户输入参数个数不为4时,调用exit函数结束 程序
2.函数返回
函数返回时,若有返回值,则先将返回值放到%rax中,再使用leave和ret 返回,leave是恢复所用栈空间,相当于push %rsp,%rbp,pop %rbp。ret 相当 于pop %rip,将%rip设置为函数调用结束后的第一条语句。
3.4 本章小结
本章主要介绍了编译器编译的概念和作用,以及通过分析hello.s代码详细说明编译器是怎么处理C语言的各个数据类型以及各类操作的。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编器(as)将.s文件翻译成机器语言指令,把这些指令打包成可重定位目标程序,并将结果保存在目标文件.o文件中。
汇编的作用:将汇编语言进一步翻译为计算机可以理解的机器语言,生成.o文件。.o文件是一个二进制文件。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
1.ELF
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。剩下的部分包含帮助链接器分析语法和解释目标文件的信息,其中包含ELF头大小、目标文件的类型、机器类型、字节头部表的文件偏移。
2.节头部表:
描述了不同节的位置和大小,具体的描述包括节的名称、类型、地址和偏移量等。
3.rela.text节
一个.text节中位置的列表,包含.text节中需要重定位的信息,需要使用链接器在组合时将这些位置链接。hello.o中的getchar,exit等的重定位信息。包含:
Offset:需要重定向文件在.text或者.data中的偏移量。
Info:包含symbol和type两部分,其中symbol占前4个字节,type占后4个字节,symbol代表重定位目标在symtab中的偏移量,type代表重定位的类型。
Type:重定向的目标类型。
Sym.name:重定向到目标名称。
Addend:重定向位置的辅助信息。
4.4 Hello.o的结果解析
objdump -d -r hello.o
与hello.s对比的差别:
(1)分支转移部分:hello.s中对跳转指令使用.Lx等助记符,反汇编代码中使用跳转地址,即主函数+段内偏移量。
(2)函数调用:hello.s中直接使用函数名称,反汇编的文件中call之后加main+偏移量。且在.rela.text节中为其添加重定位条目等待链接。
(3)汇编中mov、push、sub等指令都有表示操作数大小的后缀,比如l\q等,反汇编得到的代码中则不全有。
(4)立即数:hello.s中的立即数是十进制的,而hello.o的反汇编中则是十六进制的。
(5)汇编代码中有很多“.”开头的伪指令用来指导汇编器和链接器工作,而反汇编得到的代码中则没有。
(6)对全局变量的访问,在汇编代码中是通过段名称+%rp,而在反汇编代码中是通过%rip+0,此处的0代表占位符,在链接时需要重定位。
4.5 本章小结
本章介绍了汇编的概念与作用,分析了hello.s到hello.o的过程。用readelf等列出其各节的基本信息。并将hello.o的反汇编代码与hello.s对比,说明机器语言的构成,与汇编语言的映射关系。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接的概念:链接是指将可重定位目标文件经符号解析和重定位步骤合并成可执行目标文件,这个文件可被加载到内存并执行。
链接的作用:链接器使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
在ELF格式中,节头部表(Section Headers)对hello的各个节的信息做了说明,包括各段的起始地址(Address),大小(size),类型(Type),偏移(Offset)对齐要求(Align)等信息
Type一行已经是EXEC(可执行文件)形式。
5.4 hello的虚拟地址空间
程序从0x400000开始加载,从上图的程序头中可以读出程序第一个LOAD(代码段)地址为0x400000。此区域包含ELF头、程序头部表。
根据节头表查看.interp节地址0x4002e0,成功找到对应数据,即linux动态共享库的路径。
使用edb加载hello
5.5 链接的重定位过程分析
hello与hello.o的不同:
链接器在链接可执行文件或动态库的过程中,它会把来自不同可重定位对象文件中的相同名称的 section 合并起来构成同名的 section,在这里因为链接的时候指定了/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o,所以将会把这些.o文件的每个节与hello.o的节合并。在合并的过程中,会根据重定位信息对相应的地方进行重定位,比如hello.o中的.text节和.data节,接着,它又会把带有相同属性(比方都是只读并可加载的)的 section 都合并成所谓 segments(段)。segments 作为链接器的输出,常被称为输出section。所以说hello.o只是hello的一部分。通过将两者的反汇编代码进行比对也能看出这一点,hello的反汇编代码中也比hello.o的反汇编代码节要多:.init .plt .text .fini,在这些节中多出了一些main函数运行必要的函数:_start,_init,__libc_csu_init,__libc_csu_fini,__libc_start_main,和在hello中调用的函数:printf、sleep、getchar、exit函数。
链接的过程:
1.符号解析
目标文件(.o)定义和引用了符号;
每个符号对应着一个函数、一个全局变量或一个静态变量等;
其中函数、已初始化的全局变量或者静态变量是强符号,未初始化的全局变量或者静态变量是弱符号;
其中有三条链接时的规则:1.不允许由多个同名的强符号;2.如果有一个强符号和多个弱符号同名,那么选择强符号;3.如果由多个弱符号同名,那么从这些弱符号中任意选择一个;
符号解析的作用就是给每个符号引用分配一个精确的符号定义;
2重定位
编译器和汇编器生成的代码段和数据段的开始地址都是0;
链接器给每个符号定义分配一个内存地址,然后修改所有对这些符号的引用使得这些引用指向的是前面分配的内存地址;
链接器使用由汇编器生成的详细的指令(重定位条目)来执行这些重定位操作;
5.6 hello的执行流程
使用edb执行hello,从加载hello到_start,到call main,以及程序终止的主要过程如下:(左边是函数名,右边是对应地址)
ld-2.27.so!_dl_start 0x7f96ed2e1ea0
ld-2.27.so!_dl_init 0x7f96ed2f0630
hello!_start 0x400500
libc-2.27.so!__libc_start_main 0x7fbdf0cccab0
hello!puts@plt 0x4004b0
hello!exit@plt 0x4004e0
hello!printf@plt 0x4004c0
hello!sleep@plt 0x4004f0
hello!getchar@plt 0x4004d0
libc-2.27.so!exit 0x7f66b7b9e120
5.7 Hello的动态链接分析
动态链接:假设程序调用一个有共享库定义的函数。编译器无法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法时为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU通过一种叫做延迟绑定的技术来将地址的绑定推迟到第一次调用该过程。
5.8 本章小结
本章介绍了链接的概念和作用,分析了链接后生成的可执行文件hello的elf文件,hello的虚拟地址空间、重定位过程、执行过程的各种处理操作,展现了链接对于模块化编程的帮助,也帮助程序员更好地使用库函数。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程是一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程的作用:一个独立的逻辑控制流,为程序提供了一个假象:好像我们的程序独占地使用处理器;一个私有的地址空间,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的作用:shell-bash是一个交互型的应用级程序,它代表用户运行其他程序。能够执行一系列的读/求值(read/evaluate)步骤,然后终止。
处理流程:
(1) 终端进程读取用户由键盘输入的命令行。
(2) 分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量。
(3) 检查首个命令行参数是否是一个内置的shell命令。
(4) 若不是内部命令,调用fork()创建子进程。
(5) 在子进程中,用步骤2获取的参数,调用execve执行指定程序。
(6) 如果用户未要求后台运行(命令末尾没有&号) 等待作业终止后返回。
(7) 若要求后台运行,则shell返回。
6.3 Hello的fork进程创建过程
首先在终端输入./hello 120L0212727 cjk 1 在shell中键入命令时,判断第一个参数./hello,不是shell内置的命令,于是shell使用fork创建子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同(但独立)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。父进程和子进程是并发运行的独立进程,而内核能够以任何方式交替执行它们逻辑控制流中的指令。
6.4 Hello的execve过程
在fork之后,子进程调用execve函数,execve函数加载并运行可执行目标文件hello,并且带参数列表argv和环境变量列表envp。只有发生错误时execve才会返回到调用程序。所以,execve调用一次且从不返回。加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序。这个将程序复制到内存并运行的过程叫做加载。每个程序都有一个运行时内存映像。在程序头部表的引导下,加载器将可执行文件的片(chunk)复制到代码段和数据段。接下来,加载器跳转到程序的入口点,也就是_start函数的地址。_start函数调用系统启动函数_ _libc_start_main,该函数定义在libc.so中,它初始化执行环境,调用用户层的main函数,处理main函数的返回值,并且在需要的时候把控制返回给内核。
6.5 Hello的进程执行
上下文信息:内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。
上下文切换:内核为每个进程维持一个上下文,上下文就是在进程执行的某些时刻,内核可以决定枪战当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度。系统调用和中断也可能引发上下文切换。
逻辑控制流:即使在系统中通常有许多其他程序正在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。如果使用调试器单步调试执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,简称逻辑流。
并发流:系统为每个程序都提供了一种只有它一个程序在运行的假象,但是实际情况却不是这样的,系统中很有很多其他程序在运行,比如我现在打字的word和我的虚拟机就是两个程序,它们都在运行。那么处理器是如何执行它们的,以至于让它们看起来都在不间断的一直运行呢?答案就是并发,处理器分时间段执行进程A、B、C,这个转换的时间非常短,所以看起来就好像每个进程都在持续不断的在运行。多个逻辑控制流并发执行的一半现象被称为并发。一个进程和其他进程轮流运行的概念成为多任务,一个进程执行它的控制流的每一时间段就成为时间片。
内核模式和用户模式:内核模式和用户模式不是两个进程,而是一个进程的不同模式,由一个模式位来控制,当设置了模式位时,进程就运行在内核模式中,这时候这个进程就可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,反之,用户程序必须通过系统调用接口间接地访问内核代码和数据。运行程序代码的进程一开始是处于用户模式,只有当发生中断、故障或者陷入系统调用这样的异常时,转而去执行异常处理程序,这时进程才会变为内核模式。当它返回到应用程序代码时,处理器就把模式从内核模式改为用户模式。
切换进程:如下图(1)保存当前进程的上下文;(2)恢复某个先前被抢占的进程的上下文(3)将控制转移给这个新恢复的程序。
6.6 hello的异常与信号处理
hello执行过程中可能出现四类异常:中断、陷阱、故障和终止。
(1)中断是来自I/O设备的信号,异步发生,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码,就像没有发生过中断。
(2)陷阱是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。
(3)故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。
(4)终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。
1.输入./hello 120L021727 cjk 1,中途正常执行。
屏幕每隔一秒输出一条内容:Hello 120L021727 cjk,依次输出8条。输出完毕后等待用户继续输入。如输入ok后hello进程结束发送SIGCHLD信号,然后被shell回收掉。
2.运行中乱按
运行中乱按的字符,会被存入缓冲区,待正常输出8条hello 120L021727 cjk,进程结束后,这些字符会作为命令行的内容输入。
3.按ctrl-c
进程终止并回收
4.按ctrl-z
在hello运行过程中按下ctrl+z将会发送一个SIGTSTP信号给shell,然后shell将转发给当前执行的前台进程组,使hello进程挂起。jobs命令显示系统中的任务列表及其运行状态。ps命令显示当前进程的状态。使用ps命令查看,可以看到hello进程仍存在。另外使用pstree可以看到,在终端terminal下是bash进程,hello和pstree都是bash的子进程。
再使用fg命令使其继续运行。Hello进程继续输出字符串,在输出过程中使用ctrl+z使其停止,然后使用kill命令发送SIGKILL函数给hello进程。之前ps命令可以看出hello进程PID为47564。输入kill -9 47564。hello进程接收到SIGKILL信号后,当内核重新调度hello进程,hello进程从内核模式转为用户模式之前,先会检查hello的未被阻塞的待处理信号的集合,然后执行相应的信号处理程序,此时hello程序将会终止。进而发送SIGCHLD信号被shell回收。输入ps和jobs可以看到进程终止并被回收。
6.7本章小结
本章介绍了进程的概念与作用,简述了壳(shell)的作用与处理流程、通过fork和execve执行hello进程、内核对hello进程的调度,通过进程中输入各种指令展现了hello进程执行期间可能遇到的异常情况及其响应。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
1) 逻辑地址:是指由程序产生的与段相关的偏移地址部分。由段选择符+偏移地址构成。其中段选择符位于段寄存器(16位,CS、SS等)中。而偏移地址即为汇编、c代码中显示的地址。例如,C语言中可以读取指针变量本身值(&操作),这个值就是逻辑地址,是相对于你当前进程数据段的地址。
(2) 线性地址:是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。
(3) 虚拟地址:即线性地址。
(4) 物理地址:实际物理内存对应地址。加载到内存地址寄存器中的地址,内存单元的真正地址。在前端总线上传输的内存地址都是物理内存地址,编号从0开始一直到可用物理内存的最高端。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段选择符+偏移地址构成。
从左开始,13位是索引(或者称为段号),通过这个索引,可以定位到段描述符,而段描述符是可以真正记载了有关一个段的位置和大小信息, 以及访问控制的状态信息。
实模式下: 逻辑地址CS:EA =物理地址CS*16+EA
保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址, 段地址+偏移地址=线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
VM系统将虚拟内存分隔为称为虚拟页的大小固定的块。这个虚拟地址在访问主存前必须先转换成适当的物理地址。CPU芯片上,用内存管理单元(MMU)来实现虚拟地址和物理地址的映射。然后通过这个物理地址来访问物理内存。在页式存储管理方式中地址结构由两部分构成,前一部分是VPN(虚拟页号)(p位),后一部分是VPO(虚拟页偏移量)(n-p位)。
页表结构:在物理内存中存放着一个叫做页表的数据结构,页表将虚拟页映射到物理页,每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。
页表就是一个页表条目(PTE)数组,虚拟地址空间中的每个页在页表中的一个固定偏移量处都有一个PTE。PTE是由一个有效位和一个n个字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB缓存了页表条目PTE,MMU先查询TLB,若是含有所需的PTE,则直接命中,否则MMU再从高速缓存中查找PTE。使用TLB进行地址翻译时,会将VPN解释为两部分:TLB标记,与TLB索引。
由TLBI,访问TLB中的某一组。搜索组中每一行,若找到某一行的标记为TLBT,且有效位valid为1,,则缓存命中,该行存储的即为PPN;若未找到一行的tag等于TLBT,或找到但该行的valid为0,则缓存不命中。则需要到页表中找到被请求的块,用以替换原TLB表项中的数据。
四级页表缓存不命中后,VPN被解释成从低位到高位的4段,从高地址开始,第一段VPN作为第一级页表的索引,用以确定第二级页表的基址;以此类推直到第四级页表。第四段VPN作为第四级页表的索引,若该位置的有效位为1,则第四级页表中的该表项存储的是所需要的PPN的值。
在上述过程中,只要有一级页表条目的有效位为0,下一级页表就不存在,也就是产生缺页故障了,需要到内存中加载。从页表中取出的PPN加上与VPO相同的PPO就构成了物理地址PA。
7.5 三级Cache支持下的物理内存访问
存储器是层次结构,cpu访问内存时候最先访问三级cache。Core i7的三级cache是物理寻址的,块大小为64B。LI和L2是8路组相联的,L3是16路组相联的。
首先我们根据组号在L1cache中找到对应的组,然后挨个比较标志位,如果标志位对应且有效位为1,则说明命中,然后根据CO偏移量得到我们想要取的数据。如果未命中,则依次到L2cache、L3cache、主存中去找。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork从新进程返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的概念。
7.7 hello进程execve时的内存映射
加载hello并执行需要以下几个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构(即mmap指向的vm_area_structs)。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。bss区域时请求二进制0的,映射到匿名文件,其大小包括在hello中。栈和堆区域也是请求二进制0的,初始长度为0.
3.映射共享区域。如果hello程序域共享对象链接,比如C标准库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
(以下格式自行编排,编辑时删除)
若MMU在翻译一个虚拟地址A时,触发了一个缺页故障。这个缺页将导致控制转移到内核的缺页处理程序。缺页处理程序会执行以下的步骤:
1.检查虚拟地址A是否合法。搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止进程。
2.判断试图进行的内存访问是否合法。如果试图进行的访问时不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。
3.若以上两点都通过,则内核知道了这个缺页是由于对合法的虚拟地址进行合法的操作造成的。于是:选择一个牺牲页面,如果这个页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU,这次,MMU就能正常地翻译A而不会再产生缺页中断了。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护着一个变量brk,指向堆的顶部。
分配器有两种风格,但是这两种风格都要求应用显示的分配块。
其中显示分配器要求显示释放任何已分配的块,如malloc、new等。
隐式分配器要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。所以也叫垃圾收集器。
显示分配器有以下几点要求:能够处理任意请求序列;立即相应请求;只是用堆;对齐开;不修改已分配的块。
隐式空闲链表:块大小(最低位保存a/f)有效载荷 (只包括已分配的块)
填充(可选,为了保持双字对齐)块大小(最低位保存a/f)
显式空闲链表:只记录空闲块链表, 而不是所有块
7.10本章小结
本章介绍了虚拟内存的实现。主要介绍了hello的存储地址空间、intel的段式管理、hello的页式管理,以及TLB与四级页表支持下的VA到PA的变换过程和三级Cache支持下的物理内存访问,hello进程fork和execve时的内存映射、缺页故障的处理流程和动态存储分配器的管理。即主要过程为:cpu取数据先按照虚拟地址去找到对应的物理地址。这个过程先从TLB中找,若未命中则到页表中找。找到对应的物理地址后,根据这个地址先从三级Cache寻找对应数据,若未命中则到磁盘上找。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有的IO设备(例如网络、磁盘、终端)都被模型化为文件。每个linux文件都有一个类型来表明他在系统中的角色:
普通文件:包含任意数据。
目录:包含一组链接的文件,其中每个链接都将一个文件名映射到一个文件,这个文件可能是另一个目录。每个目录至少含有两个条目:“.”是到该目录自身的链接,以及“…”是到目录层次结构中父目录的链接。
套接字:用来与另一个进程进行跨网络通信的文件。
8.2 简述Unix IO接口及其函数
Unix I/O接口:
· 打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用陈谷只需记住这个描述符。
· Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。
· 改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始化为0.这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
· 读写文件:一个读操作就是从文件赋值n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会触发一个称为EOF的条件,应用程序能检测到这个条件。写操作类似
· 关闭文件:当应用程序完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。
IO函数
int open(char * filename, int flags, mode_t mode);
若打开文件成功则返回文件描述符,否则返回-1。
flags有多种O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(可读可写)、O_CREAT(如果文件不存在,就创建它的一个截断的空文件)、O_TRUNC(如果文件已经存在,就截断它)、O_APPEND(在每次写操作前,设置文件位置到文件的结尾处)。
mode参数指定访问权限位。
int close(int fd);
会关闭一个打开的文件,如果关闭一个已关闭的文件描述符会出错。
ssize_t read(int fd, void *buf, size_t n);
若成功则返回读的字节数,若EOF则为0,若出错则为-1。
ssize_t write(int fd, const void *buf, size_t n);
若成功则返回为写的字符数,若出错则返回-1。
read函数从描述符为fd的当前位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。
write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。可以通过调用lseek函数,显示地修改当前文件的位置。
8.3 printf的实现分析
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点。
8.4 getchar的实现分析
getchar()是stdio.h中的库函数,它的作用是从stdin流中读入一个字符,也就是说,如果stdin有数据的话不用输入它就可以直接读取了,第一次调用getchar()时,确实需要人工的输入,但是如果你输了多个字符,以后的getchar()再执行时就会直接从缓冲区中读取了。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法,简述Unix IO接口及其函数,分析了printf(),与getchar()。可以看到linux把所有的设备都模型化成了文件,这样就能用一致的一些操作来管理各种设备:打开文件、读取文件、向文件写入、关闭文件等等一系列操作。
(第8章1分)
结论
hello所经历的过程有:
编程: 在编辑器上输入hello.c代码
预处理:预处理器处理生成hello.i文件
编译: 编译器编译hello.i将其转化成汇编语言描述的hello.s文件
汇编: 汇编器将hello.s文件翻译成可重定位文件hello.o
链接: 链接器将hello.o和其他目标文件进行链接,生成可执行文件hello
运行: 在shell中输入./hello 120L021727 cjk 1,开始运行
创建新进程:shell为hello程序fork一个新进程
加载: 在新进程中调用execve函数,将hello程序映射到虚拟内存中
执行: 进行虚拟地址的翻译,此时会发生缺页,开始加载hello代码和数据到对应的物理页中,然后开始执行。
信号处理:在hello进程运行中,途中按下ctrl+z、ctrl+c等会发送信号给hello,
调用信号处理程序进行处理。
终止: 输出完8遍对应的字符串后,执行getchar(),等待用户输入,输入字符按下回车后,hello进程终止。
回收: hello进程终止后发送SIGCHLD信号给shell,回收进程,最后内核从系统中删除hello所有的信息。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c:源文件
hello.i:编译器预处理(cpp)后得到的文本文件
hello.s:编译(cc1)后得到的汇编语言程序
hello.o:汇编(as)得到的可重定位目标程序
hello:链接(ld)生成的可执行目标程序
Hello.elf:elf格式 作用:查看hello.o信息
Obj:hello.o的反汇编文件 作用:查看汇编器翻译后的汇编代码
Obj2:hello的反汇编文件 作用:查看链接器链接后汇编代码
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 《深入理解计算机系统》
[2] 教师的教学ppt
(参考文献0分,缺失 -1分)