【哈尔滨工业大学计算机系统大作业】程序人生-Hello’s P2P

目录

摘要

本文以hello.c程序在linux系统中执行的全过程为线索,详细探索了程序在计算机中执行的底层原理。为了更好地呈现hello程序执行的各个步骤,我们生成了程序执行的中间程序并具体分析其变化差异。了解程序执行的底层原理,可以帮助程序员写出对计算机硬件友好的、更加高效的程序。系统化地进行梳理,有利于读者对整一个过程拥有系统、宏观的把握。

关键词

计算机系统;计算机底层;Linux

第1章 概述

1.1 Hello简介

Hello.c的运行可概括为两大部分——Hello的P2P和Hello的020过程。

Hello的P2P

Hello的P2P (From Program to Process),是指程序Hello.c从高级语言程序,经历预处理(生成Hello.i)、编译(生成Hello.s)、汇编(生成Hello.o)和链接(生成Hello)等步骤后,当我们使用交互型应用级程序Bash时,进程管理系统(OS)将使用 fork() 函数为Hello生成一个进程(process)运行Hello的过程。

Hello的020

Hello的020 (From Zero to Zero),是指操作系统进行地址翻译、内存访问等步骤后,将Hello仅由页面调度在内存中运行,此为从无到有;运行结束后Bash将Hello的进程进行回收,删除CPU中的相关数据,此为从有到无。综合以上两步便为Hello的020过程。

1.2 环境与工具

本部分主要介绍研究大作业使用的环境与工具。

硬件环境

Intel Core i7;2.30GHz;16.0G RAM;952HD Disk;

软件环境

Windows11 64位;VirtualBox 11;Ubuntu 20.04 LTS 64位;

开发与调试工具

Visual Studio 2019 64位;vim;GDB

1.3 中间结果

以下为大作业所用到的中间文件及其大致作用。

中间结果文件名称文件作用
hello.i修改了源程序的文本
hello.s汇编程序文本
hello.o可重定位的目标程序
hello可执行目标程序
ans.txt可重定位目标程序的反汇编文本
ans2.txt可执行目标程序的反汇编文本

1.4 本章小结

本章将“Hello的自白”作为行文线索,概括性地介绍了程序的P2P过程与020过程,给出了探究hello.c执行过程的工具与环境,最后列出所需中间结果文件。为后文详细介绍“自白”所涉及过程做好铺垫。

第2章 预处理

2.1 预处理的概念与作用

程序的预处理阶段,是指预处理器(cpp)进行宏替换、头文件展开和预编译指令的处理过程。

预处理过程

预处理符号的处理

预处理器首先将程序中的预处理进行替换。常见的预处理符号见表。

预处理符号名称含义
_FLIE_进行编译的源文件
_LINE_文件当前行号
_DATE_文件被编译的日期
_TIME_文件被编译的时间
_STDC_若编译器遵循ANSI C,值为1,否则未定义

头文件的处理

头文件的处理是指程序将头文件内的所有指令包含进源文件中。

宏的处理

宏的处理是指将程序中 #define 的内容进行文本替换。宏主要的用途有定义常量,给运算符和关键字定义别名,和作为“编译开关”等。

条件编译

条件编译可以使程序的一条或一组语句编译或放弃编译。常见的条件编译指令有 #if,#endif,#else,#ifdef,#ifndef等。

2.2 在Ubuntu下预处理的命令

可以使用 gcc -E hello.c -o hello.i 命令生成预处理文件hello.i。
hello.i

2.3 Hello的预处理结果解析

行标记

hello.i 中如图三显示部分便为行标记。
预处理文件行标记
行标记会根据需要插入到输出中,意思是为下一行起源于文件filename中的linenum行。文件名后面有零个或多个标志,分别是’ 1 ‘、’ 2 ‘、’ 3 ‘或’ 4 '。如果有多个标志,则用空格隔开。标志的意义如下。

标志序号含义
1新文件的开始
2返回到一个文件
3以下文本来自系统头文件
4下面的文本应该被包装在隐式extern“C”块中

main函数

Hello.i中包含的main函数如下。

hello.i中的main函数
对比 hello.c 中的main函数可知,hello.i 中的main函数并未发生改变,原因是main函数中并未使用 #define 语法。

2.4 本章小结

本章详细地介绍了hello.c如何经过cpp的预处理生成hello.i的过程。之后直观展示了hello.i文件的具体内容并进行了简要分析。

第3章 编译

3.1 编译的概念与作用

编译的概念

程序的编译,即为编译器把用C语言表示的抽象程序转化成处理器执行的基本机器指令的过程。(需要注意的是,此处的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序。)

编译的作用

汇编代码表示非常接近于机器代码。与机器代码的二进制格式相比,汇编代码的可读性更好。汇编代码有时会显示c语言程序不会呈现的底层运行信息,有助于程序员写出基于底层优化的代码和更高效的调试工作。

3.2 在Ubuntu下编译的命令

Ubuntu20.04版本可使用 gcc -S hello.c -o hello.s 命令生成汇编代码hello.s。 如下图。

hello.s命令

3.3 Hello的编译结果解析

Hello.s 的第一部分如图6。由后文可知%edi中存储着变量argc的值,%rsi中保存着argv。如果argc的值,也就是argv中元素的数量不等于4,程序将会跳到L2部分。

hello.s程序第一部分
L2部分程序如下图。程序的这部分主要是把[%rbp-4]中存储的内容,也就是后文L4中for循环的计数器i设置为0,然后直接跳转到L3部分。

hello.s程序第二部分
程序的L4部分如下图。这部分主要是为了实现原程序中的for循环部分。首先将[%rbp-32]中存储的值,也就是argv转移到%rax中,之后将%rip中的值转移到%rdi中,由程序前面的部分我们可以直到%rip中的值为字符串"Hello %s %s\n"。然后程序调用了print函数、atoi函数和sleep函数,最后给for循环的i加上1。

hello.s程序第三部分

最后是程序的L3部分。这部分首先完成了for循环对i之后等于7的比较,如果不等则跳回L4继续执行for循环。之后调用了getchar函数,把%rax设置为0,因此函数的返回值最终为0。

ello.s程序第四部分

3.4 本章小结

本章首先解释了编译的概念和作用,区别了编译和汇编在CSAPP中的区别。之后详细分析了由hello.c编译得到的hello.s的内容。

第4章 汇编

4.1 汇编的概念与作用

汇编的概念

汇编,是指汇编器(as)将hello.s翻译成机器语言指令,并将指令打包成可重定位目标程序(relocatable object program),最后保存在目标文件hello.o中的过程。

汇编的作用

汇编将人类可以读懂的汇编语言翻译为机器的二进制语言,便于之后的链接操作。

4.2 在Ubuntu下汇编的命令

在Ubuntu20.04中可以使用gcc -c hello.s -o hello.o 命令生成hello.o文件。

生成hello.o命令

4.3 可重定位目标elf格式

在Ubuntu中使用readelf -a hello.o即可获得hello.o的elf格式内容。

首先是ELF头。ELF头以一个16字节的序列开始,这个序列描述了生成该文件的字的大小和字节顺序。ELF头剩下的部分为ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移和节头部表中条目的大小和数量。

ELF头
接下来是节头部表。节头部表说明了每个节的大小、类型、地址和对齐方式。

节头部表
之后是符号表,它用于存放程序中定义和引用的函数和全局变量的信息。然而,和编译器中的符号不同,符号表不包含局部变量的条目。

符号表
最后是重定位节。当链接器把目标文件和其他文件进行组合时,需要修改这些位置,任何调用外部函数或引用全局变量的指令都需要修改。其中,偏移量(Offset)是指需要修改的节的偏移量;信息(Info)包含符号和类型两部分,各占三字节,符号表示被修改的引用应该指向的符号,类型表示重定位的类型;类型(Type)的含义为链接器如何修改新应用的信息;符号名称(Sym.Name)是指重定向到的目标的名称;加数(Addend)是一个有符号的常数,一些重定位需要使用它对被修改引用做偏移调整。

重定位节

4.4 Hello.o的结果解析

接下来使用objdump -d -r hello.o > ans.txt 生成hello.o的反汇编到ans.txt文件中。以下为反汇编文本的main函数部分。

hello.o反汇编文本
可以看出,反汇编代码和hello.s代码内容大致相同,其中不同的点有:

  1. hello.s中全局变量地址是[段+%rip],而hello.o中是[0+%rip],这是因为全局变量的地址是重定位时确定。
  2. hello.s中跳转时使用的是段名称(L2、L3…),而hello.o中使用的是地址跳转。
  3. hello.s中直接调用函数,而hello.o中使用call和地址偏移量进行调用。可以看出此时偏移量为0,这是因为偏移量需要在链接时确定。

机器语言中包含操作码、数据和寄存器等,其中机器语言的寄存器、操作码与汇编语言相同。机器语言使用小端二进制表示,而汇编代码使用顺序十六进制表示,二者按照这样的规则具有映射关系。

4.5 本章小结

本章介绍了汇编的概念与作用,具体分析了hello.o的ELF文件格式内容,最后将hello.o反汇编文本与hello.s进行对比,阐明二者差异及机器代码与汇编语言的映射关系。

第5章 链接

5.1 链接的概念与作用

链接的概念

链接(linking)是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存中执行。在现代系统中,链接由链接器(linker)自动执行。

链接的作用

链接使分离编译(separate compilation)成为可能。程序员不需要将一个大型的应用程序组织成为一个巨大的源文件,而是可以分解为更小、更好管理的模块。改变其中一个模块时,程序员只需重新编译该模块和链接应用即可。

5.2 在Ubuntu下链接的命令

使用ld -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 -lc /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello.out命令链接文件。

链接命令

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

首先是ELF头部分。由这部分可知hello的数据为补码、小端序,类型是EXEC可执行文件,入口点地址为0x4010f0,以及文件各部分的大小。入口点地址非零,并且hello的节头部条目数大于hello.o,都说明已经进行了重定位工作。

hello的ELF头
接着是节头部表,程序在这里写明了hello中所有节的大小(size)和偏移量(offset),地址(Address)是hello被加载到虚拟地址时各个节的初始地址。可以看出每个节的大小和偏移量都不为0,说明重定位工作已经完成。

17-1
hello的节头部表
然后是重定位节。与hello.o的重定位节对比,可以发现原来的‘.rela.text’节变成了 ‘.rela.dyn’ 和 ‘.rela.plt’ 两个节,并且hello的重定位节把程序所用到的所有函数都罗列了出来,包括原程序中引用函数所使用的函数。而hello.o只罗列出了程序文本中出现的函数。此外,hello的偏移量也发生了改变,说明重定位工作的完成。

hello的重定位节
最后是hello的符号表。此部分与hello.o符号表相比,增加罗列了程序的局部变量和弱全局变量。

在这里插入图片描述
在这里插入图片描述
hello的符号表

5.4 hello的虚拟地址空间

使用edb加载hello,从Data Dump部分可以看到,hello的虚拟地址从0x401000到0x401ff0。
hello的虚拟地址空间
对比hello的节头部表可知,按地址从小到大排列,hello的前11个节和从第16节到第30节,即从 ‘.interp’ 节到 ‘.rela.plt’节、‘.fini’节到‘.shstrtab’节都处于hello的虚拟地址之外。因此,我们可以从objdump中读取到第12节到第15节的数据。
‘.init’ 节的数据如下。
‘.init’节数据
‘.plt’ 节的数据如下。
.plt’节数据
‘.plt.sec’ 节的数据如下。
.plt.sec’节数据
‘.text’ 节的数据如下。
在这里插入图片描述

在这里插入图片描述
‘.text’节数据

5.5 链接的重定位过程分析

hello和hello.o的对比

使用objdump -d -r hello > ans2.txt生成hello的反汇编文本ans2.txt。与hello.o的反汇编文本进行比较,可以发现以下几处不同。
首先hello.o只有main函数的反汇编代码,而hello中还有其他函数的反汇编代码。例如下图。

在这里插入图片描述
hello反汇编代码的其他函数
其次, hello.o的程序从0开始,而hello从0x401000开始。这表明hello完成了重定位工作,所有函数和数据都被分配好了存储空间。

hello反汇编代码开头地址

最后,hello的main函数中,所有全局变量相对于%rdi有了不为0的偏移量,而在hello.o中偏移量都为0,这说明了hello为所有的全局变量分配好了存储空间,重定位工作完成。
hello全局变量的偏移量

hello的重定位过程

当汇编器生成一个目标模块时,它不知道数据和代码最终放在内存中的什么位置,也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以当汇编器遇到对位置未知的目标引用,就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text节中,已初始化数据的重定位条目放在.rel.data中。
ELF有R_X86_64_PC32和R_X86_64_32两种重定位类型。hello.o中便使用了前者。
_X86_64_PC32示例

5.6 hello的执行流程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

程序执行过程及地址

5.7 Hello的动态链接分析

如果一个目标模块调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT。
PLT是一个数组,PLT[0]跳转到动态链接器中,PLT[1]调用系统启动函数,初始话执行环境,调用main函数并处理返回值。
GOT是一个数组,和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]时动态链接器在ld-linux.so模块中的入口点。其余的每个条目对应一个被调用的函数,其地址在运行时被解析。
从节头表可知,hello的GOT表地址如下。
GOT表地址

在edb中找到该位置,调用dl_init之前的值如下图。

调用dl_init之前的GOT
调用dl_init之后GOT的值如下图。
调用dl_init之后的GOT
由观察得,GOT[0]和GOT[1]的值发生了改变。

5.8 本章小结

本章首先介绍了链接的概念和作用,接着详细分析了由hello.o生成的hello程序的具体内容,最后分析了hello实现动态链接的过程。

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念

进程,即为一个执行中的程序的示例。每次用户向shell输入一个可执行目标文件的名字来运行程序时,shell就会创建一个新的进程,并在这个新进程中的上下文中运行这个可执行目标的文件。应用程序也可以创建新进程,并且在新进程的上下文中运行它们的代码或其他应用程序。

进程的作用

在现代系统上运行一个程序,我们可以得到程序是系统中唯一运行的程序的假象,程序独占处理器和内存的假象,和程序的代码和数据是系统内存中唯一的对象的假象。这些假象都是通过进程的概念实现的。

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

Shell-bash的作用

shell 是一个交互型应用级程序,代表用户运行其他程序。

Shell-bash的处理流程

shell的处理流程如下:

  1. 读取从键盘输⼊的命令。
  2. 判断命令是否正确,且将命令⾏的参数改造为系统调⽤ execve() 内部处理所要求的形式。
  3. 终端进程调⽤ fork() 来创建⼦进程,⾃⾝则⽤系统调⽤ wait() 来等待⼦进程完成。
  4. 当⼦进程运⾏时,它调⽤ execve() 根据命令的名字指定的⽂件到⽬录中查找可⾏性⽂件,调⼊内存并执⾏这个命令。
  5. 如果命令⾏末尾有后台命令符号& 终端进程不执⾏等待系统调⽤,⽽是⽴即发提⽰符,让⽤户输⼊下⼀条命令;如果命令末尾没有&则终端进程要⼀直等待。当⼦进程完成处理后,向⽗进程报告,此时终端进程被唤醒,做完必要的判别⼯作后,再发提⽰符,让⽤户输⼊新命令。

6.3 Hello的fork进程创建过程

在父进程中调用一次fork即可创建一个新的、处于运行状态的子进程。其中子进程返回0,父进程返回子进程的PID。之后父子进程并发、独立执行。

6.4 Hello的execve过程

在某进程中调用execve函数即可载入并运行某程序。新程序将覆盖当前进程的代码、数据和栈,并且和原进程拥有相同的PID,继承已打开的文件描述符和信号上下文。execve函数调用一次并从不返回。

6.5 Hello的进程执行

操作系统内核使用上下文切换来实现多任务。内核为每个进程维持一个上下文,其中上下文是内核重新启动一个被抢占的进程所需的状态,由通用目的寄存器、浮点寄存器、程序计数器等对象的值组成。

进程的上下文切换
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,此时内核通过上下文切换的机制控制转移到新的进程。上下文切换时将保存当前进程的上下文,然后回复某个先前被抢占的进程被保存的上下文,最后将控制传递给这个新恢复的进程。
假设进程A初始运行在用户模式中,直到A通过执行系统调用read陷入到内核。内核中的陷阱处理程序请求来自磁盘控制器的DMA传输,并且安排在磁盘控制器完成从磁盘到内存的数据传输后磁盘终端处理器。之后内核执行从进程A到进程B的上下文切换,B在用户模式下运行了一会后,当磁盘发出一个中断信号,内核判定B已经运行了足够长时间,就执行B到A的上下文切换。以此类推。

6.6 hello的异常与信号处理

程序的异常有以下四类。
程序的异常
在hello的执行过程中,四种异常均有可能出现。当hello产生异常时,就会发出信号。不同种类的异常将发出不同种类的信号。例如,当程序被中断时将发出SIGINT信号。

接下来具体分析程序运行过程中由于按键盘导致的结果。
首先是随便乱按且不包括回车时,对程序的执行没有影响,只会显示输入的内容,输出八遍后程序会停滞不会结束。
乱按不包含回车示例
若乱按且包含回车时,若按回车的次数为1,则和不按回车时情况相同。若按回车的次数大于1,程序会将第n次和第n+1次按回车之间输入的字符串都当作shell输入的命令,其中n>1。

乱按包含回车示例
如果键盘输入Ctrl+C,系统将发送SIGINT信号给前台进程组的所有进程,使前台进程组终止。
按Ctrl+C示例
如果键盘输入Ctrl+Z,系统将发送SIGTSTP信号给前台进程组的所有进程,使前台进程组暂停。

只按Ctrl+Z示例
若之后继续输入ps、jobs、pstree、fg 和kill等命令,效果如下。

输入ps、jobs、pstree、fg 和kill等命令示例
可以发现,输入jobs、ps、kill等命令时可正常执行,输入fg时hello将从暂停处继续执行。

6.7本章小结

本章首先介绍了进程的概念和作用,接着介绍了shell、进程的fork和execve函数,然后展示了hello进程的执行过程,最后详细介绍了hello程序执行过程中的不同种类的异常与信号处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址

指汇编代码中指令或操作的地址,由段标识和偏移量构成。也就是hello反汇编代码操作数前出现的十六进制数。
逻辑地址示例

线性地址

指逻辑地址变换到物理地址的中间层,由段中的偏移地址和相应段的基地址构成。

虚拟地址

是线性地址的别称。

物理地址

是指出现在CPU外部地址总线上的寻址物理内存的地址信号。

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

进程的地址空间按照程序自身的逻辑关系划分为若干个段,每个段都有一个段名,每段从0开始编址。内存以段为单位进行分配,每个段在内存中占连续空间,但各段之间可以不相邻。
为了保证程序能正常运行,必须能从物理地址中找到各个逻辑段的存放位置。因此,需要为每个进程建立一张段表。每个段对应一个段表项,记录该段在内存中的起始位置和段的长度。
分段系统的逻辑地址由段名(段号)和段内偏移量(段内地址)组成。段号的位数决定了每个进程最多能分多少段,段内地址的位数决定了每个段的最大长度是多少。
在进行地址变换时,经过编译程序的编译后,逻辑地址将会被翻译为等价的机器指令:“取出段号为x,段内地址为y的内存单元中的内容,放到z处。”
我们还需要注意对段的保护。当要访问的地址超出原有的段长,就会引发越界中断。操作系统将首先判断该段的“扩充位”,如可扩充,则增加段的长度,否则按出错处理。当系统发生缺段中断时,操作系统将首先检查内存中是否有足够的空闲空间,若有,则装入该段,修改有关数据结构,中断返回;若无,则检查内存中空闲区的总和是否满足要求,是则应采用紧缩技术,不满足则淘汰一(些)段来达到装入段的要求。

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

将整个系统的内存空间划分成一系列大小相等的块,每个块的大小固定。所有的块按物理地址递增顺序连续编号为0,1,2…每个作业的地址空间也划分成一系列与内存块一样大小的块,每一块称为一页。
一个作业,只要它的总页数不大于内存中的可用块数,系统就可以对它实施分配。系统装入作业时,以页为单位分配内存,一页分配一个块,作业所有的页所占的块可以不连续。系统同时为这个作业建立一个页号与块号的对照表,称为页表。
一个进程对应一张页表,进程中的每一页对应一个页表项,每个页表项由页号和页内偏移量构成。由于页面和物理块的大小相等,页内偏移地址和块内偏移地址是相同的。无须进行从页内地址到块内地址的转换。因此地址变换关键是将逻辑地址中的页号转换为内存中的物理块号。

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

因为页表是存放在内存中的,CPU要存取一个数据,需访问主存两次。为了提高存取速度,在地址变换机构中增设一组寄存器,用来存放访问的那些页表。人们把存放在高速缓冲寄存器中的页表叫联想存贮器(TLB)。
当进程访问一页时,系统将页号与TLB中的所有项进行并行比较。若访问的页在TLB中,即可立即进行地址转换。当被访问的页不在TLB中时,去内存中查询页表,同时将页表找到的内存块号与页号填入TLB中。
现代的大多数计算机系统,都支持非常大的逻辑地址空间(232~264)。页表就变得非常大,要占用相当大的内存空间。可采用四级页表来解决这一问题。
四级页表实现地址变换的方法如下。首先按照地址结构将逻辑地址拆分成三部分,再从PCB中读出页表的开始地址,根据一级页号查页表,找到二级页表在内存中的位置;根据二级页号查页表,找到三级页表在内存中的位置;根据三级页号查页表,找到四级页表在内存中的位置;之后根据四级页号查表,最终找到想访问的内存块号。最后结合页内偏移量即可得到物理地址。

使用k级页表的地址翻译

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

使用页表获取物理地址后,系统根据物理地址的索引位(CI)进行查找,并匹配物理地址的标志位(CT)。如果匹配成功,且标志位为1,则根据物理地址的偏移量(CO)取出块内的数据。若匹配失败,或标志位不为1,则前往下一级缓存查找数据,直至查找到第三级。之后层层往上,放置到最高级缓存中。若上一级缓存无空闲块,则使用牺牲块算法进行块数据的替换。

物理地址构成

7.6 hello进程fork时的内存映射

当fork被当前进程调用时,内核为新进程创建各种数据结构,并分配一个唯一的PID。为新进程创建虚拟内存时,内核创建了当前进程的mm_struct、区域结构和页表的原样副本。内核将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork从新进程中返回时,新进程现有的虚拟内存与调用fork时存在的虚拟内存相同。当两个进程中任意一个进行写操作时,写时复制机制就会创建新的页面。

7.7 hello进程execve时的内存映射

假设运行在当前进程中的程序执行了execve(“a.out”, NULL, NULL),程序会进行以下几个步骤。

  1. 删除已存在的用户区域。
  2. 映射私有区域。
  3. 映射共享区域。
  4. 设置程序计数器(PC)。
    加载器映射用户地址空间图解

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

当进程访问不在内存的页面时,将会引发缺页故障。由系统根据进程访问请求把所缺页面装入内存。
由缺页中断服务程序将所缺的页面调入内存,并更新PTE。若此时内存中没有空闲物理块安置请求调入的新页面,则系统将按照预定的策略自动选择一个(请求调入策略)或一些(预调入策略)在内存的页面,把它们换出到外存。用来选择淘汰哪一页的规则称为淘汰算法。淘汰算法主要有OPT、FIFO和LRU等。

7.9动态存储分配管理

当程序运行时需要额外虚拟内存时,可以使用动态内存分配器进行分配管理。
动态内存分配器维护着一个进程的虚拟内存区域(堆),并将堆视为一组大小不同的块的集合。每个块的状态要么是空闲的,要么是已分配的。
分配器有显式分配器和隐式分配器两种。隐式分配器检测到一个已分配块不再被程序使用时,就释放这个块。显式分配器要求应用显式地释放任何已分配的块。
一种简单的堆块模式如下。一个块由一个字的头部、有效载荷以及一些填充组成。头部编码了块的大小和这个块是否处于空闲状态。
块格式
之后将堆组织成一个连续的已分配块和空闲块的序列,并称其为隐式空闲链表。隐式空闲链表寻找空闲块的方法有以下三种。从头搜索链表找到第一个空闲块,或者下一次查找时从上一次查找结束的地方开始查找,亦或每次查询时都选择一个最好的空闲块。
隐式空闲链表
另外一种堆组织方式为显式空闲链表。此时堆块的格式如下。这样的格式空闲块可以组织成显式的数据结构,而堆组织成双向空闲链表,每个空闲块中都包含一个前驱和后继指针。使用双向链表可以使首次适配的时间减少到线性。释放块的时间取决于空闲块的排序策略。若使用后进先出的排序策略,释放一个块可以在常数时间内完成;若按照地址顺序维护链表,则释放块的时间为线性的。

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

7.10本章小结

本章首先介绍了hello的存储器地址空间,然后介绍了hello线性地址到物理地址、虚拟地址到物理地址的变换。之后分析了hello经过三级cache进行物理内存访问的过程,最后介绍了fork和execve函数的内存映射方法、缺页处理和动态内存分配管理的内容。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

在linux中所有的I/O设备都被模型化为文件,所有的输入和输出都被当做对相应文件的读和写来执行。

8.2 简述Unix IO接口及其函数

将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。
Unix有以下几种函数。

  1. 打开文件。使用int open(char* filename,int flags,mode_t mode)函数。
  2. 关闭文件。使用int close(fd)函数。
  3. 读写文件。使用ssize_t read(int fd,void *buf,size_t n)和
    ssize_t wirte(int fd,const void *buf,size_t n)函数

8.3 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;
}

里面引用了vsprintf。
里面引用了vsprintf。

int vsprintf(char *buf, const char *fmt, va_list args) { 
	char* p; 
	char tmp[256]; 
	va_list p_next_arg = args;
    for (p=buf;*fmt;fmt++) { 
	    if (*fmt != '%') { 
		    *p++ = *fmt; 
		    continue; 
		} 
   		fmt++; 

	    switch (*fmt) { 
		    case 'x': 
		    itoa(tmp, *((int*)p_next_arg)); 
		    strcpy(p, tmp); 
		    p_next_arg += 4; 
		    p += strlen(tmp); 
		    break; 
		    
		    case 's': 
		    break; 
		    
		    default: 
		    break; 
	    } 
    } 

 return (p - buf); 
}

8.4 getchar的实现分析

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

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;  
}

8.5本章小结

本章介绍了Linux I/O设备的概念和Unix I/O接口,之后对printf和getchar函数进行了详细分析。

结论

hello的一生

  1. 程序员编写出了hello.c程序。
  2. hello.c由预处理器进行预处理,替换某些语句后形成hello.i文件。
  3. hello.i经过编译,形成汇编代码hello.s。
  4. hello.s经过编译,生成可重定位目标文件hello.o。
  5. hello.o经过链接器链接,形成可执行文件hello
  6. shell-bash程序调用fork函数为hello生成新进程,并调用execve函数
  7. execve使hello载入内存并运行。
  8. hello程序运行的效果通过I/O设备呈现。
  9. hello运行终止后被shell回收,内核清空hello的数据结构等信息。

感悟与想法

在本学期之前我们学习的都是计算机抽象而高级的层面,通过本门课的学习,我才真正接触到了计算机系统的底层实现方法,理解了并不是只有硬件工程师才需要精通计算机底层实现,程序员如果掌握一些底层知识,才能编写出更加高效、对硬件友好的程序。
此外,我还是第一次使用这么厚重的一本书籍作为我的课本来进行一门课的学习。因此,这门课还让我思考了如何快速掌握如此多而杂的知识点的方法。这对我之后的学习应该会有一定的帮助。

附件

附件名称文件作用
hello.i修改了源程序的文本
hello.s汇编程序文本
hello.o可重定位的目标程序
hello可执行目标程序
ans.txt可重定位目标程序的反汇编文本
ans2.txt可执行目标程序的反汇编文本

参考文献

[1] [转]printf 函数实现的深入剖析 - Pianistx - 博客园[EB/OL]. [2022-5-19]. https://www.cnblogs.com/pianist/p/3315801.html.
[2] 操作系统之段式管理及段页式管理[EB/OL]. [2022-5-19]. https://zhuanlan.zhihu.com/p/391714998.
[3] 操作系统之页式管理[EB/OL]. [2022-5-19]. https://zhuanlan.zhihu.com/p/391327282.
[4] 程序的预处理_th15t13的博客-CSDN博客_程序预处理的概念[EB/OL]. [2022-5-19]. https://blog.csdn.net/th15t13/article/details/110451865.
[5] Randal E. Bryant, David R. O’Hallaron. Computer Systems A Programmer’s Perspective[M]. 龚奕利, 贺莲, 译.北京:机械工业出版社, 2016: 587-605.

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值