哈工大计算机系统 大作业 ——“程序人生-Hello’s P2P From Program to Process ”

哈工大计算机系统 大作业 ——“程序人生-Hello’s P2P From Program to Process ”

摘 要

本篇大作业主要通过对hello的一生进行探讨。通过对hello.c文件到进程被回收这个质朴却复杂的计算机系统运行过程进行解释和说明,来对计算机系统的运行加以解释和说明。主要针对程序的编译过程、进程管理、存储管理、IO管理进行研究和探讨。
关键词:计算机系统;编译;进程管理;存储管理;IO管理;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

目 录

第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 5 -
1.4 本章小结 - 5 -
第2章 预处理 - 6 -
2.1 预处理的概念与作用 - 6 -
2.2在UBUNTU下预处理的命令 - 6 -
2.3 HELLO的预处理结果解析 - 6 -
2.4 本章小结 - 7 -
第3章 编译 - 8 -
3.1 编译的概念与作用 - 8 -
3.2 在UBUNTU下编译的命令 - 8 -
3.3 HELLO的编译结果解析 - 8 -
3.3.1 数据类型 - 8 -
3.3.2 赋值 - 9 -
3.3.3 算术操作 - 9 -
3.3.4 关系操作 - 10 -
3.3.5 数组/指针/结构操作 - 10 -
3.3.6 控制转移 - 10 -
3.3.7函数操作 - 11 -
3.4 本章小结 - 12 -
第4章 汇编 - 13 -
4.1 汇编的概念与作用 - 13 -
4.2 在UBUNTU下汇编的命令 - 13 -
4.3 可重定位目标ELF格式 - 13 -
4.3.1 ELF头 - 13 -
4.3.2 节头表 - 14 -
4.4 HELLO.O的结果解析 - 15 -
4.5 本章小结 - 15 -
第5章 链接 - 16 -
5.1 链接的概念与作用 - 16 -
5.2 在UBUNTU下链接的命令 - 16 -
5.3 可执行目标文件HELLO的格式 - 16 -
5.4 HELLO的虚拟地址空间 - 17 -
5.5 链接的重定位过程分析 - 18 -
5.6 HELLO的执行流程 - 19 -
5.7 HELLO的动态链接分析 - 20 -
5.8 本章小结 - 20 -
第6章 HELLO进程管理 - 21 -
6.1 进程的概念与作用 - 21 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 21 -
6.3 HELLO的FORK进程创建过程 - 21 -
6.4 HELLO的EXECVE过程 - 22 -
6.5 HELLO的进程执行 - 22 -
6.6 HELLO的异常与信号处理 - 23 -
6.7本章小结 - 25 -
第7章 HELLO的存储管理 - 26 -
7.1 HELLO的存储器地址空间 - 26 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 26 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 27 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 28 -
7.5 三级CACHE支持下的物理内存访问 - 29 -
7.6 HELLO进程FORK时的内存映射 - 30 -
7.7 HELLO进程EXECVE时的内存映射 - 30 -
7.8 缺页故障与缺页中断处理 - 31 -
7.9动态存储分配管理 - 31 -
7.10本章小结 - 33 -
第8章 HELLO的IO管理 - 34 -
8.1 LINUX的IO设备管理方法 - 34 -
8.2 简述UNIX IO接口及其函数 - 34 -
8.3 PRINTF的实现分析 - 35 -
8.4 GETCHAR的实现分析 - 36 -
8.5本章小结 - 37 -
结论 - 38 -
附件 - 39 -
参考文献 - 40 -

第1章 概述

1.1 Hello简介

P2P(From Program to Process)过程:
hello.c 是使用c语言编写的文本文件,然后使用gcc进行预处理,把#include的文件写入文件,并且进行宏替换,生成hello.i。然后通过编译器ccl生成汇编程序hello.s。然后通过汇编器as汇编成可重定位文件hello.o。最后通过链接器链接生成hello可执行文件。然后会在shell中fork进程,最终完成了P2P过程。如图1-1所示。
在这里插入图片描述

图1 1 P2P过程
O2O(From Zero-0 to Zero-0):
Shell通过execve加载并执行该进程。操作系统为程序分配虚拟空间并且映射到物理内存空间。随后CPU为它分配逻辑控制流。随后进程终止,shell回收进程,操作系统释放虚拟空间。所以进程从0到了0。

1.2 环境与工具

软件环境:windows 10 64 位, VMware Workstation Pro,Ubuntu 16.04 LTS 64 位
硬件环境:Intel®Core™i7-9750H CPU@2.60GHz;16G RAM;2TB Disk
开发工具:gcc,gdb,codeblocks,edb

1.3 中间结果

1、hello.i:经过预处理的文件,进行了宏替换以及将#include的内容添加了进去。
2、hello.s:经过编译生成的汇编文件。
3、hello.o:汇编生成的可重定位的文件。
4、hello:链接生成的可执行文件。
5、helloobjdump:将hello反汇编生成的文件。
6、hellooobjdump:将hello.o反汇编生成的文件。
7、helloo.elf:hello的elf文件。
8、hellooo.elf:hello.o的elf文件。

1.4 本章小结

本章主要是对hello程序从0到0,以及p2p的过程简介。主要介绍了预处理、编译、汇编、链接的过程。以及进程在执行时shell和操作系统的行为。从一个大的全局层面来概括一个程序的出生到结束。
(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。
预处理中会展开以#起始的行,试图解释为预处理指令。包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
预处理的作用是把.h文件插入到程序中。并且将宏定义进行替换。

2.2在Ubuntu下预处理的命令

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

图 2 1 生成的文件
在这里插入图片描述

图 2-2 执行过程

2.3 Hello的预处理结果解析

.c源文件中含有以下三个库:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
预处理后将以上头文件内容复制进来形成.i文件,大大增大了代码的尺寸。
形成的.i文件如图所示。
在这里插入图片描述

图 2-3 .i文件

2.4 本章小结

本章对预处理做了一个介绍,通过hello.c预处理到hello.i的过程,清楚明白地介绍了预处理过程程序的过程和目的。通过实例可以发现,预处理阶段对后续程序的编译汇编都有很大的作用。
(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译之前,C语言编译器会进行词法分析、语法分析(-fsyntax-only),接着会把源代码翻译成中间语言,即汇编语言。
作用:进行词法分析和语法分析。并且会进行代码优化,生成汇编代码。

3.2 在Ubuntu下编译的命令

命令:gcc -S hello.i -o hello.s
执行如下图所示。

图 3-1 编译过程

3.3 Hello的编译结果解析

3.3.1 数据类型

1、局部变量:
Argc:如图,我们看到用-20(%rbp)跟4比较,所以传给-20(%rbp)的之前的%edi就是保存的argc。
在这里插入图片描述

图 3-2 argc
i:如图,我们发现在L2处有赋值为0,之后与7作比较的-4(%rbp),所以确定这是i。
在这里插入图片描述

图 3-3 i
字符串:两个需要打印的字符串存在LC0和LC1,如下图所示:
在这里插入图片描述

图 3-4 字符串
Argv数组:从L4中调用-32(%rbp)看出这是在调用argv数组的四个参数。
在这里插入图片描述

图 3-5 argv数组
其他数字以立即数形式出现。

3.3.2 赋值

赋值语句有一句,i=0。实现方式就是movl,如图所示。因为是int,而l是双字的意思也就是32位。
在这里插入图片描述

图 3-6 赋值语句

3.3.3 算术操作

有一处,i++,使用addl实现,如图所示。使用l的原因与3.3.2相同。
在这里插入图片描述

图 3-7 i++

3.3.4 关系操作

1、比较参数argc是不是为4,如图所示。如果等于则跳到L2。
在这里插入图片描述

图 3-8 比较argc和4
2、比较i是否满足<8,如图所示。如果小于等于7则跳到L4。
在这里插入图片描述

图 3-9 比较i和8

3.3.5 数组/指针/结构操作

1、数组有argv[],存储着输入的参数。
2、指针有argv,argv的每个元素又是一个指针,因为这是个二维数组。

3.3.6 控制转移

1、if:if(argc!=4)如图所示,用cmpl和后面的je来实现。等于则跳转L2,反之则顺着进行。
在这里插入图片描述

图 3-10 if
2、for:for(i=0;i<8;i++),如图所示,用cmpl和7作比较,如果小于等于则跳转L4,在L4的末尾有add $1的操作,实现了for循环。
在这里插入图片描述

图 3-11 for

3.3.7函数操作

1、main()函数:
参数传递:edi保存argc,rsi保存argv
函数调用:用.main:
返回:movl $0, %eax,ret,也就是return 0。
在这里插入图片描述

图 3-12 main
2、printf()函数:
参数传递:第一次:leaq .LC0(%rip), %rdi,放入rdi中;
第二次:.LC1放入%rdi, 姓名放在 %rdx,学号放在 %rsi
函数调用:call puts@PLT
3、exit()函数:
参数传递:将%edi 设置为 1。
调用函数:call exit@PLT
4、atoi()函数:
参数传递:将%rdi 设置为 argv[3]。
调用函数:call atoi@PLT
返回:返回值存储在%eax
5、sleep()函数
参数传递:将%edi 设置为 经过atoi处理后的argv[3]。
调用函数:call sleep@PLT
6、getchar()函数
调用函数:call getchar@PLT

3.4 本章小结

本章介绍了编译的概念和作用,并且分析了编译后的汇编代码的数据类型和各类操作在汇编语言中的存在和展示。对编译的过程有了更详细的认识。
(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

将汇编语言翻译为机器语言。一般而言,汇编生成的是目标代码,需要经链接器(Linker)生成可执行代码才可以执行。
作用:将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。

4.2 在Ubuntu下汇编的命令

gcc -c hello.s -o hello.o

图 4-1 汇编命令

4.3 可重定位目标elf格式

readelf -a hello.o > helloo.elf 生成了文本文件。

4.3.1 ELF头

ELF头内的type、machine等见下图所示。
在这里插入图片描述

图 4-2 ELF头

4.3.2 节头表

节头表,如图所示。
在这里插入图片描述

图 4-3 节头表
其中.test为程序代码,.data是初始化的全局变量,.bss是未初始化的全局变量,.rodata是只读数据节,.symtab是符号节,.strtab是字符串节。

4.4 Hello.o的结果解析

反汇编的截图如图所示。
在这里插入图片描述

图 4-4 反汇编程序
主要有以下不同:
1、在.s中跳转主要是跳转到名为L1之类的段,在反汇编中直接是跳转到具体的偏移地址。
2、.s的立即数使用的是十进制,而反汇编中使用的是十六进制。
3、call一个函数的时候.s文件中是call一个名字,而在反汇编中使用的是偏移地址,而且恰恰是下一条指令。主要是系统的库函数还没有被链接,所以默认为0。
机器语言的构成:机器可以识别机器语言,机器语言操作码和操作数组成。
与汇编语言的映射关系:机器语言与汇编语言是一一对应的映射关系。

4.5 本章小结

通过对汇编代码进行汇编,生成机器代码的可重定位文件,然后使用readelf进行阅读对ELF文件有了更深入的了解。再对反汇编文件和原汇编语言文件进行对比发现了机器代码和汇编代码的关系。
(第4章1分)

第5章 链接

5.1 链接的概念与作用

链接是将各种代码和数据片段收集并组合成为一个但一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行与编译时,也可以执行于加载时,甚至是运行时。由链接器来执行。
作用:将可重定位文件链接成可执行文件。链接可以帮助构造大型程序。链接还可以帮助使用共享库。

5.2 在Ubuntu下链接的命令

命令:gcc -m64 -no-pie -fno-PIC hello.c -o hello

图 5-1 链接

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

首先用readelf进行查看,然后先看elfheader,如图5-2所示。能够看到程序入口,包括elf header的大小和程序头的大小,31个section header等。
在这里插入图片描述

图 5-2 ELF头
然后再看到如图5-3所示。这是hello的节头,可以看到大小、地址以及偏移量等。
在这里插入图片描述

图 5-3 hello的节头

5.4 hello的虚拟地址空间

通过edb打开hello,可以看到虚拟地址空间各段信息。虚拟空间地址从0x401000到0x402000。
在这里插入图片描述

图 5-4 虚拟空间地址
如何可以看到各段的虚拟空间地址,如图5-5所示。
在这里插入图片描述

图 5-5 各段虚拟空间
与5.3部分的地址进行对比可以看到,现在加载的进程的地址是系统分配的虚拟空间地址,与一开始的0开头的地址不同。而且也有部分程序是动态链接,使地址看上去不同。

5.5 链接的重定位过程分析

hello反汇编生成的helloobjdump文件内容如图所示:
在这里插入图片描述

图 5-6 hello反汇编
首先可以很直观的发现,文件比hello.o的反汇编文件大很多,内容多了很多。说明链接将很多代码内容放入了文件再生成了可执行文件。
其次,可以看到.o文件的反汇编是从0开始的地址,而hello的反汇编是401000开始的地址。
然后,在可执行文件中还比.o可重定位文件中多出来了printf等问价
在这里插入图片描述

图 5-7 多出的程序
并且多了init函数。用于对程序的初始化。
重定位:
1、重定位节和符号定义。链接器将所有相同类型的节合并成同一个类型的新的聚合节。
2、重定位节中的符号应用,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。

5.6 hello的执行流程

Hello!_init
Hello!puts@plt
Hello!printf@plt
Hello!getchar@plt
Hello!atoi@plt
Hello!exit@plt
Hello!sleep@plt
Hello!.plt+0x70
Hello!.plt.got+0x10
Hello_start
Hello!main
Hello!__libc_csu_init
Hello!__libc_csu_fini
exit

5.7 Hello的动态链接分析

动态链接:等到程序运行时才进行链接,它提高了程序的可扩展性(可作为为插件)和兼容性。动态链接的基本思想是把程序按照模块拆分成各个相对独立的部分,在程序运行时才将它们连接在一起形成完整的程序。
在这里插入图片描述

图 5-8 .got.plt
根据上图,我们可以知道动态链接调用的函数的位置。
所以在dl_init前,如图所示都为0。
在这里插入图片描述

图 5-9 dl_init前
然后dl_init后现在都有了值。如图所示。
在这里插入图片描述

图 5-10 dl_init后
相应的,调用的各个函数也都从0变成有了地址。

5.8 本章小结

本章主要讲了链接的过程。一个可重定位文件经过过静态链接以及动态链接后成为了可执行的程序。链接的出现很大程度上减少了代码的体积。通过对比与.0文件的elf文件以及反汇编文件,对链接时文件的变化有了更深入的认识。
(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用

概念:进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
作用:
(1) 我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。
(2) 处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

shell是你(用户)和Linux(或者更准确的说,是你和Linux内核)之间的接口程序。你在提示符下输入的每个命令都由shell先解释然后传给Linux内核。
shell 是一个命令语言解释器(command-language interpreter)。拥有自己内建的 shell 命令集。此外,shell也能被系统中其他有效的Linux 实用程序和应用程序(utilities and application programs)所调用。
处理流程:
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
(4)如果不是内部命令,调用fork( )创建新进程/子进程
(5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait等待作业终止后返回。
(7)如果用户要求后台运行(如果命令末尾有&号),则shell返回;

6.3 Hello的fork进程创建过程

终端程序通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。
父进程和子进程最大的不同时他们的PID是不同的,父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。
对于hello来说,我们输入了命令参数后,因为命令不是系统内部的命令,所以需要fork创建一个子进程。

6.4 Hello的execve过程

Hello在fork之后,将其参数传递给了子进程的argv数组,然后调用execve执行进程。
Execve需要(1)删去用户已存在的区域。(2)映射私有区域。(3)映射共享区域。(4)设置程序计数器。

6.5 Hello的进程执行

上下文就是内核重新启动一个被强占的进程所需的状态。
进程时间片是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间。
在这里插入图片描述

图 6-1 进程执行
如图所示,进程执行,就是我们的进程hello通过一个个上下文切换,用户态和核心态的转换。处理器虽然只能同时处理一个进程,但可以通过不断的上下文切换以及用户态与核心态的转换,给不同的进程分配不同的时间片,就可以形成好像多个进程在同时进行的错觉。
有了进程hello之后,在处理缓冲区的输入时,进入到了内核模式,然后中断后重新通过上下文的切换回到用户模式。

6.6 hello的异常与信号处理

异常的种类有:中断、陷阱、故障、终止。
对于hello来说,这四种异常都是有可能的。比如中断可能是来自处理器外部的I/O设备的信号的的结果。对于陷阱,hello的exit以及sleep都会发生。对于故障,比如缺页故障。对于终止,比如硬件错误,DRAM和SRAM被损坏时发生的奇偶校验错误。
会产生的信号很多,如下图所示。
在这里插入图片描述

图 6-2 常见的产生的信号
首先,我们用ctrl-z来试一试,此时只是中断,然后分别使用了ps,Jobs命令如图所示。

图 6-3 ctrl-z,ps,jobs

然后使用pstree来查看进程如图所示

图 6-4 pstree
使用fg会返回,如图所示。

图 6-5 fg
然后试一试用kill来结束进程,如图所示。

图 6-6 kill
Ctrl-c会直接终止程序。

图 6-7 ctrl-c
乱按键盘只是会保存在输入缓冲区里,直到按回车getchar。

图 6-8 乱按键盘

6.7本章小结

本章主要是讲了进程管理,主要包括了shell、异常、信号以及进程的创建和执行过程。对这一部分进行梳理,使得对hello程序的理解进一步提升。
(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:包含在机器语言指令中用来指定一个操作数或一条指令的地址,每个逻辑地址都由一个段和偏移量组成,偏移量指明了从段开始的地方到实际地址之间的距离。
线性地址:也就是虚拟地址。是逻辑地址到物理地址变换之间的中间层。程式代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。
物理地址:是指出目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。
结合hello来说的话,比如i的逻辑地址就是&i得到的,这时只是他在进程中的当前数据段的一个地址,然后和段的基地址结合就有了虚拟地址,这个虚拟地址再映射到一个物理地址。

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

在这里插入图片描述

图 7-1 段式管理
如图所示,就是逻辑地址段标识符: 段内偏移量组成,段标识符找到段表内的该段的首地址,再通过段内地址偏移量组合形成线性地址。
也就是说,段地址+偏移地址=线性地址。
段寄存器(16位),用于存放段选择符。CS(代码段):程序代码所在段。SS(栈段):栈区所在段。DS(数据段):全局静态数据区所在段。其他3个段寄存器ES、GS和FS可指向任意数据段。
段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。用GDT或是LDT由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT。

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

页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
磁盘中的字节都有唯一的虚拟地址,然后虚拟内存被分割成虚拟也。虚拟页的大小P=2p字节。然后有个页表条目PTE的数组,虚拟地址空间中的每个页在页表中都有一个固定偏移量。
在这里插入图片描述

图 7-2 页式管理
如图所示,每个虚拟地址都映射到磁盘的一个页。然后页表中有效位为1的。可以映射到DRAM中,也就是物理内存。有效位为0,物理页号为null的就是说还未分配,物理页号不为空那就是没有存储在物理内存中。
在这里插入图片描述

图 7-3 虚拟地址到物理地址的翻译
如图所示是虚拟地址到物理地址的翻译。简单来说,虚拟地址分为两部分:前一部分为虚拟页号,可以索引到当前进程的的物理页表地址,后一部分为虚拟页偏移量,将来可以直接作为物理页偏移量,页表是一个存放在物理内存中的数据结构,页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。如果不命中,则需要从下级存储单元往上级取。
虚拟地址分为VPN和VPO。VPN又分为TLBT和TLBI,后者是TLB的组号,前者是标记。找到后就取出物理地址的PPN(物理页号),然后VPO变成PPO,CI是组号,CO是偏移,CT是标记。

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

在这里插入图片描述

图 7-4 四级页表的VA到PA的转换
如图所示为四级页表的VA到PA的转换。CR3控制寄存器指向第一级页表(L1)的起始位置。CR3的值是每个进程上下文的一部分,每次上下文切换时,C3的值都会被恢复。
36位VON被划分为了9位的片,每个片被用做到一个页表的偏移量。CR3寄存器包含L1也标的物理地址。VPN1提供到一个L1PET的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2PTE的偏移量,以此类推。
而VPO可以直接转化为VPO从而和PPN结合然后转化为物理地址。

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

I7处理器封装包括四个核、一个大的所有核共享的L3高速缓存,以及一个DDR3内存控制器。每个核包括一个层次结构的TLB、一个层次结构的数据和指令高速缓存,以及一组快速的点到点链路,这种链路基于quickpath技术,是为了让一个核和外部I/O桥直接通信。TLB是虚拟寻址的,是四路组相联的。
在这里插入图片描述

图 7-5 高速缓存
每一级高速缓存都是类似的。比如如图7-5所示,一个地址前t位为标记,s位为组索引,b位为块偏移。然后就可以去高速缓存中寻找。如果命中则取出。如果不命中则需要逐级往下寻找并取出然后放在相应索引的组的一个行中。如果有空闲则直接放入,如果没有则需要寻找牺牲块,可以用LFU方法去寻找。

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

程序执行了execve调用:execve(“hello”,NULL,NULL);
1、删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2、映射私有区域。为新的程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.test和.data区。Bss区域是请求二进制零的,映射到匿名文件,其大小包括在hello中。栈和堆区域也是请求二进制零的,初始长度为0.
3、映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库Libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4、设置程序计数器(PC)。Execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
在这里插入图片描述

图 7-6 execve

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

在这里插入图片描述

图 7-7 缺页故障与缺页中断
当指令引用一个虚拟地址,而与改地址相对应的物理页面不在内存中,因此必须从磁盘中取出时,就会发生故障。就像我们将在第九章中看到的那样,一个页面就是虚拟内存的一个连续的块(典型的是4KB)。缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中了,指令就可以没有故障地运行完成了。

7.9动态存储分配管理

动态内存分配器维护着堆,堆顶指针是brk。有两种风格,一种叫显式分配器,使用malloc和free等;一种叫隐式分配器,也叫垃圾收集器。
显式分配器必须要满足以下条件:1、处理任意请求序列;2、立即响应请求;3、只使用堆;4、对齐块;5、不修改已分配的块。在这些限制条件下,分配器试图实现吞吐率最大化和使用率最大化。吞吐率就是每个单位时间里完成的请求数。内存利用率可以用峰值利用率来衡量,也就是有效载荷占已堆当前当前大小的比值。
造成堆利用率的一个原因是碎片现象。碎片分为内部碎片和外部碎片。你不水平是分配一个已分配块比有效载荷大时发生的。外部碎片是当空闲内存合计一起来满足一个分配请求但没有一个单独的空闲块足够大时发生的。
为了实现一个分配器,必须考虑:1、空闲块的组织;2、放置;3、分割;4、合并。
可以设计一个隐式空间链表,如图所示。在这种情况下,一个块是由字的头部和有效载荷组成的。
在这里插入图片描述

图 7-8
放置已分配的块的策略有首次适配、下一次适配和最佳适配。首次适配从头开始搜索空闲链表,选择第一个合适的空闲块。下一次适配是从上一次查询家属的地方开始检查空闲块。最简式配检查每个空闲块,选择所需请求的最小空闲块。
分割空闲块通常是把第一部分变成分配块,剩下的变成新的空闲块。
当合适的空闲块不够的时候将申请额外的堆内存,插入空闲链表中。
合并空闲块,当分配器释放一个分配块时,可能有其他空闲块与这个新释放的空闲块相邻,就必须合并。与下一块合并很简单,但是和链表中的上一块合并很困难,所以提出了边界标记,就是在结尾处增加一个脚部,如下图所示。
在这里插入图片描述

图 7-9 边界标记
在这里插入图片描述

图 7-10 显式空闲链表
还可以使用显式空闲链表。也就是堆可以组织成一个双向空闲链表。使得首次适配的分配时间从块综述的线性时间减少到了空闲块的线性时间。对于释放可以使用后进先出的顺序对链表进行维护。
对于一个使用单向空闲块链表的分配器为了减少分配时间还可以使用分离存储。就是维护多个空闲链表,一般是将所有可能的块大小分成一些等价类,也叫做大小类。有两种基本方法:1、简单分离存储;2、分离适配。还有一种特例叫伙伴系统。
程序应当使用free来释放堆块。也有一种动态内存分配器叫垃圾收集器,可以自动释放程序不再需要的已分配块。基本想法就是,将内存视为一张可达图,对于不可达的点那么就是垃圾就可以回收。C语言可以使用Mark&Sweep垃圾收集器,但是是保守的,也就是说平衡树方法会保证标记所有根节点可达的节点,但可能不正确地标记实际上不可达的块。

7.10本章小结

本章主要讲了hello的存储管理,主要讲了hello的存储器地址空间,Intel逻辑地址到线性地址的变换-段式管理,Hello的线性地址到物理地址的变换-页式管理,TLB与四级页表支持下的VA到PA的变换,三级Cache支持下的物理内存访问,hello进程fork时的内存映射,hello进程execve时的内存映射,缺页故障与缺页中断处理以及动态存储分配管理。对整个hello程序的存储管理有了更深入的认识。
(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

一个Linux文件就是一个m个子节点序列:B0,B1…,Bk,…,Bm-1。
所有的IO设备都被模型化为文件,而所有的输入和输出都被当作相应文件的读和写来执行。这种将设备优雅地映射为文件的方法,允许Linux内核引出一个简单、低级的应用接口,称为Unix IO,这使得所有输入和输出都能以一种统一且一致的方式来执行:
1、打开文件2、Linux shell创建的每个进程开始时都有三个打开的文件。3、改变当前的文件位置。4、读写文件。5、关闭文件。

8.2 简述Unix IO接口及其函数

Unix I/O 接口:
1、打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想 要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在 后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文 件的所有信息。
2、Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标 准错误。
3、改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
4、读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文 件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文 件,当 k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。
5、关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢 复到可用的描述符池中去。
函数:
1、int open(char* filename,int flags,mode_t mode) ,进程通过调用 open 函 数来打开一个存在的文件或是创建一个新文件的。 open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在 进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访 问这个文件,mode 参数指定了新文件的访问权限位。
2、int close(fd),fd 是需要关闭的文件的描述符,close 返回操作结果。
3、ssize_t read(int fd,void *buf,size_t n),read 函数从描述符为 fd 的当前文 件位置赋值最多 n 个字节到内存位置 buf。返回值-1 表示一个错误,0 表示 EOF,否则返回值表示的是实际传送的字节数量。
4、ssize_t wirte(int fd,const void *buf,size_t n),write 函数从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置。

8.3 printf的实现分析

研究printf的实现,首先来看看printf函数的函数体
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);
return i;
}
printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
然后让我们追踪下write:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
这里是给几个寄存器传递了几个参数,然后一个int结束。将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址。
再来看看sys_call的实现:
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
完成了printf。

8.4 getchar的实现分析

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时,程序发生陷阱的异常。当按键盘时会产生中断。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章主要讲了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数的实现。通过对这些知识点讲解,对系统的IO有了更深刻地理解。
(第8章1分)

结论

1、首先有高级语言产生的hello.c文件
2、经过预处理后,hello.c进行了宏替换并且还将库函数.h文件等添加进入文件生成了hello.i
3、再通过编译器编译产生hello.s,变成了汇编语言。
4、再通过汇编器产生了hello.o,变成了可重定位文件。
5、通过链接器,将其他可重定位的文件写入进来变成hello可执行文件。
6、在shell中按要求输入并执行该可执行文件,shell会先fork一个子进程然后execve,加载运行hello。
7、创建了虚拟内存空间,并映射到物理内存。
8、在sleep函数时会发生异常,然后上下文切换。
9、printf函数会使用malloc函数申请堆空间,使用到动态内存管理技术。
10、printf函数会使用IO设备管理进行输出。
11、最后,进程结束被回收。Hello结束。

我的感想:
计算机系统的实现分很多层次,以一个程序员的眼光去分析计算机系统,主要需要考虑到计算机系统的硬件结构,然后在其之上搭建相应的软件。很多时候程序的效率都需要考虑到硬件的实现方法,才能有效地提升软件的效率。而在计算机系统再往上的高级语言设计的层次,也需要考虑到系统是如何工作的。这对我们解决很多潜在的问题有帮助,一个小小的细节可能对整个程序而言是个巨大的提升。
(结论0分,缺失 -1分,根据内容酌情加分)

附件

 Hello.c 源文件
 Hello.i 预处理后的文件
 Hello.s 编译后的汇编文件
 Hello.o 汇编后的可重定位文件
 Hello 链接后的可执行文件
 Hello.elf hello的elf文件
 Helloo.elf hello.o的elf文件
 Hellooobjdump hello.o的反汇编文件
 Helloobjdump helloob的反汇编文件
(附件0分,缺失 -1分)

参考文献

[1] https://www.csdn.net/ csdn官网
[2] 《深入理解计算机系统》,Bryant,R.E. ,机械工业出版社,2016.11.15
[3] 大作业PPT、Word资料
[4] https://www.freesion.com/ 灰信网
[5] https://baike.baidu.com/ 百度百科
[6] https://www.cnblogs.com/ 博客园

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值