计算机系统大作业——程序人生

第1章 概述
1.1 Hello简介
P2P:From Program to Process
用高级语言编写得到.c文件,再经过编译器预处理得到.i文件,进而对其进行编译得到.s汇编语言文件。此后通过汇编器将.s文件翻译成机器语言,将指令打包成可重定位的.o目标文件,再通过链接器与库函数链接得到可执行文件hello,执行此文件,操作系统会为其fork产生子进程,再调用execve函数加载进程。至此,P2P结束。

020: From Zero-0 to Zero-0
子进程调用execve,映射虚拟内存并载入物理内存,进入程序入口处开始执行,同时,CPU为运行的hello分配时间片并执行逻辑控制流。在中途调用异常处理函数处理可能出现的异常。最后当hello进程终止,父进程shell将回收hello,接着内核删除相关数据结构的整个过程叫做020。
1.2 环境与工具
硬件:Intel(R)Core™i5-7300HQ CPU 2.50GHz 2.50GHz 8G RAM
软件:Win10教育版 64位
虚拟机VMware Workstation Pro12.0
Ubuntu18.4
开发工具:gcc ld readelf gedit objdump edb gdb hexedit
Visual Studio 2019;CodeBlocks 64位
1.3 中间结果
文件 说明
hello.c 源程序
hello.i hello.c预处理得到的文件
hello.s hello.i经过ccl翻译成的汇编语言文件
hello.o hello.s经过汇编器汇编得到可重定位目标文件
hello hello.o通过连接器得到的可执行文件
hello_objdump Objdump hello生成的文件
hello_elf Readelf hello 生成的elf文件

1.4 本章小结
本章对hello的P2P,020的整个过程作了简要介绍,列出了运行的环境及使用的工具以及中间结果,是后面章节的基础。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
概念:预处理,即为编译做的预备工作的阶段,是指在程序源代码被编译之前,由预处理器(cpp)对程序源代码进行的处理。预处理器执行以#开头的命令(宏定义、条件编译、读取头文件)、删除注释等来修改c程序生成.i文件
作用:1、用实际值替换宏定义的字符串2、文件包含:将头文件中的代码插入到新程序中3、条件编译:根据if后面的条件决定需要编译的代码
2.2在Ubuntu下预处理的命令

以下是hello.i内容部分截图

2.3 Hello的预处理结果解析
预处理会将所有的#define删除,并且展开所有的宏定义;预处理过程主要处理那些源代码中以#开始的预处理指令,主要处理规则如下:

1、处理所有条件编译指令,如#if,#ifdef等;
2、处理#include预处理指令,将被包含的文件插入到该预处理指令的位置。该过程递归进行,及被包含的文件可能还包含其他文件。
3、删除所有的注释//和 /**/; ·添加行号和文件标识,如#2 “hello.c”以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号信息。
4、保留所有的#pragma编译器指令

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
概念:编译是利用编译程序从预处理文本文件(.i)产生汇编程序(.s)的过程。
作用:进行词法分析、语法分析、目标代码的生成,检查无误后生成汇编语言。汇编语言为不同的编译器提供了通用的输出语言
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

3.2 在Ubuntu下编译的命令

helllo.s内容截图:

3.3 Hello的编译结果解析
3.3.1指令内容及作用
.file 声明源文件
.text 声明数据段
.section .rodata 只读数据,rodata节
.align 声明指令或数据的存放地址的对齐方式
.global 全局变量
.type 用来指定是函数类型或是对象类型
.size 声明大小
3.3.2数据
1、全局变量main
如图可知main为全局变量,且类型为function
main在.text节中定义。.text节包含已经编译程序的机器代码
2.局部变量int i,int argc
局部变量通常会被分配到栈中(隶属于各自的进程,生命周期结束后会在栈上被释放)
int i:存放在%rbp-4中

int argc:由gdb得,存放在%edi中,及%rbp-20

3、指针数组变量:
指针数组变量char * argv[] 其地址内含的值为字符型变量

可知,argv[0]存放在%rbp-32中,argv[1]存放在%rbp-24,,argv[2]存放在%rbp-16.argv[3]存放在%rbp-8中,

4、字符串常量:“用法: Hello 学号 姓名 秒数!\n"和"Hello %s %s\n”
由下图得:在.LC0、.LC1段声明的字符串常量,且都是在.rodata只读数据节中,“\347\224……”是对应汉字在UTF-8中的编码。其中汉字要占3个字节一个\代表一个字节。

3.3.3赋值
指令:mov指令。mov是汇编指令,其种类有三:普通的mov指令、做符号扩展的movs、做零扩展的movz
(1) 对应hello.c为exit(1)。将1赋值给寄存器%edi,作为调用的exit函数的参数
(2)
对应hello.c中 i=0,将0赋值给%rbp-4即i
(3)
对应hello.c为return 0。将1赋值给寄存器%eax,作为main的返回值

3.3.4运算
常用算数操作:

(1)for循环中i++

(2)通过数组argv[]得到数组首地址即argv[0],再通过运算:即argv[i] = argv[0]+8*i来得到argv[i]的地址。

(3)
输出字符串"用法: Hello 学号 姓名 秒数!\n"和"Hello %s %s\n"时使用到lead

3.3.5关系操作

指令:cmp,je,jle
(1)if(argv!=4):

!=相当于cmp与je结合。
(2)for(i=0;i<8;i++)中i<8

<相当于cmp,jle结合

3.3.6数组/指针操作
系统经(%rbp-32)得到argv[0]的指针的地址、然后+8得到argv[1]的地址、+16得到了argv[2]的地址。实质即为:argv[i]=argv[0]+8*i

3.3.7函数操作
调用函数指令:call
(1)c语言:

	汇编语言:

其中,通过寄存器%rdi储存参数,传递给函数puts,通过call调用函数puts
(2)exit(1)

通过寄存器%edi储存参数,传递给函数exit,通过call调用函数exit
(3)c语言:

汇编:

通过寄存器%rsi,%rdi保存参数,传递给printf,用call调用printf

将%rbp-8即argv[3]用%rdi保存,传递给atoi函数,并用call调用

将atoi返回值用寄存器%edi储存,传递给sleep函数,并用call调用。
(4)直接调用getchar,没有参数传递

(5)函数返回:返回值通常在寄存器%rax中

3.3.8控制转移
指令:je,jle,jmp
(1)c语句:

汇编语句:通过cmp,je如果!=4,跳转到.L2,执行printf

(2)c语句:

汇编:
先将0赋值给i,跳转到.L3

再比较i与7,如果是<=7,跳转到.L4

3.4 本章小结
本章主要解释了编译器是如何根据C语言的不同数据类型、不同操作将.i程序编译为.s汇编代码。并介绍了汇编语言的相关知识。
至此,hello.i已经晋级为hello.s
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
概念:汇编器as将.s汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中,.o文件是一个二进制文件,它包含程序的指令编码。
作用:将汇编代码转为机器指令,使其在链接后能被机器识别并执行。

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式
1、ELF头:ELF头将提供Magic,类别,数据,版本,OS/ABI,ABI,ELF头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息

(2)节头表:显示各节基本信息)

(3)重定位节

偏移量:指需要进行重定向的代码需要被修改的引用的节偏移,8个字节。
信息:前4个字节代表重定位到的目标在.symtab中的偏移量,后四个字节代表重定位到的目标的类型
类型:如何修改新的引用,即重定位到的目标的类型
符号名称:标识被修改引用应该指向的符号
加数:加数:计算重定位位置的辅助信息,使用它对被修改引用的值做偏移调整

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.4 Hello.o的结果解析
hello.s截图如下:

objdump截图如下:

1、分支转移:反汇编代码跳转指令从原来的符号(助记符)变成了具体地址
2、函数调用:在.s文件中,call后直接跟着函数名称,而在反汇编程序中, call之后加main+偏移量(定位到call的下一条指令),因为call后跟的是相对地址,需要在链接之后重定位才能确定,因此机器码为00 00 00 00 ,在链接后会自动修改
3、访问字符串:汇编代码中使用.LC0(%rip),反汇编代码中为0x0(%rip),因为访问时需要重定位。
4.5 本章小结
本章介绍了从.s到.o的过程。通过汇编文件和elf格式与.s比较了解机器语言和汇编语言的映射关系。

第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码个数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。
作用:可重定位目标文件不能直接执行,需要链接过后形可执行目标文件才能让计算机执行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.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
(1) ELF头

(2) 节头表:

5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
Loaded symbols :各段。的地址

数据段:数据段起始地址为0x00400000即

堆栈段:

(1).text

(2).rodata

(3).data

(4).bss

5.5 链接的重定位过程分析
下图是objdump -d -r hello:

下图是objdump hello.o

不同:(1)hello将hello.o需要重定位的进行了重定位,将其地址转变成虚拟空间中绝对地址,并在后面标出执行函数的地址和函数名(2)hello添加了许多库函数:puts@plt,printf@plt等(3)添加了节:.init,.plt等

链接的过程:(1)符号解析: 程序中所有有定义和引用的符号(包括变量和函数),编译器会讲定义的符号存放在一个符号表中。这个符号表是一个结构数组,其中每个表项包含符号名、长度和位置等信息。而链接器的工作就是将每个符号的引用都与一个确定的符号定义链接起来(2)重定位:将多个代码段与数据段分别合并为一个单独的代码段和数据段,计算每个定义符号在虚拟地址空间中的绝对地址,将可执行文件中的符号引用处的地址修改为重定位后的地址信息。

Hello重定位分析:
(1)首先合并相同节:函数<_start>就是系统代码段(.text)与hello.o中的.text节合并得到的最后的一个单独的代码段。
(2)然后对定义符号进行重定位(确定其地址):
比如为函数确定首地址,从而确定每条指令的地址。
未链接前main函数地址为0x0000000,连接后编程0x0400582。
(3)对引用符号进行重定位。修改.text节 和 .data节中对每个符号的引用(地址),该过程需要用到在.rel_data和.rel_text节中保存的重定位信息。
5.6 hello的执行流程

加载程序 ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
hello!_start
libc-2.27.so!_libc_start_main
libc-2.27.so!_cxa_atexit
libc_2.27.so!_new_exitfn
hello!_libc_csu_init
hello!_init
libc-2.27.so!_sigsetjmp
libc-2.27.so!_sigjmp
Call main hello!main
终止 libc-2.27.so!exit

5.7 Hello的动态链接分析
动态链接库中的函数在程序执行的时候才会确定地址,所以编译器无法确定其地址,在汇编代码中也无法像静态库的函数那样体现。

hello程序对动态链接库的引用,基于数据段与代码段相对距离不变这一个事实,因此代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量。

GNU编译系统采用延迟绑定技术来解决动态库函数模块调用的问题,它将过程地址的绑定推迟到了第一次调用该过程时。

延迟绑定通过全局偏移量表(GOT)和过程链接表(PLT)实现。如果一个目标模块调用定义在共享库中的任何函数,那么就有自己的GOT和PLT。前者是数据段的一部分,后者是代码段的一部分。

下图为dl_init之前,GOT表的起始位置,即0x601000。在dl_init调用之前可以查看其值,发现均为0

下图是调用之后

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
本章讨论了链接过程中对程序的处理。Linux系统使用可执行可链接格式,即ELF,具有.text,.rodata等节,并且通过特定的结构组织。

经过链接,ELF可重定位的目标文件变成可执行的目标文件,链接器会将静态库代码写入程序中,以及动态库调用的相关信息,并且将地址进行重定位,从而保证寻址的正确进行。静态库直接写入代码即可,而动态链接过程相对复杂一些涉及共享库的寻址。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存中程序的代码和数据,栈,通用目的寄存器的内容,程序计数器,环境变量以及文件描述符的集合。
作用:进程为应用程序提供两个抽象,一是独立的逻辑控制流,一个是私有的地址空间。提高CPU的执行效率,减少因为程序等待带来的CPU空转以及其它计算机软硬件资源浪费
6.2 简述壳Shell-bash的作用与处理流程
Shell是个交互型应用及程序,代表用户运行其他命令,基本作用是解释并运行用户的指令。
流程:(1)读取用户命令行输入
(2)分析命令行字符串,获取命令行参数,构造传递给execvedargv向量
(3)检查第一个命令行参数是否是一个内置的shell命令
(4)如果不是,用fork创建子程序
(5)子进程中,进行步骤(2)获得参数,调用exceve()执行制定程序
(6)命令行末尾没有&,代表前台作业,shell使用waitpid等待作业终止后返回
(7)命令行末尾有&,代表后台作业,shell返回
6.3 Hello的fork进程创建过程
在终端输入./hello,shell进行对命令行的解释,因为不是内置shell命令,因此调用fork创建一个新的运行子进程,执行可执行程序hello。新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的但独立一份副本,因此当父进程调用fork时,子进程可以读写父进程中打开的任何文件。
6.4 Hello的execve过程
下图为execve创建新程序后的栈结构:

子进程调用execve函数,使用驻留在储存器中的称为加载器的操作系统代码来加载并运行可执行目标文件hello,并映射私有区域,为程序的代码,数据,bss,栈区域创建新的区域结构。代码和数据区域映射为hello中的.text,.data区,bss请求二进制零,映射到匿名文件。栈和堆请求二进制零,初始长度为0。
接着,映射共享区域;最后设置当前进程上下文中的程序计数器PC,指向代码区域的入口点即-start函数的地址。start函数调用系统启动函数 _libc_start_main来初始化执行环境,并调用用户层的main函数,此时构造的argv向量被传递给主函数。
6.5 Hello的进程执行
(1)进程上下文:进程的物理实体(代码和数据等)和支持进程的运行的环境合称为进程的上下文 而由进程的程序块、数据块、运行时的堆和用户栈(两者统称为用户堆栈)等组成的用户空间信息被称为用户级上下文;
由进程标识信息、进程现场信息、进程控制信息和系统内核栈等组成的内核空间信息被称为系统级上下文;处理器中各个寄存器的内容被称为寄存器上下文(也称硬件上下文),即进程的现场信息;
(2)进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
(3)用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

操作系统内核会为每一个进程维持一个上下文,而在进程执行的某些时刻,内核可以进行调度、抢占当前进程,并重新开始一个先前被抢占了的进程。其过程如下:
保存当前进程的上下文
恢复先前被抢占进程保存的上下文
控制权传递给这个新恢复的进程

下图为进程间的上下文切换示意图:

进行上下文切换可以通过调用系统函数和中断实现:
(1)hello进程初始运行在用户模式时需要调用getchar函数,系统通过执行系统调用getchar陷入到内核,内核中的陷阱处理程序请求来自磁盘控制器的DMA传输,并且安排在磁盘控制器完成从磁盘到内存的数据传输后,磁盘中断处理器。
(2)除此之外,当系统周期定时器中断时,内核判定当前进程已经运行了足够长的时间,则切换到一个新的进程.
6.6 hello的异常与信号处理
hello执行过程中可能出现四类异常:中断、陷阱、故障和终止。

中断是来自I/O设备的信号,异步发生,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码,就像没有发生过中断。

陷阱是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。

故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。

终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。
(1)乱按直到回车

用户键盘输入的字符被读进stdin缓冲区中,对程序执行没有影响,直到按回车键,退出程序
(2)Ctrl—c:退出程序,内核发送一个SIGINT信号到前台进程组中的每个进程,终止前台作业,此时hello进程已结束并被回收

(3)Ctrl-z:程序停止内核发送一个SIGSTP信号到前台进程组中的每个进程,前台作业被挂起。输入ps查看进程信息,发现hello并没有被回收。

(4)ps:显示执行进程的pid

(5)jobs:显示前台作业状态

(6)pstree:

(7)fg:输入fg 1将hello调到前台继续执行,执行结束后进程被回收

(8)kill :将信号9杀死程序,发给24108即hello进程号

6.7本章小结
本章介绍了程序在shell执行及进程的相关概念。程序在shell中执行是通过fork函数及execve创建新的进程并执行程序。进程拥有着与父进程相同却又独立的环境,与其他系统进并发执行,拥有各自的时间片,在内核的调度下有条不紊的执行着各自的指令。

程序运行中难免遇到异常,异常分为中断、陷阱、故障和终止四类,均有对应的处理方法。操作系统提供了信号这一机制,实现了异常的反馈。这样,程序能够对不同的信号调用信号处理子程序进行处理。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
物理地址:计算机的主存被组织成一个由M个连续的字节大小的单元组成的数组,每个字节都有一个唯一的物理地址,CPU访问内存的最自然的方式就是物理寻址
虚拟地址:程序使用的地址,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存之前通过硬件与操作系统被转换成适当的物理地址
逻辑地址:由程序产生的与段相关的偏移地址部分,在有地址变换功能的计算机中,访存指令给出的地址叫逻辑地址,也叫相对地址。逻辑地址由段基值和偏移量组成。逻辑地址空间的格式为“段地址:偏移地址”,例如“23:8048000”,在实模式下可以转换为物理地址:逻辑地址CS:EA = 物理地址CS × 16 + EA。保护模式下以段描述符作为下标,通过在GDT/LDT表获得段地址,段地址加偏移地址得到线性地址。
线性地址:逻辑地址经过段变换后的地址,即虚拟地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
(1)实地址模式:在实地址模式下,处理器使用20位的地址总线,可以访问1MB(0~FFFFF)内存。而8086的模式,只有16位的地址线,不能直接表示20位的地址,采用内存分段的解决方法。段地址存放于16位的段寄存器中(CS、DS、ES或SS)

(2)保护模式:在保护模式下,段寄存器存放段描述符在段描述符表中的索引值,称为段选择器,此时CS存放代码段描述符的索引值,DS存放数据段描述符的索引值,SS存放堆栈段描述符的索引值

RPL表示程序的当前优先级,TI位表示段描述符的位置,TI=0段描述符在GDT中,TI=1段描述符在LDT中
48位的全局描述符表寄存器GDTR指向GDT,即GDT在内存中的具体位置,16位局部描述符表寄存器LDTR指向LDT段在GDT中的位置。唯一的全局描述符表GDT包含操作系统使用的代码段、数据段、堆栈段的描述符,各程序的LDT段,每个程序有一个独立的局部描述符表LDT,包含对应程序私有的代码段、数据段、堆栈段的描述符、对应程序使用的门描述符:任务门、调用门等

保护模式下:线性地址 = 基址(由段描述符给出)+ 偏移量
7.3 Hello的线性地址到物理地址的变换-页式管理
页表是一个页表条目(PTE)的数组,将虚拟页地址映射到物理页地址:
虚拟地址空间中的每个页(VP)在页表固定位置有一个PTE 
PTE由一个有效位和一个n位地址字段组成,有效位表示虚页是否被缓存,当有效位为0,空地址(地址为空)表示虚拟页还未分配

页表基址寄存器存放当前进程的物理页表地址,虚拟地址通过在页表中匹配页表条目来获得PPN,加上物理页偏移量即物理地址。若与页表中的条目不匹配则触发缺页错误,缺页处理程序调入新的页面并更新PTE,再次执行访存指令

7.4 TLB与四级页表支持下的VA到PA的变换
TLB(翻译后备缓冲器)是一个位于MMU中的小的虚拟地址的具有较高相联度的缓存,其每一行都是一组由数个PTE组成的块,TLB极大地减小了CPU访问PTE的开销,且能实现虚拟页面向物理页面的映射,同时对于页面数很少的页表可以完全包含在TLB中。

使用K级页表:虚拟地址被分成4个VPN和一个VPO,每个VPNi都是一个到第i级页表的索引,第j级页表中的每个PTE指向j+1级某个页表的基址。在翻译虚拟地址时通过四级页表查询到PPN,与VPO结合成PA。

1~3级页表条目:
P: 子页表在物理内存中 (1)不在 (0).
R/W: 对于所有可访问页,只读或者读写访问权限.
U/S:对于所有可访问页,用户或超级用户 (内核)模式访问权限.
WT: 子页表的直写或写回缓存策略.
A: 引用位 (由MMU在读或写时设置,由软件清除).
PS: 页大小为4 KB 或 4 MB (只对第一层PTE定义).
页表物理基地址:子页表的物理基地址的最高40位 (强制页表 4KB 对齐)
XD: 能/不能从这个PTE可访问的所有页中取指令

4级页表条目:4级页表条目桶1-3级不同在于其没有了PS项,多了D:修改位(由MMU 在读或写时设置,由软件清除).

翻译过程:

7.5 三级Cache支持下的物理内存访问

i7处理器每个CPU有4个核,每个核有自己私有的L1 i-cache、L1 d-cache和L2高速缓存,所有核共享一个L3缓存。CPU寄存器中保存着从L1缓存中取出的字,L1缓存保存着从L2缓存中取出的缓存行,L2缓存保存着从L3缓存中取出的缓存行,L3缓存保存着从主存中取出的缓存行。
Cache分为以下三类:
(1)直接映射高速缓存

直接映射高速缓存每个组只有一行,当CPU执行一条读内存字w的指令,它会向L1高速缓存请求这个字。如果L1高速缓存中有w的一个缓存副本,那么就会得到L1高速缓存命中,高速缓存会很快抽取出w,并将它返回给CPU。否则就是缓存不命中,当L1高速缓存向主存请求包含w的块的一个副本时,CPU必须等待。当被请求块最终从内存到达时,L1高速缓存将这个块存放在它的一个高速缓存行里,从被存储的块中抽取出字w,然后将它返回给CPU。确定是否命中然后抽取的过程分为三步:1)组选择;2)行匹配;3)字抽取。
组选择即从w的地址中间抽取出s个索引位,将其解释为一个对应组号的无符号整数,从而找到对应的组;行匹配即对组内的唯一一行进行判断,当有效位为1且标记位与从地址中抽取出的标记位相同则成功匹配,否则就得到不命中;而字选择即在行匹配的基础上通过地址的后几位得到块偏移,从而在高速缓存块中索引到数据。

(2)组相联高速缓存

组相联高速缓存每个组内可以多于一个缓存行,总体逻辑类似于直接映射高速缓存,不同之处在于行匹配时每组有更多的行可以尝试匹配,遍历每一行。如果不命中,有空行时也就是冷不命中则直接存储在空行;如果没有空行也就是冲突不命中,则替换已有行,通常有LFU(最不常使用)、LRU(最近最少使用)两者替换策略。

(3)全相联高速缓存

全相联高速缓存只有一个组,且这个组包含所有的高速缓存行(即E =
C/B)。对于全相联高速缓存,因为只有一个组,组选择变的十分简单。地址中不存在索引位,地址只被划分为一个标记位和一个块偏移。行匹配和字选择同组相联高速缓存。

7.6 hello进程fork时的内存映射
当fork函数被shell进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。

它将两个进程中的每个页面都标记为只读,并将每个进程中的每个区域结构都标记为写时复制。

7.7 hello进程execve时的内存映射
加载并运行hello需要以下几个步骤:
(1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
(2)映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
(3)映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点_start.
7.8 缺页故障与缺页中断处理

假设MMU在试图翻译某个虚拟地址A时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序随后就执行下面的步骤:

首先判断虚拟地址A是否合法,缺页处理程序会搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较。如果指令不合法则触发段错误,从而终止该进程。

然后处理程序会判断试图进行的内存访问是否合法,也就是进程是否有读写这个区域内页面的权限。如果访问不合法,那么处理程序会触发一个保护异常,终止这个进程。

最后,确保了以上两点的合法性后,根据页式管理的规则,牺牲一个页面,并赋值为需要的数据,然后更新页表并再次触发MMU的翻译过程。
7.9动态存储分配管理
在程序运行时程序员使用动态内存分配器 (比如malloc) 获得虚拟内存,动态内存分配器维护着进程的一个虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。分配器分为显式分配器和隐式分配器。在维护这些块时有不同的记录方法:
(1)隐式空闲链表

A.找到一个空闲块,有以下适配方法:
首次适配 (First fit):从头开始搜索空闲链表,选择第一个合适的空闲块。此时搜索时间与总块数是线性关系,且倾向在靠近链表起始处留下小空闲块的
“碎片”,增加对较大块的搜索时间
下一次适配 (Next fit):和首次适配相似,只是从链表中上一次查询结束的地方开始,这种适配比首次适应更快,可以避免重复扫描那些无用块
最佳适配 (Best fit):查询链表,选择一个最好的空闲块:满足适配且剩余空闲空间最少,保证碎片最小,提高内存利用率,运行速度通常会慢于首次适配
B. 分割 (splitting):申请空间比空闲块小,可以把空闲块分割成两部分
C. 释放并分配:清除已分配标志,合并相邻的空闲块,和下一个空闲块合并或者双向合并
(2)显式空闲链表:

显式空闲链表采用的方式是维护空闲块链表, 而不是所有块。在空闲块中储存前/后指针,而不仅仅是大小,此外还需要边界标记,用于块合并。幸运的是,只需跟踪空闲块,因此可以使用有效载荷区域
A.维护显式空闲链表方法:
LIFO(last-in-first-out)策略:后进先出法。将新释放的块放置在链表的开始处。此方法优点是简单,常数时间,缺点是研究表明碎片比地址顺序法更糟糕
地址顺序法(Address-ordered policy):按照地址顺序维护链表。addr(前一个块) < addr(当前回收块) < addr(下一个块)。此方法优点是研究表明碎片要少于LIFO,缺点是需要搜索
(3)分离的空闲链表:
分离存储,是一种流行的减少分配时间的方法。一般思路是将所有可能的块大小分成一些等价类/大小类。
分配器维护着一个空闲链表数组,每个大小类一个空闲链表,按照大小的升序排列。
维护方法:
1.简单分离存储
每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小。
2.分离适配
每个空闲链表是和一个大小类相关联的,并且被组织成某种类型的显示或隐式链表,每个链表包含潜在的大小不同的块,这些块的大小是大小类的成员。
7.10本章小结
本章介绍了存储器地址空间、段式管理、页式管理,VA到PA的变换、物理内存访问,还介绍了进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理在真正运程序的时候。同时讲述了在涉及到系统管理内存时,系统该采用什么样的方法,提高程序运行效率进而减少时间
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
一个Linux文件就是一个m个字节的序列,所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这个设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得输入和输出都能以一种统一且一致的方式的来执行。
一个应用程序通过要求内核打开相应的文件来宣告它想访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,而文件的相关信息由内核记录,应用程序只需要记录这个描述符。
Linux shell创建的每个进程开始时都包含标准输入、标准输出、标准错误三个文件,供其执行过程中使用。
对于每个打开的文件,内核保持着一个文件位置k,初始为0,即从文件开头起始的字节偏移量,应用程序能够通过执行seek操作来显式的改变其值。
至于读操作,就是从文件复制n个字节到内存,并将文件位置k增加为k + n。当k大于等于文件大小时,触发EOF条件,即读到文件的尾部。
最后,在结束对文件的访问后,会通过内核关闭这个文件,内核将释放打开这个文件时创建的数据结构,并将描述符恢复到可用的描述符池中。
8.2 简述Unix IO接口及其函数
打开文件:int open(char *filename,int flags,mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,而mode参数指定了新文件的访问权限位。

关闭文件:int close(int fd);
fd是需要关闭的文件的描述符,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的当前文件位置。write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
Printf函数的实现:

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
其中*fmt是格式化用到的字符串,而后面省略的则是可变的形参,即printf(“%d”, i)中的i,对应于字符串里面的缺省内容。

va_start的作用是取到fmt中的第一个参数的地址,下面的write来自Unix I/O,而其中的vsprintf则是用来格式化的函数。这个函数的返回值是要打印出的字符串的长度,也就是write函数中的i。该函数会将printbuf根据fmt格式化字符和相应的参数进行格式化,产生格式化的输出,从而write能够打印。

在Linux下,write函数的第一个参数为fd,也就是描述符,而1代表的就是标准输出。查看write函数的汇编实现可以发现,它首先给寄存器传递了几个参数,然后调用syscall结束。write通过执行syscall指令实现了对系统服务的调用,从而使内核执行打印操作。

内核会通过字符显示子程序,根据传入的ASCII码到字模库读取字符对应的点阵,然后通过vram(显存)对字符串进行输出。显示芯片将按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),最终实现printf中字符串在屏幕上的输出。
8.4 getchar的实现分析
Getchar函数代码:

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
在需要从键盘读入一个字符时,内核中断当前进程,控制权交给键盘中断处理程序。getchar函数中调用了read系统函数,通过系统调用读取按键ascii码,保存到系统的键盘缓冲区,直到接受到回车键才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾则返回-1(EOF),且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取
8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf函数和getchar函数。
(第8章1分)
结论
Hello的历程:
1、编写代码:用高级语言写.c文件
2、预处理:从.c生成.i文件,将.c中调用的外部库展开合并到.i中
3、编译:由.i生成.s汇编文件
4、汇编:将.s文件翻译为机器语言指令,并打包成可重定位目标程序hello.o
5、链接:将.o可重定位目标文件和动态链接库链接成可执行目标程序hello
6、运行:在shell中输入命令
7、创建子进程:shell用fork为程序创建子进程
8、加载:shell调用execve函数,将hello程序加载到该子进程,映射虚拟内存
9、执行指令:CPU为进程分配时间片,加载器将计数器预置在程序入口点,则hello可以顺序执行自己的逻辑控制流
10、访问内存:MMU将虚拟内存地址映射成物理内存地址,CPU通过其来访问
11、动态内存分配:根据需要申请动态内存
12、信号:shell的信号处理函数可以接受程序的异常和用户的请求
13、终止:执行完成后父进程回收子进程,内核删除为该进程创建的数据结构
至此,hello运行结束
(结论0分,缺失 -1分,根据内容酌情加分)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值