计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 119020050
班 级 1936603
学 生 郑米
指 导 教 师 刘宏伟
计算机科学与技术学院
2021年6月
摘 要
本文主要阐述hello程序在Linux系统的生命周期,探讨hello程序从hello.c经过预处理、编译、汇编、链接生成可执行文件的全过程。通过讨论hello的整个生命周期,结合计算机系统所学来将我们的知识融会贯通。
关键词:预处理;编译;汇编;链接;进程管理;存储管理;I/O管理
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述 - 4 -
1.1 Hello简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 4 -
第2章 预处理 - 5 -
2.1 预处理的概念与作用 - 5 -
2.2在Ubuntu下预处理的命令 - 5 -
2.3 Hello的预处理结果解析 - 5 -
2.4 本章小结 - 5 -
第3章 编译 - 6 -
3.1 编译的概念与作用 - 6 -
3.2 在Ubuntu下编译的命令 - 6 -
3.3 Hello的编译结果解析 - 6 -
3.4 本章小结 - 6 -
第4章 汇编 - 7 -
4.1 汇编的概念与作用 - 7 -
4.2 在Ubuntu下汇编的命令 - 7 -
4.3 可重定位目标elf格式 - 7 -
4.4 Hello.o的结果解析 - 7 -
4.5 本章小结 - 7 -
第5章 链接 - 8 -
5.1 链接的概念与作用 - 8 -
5.2 在Ubuntu下链接的命令 - 8 -
5.3 可执行目标文件hello的格式 - 8 -
5.4 hello的虚拟地址空间 - 8 -
5.5 链接的重定位过程分析 - 8 -
5.6 hello的执行流程 - 8 -
5.7 Hello的动态链接分析 - 8 -
5.8 本章小结 - 9 -
第6章 hello进程管理 - 10 -
6.1 进程的概念与作用 - 10 -
6.2 简述壳Shell-bash的作用与处理流程 - 10 -
6.3 Hello的fork进程创建过程 - 10 -
6.4 Hello的execve过程 - 10 -
6.5 Hello的进程执行 - 10 -
6.6 hello的异常与信号处理 - 10 -
6.7本章小结 - 10 -
第7章 hello的存储管理 - 11 -
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 -
7.9动态存储分配管理 - 11 -
7.10本章小结 - 12 -
第8章 hello的IO管理 - 13 -
8.1 Linux的IO设备管理方法 - 13 -
8.2 简述Unix IO接口及其函数 - 13 -
8.3 printf的实现分析 - 13 -
8.4 getchar的实现分析 - 13 -
8.5本章小结 - 13 -
结论 - 14 -
附件 - 15 -
参考文献 - 16 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:在Linux中,hello.c经过预处理、编译、汇编、链接后最终成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork出一个子进程,使hello变成进程。
020: shell调用execve后映射虚拟内存,先删除当前虚拟地址的数据结构并为hello创建新的区域结构,进入程序入口后载入物理内存,再进入main函数执行代码。当程序运行结束后,父进程负责回收hello进程,内核删除相关数据结构。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位
开发与调试工具:gcc,vim,gdb,readelf,HexEdit
1.3 中间结果
hello.c:源代码
hello.i:预处理后的文本文件
hello.s:编译之后的汇编文件
hello.o:汇编之后的可重定位目标执行文件
hello:链接之后的可执行文件
1.4 本章小结
本章主要介绍了hello的P2P,020过程,以及进行实验时的软硬件环境及开发与调试工具和在本论文中生成的中间结果文件。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:程序源代码被编译之前,由预处理器(Preprocessor)对程序源代码进行的处理。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的符号用来支持宏调用。
作用:预处理中会展开以#起始的行,试图解释为预处理指令(preprocessing directive) ,其中ISO C/C++要求支持的包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令) 。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
2.2在Ubuntu下预处理的命令
gcc hello.c -E -o hello.i
2.3 Hello的预处理结果解析
经过预处理之后,hello.c转化为hello.i文件,文件的内容增加。对原文件中的宏进行了宏展开,头文件中的内容被写入该文件中导致文件内容剧增。例如声明函数、定义结构体、定义变量、定义宏等内容。
2.4 本章小结
本章介绍了预处理的相关概念及其所进行的一些处理,例如实现将定义的宏进行符号替换、引入头文件的内容、根据指令进行选择性编译等。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。
作用:把源程序(高级语言)翻译成目标程序。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1数据
1.局部变量i
main函数声明了一个局部变量i,编译器将i变量存储在栈上的局部空间-4(%rbp)中,i可以作为一个int类型大占4个字节。
2.字符串
程序中有两个字符串,这两个字符串都在只读数据段中,如图所示:
3.各种立即数
立即数直接体现在汇编代码中
4.argc
参数 argc 作为用户传给main的参数,作为第一个参数存在存在寄存器edi中
5.数组:char *argv[]
hello.c中唯一的数组是作为main函数的第二个参数,数组的每个元素都是一个指向字符类型的指针。数组的起始地址存放在栈中-32(%rbp)的位置,被两次调用找参数传给printf
3.3.2全局函数main
3.3.3算术操作
通过addl $1,-4(%rbp)实现对int型变量i加1,并将结果存储在i中的操作
3.3.4赋值操作
程序中的赋值操作主要有:i=0这条赋值操作在汇编代码主要使用mov指令来实现,而根据数据的类型有不同的指令。
movb:一个字节
movw:两个字节
movl:四个字节
movq:八个字节
3.3.5关系操作
(1)argc!=3;条件判断语句,进行编译时,指令执行后还有设置条件码,根据条件码来判断是否需要je跳转到分支中。
(2)i<8,在hello.c作为判断循环条件,指令执行后还有设置条件码,根据条件码来用jle进行跳转。
3.3.6控制转移指令
(1)判断i是否为3,如果i等于3,则不执行if语句,否则执行if语句,对应的汇编代码为
(2)for(i=0;i<8;i++):
i被存放在-4(%rbp)中,每次循环+1,直到符合比较进行跳转。
3.3.7其他函数
printf,exit,sleep ,getchar函数
两次printf函数的参数恰好是那两个字符串
exit参数是1,sleep函数参数是atoi(argv[3])
函数的返回值存储在%eax寄存器中。
3.4 本章小结
本节,对汇编的概念和作用进行了阐述,对汇编代码进行了分析和解释,并与C语言中的语句进行对比,生成了hello.s文件。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:汇编器(as)将汇编文件(hello.s)和翻译成机器语言,并将其打包成可重定位目标程序,并将结果保存在二进制文件中(hello.o)
作用:生成可重定位目标文件。
4.2 在Ubuntu下汇编的命令
gcc hello.s -c -o hello.o
4.3 可重定位目标elf格式
(1)ELF头
包含目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、 字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。
(2)节头目表
节头部表,包含了文件中出现的各个节的语义,包括节 的类型、位置和大小等信息。
.text:已经编译程序的机器代码
.rodata:只读数据
.data:初始化的全局和静态变量
.bss:未初始化的全局和静态变量
.symtab:符号表
.rel.text:一个.text节中位置的列表
.rel.data:别模块引用或定义的所有全局变量和重定位信息
.debug:调试符号表
.line:行号与.text的机器指令之间的映射
.strtab:一个字符串表
(3)符号表
存放程序中定义和引用的函数和全局变量的信息。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编
发现汇编语言的指令并没有什么不同的地方,只是反汇编代码所显示的不仅仅是汇编代码,还有机器代码,机器语言程序的是二进制机器指令的集合,是电脑可以识别的语言。
机器指令由操作码和操作数构成,汇编语言是人们比较熟悉的词句直接表述CPU动作形成的语言,是最接近CPU运行原理的语言。
每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言和二进制机器语言建立一一映射的关系。
(1)分支转移:反汇编的跳转指令用的不是段名称而是用的确定的地址,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
(2)函数调用:在.s 文件中,函数通过函数名调用,而反汇编代码中,使用call跳转到下一条指令地址。由于在该程序中调用的函数是共享库中的函数,所以最终需要通过链接器才能确定函数的运行时地址。在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为0,然后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。
4.5 本章小结
本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的各个部分的作用,比较了hello.s和hello.o反汇编代码的不同之处,分析了从汇编语言到机器语言的一一映射关系。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代 码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。链接是由叫做链接器的程序执行的。链接器使得分离编译成为可能。
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.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
(1)ELF头
(2)节头部表
(3)符号表
5.4 hello的虚拟地址空间
hello的虚拟地址空间开始于0x401000,结束与0x401ff0
5.5 链接的重定位过程分析
hello重定位的过程:
(1)重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
(2)重定位节中的符号引用这一步中,链接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。
(3)重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成重定位条目放在.rel.txt中。
重定位后反汇编代码:
5.6 hello的执行流程
5.7 Hello的动态链接分析
动态链接器将程序所需要的所有共享库装载到进程的地址空间,并且将程序汇总所有为决议的符号绑定到相应的动态链接库(共享库)中,并进行重定位工作。
每一个共享模块,都会在其代码段有一个GOT段,Got 是一个指针数组,用来存储外部变量的8字节地址,而代码相对于GOT的距离是固定的,当对外部模块变量数据和函数进行访问时,就去访问变量在GOT中的位置。编译器还为GOT中每个条目生成一个重定位记录。在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。
PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
动态链接基本分为三步:先是启动动态链接器本身,然后装载所有需要的共享对象,最后重定位和初始化。
在dl .init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。因而运行到时会跳回PLT条目,在把puts 的ID 0压入栈后,会转到PLT[0]的位置,PLT[0]通过GOT[1]间接地把动态链接器的一个参数压入栈中,然后通过GOT[2]跳转进动态链接器中。动态链接器使用两个栈条目来确定puts 的运行时位置,用这个地址重写puts 的GOT项,再把控制传递给puts。在下一次执行到puts对应的PLT条目时,GOT项已经被修改,因此利用GOT项进行的间接跳转就会直接跳转到puts函数了。
5.8 本章小结
本章主要介绍了linux 下 hello链接的方法、过程和结果以及运行结果,分析了加载时重定位的共享库函数的重定位过程
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个针对执行中的应用程序的程序的实例。系统中的每个应用程序都可以运行在某个应用进程的可执行上下文中。每次程序用户可以通过向系统中的shell应用程序输入一个可执行程序的英文名字,运行这个应用程序时,shell就可能会自动创建一个新的应用进程,然后在这个新应用进程的可执行上下文中自动运行这个可执行文件,应用程序也同样可以自动创建新的可执行进程,并且在这个新进程的可执行上下文中用户可以运行他们自己的可执行代码或者其他的应用程序。
6.2 简述壳Shell-bash的作用与处理流程
(0)Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。
其基本功能是解释并运行用户的指令,重复如下处理过程:
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
(3)如果不是内部命令,调用fork( )创建新进程/子进程
(4)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(5)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait…)等待作业终止后返回。
(6)如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
终端程序通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的 逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
以我们的hello为例,当我们输入 ./hello1190200506 郑米 的时候,首先shell对我们输入的命令进行解析,由于我们输入的命令不是一个内置的shell命令,因此shell会调用fork()创建一个子进程。
6.4 Hello的execve过程
当创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,加载并运行需要以下几个步骤:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
(2)映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。如图6.4
(3)映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
(4)设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据 复制。直到 CPU 引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用 它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
进程向每个程序人员提供一种假象,好像他们在一个独占的程序中使用了处理器,这种处理效果的具体实现效果本身就是一个逻辑控制流,它指的是一系列可执行程序的计数器pc的值,这些计数值唯一的定义对应于那些包含在程序的可执行文件目标对象中的可执行指令,或者说它指的是那些包含在程序运行时可以动态通过链接触到可执行程序的共享文件对象的可执行指令。时间片是指一个进程在执行控制流时候所处在的每一个时间段。
处理器通过设置在某个控制寄存器中的一个模式位来限制一个程序可以可以执行的指令以及它可以访问的地址空间。没有设置模式位时,进程就运行在用户模式中。用户模式下不允许执行特权指令,不允许使用或者访问内核区的代码或者数据。设置模式位时,进程处于内核模式,该进程可以 访问系统中的任何内存位置,可以执行指令集中的任何命令。进程从用户模式变为内核模式的唯一方式是使用诸如中断,故障或陷入系统调用这样的异常。异常发生时,控制传递到异常处理程序,处理器从用户模式转到内核模式。
上下文在运行时候的状态这也就是一个进程内核重新开始启动一个被其他进程或者对象库所抢占的网络服务器时该进程所可能需要的一个下文状态。它由通用寄存器、浮点数据寄存器、程序执行计数器、用户栈、状态数据寄存器、内部多核栈和各种应用内核数据结构等各种应用对象的最大值数据寄存器组合构成。
在调用进程发送sleep之前,hello在当前的用户内核模式下进程继续运行,在内核中进程再次调用当前的sleep之后进程转入用户内核等待休眠模式,内核中所有正在处理等待休眠请求的应用程序主动请求释放当前正在发送处理sleep休眠请求的进程,将当前调用hello的进程自动加入正在执行等待的队列,移除或退出正在内核中执行的进程等待队列。
之后设置定时器,休眠的时间等于自己设置的时间,当计时器时间到时候,发送一个中断信号。内核收到中断信号进行中断处理,hello被重新加入运行队列,等待执行,这时候hello就可以运行在自己的逻辑控制流里面了。
6.6 hello的异常与信号处理
hello程序执行过程可能出现的异常一共有四种:中断、陷阱、故障、终止。
(1)中断:来自I/O设备的信号,异步发生,总是返回到下一条指令。
hello程序执行过程中可能会出现外部I/O设备引起的异常
(2)陷阱:有意的异常,同步发生,总是返回到下一条指令。
hello调用getchar函数时会触发此异常
(3)故障:潜在可恢复的错误,同步发生,可能返回到当前指令或终止。
hello程序运行时可能会出现缺页异常的情况
(4)终止:不可恢复的错误,同步发生,不会返回。
hello程序运行过程中硬件系统发生的一些错误可能会导致终止
如下图所示
(1)正常执行hello程序:
(2)Ctrl+c 终止:
键入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,(默认)终止前台作业
(3)Ctrl+z 暂停:
键入Ctrl+z将(默认)挂起前台的作业,hello进程此时并没有被回收,而是运行在后台
(4)程序运行过程中乱按键盘:
无关输入被缓存到stdin,并随着printf指令被输出。
(5)输入ps打印当前进程的状态
(6)输入jobs列出当前作业
(7)输入pstree打印进程树
(8)输入fg 1,继续执行前台进程1
(9)输入kill,杀死hello
6.7本章小结
本章系统的回顾了进程的基本定义以及其作用,对shell的工作方式和各种类型的信号处理的方法进行了简单的分析,学习了与进程相关的几个重要的函数,了解了如fork创建一个新进程,execve是怎么样加载的,还有这些函数的作用和原理,同时我们以hello这个应用程序为例针对异常控制和信号处理进行简单的分析。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。
线性地址:也叫虚拟地址,和逻辑地址类似,也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件也是内存的转换前地址。
虚拟地址:也就是线性地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽像。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符:段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。
在段式管理系统中,整个进程的地址空间是二维的,即其逻辑地址由段号和段内地址两部分组成。为了完成进程逻辑地址到物理地址的映射,处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到实际的物理地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
系统将每个段分割为被称为虚拟页(VP)的大小固定的块来作为进行数据传输的单元,在linux下每个虚拟页大小为4KB,类似地,物理内存也被分割为物理页(PP/页帧),虚拟内存系统中MMU负责地址翻译,MMU使用存放在物理内存中的被称为页表的数据结构将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。
7.4 TLB与四级页表支持下的VA到PA的变换
将VPN分成三段,对于TLBT和TLBI来说,可以在TLB中找到对应的PPN,但是有可能出现缺页的情况,这时候就需要到页表中去找。此时,VPN被分成了更多段(这里是4段)CR3是对应的L1PT的物理地址,然后一步步递进往下寻址,越往下一层每个条目对应的区域越小,寻址越细致,在经过4层寻址之后找到相应的PPN让你和和VPO拼接起来。
7.5 三级Cache支持下的物理内存访问
(1) 组选择取出虚拟地址的组索引位,将二进制组索引转化为一个无符号整数,找到相应的组。
(2) 行匹配把虚拟地址的标记为拿去和相应的组中所有行的标记位进行比较,当虚拟地址的标记位和高速缓存行的标记位匹配时,而且高速缓存行的有效位是1,则高速缓存命中。
(3) 字选择一旦高速缓存命中,我们就知道我们要找的字节在这个块的某个地方。因此块偏移位提供了第一个字节的偏移。把这个字节的内容取出返回给CPU即可。
(4)不命中如果高速缓存不命中,那么需要从存储层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。一种简单的 放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块, 产生冲突(evict),则采用最近最少使用策略 LFU 进行替换。
7.6 hello进程fork时的内存映射
当 fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给 它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。 加载并运行 hello 需要以下几个步骤:
1.删除已存在的用户区域;
2.创建新私有区域;
3.创建新共享区域;
4.设置PC,指向代码的入口点
7.8 缺页故障与缺页中断处理
当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面 到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成。
7.9动态存储分配管理
动态内存分配器维护者一个进程的虚拟内存区域,成为堆。(如图7.9.1所示),分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格。两种风格都是要求显示的释放分配块。
(1) 显式分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。
(2) 隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。
显示空闲链表:
一种更好的方法是将空闲块组织为某种形式的显式数据结构.因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面.例如,堆可以组织成一一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针.
隐式空闲链表:
块与块之间是通过头部的块大小隐式连接在一起,意思是我们要找到下一个块,只需要用当前块地址加上块大小即可。
这种结构的优点就是简单,当任何操作,如放置分配的块,都需要对整个链表搜索,全是线性级。
我们在搜索一个可以用户请求的空闲块时,一般有三种策略:首次适配(从头搜索的第一个合适的块)、下一次适配(从上一次分配结束的位置开始的首次适配)、最佳适配(检查所有块,选择最适合的那个块)。
7.10本章小结
本章主要介绍了hello的存储器的地址空间,介绍了四种地址空间的差别和地址的相互转换。同时介绍了hello的四级页表的虚拟地址空间到物理地址的转换。阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m个字节的序列,所有的 IO 设备都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。
8.2 简述Unix IO接口及其函数
Unix I/O接口:
1.打开文件.一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O 设备.内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件.内核记录有关这个打开文件的所有信息.应用程序只需记住这个描述符.
2.Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) .头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值.
3.改变当前的文件位置.对于每个打开的文件,内核保持着一个文件位置k, 初始为0.这个文件位置是从文件开头起始的字节偏移量.应用程序能够通过执行seek 操作,显式地设置文件的当前位置为K .
4.读写文件.一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n .给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件.在文件结尾处并没有明确的“EOF 符号” .类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k .
5.关闭文件.当应用完成了对文件的访问之后,它就通知内核关闭这个文件.作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中.无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源.
Unix I/O 函数:
int open(char* filename,int flags,mode_t mode) 。open 函数将 filename(文件名,含后缀)转换为一个文件描述符(C 中表现为指针),并且 返回描述符数字。
int close(fd),fd 是需要关闭的文件的描述符(C 中表现为指针),close 返回操作结果。
ssize_t read(int fd, void *buf, size_t n);read 函数从描述符为fd 的当前文件位置复制最多n 个字节到内存位置buf .返回值-1表示一个错误,而返回值0 表示EOF.否则,返回值表示的是实际传送的字节数量.
ssize_t write(int fd, const void *buf, size_t n);
write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置.图10-3 展示了一个程序使用read 和write 调用一次一个字节地从标准输入复制到标准输出.返回:若成功则为写的字节数,若出错则为-1.
8.3 printf的实现分析
源代码:
static int printf(const char *fmt, …)
{
va_list args;
int i;
va_start(args, fmt);
. write(1,printbuf,i=vsprintf(printbuf, fmt, args));
va_end(args);
return i;
}
printf,需要做的事情是:接受一个fmt的格式,然后将匹配到的参数按照 fmt格式输出。printf用了两个外部函数,一个是vsprintf,还有一个是 write。
vsprintf的作用就是格式化。它接受确定输出格式的格式字符串 fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
write函数是将buf中的i个元素写到终端的函数。
字符显示驱动子程序:从ASCII到字模库到显示 vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取 vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
源代码:
int getchar(void)
{
static char buf[BUFSIZ];
static char *bb = buf;
static int n = 0;
if(n == 0)
{
n = read(0, buf, BUFSIZ);
bb = buf;
}
return(–n >= 0)?(unsigned char) *bb++ : EOF;
}
getchar是stdio.h.中的库函数,它的作用是从stdin流中读入一个字符,也就是说,如果stdin有数据的话不用输入它就可以直接读取了,第一次 getchar(O时,确实需要人工的输入,但是如果你输了多个字符,以后的getchar()再执行时就会直接从缓冲区中读取了。﹒
实际上是输入设备->内存缓冲区->程序getchar
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux的系统级IO设备概念,分析了printf.函数和 getchar.函数的实现。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
1.预处理,解析替换#开头的语句,生成代码文件 hello.i
2.编译:将代码文件翻译成汇编语言,生成 hello.s文件
3.汇编:将汇编语言转换成机器语言,生成可重定位文件 hello.o
4.链接:由链接器将可重定位文件链接,生成可执行文件 hello
5.创建进程、加载程序: shell收到运行./hello的指令之后,通过fork创建子
进程,并在其中由execve.创建虚拟内存空间映射、调用高速缓存与缺页处理将hello加载进内存与cpu.
6.上下文切换:hello调用sleep函数之后进程陷入内核模式,处理休眠请求主动释放当前进程,内核进行上下文切换将当前进程的控制权交给其他进程,当sleep函数调用完成时,内核执行上下文切换将控制传递给当前进程。
7.动态申请内存:当hello程序执行printf函数是, 会调用 malloc 向动态内存分配器申请堆中的内存。
8.信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
9.终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有 数据结构。
计算机系统的设计相当精巧:比如为了解决快的设备存储小、存储大的设备慢的不平衡,根据系统的局部性原理,设计了高速缓存来作为更底层的存储设备的缓存,大大提高了CPU访问主存的速度。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
(附件0分,缺失 -1分)
hello.c:源代码
hello.i:预处理后的文本文件
hello.s:编译之后的汇编文件
hello.o:汇编之后的可重定位目标执行文件
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.
(参考文献0分,缺失 -1分)