HIT CS:APP Hello’s P2P

第1章 概述

1.1 Hello简介

hello的P2P(From Program to Process):
首先我们通过键盘输入代码得到hello.c程序。然后hello.c经过预处理形成hello.i文本文件,接着编译生成hello.s汇编语言文本文件,其次经过汇编器,它成为hello.o可重定位目标程序二进制文件,最终链接器处理合并,结果得到可执行目标程序hello。执行该目标文件,操作系统会使用fork函数形成一个子进程,使用execve函数加载此进程。至此,hello由一个‘程序’变成了‘进程’。
hello的020(From Zero-0 to Zero-0):
在hello运行的过程中。 程序对数据进行处理时,其空间在内存上申请。shell 为其映射虚拟内存,CPU访问相关数据需要MMU的虚拟地址到物理地址的转化,其中TLB和四级页表提高了地址翻译的速度。计算机的三级高速缓存结构以下一级作为上一级的缓存,让hello的数据能够从磁盘传输到寄存器。CPU为运行的hello分配时间片,执行逻辑控制流。操作系统将I/O设备都抽象为文件,让hello程序能够调用硬件进行从键盘读入字符,向屏幕输出内容的输入输出。最后shell负责回收hello进程,内核删除相关数据,释放其运行过程中占用的内存空间。

1.2 环境与工具

1.硬件环境:Intel Core i5-7300HQ x64CPU @2.50GHz;8.00GB;
2.软件环境:Windows 10 64 位操作系统;VMware 14;Ubuntu18.04.1 LTS
3.开发与调试工具:GCC;EDB;READELF;objdump;gedit;

1.3 中间结果

名称 作用 生成指令
hello.i 预处理后的文本文件 gcc -E hello.c -o hello.i
hello.s 编译后的汇编文件 gcc -S hello.i -o hello.s
hello.o 汇编后的可重定位目标文件 gcc -no-pie -fno-PIC -c hello.c -o hello.o
hello 链接后的可执行目标文件 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
helloo.elf hello.o的ELF格式 readelf -a hello > helloo.elf
hello.elf hello的ELF格式 readelf -a hello > hello.elf
helloo.txt hello.o的反汇编代码 objdump -d -r hello.o > helloo.txt
hello.txt hello的反汇编代码 objdump -d -r hello > hello.txt

1.4 本章小结

本章介绍了hello从p2p到020全过程,大致介绍了hello的完整而又波折的一生,并且列出了做本次作业的软硬件环境以及开发和调试工具,最后列出了本次作业过程中产生的中间文件。

第2章 预处理

2.1 预处理的概念与作用

 图2.1 hello.c预处理在全过程中的位置
概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。
作用:预处理在源代码编译之前对其进行的一些文本性质的操作,对源程序编译之前做一些处理,生成扩展的C源程序。

  • 1.处理#include预编译指令,将头文件中的内容(源文件之外的文件)插入到源文件中。
  • 2.进行了宏替换的过程,定义和替换了由#define指令定义的符号
  • 3.删除掉注释的过程,注释不会带入到编译阶段
  • 4.处理所有条件编译指令,如#if,#ifdef等;

2.2在Ubuntu下预处理的命令

命令:gcc -E hello.c -o hello.i
图2.2   使用gcc命令生成hello.i文件
图2.3 Hello的预处理结果解析
打开hello.i发现程序已经拓展为3118行,只有最后数十行是原hello.c程序内容。观察可发现,程序开头注释已被删除,并且头文件以被插入到源文件中。
图2.3   hello.i文件
打开hello.i后发现预处理具体包括对头文件的包含,宏定义的扩展,条件编译的选择等。预处理主要是根据#符号进行处理,在hello.c中,就是将相应的.h文件的内容插入到源程序文件中,再对宏定义进行拓展,再进行条件编译的选择,即根据条件值来决定是否执行包含其中的逻辑。

2.4 本章小结

本章介绍了在C语言编译的时候,首先经历的预处理阶段。有关于预处理的定义及作用、Linux下预处理的命令及结果解析。
预处理功能是编译程序的第一环,也不能忽视——合理地使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

第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.字符串:
“Usage: Hello 学号 姓名!\n”,“Hello %s %s\n”
在这里插入图片描述
在这里插入图片描述
可以看出两个字符串都声明在了.rodata 只读数据节。
2.整数:
int sleepsecs在hello.c程序中为已经被赋值全局变量,因为.data节存放已经初始化的全局和静态C变量,可知sleepsecs因存放在.data节中。如下图3.4。
在这里插入图片描述
int i局部变量存储在寄存器或者栈中,这里存储在栈-4(%rbp)中。
在这里插入图片描述

3.3.2 赋值

int sleepsecs = 2.5该变量是全局变量,直接在.data节中声明为值2的long类型数据。(因为int类型,所以不是2.5)
在这里插入图片描述
i=0使用mov指令完成。
在这里插入图片描述

3.3.3 类型转换

隐式类型转换:
全局变量int sleepsecs=2.5,将浮点数类型的2.5转换为int型。浮点数转换为整型,遵循向偶数舍入的原则,将2.5舍入为2。

3.3.4 算数操作

hello.c中涉及的算数操作有:i++
在这里插入图片描述

3.3.5 关系操作

hello.c中涉及的关系操作有:
argc!=3 计算argc-3然后设置条件码,如果等于0就进行跳转。
在这里插入图片描述
i<10计算i-9然后设置条件码,如果小于等于0就进行跳转,重复循环。
在这里插入图片描述

3.3.6 数组/指针/结构操作

char *argv[] ,主函数传入的第二个参数,指针类型数组。
在这里插入图片描述
在64位系统里。无论什么类型的指针大小都是8字节,main函数中访问数组元素argv[1],argv[2]时,从起始地址-16(%rbp)开始每8字节取一次指针数据。

3.3.7 控制转移

hello.c中涉及的控制转移有:
if (argv!=3):
在这里插入图片描述
首先使用cmpl指令,计算argc-3然后设置条件码,使用je判断ZF标志位,如果为0,说明 argv==3,不执行if中的代码跳转到.L2,否则执行if中的代码。
for(i=0;i<10;i++):
在这里插入图片描述
首先无条件跳转到位于.L3的比较代码如果i<=9,则跳入.L4执行循环体,否则说明循环结束。

3.3.7 函数操作

C语言函数操作包含参数传递、函数调用和函数返回功能。
hello.c中涉及的函数操作的有:
printf函数:
A:将%rdi设置为“Usage: Hello 学号 姓名!\n”字符串的首地址,作为传入的参数。
B:设置%rdi为“Hello %s %s\n”的首地址,设置%rsi为argv[1],%rdx为argv[2]。
(两次区别:第一次printf因为只有一个字符串参数,所以call puts@PLT;第二次printf传入三个参数使用call printf@PLT)

exit函数:将%edi设置为1,作为传入参数。

sleep函数: 将%edi设置为sleepsecs,作为传入参数。

getchar函数: 略

3.4 本章小结

本章主要介绍了编译的概念和作用以及编译指令和C语言的各类操作的汇编代码。
编译这个阶段编译器主要做词法分析、语法分析、语义分析等,在检查无错误后后,把代码翻译成汇编语言。 编译器将文本文件hello.i 翻译成文本文件hello.s, 它包含一个汇编语言程序,即一条低级机器语言指令。

第4章 汇编

4.1 汇编的概念与作用

概念:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o 中。
作用:汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。

4.2 在Ubuntu下汇编的命令

gcc -no-pie -fno-PIC -c hello.c -o hello.o
在这里插入图片描述

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。使用readelf -a hello.o > hello.elf指令——
1.ELF头:以一个16字节的序列开始,这个描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、 字节头部表的文件偏移,以及节头部表中条目的大小和数量。
在这里插入图片描述
2.节头部表:不同节的位置和大小。
在这里插入图片描述
3.rela.text:一个.text 节中位置的列表,包含.text节中需要进行重定位的信息。
在这里插入图片描述
4.rela.eh_frame:eh_frame节的重定位信息
在这里插入图片描述
5.symtab:一个符号表,它存放程序中定义和引用的函数和全局变量的信息。
在这里插入图片描述

4.4 Hello.o的结果解析

使用objdump -d -r hello.o > helloo.txt分析hello.o的反汇编,并与第3章的hello.s进行对照分析。
发现有以下几点不同——
1.函数调用:原来的函数名字被替换成了函数的相对偏移地址
2.跳转语句:由原来的段地址变成了相对偏移地址。
3.全局变量:由使用段名称+%rip,变成了在反汇编代码中0+%rip。这是因为 rodata中数据地址也需要重定位。
4.操作数:由hello.s里面的十进制,变成了在反汇编代码中的十六进制。

汇编语言以及机器码的对应:
一条指令就是机器语言的一个语句,是一组有意义的二进制代码。指令的基本格式:操作码字段和地址码字段,其中操作码指明了指令的操作性质及功能,地址码则给出了操作数或操作数的地址。

4.5 本章小结

本章介绍了从hello.s到hello.o的汇编过程,首先理清了汇编的概念和作用,知道了Linux下如何汇编以及反汇编。查看hello.o的可重定位目标elf格式,并通过使用objdump得到反汇编代码与hello.s进行比较。
汇编器将汇编语言转化成机器语言,机器语言是用二进制代码表示的计算机能直接识别和执行的一种机器指令的集合。汇编语言作为最基础也是最古老的语言我们也需要加以了解,这将有助于我们编写出质量更高的代码。

第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 -a hello > helloo.elf命令列出其各段的基本信息,包括各段的起始地址,大小等信息。节头对给出了所有的节的偏移位置和大小。
在这里插入图片描述

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
在这里插入图片描述
在0x400000~0x401000段中,程序被载入。之间每个节的排列即开始结束同5.3图中各段声明。

5.5 链接的重定位过程分析

使用 objdump -d -r hello.o > helloo.txt指令获得hello.o的反汇编代码。使用objdump -d -r hello > hello.txt指令获得hello的反汇编代码。
在这里插入图片描述
先使用文本编辑器对二者进行对比——总览如下图,下面依次进行细节分析
在这里插入图片描述
hello相对于hello.o反汇编后有如下不同:
1.增加了节

hello.o反汇编 .text - - -
hello反汇编 .text .init .plt .fini

2.链接器加入外部函数。如初始化函数_init,程序中用到的printf、sleep、getchar、exit函数等等。
3.重定位符号引用。如对跳转以及函数调用的地址,对于它们链接器修改代码节和数据节中对每个符号的引用,使其指向正确的虚拟内存地址。
4.重定位符号定义,链接器计算相对距离,将运行时内存地址赋给新的节,以及赋给输入模块定义的每一个符号(函数)。

链接的过程:
链接需要完成两个主要任务,首先是符号解析,将每一个符号引用正好和一个符号定义关联起来。其次进行重定位,重定位又由两部组成:
1.重定位节和符号定义:链接器将所有输入相同类型的节合并为同一类型的新的聚合节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每一个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
2.重定位节中的符号引用,链接器会修改代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。

5.6 hello的执行流程

使用edb执行hello,说明从加载hello到_start到call main,以及程序终止的所有过程。列出其调用与跳转的各个子程序名或程序地址。
程序名称 程序地址

假装有表
----

5.7 Hello的动态链接分析

观察dl_init前后动态链接项目的变化:首先知道.got.plt节的起始地址是 0x601000,在DataDump中找到该位置。
在这里插入图片描述
图 5.6 dl_init 前的.got.plt 节
在这里插入图片描述
图 5.7 dl_init 后的.got.plt 节
使用edb执行至dl_init,发现在地址0x601000 发生了变化:(如上图5.6和5.7) 可以看到 dl_init 后出现了两个地址,0x7f74faad8170 和 0x7f74fa8c6750。这其实就是GOT[1]和 GOT[2]。
由此可以印证动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。而且需要用到动态链接库。比如在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。

5.8 本章小结

本章中主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。
本章我们对链接的步骤和过程进行了详细的分解和解析,得知链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块。

第6章 hello进程管理

6.1 进程的概念与作用

概念:进程是一个执行中程序的实例。是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
作用:进程为应用程序提供了关键抽象——一个独立的逻辑控制流,它提供一个假象,好像是我们的程序独占的使用处理器;一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用内存系统。

6.2 简述壳Shell-bash的作用与处理流程

作用:
Shell是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。主要功能如下:
1.可交互,和非交互的使用shell。在交互式模式,shell从键盘接收输入;在非交互式模式,shell从文件中获取输入。
2. shell中可以同步和异步的执行命令。在同步模式,shell要等命令执行完,才能接收下面的输入。在异步模式,命令运行的同时,shell就可接收其它的输入。重定向功能,可以更细致的控制命令的输入输出。另外,shell允许设置命令的运行环境。
3. shell提供了少量的内置命令,以便自身功能更加完备和高效。
4. shell除了执行命令,还提供了变量,流程控制,引用和函数等,类似高级语言一样,能编写功能丰富的程序。

处理流程:
1.读取输入的命令行.
2.解析引用并分割命令行为各个单词,各单词称为token。其中重定向所在的token会被保存下来,直到扩展步骤(5)结束后才进行相关处理,如进行扩展、截断文件等。
3.检查命令行结构。主要检查是否有命令列表、是否有shell编程结构的命令,如if判断命令、循环结构的for/while/select/until,这些命令属于保留关键字,需要特殊处理。
4.对第一个token进行别名扩展。如果检查出它是别名,则扩展后回到(2)再次进行token分解过程。如果检查出它是函数,则执行函数体中的复合命令。如果它既是别名,又是函数(即命令别名和函数同名称的情况),则优先执行别名。在概念上,别名的临时性最强,优先级最高。
5.进行各种扩展。扩展顺序为:大括号扩展;波浪号扩展;参数、变量和命令替换、算术扩展(如果系统支持,此步还进行进程替换);单词拆分;文件名扩展。
6.引号去除。
7.搜索和执行命令。
8.返回退出状态码。

6.3 Hello的fork进程创建过程

键入‘./hello 1170301023 王琦’后,Shell解析命令行,发现不是内置命令,判断为执行当前目录的可执行目标文件,通过调用fork函数创建一个新的运行的子进程int fork(void)。其中子进程返回0,父进程返回子进程的PID,新创建的子进程几乎但不完全与父进程相同:子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本(代码、数据段、堆、共享库以及用户栈),但有不同于父进程的PID。
在这里插入图片描述

6.4 Hello的execve过程

execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到filename,execve 才会返回到调用程序。与fork一次调用返回两次不同,execve调用一次并从不返回。在execve加载了filename之后,调用启动代码,启动代码设置栈,并将控制传递给新程序的主函数。

6.5 Hello的进程执行

内核为每个进程维持一个上下文,上下文是内核重新启动一个被抢占的进程所需要的状态。内核可以抢占当前进程,并重新开始一个被抢占的进程所需的状态 ,叫做调度。如果hello程序不被抢占则顺序执行,假如发生被抢占的情况,则使用上下文切换机制将控制转移到新的进程,需要:1.保存以前进程的上下文;2.恢复新恢复进程被保存的上下文;3.将控制传递给这个新恢复的进程。
当用户态与核心态相互转换时可能发生上下文切换。例如hello初始运行在用户态,在hello进程系统调用sleep函数后,显式地请求让调用进程休眠。将hello进程从运行队列中移出,定时器开始计时。此时发生上下文切换将当前进程的控制权交给其他进程。过程如下图——
在这里插入图片描述
这其中一个进程执行它的控制流的一部分的每一时间段叫做时间片。例如上图每两条横线中间为一个时间片。

6.6 hello的异常与信号处理

1.正常运行:
在这里插入图片描述
2.Ctrl-z:产生SIGSTP信号,将hello进程挂起。
在这里插入图片描述
3.指令ps:
在这里插入图片描述
4.指令jobs:
在这里插入图片描述
5.指令pstree:
在这里插入图片描述
6.指令fg:发送SIGCONT信号继续执行停止进程,将hello调到前台
在这里插入图片描述
7.Ctrl-c:产生SIGINT信号,结束并回收hello进程。
在这里插入图片描述
8.kill -9 pid:发送SIGKILL信号给指定的pid杀死进程。
在这里插入图片描述

6.7本章小结

本章介绍了进程的概念与作用,Shell-bash的作用与处理流程,如何调用fork创建新进程,以及调用execve执行hello,hello的进程执行,hello的异常与信号处理。了解了本章Linux下异常处理的机制,可以使得我们在以后编写程序的时候尽量减少异常的发生。

第7章 hello的存储管理

7.1 hello的存储器地址空间

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:程序代码经过编译后出现在汇编程序中地址。是hello.o的相对偏移地址。
线性地址:是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。是hello里面的虚拟内存地址。
虚拟地址:CPU可以生成一个虚拟地址。虚拟地址是Windows程序时运行在保护模式下,访问存储器所使用的逻辑地址,可以写为“段:偏移量”的形式。是hello里面的虚拟内存地址。
物理地址:计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址。其出现在CPU地址总线上的寻址物理内存的地址信号,是地址变换的最终结果。是hello在运行时虚拟内存地址对应的地址。

7.2 Intel逻辑地址到线性地址的变换-段式管理

在段式存储管理中,将程序的地址空间划分为若干个段,这样每个进程有一个二维的地址空间。系统为每个段分配一个连续的分区,而进程中的各个段可以不连续地存放在内存的不同分区中。程序加载时,操作系统为所有段分配其所需内存,这些段不必连续,物理内存的管理采用动态分区的管理方法。
在为某个段分配物理内存时,可以采用首先适配法、下次适配法、最佳适配法等方法。在回收某个段所占用的空间时,要注意将收回的空间与其相邻的空间合并。
为了实现段式管理,操作系统需要如下的数据结构来实现进程的地址空间到物理内存空间的映射,并跟踪物理内存的使用情况,以便在装入新的段的时候,合理地分配内存空间。在这里插入图片描述
在段式管理系统中,整个进程的地址空间是二维的,即其逻辑地址由段号和段内地址两部分组成。为了完成进程逻辑地址到物理地址的映射,处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到实际的物理地址。操作系统需在进程切换时,将进程段表的首地址装入处理器的段表地址寄存器。
在这里插入图片描述

7.3 Hello的线性地址到物理地址的变换-页式管理

线性地址到物理地址的变换是通过分页机制完成的。
将程序的逻辑地址空间划分为固定大小的页,而物理内存划分为同样大小的页框。程序加载时,可将任意一页放人内存中任意一个页框,这些页框不必连续,从而实现了离散分配。该方法需要CPU的硬件支持,来实现逻辑地址和物理地址之间的映射。在页式存储管理方式中地址结构由两部构成,前一部分是页号,后一部分为页内地址w。
在页式系统中进程建立时,操作系统为进程中所有的页分配页框。当进程撤销时收回所有分配给它的页框。在程序的运行期间,如果允许进程动态地申请空间,操作系统还要为进程申请的空间分配物理页框。操作系统必须记录系统内存中实际的页框使用情况。为了完成上述的功能,一般要采用如下的数据结构。
在这里插入图片描述
在页式系统中,指令所给出的地址分为两部分:虚拟页号和虚拟页偏移量。CPU中的内存管理单元按逻辑页号通过查进程页表得到物理页框号,将物理页框号与页内地址相加形成物理地址。
虚拟页号和虚拟页偏移量->查进程页表,得物理页号->物理地址:
在这里插入图片描述
上述过程通常由处理器的硬件直接完成,不需要软件参与。通常,操作系统只需在进程切换时,把进程页表的首地址装入处理器特定的寄存器中即可。一般来说,页表存储在主存之中。这样处理器每访问一个在内存中的操作数,就要访问两次内存:第一次用来查找页表将操作数的 逻辑地址变换为物理地址;第二次完成真正的读写操作。

7.4 TLB与四级页表支持下的VA到PA的变换

在这里插入图片描述
TLB通过虚拟地址VPN部分进行索引,分为索引与标记两个部分。若TLB命中,则从TLB中可以直接找到各级页表,然后得到PPN,与PPO结合即可得到物理地址。若TLB不命中,则需要从高速缓存中到PPN。

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

得到物理地址后,对物理地址进行分析,将其拆分成CT(缓存标记位)、CI(缓存组索引)、CO(缓存偏移)。首先在一级cache中寻找:如果匹配成功且块的标志位为1,则命中(hit),根据数据偏移量CO取出数据返回。如果没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中查询数据,找到之后返回结果。具体处理如下——
Cache读策略:
1.命中,则从cache中读相应数据到CPU或上一级cache中。
2.失败,则从主存或下一级cache中读取数据,并替换出一行数据,通常采用LRU算法。
Cache写策略:
1.命中
(1)写回:只写本级cache,暂时不写数据到主存或下一级cache,等到该行被替换出去时,才将数据写回到主存或下一级cache。
(2)直写:写本级cache,同时写数据到主存或下一级cache,等到该行被替换出去时,就不用写回数据了。
2.失败
(1)按写分配,分两种:
[1]先写数据到主存或下一级cache,并从主存或下一级cache读取刚才修 改过的数据,即:先写数据,再为所写数据分配cache line;
[2]先分配cache line给所写数据,即:从主存中读取一行数据到cache, 然后直接对cache进行修改,并不把数据到写到主存或下一级cache,一直等 到该行被替换出去,才写数据到主存或下一级cache。
(2)写不分配:
直接写数据到主存或下一级cache,并且不从主存或下一级cache中读取 被改写的数据,即:不分配cache line给被修改的数据。

7.6 hello进程fork时的内存映射

当fork函数被shell调用时,内核为新进程hello创建各种数据结构,并分配给它一个唯一的PID。为了给新进程hello创建虚拟内存,它创建了当前进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork 在新进程hello中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

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

7.8 缺页故障与缺页中断处理

缺页故障:DRAM缓存不命中称为缺页。如CPU引用了VP 3中的一个字, VP 3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE3, 从有效位推断出VP3 未被缓存,并且触发一个缺页异常。
在这里插入图片描述
缺页中断处理:缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,然后将控制返回给引起故障的指令。当指令再次执行,相应的物理页面已经驻留在内存中了,指令可以没有故障地运行完成。

7.9动态存储分配管理

动态内存分配器的基本原理:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器主要有:显式分配器,要求应用显式地释放任何已分配的块;
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块, 自动释放未使用的已经分配的块的过程叫做垃圾收集。

带边界标签的隐式空闲链表分配器原理:
隐式空闲链表并不直接对空闲块进行链接,而是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合,将所有块组织成一个大链表。其中Header和Footer中的block大小间接起到了前驱、后继指针的作用。
带边界标签的空闲块合并:边界标记在每个块的结尾处添加脚部(是头部的一个副本)。分配器可以通过检查脚部,判断前面一个块的起始位置和状态。脚部在据当前块开始位置一个字的距离。
在这里插入图片描述

情况1:两个邻接的块都是已分配的,不可能合并,当前块的状态从已分配变成空闲。
情况2:当前块与后面的块合并,用当前块与后面块的大小的和来更新当前块的头部和后面块的脚部。
情况3:前面块与当前块合并,用两个块的大小的和来更新前面块的头部和当前块的脚部。
情况4:合并三个块形成一个单独的空闲块,用三个块的大小的和来更新前面块的头部和当前块的脚部。
显式空闲链表的基本原理:
显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继指针。
使用双向链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。释放一个块的时间可以是线性的,也可能是常数,取决于我们所选择的空闲链表中块的排序策略。
维护链表:
1.后进先出(LIFO)的顺序,将新释放的块位置放在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块。在这种情况下,释放一个块可以在常数时间内完成。如果使用了边界标记,那么合并也可以在常数时间内完成。
2.按照地址顺序,其中链表中每个块的地址都小于它后继的地址。在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序的首次适配比LIFO排序的首次适配有更高的存储器利用率,接近最佳适配的利用率。

7.10本章小结

本章介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以 介绍了TLB和四级页表支持下的VA到PA的变换还有三级cache支持下的物理内存访问,还介绍了hello进程fork时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
虽然hello是一个最基础的程序,但是它却具有相当的代表性。许许多多的进程就相当于但无数个hello放在一起管理,计算机需要有逻辑清晰的存储和访问机制才能保证访存的速度。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件
设备管理:所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这使得所有的输入和输出都能以一种统一且一致的方式来执行。
1.打开文件。
2.改变当前的文件位置。
3.读写文件。
4.关闭文件。

8.2 简述Unix IO接口及其函数

Unix IO接口:
将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,以文件的方式对I/O设备进行读写,将设备均映射为文件。对文件的操作,即Unix I/O接口。

函数:

1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O设备。内核返回一个小的非负整数叫做描述符。它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。Linux shell创建的每个进程开始时都有三个打开的文件:标准输入、标准输出和标准错误。
进程是通过调用open 函数来打开一个已存在的文件或者创建一个新文件的:
int open(char *filename, int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。返回,若成功则为新文件描述符,若出错为-1。

2.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k≥m时执行读操作会触发一个称为end-of-file的条件,应用程序能检测到这个条件。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。返回,若成功则为读的字节数,若EOF则为0, 若出错为-1。
write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。返回,若成功则为写的字节数,若出错则为-1。

3.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件,作为相应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程以何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。关闭文件是通知内核你要结束访问一个文件。返回,若成功则为0, 若出错则为-1。

8.3 printf的实现分析

printf函数代码如图8.5所示:
在这里插入图片描述

我们可以发现,它调用了两个外部函数,一个是vsprintf,还有一个是write。vsprintf函数的作用是将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度write函数是将buf中的i个元素写到终端的函数。
在这里插入图片描述

从vsprintf生成显示信息,传送到write系统函数,到陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序。从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

#define getchar() getc(stdin)。getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中,直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
在这里插入图片描述
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章讲述了linux的I/O设备管理机制,了解了打开、关闭、读写、转移文件的接口及相关函数,简单分析了printf和getchar函数的实现方法以及操作过程。

结论

至此,我们已经遍历了程序hello的一生,虽然它是极为简单和基础的,但是它的一生却丝毫不注水,不拖沓,显得井井有条,充满了程序员的‘逻辑美’。下面我们来简单地回顾一下hello的精彩的一生——

  1. hello.c经过预处理器 cpp 的预处理得到 hello.i。
  2. 编译器ccl将得到的 hello.i 编译成汇编文件 hello.s。
  3. 汇编器as将hello.s翻译成机器语言指令得到可重定位目标文件hello.o
  4. 链接器ld将hello.o与动态链接库链接生成可执行目标文件hello
  5. 在shell中输入./hello 1170301023 王琦,内核为hello fork一个子进程。
    6.execve 通过加载器将hello中的代码和数据从磁盘复制到内存,为其创建虚拟内存映像,加载器在程序头部表的引导下将hello的片复制到代码段和数据段,执行_start函数。
  6. 当CPU访问 hello 时,请求一个虚拟地址,MMU通过页表将虚拟地址映射到对应的物理地址通过三级缓存结构完成访存。
  7. hello运行过程中可能遇到各种信号,shell 为其提供了各种信号处理程序。
    9.内核通过调度完成hello和其他所有进程的上下文切换,成功运行hello。
  8. Unix I/O 帮助 hello 实现了输出到屏幕和从键盘输入的功能。
  9. 最后 hello 执行 return 0;shell父进程回收hello,内核删除hello进程的所有痕迹。
    hello的一生圆满结束,它死而无憾,因为有我,以及许许多多的准备役程序员们的见证!

猜你喜欢

转载自blog.csdn.net/wq_xiaozz/article/details/85461183
今日推荐