HIT深入理解计算机系统大作业

计算机系统大作业

题 目 : 程序人生-Hello’sP2P

专 业: 计算机科学与技术

学  号: 1180300422

班  级: 1803004

学 生: 吴仁龙

指 导 教 师: 史先俊

计算机科学与技术学院

2019年12月

摘 要

本文通过分析hello.c从诞生到死亡的全过程,包括预处理、编译、汇编、链接、在进程中执行及被销毁被操作系统回收,较全面的回答了计算机如何操控大局“软”“硬”结合完成程序的执行的问题,内容涉及汇编、链接、存储管理、进程管理、IO管理等。

关键词:hello.c;程序;编译;链接;进程;

目 录

第1章
概述… - 4 -

1.1 Hello简介… - 4 -

1.2 环境与工具… - 4 -

1.3 中间结果… - 4 -

1.4 本章小结… - 4 -

第2章
预处理… - 6 -

2.1 预处理的概念与作用… - 6 -

2.2在Ubuntu下预处理的命令… - 6 -

2.3 Hello的预处理结果解析… - 7 -

2.4 本章小结… - 7 -

第3章
编译… - 8 -

3.1 编译的概念与作用… - 8 -

3.2 在Ubuntu下编译的命令… - 8 -

3.3 Hello的编译结果解析… - 8 -

3.4 本章小结… - 11 -

第4章
汇编… - 12 -

4.1 汇编的概念与作用… - 12 -

4.2 在Ubuntu下汇编的命令… - 12 -

4.3 可重定位目标elf格式… - 12 -

4.4 Hello.o的结果解析… - 14 -

4.5 本章小结… - 15 -

第5章
链接… - 16 -

5.1 链接的概念与作用… - 16 -

5.2 在Ubuntu下链接的命令… - 16 -

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

5.4 hello的虚拟地址空间… - 18 -

5.5 链接的重定位过程分析… - 18 -

5.6 hello的执行流程… - 19 -

5.7 Hello的动态链接分析… - 20 -

5.8 本章小结… - 21 -

第6章
hello进程管理… - 22 -

6.1 进程的概念与作用… - 22 -

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

6.3 Hello的fork进程创建过程… - 22 -

6.4 Hello的execve过程… - 23 -

6.5 Hello的进程执行… - 23 -

6.6 hello的异常与信号处理… - 24 -

6.7本章小结… - 27 -

第7章
hello的存储管理… - 28 -

7.1 hello的存储器地址空间… - 28 -

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

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

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

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

7.6 hello进程fork时的内存映射… - 32 -

7.7 hello进程execve时的内存映射… - 32 -

7.8 缺页故障与缺页中断处理… - 33 -

7.9动态存储分配管理… - 34 -

7.10本章小结… - 36 -

第8章
hello的IO管理… - 37 -

8.1 Linux的IO设备管理方法… - 37 -

8.2 简述Unix IO接口及其函数… - 37 -

8.3 printf的实现分析… - 38 -

8.4 getchar的实现分析… - 39 -

8.5本章小结… - 39 -

结论… - 40 -

附件… - 41 -

参考文献… - 42 -

第1章 概述

1.1 Hello简介

P2P:在Linux下,Hello.c经过预处理、编译、汇编、链接生成可执行文件Hello,在shell中输入执行命令后,shell通过OS进程管理为其fork产生子进程,在该过程中,Hello.c从程序(Program)实现成进程(Process),即From Program to
Process,简称P2P。

020:在P2P之后,shell通过OS进程管理execve加载执行Hello进程,映射虚拟内存,进入程序入口后程序载入物理内存,CPU为Hello分配时间片一执行逻辑控制流,在程序结束后,shell回收Hello进程,删除其相关存储,Hello结束生命,整个过程From Zero-0 to
Zero-0,简称020。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上

软件环境:Win10

开发工具:Visual Studio
2019;VMware;Ubuntu;edb;gcc;

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

hello.c:hello源程序

hello.i:hello.c预处理产生的ASCII文件

hello.s:hello.i编译产生的汇编代码文件

hello.o:汇编后产生的可重定位文件

hello:可重定位目标文件链接后的可执行文件

1.4 本章小结

本节粗略描述了一个hello程序从诞生到死亡的过程及在描述整个过程时所使用的软件环境和生成的中间文件。

第2章 预处理

2.1 预处理的概念与作用

预处理器(cpp)会将以字符#开头的命令试图解释为预处理指令,修改原始的c程序。其中ISO C/C++要求支持的包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。比如,hello.c 中的第一行#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h中的内容,并把它直接插入程序文本中,结果就得到了另一个c程序,通常以.i作为文件扩展名。

2.2在Ubuntu下预处理的命令

预处理命令行:gcc -E hello.c -o hello.i 生成hello.i文件
在这里插入图片描述

2.3 Hello的预处理结果解析

hello.c预处理之后生成hello.i文件,gedit hello.i打开文件:
在这里插入图片描述

源程序中hello.c的程序被放在了hello.i的文件末尾,预处理器将头文件stdio.h、unistd.h、stdlib.h依次展开,若在展开该头文件的过程中仍然遇到了以#开头的define,预处理器会对此继续展开。

2.4 本章小结

   在预处理阶段,预处理器按一定规则解析以#开头的预处理指令,向helo.i文件中引入系统文件等,为程序的进一步编译做好准备。

第3章 编译

3.1 编译的概念与作用

在编译阶段,编译器检查是否有语法错误,检查无误后,编译器将.i文件翻译成.s文件,它包含一个汇编语言程序,该程序包含main函数的定义。汇编语言程序为不同高级语言的不同编译器提供了通用的输出语言,为低级机器语言。

3.2 在Ubuntu下编译的命令

编译命令:gcc -S hello.i -o hello.s 生成hello.s 文件
在这里插入图片描述

3.3 Hello的编译结果解析

3.3.1主函数

主函数main被解析成全局函数,其中使用的字符串常量也被放置在数据区。
在这里插入图片描述

Main函数有两个参数,分别为有符号数argc和字符型数组指针argv,根据寄存器使用规则,这两个参数分别通过%edi和%esi传递。在程序最开始,为main函数建立栈帧,并完成参数传递。
在这里插入图片描述

3.3.2 赋值操作

对于局部变量i,源程序中有赋值为0的操作,在hello.s文件中通过mov语句实现。
在这里插入图片描述

3.3.3 类型转换

Sleep函数的参数为int值,而argv为字符串数组,在hello.c中用atoi将字符串转化成int型,在hello.s中用call语句调用atoi函数强制处理该类型转换。
在这里插入图片描述

3.3.4 关系操作

在hello.c中痴线了两处关系操作,第一处是argc!=4,判断用户键入的参数个数是否是4,另一处是i<8,判断i的值是否小于8,来决定循环要不要继续。在hello.s中都通过cmp语句来判断关系。
在这里插入图片描述
在这里插入图片描述

在比较i和8的关系的时候,编译器并没有和8比较,而是和7,结合程序功能,当i>=8时结束循环,和当i<=7时继续循环是等价概念(i初值为0,且只增不减)。

3.3.5 算术操作

在hello.c文件中通过i++语句实现对i值的自增,且自增步长为1,在hello.s文件中通过add语句实现该运算。
在这里插入图片描述

3.3.6 数组/指针/结构操作

在hello.c中,字符串指针数组argv,多次引用argv[1]、argv[2]、argv[3],分别存储着学号、姓名及秒数。

传递argv[2]:
在这里插入图片描述

传递argv[1]:
在这里插入图片描述

传递argv[3]:
在这里插入图片描述

3.3.7 控制转移

在hello.c中有两处有关转移,一处是if语句实现的转移,一处是for语句实现的转移。

If语句在判断argc等于4之后,执行相应的功能语句,在hello.s中用je语句实现:
在这里插入图片描述

For语句在判断i仍然小于8后,继续转到循环起始处执行,在hello.s中用jle实现:
在这里插入图片描述

3.3.8 函数操作

在hello.c中多处涉及函数操作,调用另一个函数来执行当前任务,如printf、atoi、getchar、sleep、exit,在hello.s中对此的处理均是先完成参数传递(有入口参数的情况下),然后在用call语句转到相应函数的入口处执行。

printf,用%rdi传参:
在这里插入图片描述
exit,用%edi传参:
在这里插入图片描述
atoi,用%rdi传参:
在这里插入图片描述
Sleep,用%edi传参:
在这里插入图片描述

3.4 本章小结

编译器通过词法分析和语法分析,来检查原始代码有没有错误,在确认没有错误之后,编译器会按照一定的规范生成与原始代码等价的中间代码或汇编代码,在这个过程中,编译器可能会按照自己的理解(一系列算法),对原始代码结构和数据做出调整。

第4章 汇编

4.1 汇编的概念与作用

   在该阶段,汇编器(as)通过汇编程序将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在hello.o文件中,实现从汇编程序到机器指令的转换。Hello.o文件是一个二进制文件,包含程序的指令编码。汇编语言的指令与机器语言的指令大体上保持一一对应的关系,汇编算法采用的基本策略是简单的。通常采用两遍扫描源程序的算法。第一遍扫描源程序根据符号的定义和使用,收集符号的有关信息到符号表中;第二遍利用第一遍收集的符号信息,将源程序中的符号化指令逐条翻译为相应的机器指令。

4.2 在Ubuntu下汇编的命令

   汇编命令:gcc -c hello.s -o hello.o

在这里插入图片描述

4.3 可重定位目标elf格式

打开ELF命令行:readelf -a
hello.o

ELF文件从ELF头开始,ELF头以一个16个字节的序列开始,这个序列描述了生成该文件的系统的字大小和字节顺序。在该ELF头中显示,以小端码机器存储,文件类型为可重定位目标文件。
在这里插入图片描述

接着是节头。节头描述了目标文件中每一个节的位置和大小,每个节都有一个固定的条目,包括名称、类型、地址及偏移量等。
在这里插入图片描述

然后看重定位节。当汇编器生成一个目标模块时,它并不知道数据和代码最终将存放在存储器中的什么位置。它也不知道这个模块引用的任何外部定义的函数和全局变量。所以,无论何时汇编器遇到对最终位置未指定目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并可执行文件时如何修改这个引用。代码重定位条目放在.rel.text中。已经初始化数据的重定位条目放在.rel.data中。重定位条目中描述了需重定位符号的偏移量、信息、类型、符号值及名称等。

重定位条目的格式可用以下数据结构描述:

typedef struct{
	long offset;//offset of the reference to locate
	long type:32;//relocation type
		symlol :32;//symbol table index
	long attend;//constant part of relocation expression
}ELF64_Rela

被修改的引用的节偏移;type描述了重定位类型,有两种最基本的重定位类型:R_X86_64_PC32和R_X86_64_32,R_X86_64_PC32重定位一个使用32位PC相对地址的引用,R_X86_64_32重定位一个使用32位绝对地址的引用;symbol标识了被修改引用应指向的符号;addend为一个符号常数,表示对修改引用的值做出的偏移调整。
在这里插入图片描述

最后,看.symtab节。这是一个符号表,存放着在程序中定义和引用的函数和全局变量的信息。
在这里插入图片描述

4.4 Hello.o的结果解析

反汇编命令行:objdump -d -r hello.o
在这里插入图片描述

与hello.s对比,主要差异如下:

  1. hello.s中call语句后紧跟着函数名,而在hello.o反汇编中call语句后跟着的是相对地址,显示相应的重定位条目,因为未链接无法确定绝对地址。

  2. hello.s中跳转语句后紧跟着的是.L2.L3这样的标签,而在hello.o反汇编中跳转语句后跟着的是相对地址。

  3. hello.s中的操作数是十进制,而在hello.o反汇编中操作数是十六进制。

4.5 本章小结

  在汇编阶段,汇编器生成与汇编代码对应的机器指令,并处理分配ELF文件各节的信息,为链接生成可执行文件做好准备。

第5章 链接

5.1 链接的概念与作用

链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存中并执行。链接可以中兴于编译时、也就是源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时。也就是由应用程序来执行。

链接主要完成两个任务:符号解析和重定位。符号解析将每个符号引用正好和一个符号定义关联起来;重定位将每个符号定义与一个内存位置关联起来,修改所有对这些符号的引用,使得他们指向这个内存位置。

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的格式类似于可重定位目标文件hello.o的格式。ELF头描述文件的总体格式。它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。.text、.rodata和.data节与可重定位目标文件的节是相似的,除了这些节已经被重定位到它们最终的运行时内存外。.init节定义了一个小函数,叫_init,程序的初始化会调用它。因为可执行文件hello是完全链接的,所以不再需要.rel节。
在这里插入图片描述

5.4 hello的虚拟地址空间

在5.3节中包含各节在ELF文件中的偏移信息,比如.text的偏移量为0x00000550,则.text节的位置为0x400550:
在这里插入图片描述

再比如.rodata节的偏移为0x690,则.rodata节的位置为0x400690:
在这里插入图片描述

5.5 链接的重定位过程分析

较hello反汇编结果和hello.o反汇编结果,不同之处主要在于:

  1. 在hello.o中call、jmp指令后紧跟着的是相对地址,而hello中紧跟的是虚拟内存的确定地址,原因在于链接器完成了重定位过程,可以确定运行时的地址

  2. 在hello中增加了一些在hello.o中没有的函数,这些都是在hello.c中没有定义却直接使用的函数,这些函数定义在共享库中,在链接时完成了符号解析和重定位,如printf、sleep等。

下面结合call sleep来解释hello中是怎么对其重定位的:
在hello.o的反汇编中可以看到,call sleep语句是这么写的:
在这里插入图片描述

再去查看hello.o的ELF中有关sleep的可重定位条目:
在这里插入图片描述

Offset=0x74,symbol=sleep,type=0xR_X86_64_PLT32,addend=-4

这些信息告诉链接器修改开始于偏移量0x74处的32位PLT相对引用,这样在运行时会指向sleep的例程,接着链接器会按照0xR_X86_64_PLT32重定位类型具体的规定计算出相应的数值。

对于R_X86_64_PLT32类型的引用是动态链接的,也就是在静态链接过程中只是简单的构造过程链接表(PLT)和全局偏移量表(GOT),然后在程序加载到内存里运行的过程中才会完成最终的重定位工作。
在这里插入图片描述

5.6 hello的执行流程

(Edb命令行:./edb --run
/mnt/hgfs/hitics/CS_Hello/hello 1180300422 吴仁龙 1)

ld-2.27.so!_dl_start

ld-2.27.so!_dl_init

hello!_start

ld-2.27.so!_dl_start_main

ld-2.27.so!_cxa_atexit

hello!_libc_csu_init

libc-2.27.so!setjump

hello!printf@plt

hello!atoi@plt

hello!sleep@plt

hello!getchar@plt

hello!exit@plt

5.7 Hello的动态链接分析

通过查询hello的ELF文件,得.GOT.PLT的地址为0x601000
在这里插入图片描述
在运行dl_start和dl_init之前,GOTPLT表的内容如图所示:
在这里插入图片描述
在运行dl_start和dl_init之后,GOTPLT表的内容如图所示:
在这里插入图片描述

动态链接是一项有趣的技术。考虑一个简单的事实,printf,getchar这样的函数实在使用的太过频繁,因此如果每个程序链接时都要将这些代码链接进去的话,一份可执行目标文件就会有一份printf的代码,这是对内存的极大浪费。为了遏制这种浪费,对于这些使用频繁的代码,系统会在可重定位目标文件链接时仅仅创建两个辅助用的数据结构,而直到程序被加载到内存中执行的时候,才会通过这些辅助的数据结构动态的将printf的代码重定位给程序执行。即是说,直到程序加载到内存中运行时,它才知晓所要执行的代码被放在了内存中的哪个位置。这种技术被称为延迟绑定,将过程地址的绑定推迟到第一次调用该过程时。而那两个辅助的数据结构分别是过程链接表(PLT)和全局偏移量表(GOT),前者存放在代码段,后者存放在数据段。

5.8 本章小结

链接技术通过符号解析和重定位完成实现将各种代码和数据片段收集并组合成一个单一文件的功能,该技术使得分离编译成为可能,为大型软件的模块化开发和维护奠定了基础。

第6章 hello进程管理

6.1 进程的概念与作用

进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。进程提供给应用程序的关键抽象如下:一个独立的逻辑控制流和一个私有的地址空间。

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

shell是一个交互型应用级程序,其基本功能是解释并执行用户打入的各种命令,实现用户与Linux核心的接口。系统初启后,核心为每个终端用户建立一个进程去执行Shell解释程序。它的执行过程基本上按如下步骤: (1)读取用户由键盘输入的命令行。 (2)分析命令,以命令名作为文件名,并将其它参数改造为系统调用execve( )内部处理所要求的形式。 (3)终端进程调用fork( )建立一个子进程。 (4)终端进程本身用系统调用wait4( )来等待子进程完成(如果是后台命令,则不等待)。当子进程运行时调用execve( ),子进程根据文件名(即命令名)到目录中查找有关文件(这是命令解释程序构成的文件),将它调入内存,执行这个程序(解释这条命令)。 (5)如果命令末尾有&号(后台命令符号),则终端进程不用系统调用wait4( )等待,立即发提示符,让用户输入下一个命令,转⑴。如果命令末尾没有&号,则终端进程要一直等待,当子进程(即运行命令的进程)完成处理后终止,向父进程(终端进程)报告,此时终端进程醒来,在做必要的判别等工作后,终端进程发提示符,让用户输入新的命令,重复上述处理过程。

6.3 Hello的fork进程创建过程

父进程可以通过fork函数创建一个新的运行的子进程,其函数声明为:

pid_t fork(void),子进程享有与父进程相同但各自独立的上下文,包括代码、堆、数据段、共享库以及用户栈。

在父进程中,fork函数返回子进程的PID,在子进程中,fork函数返回0.

当我们在终端中输入./hello时,shell会先判断发现这个参数并不是Shell内置的命令,于是就把这条命令当作一个可执行程序的名字,它的判断显然是对的。

接下了shell会执行fork函数为hello创建进程。

6.4 Hello的execve过程

当前进程可通过execve函数在自己的上下文中加载并运行一个新程序,其函数声明为:int execve(const char *filename,const char *argv[],const char *envp[]),

Execve函数加载并运行可执行目标文件filename,且带参数列表argc和环境变量列表envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序,所以,与fork函数一次调用返回两次不同,execve调用一次从不返回。

在execve加载了hello之后,它会调用系统提供的启动代码,启动代码设置栈,系统会用execve构建的数据结构覆盖其上下文,替换成hello的上下文,然后将控制传递给新程序的主函数main。

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

进程为每个程序提供一种假象,好像它在独占地使用处理器。事实上,CPU为每个进程分配时间片,通过逻辑控制流不停的切换当前进程。每个进程执行它的流的一部分,然后被抢占(暂时挂起),然后轮到其它进程。
在这里插入图片描述

操作系统内核使用上下文切换的较高层次的异常控制流来实现多任务。内核为每个进程维护一个上下文,上下文由一些对象组成,通常包括通用目的寄存器、浮点寄存器、程序计数器、用户栈和各种内核数据结构。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的新的进程,这种决策称为调度。hello进程在内存中执行的过程中,也并不是一直占用着cpu的资源。因为当内核代表用户执行系统调用时,可能会发生上下文切换,比如说hello中的sleep语句执行时,或者当Hello进程以及运行足够久了的时候。每到这时,内核中的调度器就会执行上下文切换,将当前的上下文信息保存到内核中,恢复某个先前被抢占的进程的上下文,然后将控制传递给这个新恢复的进程。再比如,假如hello中有读写磁盘的操作,就会发生下图所示的调度:
在这里插入图片描述

6.6 hello的异常与信号处理

乱按键盘,正常执行:
在这里插入图片描述

按下ctrl+z,进程收到SIGSTP信号,暂时挂起:
在这里插入图片描述
使用ps查看进程:
在这里插入图片描述
使用jobs查看当前作业:
在这里插入图片描述

使用fg运行前台进程:
在这里插入图片描述

使用pstree查看计算机当前正在执行的所有进程之间的关系:
在这里插入图片描述

按下ctrl+c,向进程发送终止信号SIGINT:
在这里插入图片描述

当某种异常发生时,就会向进程发送某种异常信号,进程接受到信号后就会转到异常处理子程序处处理该异常。

6.7本章小结

进程是计算机科学中最深刻最成功的概念,独立的逻辑控制流给我们提供我们的程序独立的占有处理器的假象,私有的地址空间给我们提供我们的程序独立的使用内存系统的假象;异常的概念为进程之间的沟通架起了桥梁,也为软件层面和硬件层面的沟通架起了桥梁。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:指机器语言指令中,用来指定一个操作数或者是一条指令的地址。一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量构成的。通俗的说:逻辑地址是给程序员设定的,底层代码是分段式的,代码段、数据段、每个段最开始的位置为段基址,放在如CS、DS这样的段寄存器中,再加上偏移,这样构成一个完整的地址。

线性地址:地址空间是一个非负整数地址的有序集合,如果地址空间中的非负整数都是连续的,那么就称该地址空间是一个线性地址空间,线性地址是是逻辑地址到物理地址变换之间的中间层,程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。

物理地址:计算机系统的主存被组织成一个由若干个连续的字节大小的单元组成的数组,每个字节都有唯一的确定的编号,这个编号称为物理地址。

虚拟地址:现代计算机不直接使用物理地址,而是使用一种称为虚拟地址的中间层地址。虚拟地址和物理地址间存在着映射关系,当CPU生成一个虚拟地址来访问主存,这个虚拟地址通过地址翻译转换成对应的物理地址,进而去访问真实的物理空间。

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

程序代码会产生逻辑地址,一个逻辑地址由两部份组成,段标识符及段内偏移量。段标识符放在段描述表中,在保护方式下,在保护方式下,每个段由如下三个参数进行定义:段基地址(Base Address)、段界限(Limit)和段属性(Attributes)。:

(1):段基地址规定线性地址空间中段的开始地址;

(2):段界限规定段的大小。

(3):段的属性表示段的特性。例如,该段是否可被读出或写入,或者该段是否作为一个程序来执行,以及段的特权级等。

下图表示一个段如何从虚拟地址空间定位到线性地址空间。图中BaseA等代表段基地址, LimitA等代表段界限。另外,段C接在段A之后,也即BaseC=BaseA+LimitA。
在这里插入图片描述

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

分页机制把线性地址空间和物理地址空间分别划分为大小相同的块。这样的块称之为页。通过在线性地址空间的页与物理地址空间的页之间建立的映射,分页机制实现线性地址到物理地址的转换。这种映射关系是通过一种叫做页表的数据结构实现的,页表就是一个页表条目(PTE)的数组,其基本的组织结构如下:
在这里插入图片描述

CPU中的一个控制寄存器,页表基址寄存器指向当前页表,n位的虚拟地址由虚拟页面偏移(VPO, n位)和虚拟页号(VPN, n - p位)组成。MMU利用VPN来选择适当的PTE(页表条目),将页表条目中的物理页号(PPN)与虚拟地址的页面偏移量(VPO)串联起来,就得到相应的物理地址。

页面命中时,CPU硬件执行的步骤如下:

  1. 处理器生成一个虚拟地址,并将其传送给MMU

  2. MMU生成PTE地址,并从高速缓存/内存中请求得到它

  3. 高速缓存/内存向MMU返回PTE(即MMU 使用内存中的页表生成PTE)

  4. MMU构造物理地址,将其传送给高速缓存/主存

  5. 高速缓存/主存返回所请求的数据字给处理器

页面不命中时,CPU硬件执行的步骤如下:

  1. 处理器生成一个虚拟地址,并将其传送给MMU

  2. MMU生成PTE地址,并从高速缓存/内存中请求得到它

  3. 高速缓存/内存向MMU返回PTE(即MMU 使用内存中的页表生成PTE)

  4. PTE中的有效位为零, 因此 MMU触发缺页异常

  5. 缺页处理程序确定物理内存中牺牲页 (若页面被修改,则换出到磁盘)

  6. 缺页处理程序调入新的页面,并更新内存中的PTE

  7. 缺页处理程序返回到原来进程,再次执行导致缺页的指令

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

多级页表的设计的初衷是为了减少页表内存驻留过大的问题:

假如有一个32位的地址空间,4KB的页面和一个4字节的PTE,那么即使应用所引用的只是虚拟地址空间中很小的一部分,也总是需要4MB的页表驻留在内存中。

一级页表指向二级页表,二级页表指向三级页表,以此类推。如果片i中每个页面都未分配,那么一级PTEi就为空,如果片i中至少有一个页是分配了的,那么一级PTEi就指向一个二级页表的基址,其结构示意图如下:
在这里插入图片描述

MMU将虚拟地址VA做处理,取出VPN1、VPN2、VPN3、VPN4(VPNi为指向第i级页表的索引)及VPO(虚拟页面偏移量)。接着用VPN1在一级页表中匹配,若匹配PTE不为空,则用一级页表PTE的内容到二级页表中继续匹配;若匹配为空,则代表该页未分配,产生缺页,需要跳转至缺页处理子程序处理。在访问完4个页表之后,获得物理页面的PPN,再配合PPO(与VPO相等),可以获得物理地址VP。

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

MMU根据VP解析出相应的PP后,需要用PP在cache中寻找相应的数据,

根据第一级Cache的相关参数解析PP的索引位,具体说来,根据B大小解析PP的低b位作为块内偏移,根据S的大小解析紧跟着的s位作为组索引,剩下的位全部作为tag标记位,即如下图所示:
在这里插入图片描述

接着根据这些索引位在Cache中匹配,若匹配上,根据匹配的结果在第二级Cache中重复类似上述操作,最终,第三级Cache中存放着相应的所需数据。如果没有在cache中找到,就会发生不命中,此时会从下一级存储中加载相应的块到Cache中完成访问。

7.6 hello进程fork时的内存映射

内存映射:Linux通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个内存区域的内容,这个过程称为内存映射。一个虚拟页面一旦被初始化了,它就在一个由内核维护的专门的交换文件之间换来换去。交换文件也叫交换空间或者交换区域,在任何时刻,交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数。

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间。
在这里插入图片描述

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行新程序hello时:

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

·创建新的区域结构,这些新的区域都是私有的、写时复制的,代码和初始化数据映射到.text和.data区,.bss和栈堆映射到匿名文件。

·映射共享区域。如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,再映射到用户虚拟地址空间中的共享区域内。

·设置程序计数器(PC)。设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
在这里插入图片描述

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

缺页中断:进程线性地址空间里的页面不必常驻内存,在执行一条指令时,如果发现他要访问的页没有在内存中(即存在位为0),那么停止该指令的执行,并产生一个页不存在的异常,对应的故障处理程序可通过从外存加载该页的方法来排除故障,之后,原先引起的异常的指令就可以继续执行,而不再产生异常。

页面调度算法:将新页面调入内存时,如果内存中所有的物理页都已经分配出去,就按照某种策略来废弃整个页面,将其所占据的物理页释放出来。

缺页中断的处理:处理函数为do_page_fault函数,大致流程中为:

(一)地址为内核空间:

1,当地址为内核地址空间并且在内核中访问时,如果是非连续内存地址,将init_mm中对应的项复制到本进程对应的页表项做修正;

2,地址为内核空间时,检查页表的访问权限;

3,如果1,2没有处理完全,跳到非法访问处理;

(二)地址为用户空间:

4,如果使用了保留位,打印信息,杀死当前进程;

5,如果在中断上下文中火临界区中时,直接跳到非法访问;

6,如果出错在内核空间中,查看异常表,进行相应的处理;

7,查找地址对应的vma,如果找不到,直接跳到非法访问处,如果找到正常,跳到good_area;

8,如果vma->start_address>address,可能是栈太小,对齐进行扩展;

9,good_area处,再次检查权限;

10,权限正确后分配新页框,页表等;

7.9动态存储分配管理

动态内存管理的基本方法与策略:

动态内存分配器维护着一个进程的虚拟虚拟内存区域,称为堆(heap)。分配器将堆视为一组不同大小的块的集合来维护,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留供应用程序使用;空闲块保持空闲,直到它显示地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

分配器有两种风格,两种风格都要求应用显式地分配块,不同之处在于由哪个实体来负责释放已分配的块:

(一):显式分配器

要求应用显式地释放任何已分配的块。显式分配器的设计需要满足以下约束条件:处理任意请求序列;立即相应请求;只使用堆;对齐块;不修改已分配的块。除此之外,一个好的显式分配器需达到以下两个目标:最大化吞吐率和最大化内存利用率。不幸的是,最大化吞吐率和最大化内存利用率二者是相互牵制的,分配器的设计就需要在二者之间找到适当的平衡,通常需要考率以下问题:

  1. 空闲块组织:如何记录空闲块?

  2. 放置:如何选择一个合适的空闲块来放置已分配块?

  3. 分割:在将一个新分配的块放置到某个空闲块之后,如何处理这个空闲块中的剩余部分?

  4. 合并:如何处理一个刚刚被释放的块?

空闲块组织的三种基本数据结构:

  1. 不带脚部的隐式空闲链表
    在这里插入图片描述

  2. 带脚部的隐式空闲链表
    在这里插入图片描述

  3. 显式空闲链表
    在这里插入图片描述

放置策略:

  1. 首次适配。从头开始搜索空闲链表,选择第一个合适的块。

  2. 下一次适配。从上一次查询结束的地方开始,选择第一个合适的块。

  3. 最佳适配。检测每个空闲块,选择适合所需请求大小的最小空闲块。

合并空闲块策略:

  1. 立即合并。每次在一个块释放时,就合并所有相邻块。

  2. 推迟合并。直到某个分配请求失败,扫描整个堆,合并所有的空闲块。

(二):隐式分配器

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

7.10本章小结

本章简述了在计算机中的虚拟内存管理,虚拟地址、物理地址、线性地址、逻辑地址的区别以及它们之间的变换模式,重新认识了共享对象、fork和execve,也简单介绍了动态内存分配的方法与原理。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件,所有的输入和输出都能被当做相应文件的读和写来执行。

设备管理:unix io接口,使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix
IO接口及其函数

Unix IO接口简述:

1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。

2.Linux创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_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.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

UnixIO函数:

1.打开文件:int open (char *filename, int
flags, mode_t mode);

2.关闭文件:int close (int fd);

3.读文件:ssize_t read (int fd, void *buf,
size_t n);

4.写文件:ssize_t write (int fd, const
void *buf, size_t n);

8.3 printf的实现分析

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

先看paintf函数的函数体:

int printf(const char *fmt,...)
{
	int i;
	char buf[256];
	va_list arg = (va_list)((char*)(&fmt)+4);
	i=vsprintf(buf,fmt,arg);
	write(buf,i);
	rerurn i;
}

解释一下参数列表中的“…”:这个是可变形参的一种写法,当传递参数的个数不确定时,就可以用这种方式来表示。

通过va_list arg = (va_list)((char*)(&fmt) + 4)语句找到“…”中的第一个参数。

接着调用了vsprintf函数,该函数返回要打印字符串的长度。

接下来,printf函数会调用系统IO函数:write,其作用就是从缓存buf中最多读i个字节复制到一个文件位置。

在linux系统中,系统IO被抽象为文件,包括屏幕。对于系统来说,我们的显示屏也是一个文件,我们只需要将数据传送到显示屏对应的文件,就已经完成了系统端的任务,余下的工作独立的由显示器来进行了。于是在这里,write会给寄存器传递几个参数,初始化执行环境,然后执行sys call指令,这条指令的作用是产生陷阱异常。陷阱是有意的异常,用户程序执行了系统调用的命令(syscall)之后,就导致了一个到异常处理程序的陷阱,这个处理程序解析参数,并调用适当的内核程序。

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

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

8.4 getchar的实现分析

函数声明:int getchar(void)

getchar 有一个int型的返回值。当程序调用getchar时,用户输入的字符被存放在键盘缓冲区中,直到用户按回车为止(回车字符也放在缓 冲区中)。当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ASCII 码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。

进入getchar之后,进程会进入阻塞状态,等待外界的输入。系统开始检测键盘的输入。此时如果按下一个键,就会产生一个异步中断,这个中断会使系统回到当前的getchar进程,然后根据按下的按键,转化成对应的ascii码,保存到系统的键盘缓冲区。

接下来,getchar调用了read函数。read函数会产生一个陷阱,通过系统调用,将键盘缓冲区中存储的刚刚按下的按键信息读到回车符,然后返回整个字符串。

接下来getchar会对这个字符串进行处理,只取其中第一个字符,将其余输入简单的丢弃,然后将字符作为返回值,并结束。

8.5本章小结

本章节简述了Linux系统下I/O的机制,了解了有关打开、关闭与读写文件的操作,并且分析了printf和getchar两个函数的实现过程。

结论

hello的一生会经历如下阶段:(1) 预处理:预处理器cpp将.c文件翻译成.i的文件;(2) 编译:gcc编译器将.i文件翻译成.s格式的汇编语言文件;(3) 汇编:as汇编器将.s文件转换成十六进制机器码的.o文件;(4) 链接:ld链接器将一系列.o文件链接起来形成最终的可执行文件hello;(5) 进程创建:shell为hello程序fork一个子进程;(6) 程序运行:shell调用execve函数,映射虚拟内存,载入物理内存,进入main函数;(7) 指令执行:hello和其他进程并发地运行,CPU为其分配时间片;(8) 进程回收:shell回收子进程,系统释放该进程的数据所占的内存空间。

附件

hello.c:hello源程序

hello.i:hello.c预处理产生的ASCII文件

hello.s:hello.i编译产生的汇编代码文件

hello.o:汇编后产生的可重定位文件

hello:可重定位目标文件链接后的可执行文件

参考文献

[1] https://blog.csdn.net/ylcangel/article/details/18188921

[2] https://blog.csdn.net/i_am_jm/article/details/90721973

[3] https://zhuanlan.zhihu.com/p/39354226.

[4]https://blog.csdn.net/zheng123123123123/article/details/13017655

[5] https://blog.csdn.net/fallingu/article/details/75221276

[6] https://blog.csdn.net/balian8/article/details/78831626

[7]https://blog.csdn.net/m0_37962600/article/details/81448553

[8]https://blog.csdn.net/hulifangjiayou/article/details/40480467

[9] 深入理解计算机系统第三版

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章