计算机系统

大作业

题     目  程序人生-Hellos P2P  

专       业             计算学部           

学     号           120L020319             

班   级            2003010            

学       生             刘轩     

指 导 教 师                郑贵滨    

计算机科学与技术学院

2022年5月

摘要     

                                                本文通过跟踪 hello 程序在 linux 系统下的生命周期——从它被程序员创建开始到在系统上运行,响应输入信号,输出简单消息,然后终止,来复习并贯通计算机系统这门课程的知识体系。

关键词:HIT,hello,Linux,计算机系统;                            

(摘要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简介

P2P:通过键盘输入完成 hello.c程序的编程,然后 hello.c源程序经过 cpp 预处理器、ccl 编译器、as 汇编器和 ld 链接器,翻译成一个可执行目标文件 hello。在 shell中输入./hello命令,系统会为 hello程序fork 一个子进程。这样,hello.c 就从程序变成了进程。

O2O:shell接受命令后,映射虚拟内存,当 CPU引用一个被映射的虚拟页时,可执行目标文件hello中的数据从磁盘复制到物理内存,然后通过跳转到程序入口点_start 函数的地址,最终调用执行 hello中的main函数,内核为 hello进程分配时间片执行逻辑控制流。程序运行结束后,父进程会回收这个僵死子进程,内核会从系统中彻底删除它,这就是From Zero-0 to Zero-0。

1.2 环境与工具

1.2.1 硬件环境

CPU:Intel(R) Core(TM) i7-8550U CPU@ 1.80GHz 1.99GHz

内存:8.00GB(7.90GB可用)

磁盘: 360GB SSD + 500GB HDD

1.2.2 软件环境

Windows10 64位;

Oracle VM VirtualBox

Ubuntu 18.04 64位;

1.2.3工具

gedit,gcc, readelf, objdump, hexedit, edb

1.3 中间结果

hello.c

hello源代码

hello.i

预处理之后的文本文件

hello.s

hello的汇编代码

hello_o-objdump.txt

hello.o的反汇编代码

hello-objdump.txt

hello的反汇编代码

hello.o

hello的可重定位文件

hello

hello的可执行文件

hello.elf

hello的elf文件

hello_o.elf

hello.o的elf文件

1.4 本章小结

本章对hello的P2P和O2O过程做了简介,列出了支持这次实验的环境和工具以及实验中生成的中间结果。

(第1章0.5分)


第2章 预处理

2.1 预处理的概念与作用

预处理是指程序源代码被翻译成目标代码、生成二进制文件之前的过程。是由预处理器(cpp)对源代码文本进行处理,得到的结果再交给编译器进一步编译。这个过程会把源代码分割处理成特定的单位,用以支持语言特性。

预处理器为根据以#为开头的命令修改源程序,比如c程序开头的#include<stdio.h>告诉预处理器读取系统头文件stdio.h的内容,把它直接插人程序文本中,并拓展所有用#define声明指定的宏。结果就得到了另一个C程序,通常是以.i作为文件扩展名。

2.2在Ubuntu下预处理的命令

图 2.1 预处理生成 hello.i 文件

2.3 Hello的预处理结果解析

                图 2.2 hello.i 文件截图

#后的数字是源代码的行号,后面跟着的文件名是它来自的文件,最后的数字是标志,1指文件开始,2指返回文件,3指来自系统头文件,4指文本被视为包含在隐式 extern "C"块中。

整个程序长达3060行,预处理器在处理过程中会删除注释行,递归处理 “#include”预编译指令,将被包含的文件插入到该预编译指令的位置,还会进行#define的宏替换,从3047行开始是main函数的内容:

图 2.3 hello.i 中的main函数

2.4 本章小结

本章介绍了预处理及其相关操作。预处理是hello.c源程序在编译系统里走过的第一步,它主要是拓展源代码,插入所有#include指定的文件,拓展#define声明等。经过这个阶段,我们得到了一个hello.i。

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

编译是指编译器(ccl)将文本文件hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式描述了一条低级机器语言指令。

汇编语言是很有用的,它为不同语言的不同编译器提供了通用的语言

3.2 在Ubuntu下编译的命令

                       图 3.1 编译生成 hello.s 文件

3.3 Hello的编译结果解析

                       图 3.2 hello.s 文件部分截图

如图3.2所示,所有以.为开头的都是伪命令

下面分析C语言的数据与操作:

3.3.1 数据

c语言中的数据大致有常量,变量,表达式,类型等等。

常量大多以立即数形式出现

                  图 3.3

比如图3.3中的4对应源程序中的argc!=4中的4,图3.4中的1对应exit(1)中的1

          图3.4

变量有全局变量,局部变量,它们分别存放在.data节和.bss节,但由于hello.c中没有全局变量和局部变量,所以开头的伪指令没有.data和.bss,但有一个只读数据节.rodata,这里面存放了printf的格式串。源程序中的printf("用法: Hello 学号 姓名 秒数!\n");的汇编代码是movl $.LC0, %edi 和call puts,LC0中正是要输出的字符串,汉字转化成了相应的编码:

                    图3.5 hello.s中的LC0

源程序中有3个表达式:i=0,argc!=4和i<8,argc!=4图3.3已解释过,i=0和i<8的汇编代码分别见图3.6和3.7,其中编译器把i<8解释成了i<=7

图3.6  i=0的汇编代码

            图3.7   i<8的汇编代码

3.3.2 操作

算术操作:hello.s中使用addl $1, -4(%rbp)实现i++的算术操作

赋值操作:用mov指令,movl $0, -4(%rbp)实现i=0的赋值操作

关系操作:用cmp指令,cmpl $4, -20(%rbp)用来实现argc!=4关系操作,cmpl $7, -4(%rbp)用来实现i<8的关系操作

控制转移:用jump指令,

                      图3.8 if结构

图3.8是if结构的实现,用je比较argc和4是否相等,若相等则跳转到L2。

for循环:

首先对i赋值为0,然后跳进循环判断,最后用add指令实现i++,如图3.8

图3.8  for循环

数组操作:程序中唯一的数组是argv,如图3.9所示,$16,%rax,$8,%rax分别对应源程序中的argv[1],argv[2],表明数组元素存放在连续的地址中

图3.9  数组操作

函数操作:

printf函数:

图3.10   printf函数

直接将参数.LC0传递给%edi,然后调用puts函数

exit函数:

图3.11   exit函数

直接把1传递给%edi,然后调用exit函数

sleep和atoi函数:

图3.12  sleep和atoi函数

把-32(%rbp)的值传递给%rax,%rax再加上24,得到argv[3],传递给%rdi,调用atoi函数,再将返回值%rax作为参数传递给sleep函数。

函数在调用时通过call语句,将返回地址压入栈中,并将PC设置为调用函数的起始地址,结束时通过%rax作为返回值,并通过ret指令从栈中弹出返回地址,并将PC设为返回地址。

3.4 本章小结

本章介绍了编译及其相关操作。编译是将hello.i编译成汇编文本文件hello.s。理解汇编代码是一项很重要的技能,通过理解这些汇编代码,我们能够理解编译器的优化能力,从而找出更多优化程序性能的方法。

(第3章2分)


第4章 汇编

4.1 汇编的概念与作用

汇编是汇编器将hello.s翻译成机器语言指令,并将这些指令打包成可重定位程序的格式,并最终生成目标文件hello.o的过程。hello.o是一个二进制文件,在文本编辑器中打开hello.o,将看到一堆乱码。

4.2 在Ubuntu下汇编的命令

图4.1  汇编产生的 hello.o 文件

4.3 可重定位目标elf格式

使用readelf -a hello.o命令查看信息

4.3.1 ELF头

                         图 4.2 ELF 头

ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF 头剩下的部分包括帮助链接器语法分析和解释目标文件的 信息。其中包括 ELF头大小,目标文件的类型(如可重定位、可执行和可共享的)、 机器类型(如 x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数 量。

4.3.2 节头部表

                          图 4.3 节头部表

描述了不同节的位置和大小。地址一列全为0,说明是虚拟地址, 这表明 hello.o 文件并没有分配运行时地址。

4.3.3

图 4.4

表明本文件中没有上述内容

4.3.4重定位节

重定位节.rela.text:存放着代码的重定位条目。

图 4.5 重定位节.rela.text

重定位节.rela.eh_frame:eh_frame 节的重定位信息:

图 4.6 重定位节.rela.eh_frame

    4.3.5符号表

符号表:用来存放程序中定义和引用的函数和全局变量的信息。

                         图 4.7 符号表

4.4 Hello.o的结果解析

图 4.8 hello.o的反汇编

4.4.1 机器语言的构成

机器语言是用二进制代码表示的计算机能直接识别和执行的一种机器指令的集合。一条指令就是机器语言的一个语句,由操作码和地址码构成,操作码指明了指令的操作性质及功能,地址码则给出了操作数或操作数的地址。如图4.8第一列为操作码,后面跟着的就是地址码。

4.4.2 与汇编语言的映射关系

反汇编器是基于机器代码文件中的字节序列来确定汇编代码的,不需要访问源代码和汇编代码,与gcc产生的汇编代码有细微差别,比如省略了许多指令末尾的q。

反汇编中的立即数是十六进制的,而hello.s中是十进制的。

在分支跳转和函数调用时也有区别:

分支跳转时,比如判断argc!=4,hello.s是跳转到L2

反汇编中则是跳转到地址19:

函数调用时,hello.s是call后直接跟着调用函数名:

而反汇编代码中callq后跟着的是地址值,并且还有一个重定位条目,用于链接时重定位。

4.5 本章小结

本章介绍了汇编及其过程。汇编器将汇编语言翻译成机器语言指令,把这些指令打包成可重定位目标程序,并将结果保存在hello.o中。hello.o文件是一个二进制文件,它包含的是函数main的指令编码。多个可重定位目标文件可以在链接时合并在一起,创建一个可执行的目标文件。

(第4章1分)


5链接

5.1 链接的概念与作用

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行于编译时、加载时、运行时。在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做链接器(linker)的程序自动执行的。

链接使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接,而不必重新其他文件。

5.2 在Ubuntu下链接的命令

                      图 5.1 链接生成 hello 可执行目标文件

5.3 可执行目标文件hello的格式

使用readelf -a hello > hello.elf 命令生成hello 程序的ELF 格式文件。

首先是ELF头,描述文件的总体格式:

                            图 5.2 ELF 头

.text、.rodata和.data节与可重定位目标文件中的节是类似的,这些节已经被重定位到最终运行时的内存地址。

节头:记录每个节的名称、偏移量、大小、位置等信息

                       图 5.3 节头

程序头:

图 5.4 程序头

段节:

图 5.5 段节

Dynamic section:

图 5.6 Dynamic section

重定位节:

图 5.7 重定位节

符号表:

图 5.7 符号表

其他信息:

图 5.8 其他信息

5.4 hello的虚拟地址空间

分析程序头中的LOAD,加载程序段地址为0x401000

使用edb加载hello,在data dump查看本进程的虚拟地址空间各段信息。

图 5.8 edb 中 Data Dump 窗口

虚拟地址0x401000开始,到0x400fff结束可以根据图5.3追溯到各个节

             

    

5.5 链接的重定位过程分析

使用objdump -d -r hello 指令反汇编hello。通过分析main函数来体会hello.o和hello的区别

                   图 5.9 hello反汇编中的main函数

可以看到,call语句后都跟上了函数的地址,例如printf:

它的地址是0x401090,它的下一条语句地址为0x401148,前者减后者得到0xffffff48,再用小端法得48 ff ff ff,这便是地址0x401143后的机器指令中的字节。

再看hello.o反汇编中的printf,每个call后都带有一个重定位条目。

5.6 hello的执行流程

使用edb单步调试运行程序,观察其调用的函数,这里可以发现在调用main之前主要进行了初始化的工作调用了_init,之后调用了_start这个就是起始的地址,准备开始执行main的内容,之后是一系列在这个程序中所用到的库函数(printf,exit,atoi等等),最后调用exit终止程序。

_start   0x4010f0

main     0x401125   

_printf  0x4010b0

_exit    0x4010c0

_sleep   0x4010d0

_getchar 0x4010e0

     

5.7 Hello的动态链接分析

对于动态共享链接库中 PIC 函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,GNU 编译系统使用延迟绑定技术,将过程地址的绑定推迟到第一次调用该过程时。延迟绑定是通过两个数据结构的交互来实现的,这两个数据结构是 GOT(全局偏移量表)和 PLT(过程链接表)。

下面观察 dl_init 前后动态链接项目的变化。查看图5.3可知.got.plt节的起始地址为0x404000,在 DataDump 中找到该位置,

dl_init前:

使用 edb 执行至 dl_init,按 F8,发现地址 0x404000 后发生了变化:

可以看到 dl_init 后出现了两个地址0x7f8a0d3f2190和0x7f8a0d3dbbb0,这便是 GOT[1]和 GOT[2]。

5.8 本章小结

本章介绍了链接及其过程。分析了 hello的 ELF文件格式,查看了hello的虚拟地址空间,对链接的重定位过程进行了分析,分析了 hello 的执行流程和动 态链接分析。

(第5章1分)


6hello进程管理

6.1 进程的概念与作用

进程是一个执行中的程序的实例,同时也是系统进行资源分配和调度的基本单位。每一个进程都有它自己的地址空间,包括文本区域、数据区域和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。

进程给应用程序提供两个关键抽象:一个独立的逻辑流,它提供一个假象,好像我们的程序独占地使用处理器;一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。

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

由于安全、复杂、繁琐等原因,用户不能直接接触内核,需要另外再开发一个程序,让用户直接使用这个程序,该程序的作用就是接收用户的操作,并进行简单的处理,然后再传递给内核。在Linux 下,这个命令行程序叫做 Shell。Shell的处理流程如下:

  1. 解析输入的命令行参数
  2. 如果该命令是内置的 shell 命令则马上解释该命令
  3. 若该命令是一个可执行目标文件,会在一个新的子进程的上下文中 execve 并运行这个文件
  4. 运行过程中,shell 会处理异常

5. 进程终止后,shell 负责回收该子进程。

6.3 Hello的fork进程创建过程

由于命令行参数是一个可执行目标文件,父进程通过调用fork 函数创建一个新的运行的子进程。输入 Ctrl+Z 挂起子进程 hello,使用 ps 命令查看进程信息。

              图 6.1 使用ps指令查看bash创建子进程hello

6.4 Hello的execve过程

fork 之后,shell 在子进程中调用 execve 函数,在当前进程的上下文中加载并运行 hello 程序,execve 调用启动加载器来执行 hello 程序,具体加载过程是删除已存在的用户区城、映射私有区城、映射共享区城、设置程序计数器(PC)、_start 函数调用系统启动函数__libc_start_main。它初始化执行环境,调用 main 函数,处理 main 函数的返回值,并且在需要的时候把控制返回到内核。

6.5 Hello的进程执行

操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务:内核为每个进程维持一个上下文,上下文就是内核重新启动一个被抢占的进程所需的状态,它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程一打开文件的信息的文件表。

hello程序在调用sleep函数时,内核中的陷阱处理程序让进程休眠,当计时器到达argv[3],它会产生一个中断信号,中断当前正在进行的进程,进行上下文切换,恢复hello 在休眠前的上下文信息,控制权回到hello继续执行,进程 hello继续运行,直到下一次异常发生,之后hello又调用getchar 函数,进入内核模式,内核中的陷阱处理程序请求来自键盘缓冲区的数据传输,并执行上下文切换,并把控制转移给其他进程。当完成键盘缓冲区到内存的数据传输后,引发一个中断信号,此时内核从其他进程切换回 hello 进程,然后 hello执行 return,进程终止。

6.6 hello的异常与信号处理

hello 执行过程中可能出现四类异常:中断、陷阱、故障和终止。

Hello 执行过程中会出现陷阱和终止。

正常情况下系统会调用 sleep,期间可以随便乱按键盘,回车,子进程结束,shell 回收该进程。

             图 6.2  hello程序运行时乱按

当打印了三行时,如果输入 Ctrl+C,内核会发送一个 SIGINT信号给前台进程组中的每个进程,终止前台作业,即终止hello 进程。然后shell会回收该进程。

               图6.3 中途输入Ctrl+C

如果中途输入 Ctrl+Z,会发送一个 SIGTSTP 信号到前台进程组中的每个进程,挂起前台作业,即挂起进程 hello。使用 ps 命令,发现它还在,只是被挂起了,使用jobs 命令查看前台进程,使用 pstree 查看 hello进程所在位置, 使用 fg 命令让 hello 程序在前台运行,使用 kill 命令杀死 hello 进程。

                  图6.4 中途输入Ctrl+Z

               图6.5 使用fg命令

                            图6.6 使用pstree命令

                       图6.7 使用kill命令

6.7本章小结

本章分析了hello 程序是如何被 fork、execve 和执行的,并探讨了hello 进程运行过程中可能的异常和信号处理。

(第6章1分)


7hello的存储管理

7.1 hello的存储器地址空间

逻辑地址(Logical Address)是指由程序产生的和段相关的偏移地址部分。 例如在c语言中,能读取指针变量本身值的&操作,实际上这个值就是逻辑地址,它是相对于当前进程数据段的地址,和绝对物理地址没有关系。 逻辑地址也就是在Intel保护模式下程式执行代码段限长内的偏移地址。

线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程式代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。

虚拟地址(Virtual Address)虚拟内存为每个程序提供了一个大的、一致的和私有的地址空间。其每个字节对应的地址成为虚拟地址。

物理地址(Physical Address) 是指出目前 CPU 外部地址总线上的寻址物理内 存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地 址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线 性地址就直接成为物理地址了。

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

要想由逻辑地址得到线性地址,需要根据逻辑地址的前16位获得段地址,这 16位存放在段寄存器中。

一个逻辑地址由两部分组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。段选择符格式如下

         

           图7.1 段选择符格式

其中TI=0,选择全局描述符表(GDT);TI=1,选择局部描述符表(LDT);RPL=00为第0级,位于最高级的内核态;RPL=11 为第3级,位于最低级的用户态。高13位索引用来确定当前使用的段描述符在描述符表中的位置。

具体过程是首先根据段选择符的 TI 部分判断需要用到的段选择符表是全局描述符表还是局部描述符表,随后根据段选择符的高13位的索引到对应的描述符表中找到对应的偏移量的段描述符,从中取出 32 位的段基址地址,将32位的段基址地址与 32位的段内偏移量相加得到32位的线性地址。如下图所示:

图7.2 逻辑地址到线性地址的转化过程

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

页式管理就是 CPU产生虚拟地址然后转换成物理地址的过程,下面对虚拟地址到物理地址的翻译过程进行描述。

如图7.3,即教材原图所示,假设系统只用一个页表进行翻译。CPU中的一个页表基址寄存器指向当前页表。虚拟地址包括两部分:一 个 12 位的虚拟页偏移量 VPO,一个 36 位的虚拟页号 VPN。MMU(CPU芯片上的内存管理单元硬件)利用 VPN选择适当的页表条目PTE。例如,VPN0 选择 PTE0, VPN1 选择 PTE1,以此类推。将页表条目中物理页号 PPN(40 位)和虚拟地址中的VPO(12 位)串联起来,就得到相应的物理地址(52 位)。

                     图7.3 使用页表的地址翻译

然而现实情况下使用一个页表是远远不够的,实际上,Core i7 采用的是四级页表层次结构。

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

为了消除每次 CPU 产生一个虚拟地址,MMU 就查阅一个 PTE带来的时间开销,许多系统都在 MMU 中包括了一个关于 PTE的小的缓存,称为翻译后备缓冲器(TLB)。

TLB与四级页表支持下的VA 到 PA的变换过程:

  1. CPU 产生一个虚拟地址
  2. MMU 从虚拟地址中抽取VPN,并且检查TLB,看它是否因为前面某个内存引用而缓存了 PTE的一个副本。TLB 从 VPN 中抽取 TLB 索引和TLB 标记,用TLB 标记去匹配每一行的标记位,看能否找到有效的匹配。如果命中就将PPN的内容返回给 MMU
  3. 如果 TLB 不命中,MMU 就需要从主存中取出相应的PTE。
  4. 如果得到的 PTE 是无效的,就产生一个缺页,内核必须调入合适的页面, 重新运行刚才的指令。
  5. 最后 MMU 得到了来自PTE的PPN 和来自虚拟地址的 VPO,把它们连接, 就形成了物理地址。

                          图7.4  TLB命中和不命中的操作图

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

                 

                    图7.5 Core i7的内存系统

如图7.5为Core i7的内存系统

如图7.6所示,首先,MMU发送物理地址给高速缓存 L1,L1 从物理地址中抽取缓存偏移 CO、缓存索引 CI 以及缓存标记 CT。然后依次根据缓存索引CI进行组选择,根据缓存标记CT进行行匹配,根据根据缓存偏移CO选择字。如果命中,就返回给CPU。如果L1不命中,需要继续访问下一级 cache,访问的原理与 L1 相同,若是三级 cache 都没有要访问的数据,则需要访问内存,从内存中取出数据并放入cache。

                         图7.6  Core i7地址翻译

7.6 hello进程fork时的内存映射

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

                    图7.7 Linux是如何组织虚拟内存的

7.7 hello进程execve时的内存映射

fork创建新的子进程后,内核调用 execve 函数在子进程中加载并运行包含在 可执行目标文件 hello 中的程序,用 hello程序有效地替代了当前程序。加载并运行hello 需要以下几个步骤:  

1. 删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的 区域结构。

2. 映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构, 所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。

3. 映射共享区域,hello 程序与共享对象 libc.so 链接,libc.so 是动态链接到 这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

4. 设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程上下 文的程序计数器,使之指向代码区域的入口点。

           图7.8 加载器是如何映射用户地址空间的区域的

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

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

  1. 虚拟地址 A 是合法的吗?换句话说,A 在某个区城结构定义的区城内吗? 为了回答这个问题,缺页处理程序搜索区城结构的链表,把 A和每个区城结构中 的 vm_start 和 vm_end 做比较。如果这个指令是不合法的,那么缺页处理程序就触 发一个段错误,从而终止这个进程。这个情况在图 7.9 中标识为“1”。 因为一个进程可以创建任意数量的新虚拟内存区城(使用 mmap 函数),所 以顺序搜索区城结构的链表花销可能会很大。因此在实际中,Linux 使用某些我们 没有显示出来的字段,Linux 在链表中构建了一裸树,并在这裸树上进行查找。
  2. 试图进行的内存访问是否合法?换句话说,进程是否有读、写或者执行这个 区域内页面的权限?例如,这个缺页是不是由一条试图对这个代码段里的只读页 面进行写操作的存储指令造成的?这个缺页是不是因为一个运行在用户模式中的 进程试图从内核虚拟内存中读取字造成的?如果试图进行的访问是不合法的,那 么缺页处理程序会触发一个保护异常,从而终止这个进程。这种情况在图 7.9 中标识为“2”。
  3. 此刻,内核知道了这个缺页是由于对合法的虚拟地址进行合法的操作造成 的。它是这样来处理这个缺页的:选择一个牺牲页面,如果这个牺牲页面被修改 过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时, CPU 重新启动引起缺页的指令,这条指令将再次发送 A 到 MMU。这次,MMU 就能正常地翻译 A,而不会再产生缺页中断了。

             图7.9 Linux 缺页处理

7.9动态存储分配管理

malloc函数返回一个指针,指向大小为至少size字节的内存块,这个块会为可能包含在这个块内的任何数据对象类型做对齐。实际中,对齐依赖于编译代码在32位模式还是64位模式中运行。在32位模式中,malloc返回的块的地址总是8的倍数。在64位模式中,该地址总是16的倍数。

动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap),如图 7.10.

分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。

   分配器分为两种基本风格:显式分配器、隐式分配器。

1. 显式分配器:要求应用显式地释放任何已分配的块。

2. 隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这 个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。

 

图7.10   堆

隐式分配器的两种实现原理:

  1. 隐式空闲链表分配器原理。 

任何实际的分配器都需要一些数据结构,允许它来区别块边界,以及区别已 分配快和空闲块。可以将这些信息嵌入块本身。一个简单方法如图 7.11。但是, 这种只带头部的隐式空闲链表结构会使得合并前面块耗费大量时间,可以采用带 标签的隐式空闲链表.

            图7.11 一个简单的堆块的格式

  1. 显式空间链表的基本原理

显示空闲链表把块组织成为某种形式的显式数据结构。它把实现这个数据结 构的指针存放在这些空闲块的主体里面。例如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个pred 前驱和succ 后继。

             图7.12 使用双向空闲链表的堆块的格式

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线 性时间减少到了空闲块数量的线性时间。释放一个块的时间可以是线性的,也可能是个常数,这取决于我们选择的空闲链表中块的排序策略。一种方法是用后进先出的顺序维护链表,将新释放的块放置在链表的开始处。 使用后进先出的顺序和首次适配的放置策略,分配器会检查最近使用过的块。在这种情况下,释放一个块可以在常数时间内完成。如果使用了边界标记,那么合并也可以在常数时间内完成。另一种方法是按照地址顺序来维护链表,其中链表每个块的地址都小于它后 继的地址。在这种情况下每释放一个快需要线性时间搜索来定位合适的前驱。

7.10本章小结

本章我们了解了 hello 的内存管理。分析了 Intel CPU 的段式管理和页式管理, 解析了在 linux 系统下处理器产生的虚拟地址是如何被翻译成物理地址的,以及如 何根据物理地址访问 cache 和内存,还介绍了linux 系统虚拟内存的组织和异常处理,最后简单了解了系统的动态内存分配。

(第7章 2分)


8hello的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. 关闭文件:内核释放打开文件时创建的数据结构以及占用的内存资源, 并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

Unix I/O 函数:

 1. int open(char *filename, int flags, mode_t mode);

open 函数将 filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。

 2.int close(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的实现分析

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

这是printf函数的定义:

其中fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素。(char*)(&fmt) + 4)表示的是“...”中的第一个参数的地址。

这是vsprintf函数:

vsprintf 的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。vsprintf 返回的是要打印 出来的字符串的长度,也就是i。

最后是 write(buf, i)这条指令,它把 buf 中的 i 个元素的值写到终端。

从 vsprintf 生成显示信息,到 write系统函数,到陷阱-系统调用int 0x80 或 syscall.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成 ASCII 码,保存到系统的键盘缓冲区之中。

这是getchar的代码:

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章介绍了 Linux 的 I/O 设备管理方法、Unix 的接口及函数,对 printf 函数和 getchar 函数的实现做了分析。

(第8章1分)

结论

hello的一生发生的几件大事:

  1. 出生。由程序员编写hello.c源代码
  2. 预处理。hello.c 文本翻译为 hello.i文本,预处理器cpp替换掉源码中的头 文件和宏。
  3. 编译。hello.i转变为 hello.s,ccl编译器把源码转变为汇编码。
  4. 汇编。汇编器把汇编码转换为机器码,生成了 hello.o 可重定位目标文件。
  5. 链接。静态链接,把外部函数的代码添加到可执行文件中;动态链接,设置过程链接表PLT 和全局偏移量表 GOT,只在运行时动态引用相关代码。最后生成了 hello可执行文件。
  6. 运行。hello程序被shell父进程 fork、execve,内核为 hello进程分配时间片执行逻辑控制流。hello进程运行过程中响应可能的异常和信号处理。
  7. 死去。程序运行结束后,shell父进程回收这个僵死子进程,内核会从系统中删除它的所有痕迹。hello的一生到此结束。

(结论0分,缺失 -1分,根据内容酌情加分)


附件

列出所有的中间产物的文件名,并予以说明起作用。

hello.c

hello源代码

hello.i

预处理之后的文本文件

hello.s

hello的汇编代码

hello_o-objdump.txt

hello.o的反汇编代码

hello-objdump.txt

hello的反汇编代码

hello.o

hello的可重定位文件

hello

hello的可执行文件

hello.elf

hello的elf文件

hello_o.elf

hello.o的elf文件

(附件0分,缺失 -1分)


参考文献

为完成本次大作业你翻阅的书籍与网站等

  1. 深入理解计算机操作系统pdf,课程PPT,实验资料等。
  2. printf函数实现的深入剖析[E].[转]printf 函数实现的深入剖析 - Pianistx - 博客园 (cnblogs.com)2013.09.11.
  3. 逻辑地址 、线性地址 、 物理地址和虚拟地址理解 : 逻辑地址、线性地址、物理地址和虚拟地址理解_do2jiang的博客-CSDN博客

(参考文献0分,缺失 -1分)

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐