哈工大计算机系统大作业——程序人生

摘 要
本文通过分析hello.c程序的一生,来进一步了解一个程序从生成到结束这其中所经过的过程。从而理解程序的预处理、编译、汇编、链接等概念,熟悉程序的进程管理、存储管理和IO管理。

关键词:hello,编译,链接,进程管理,存储管理,IO管理

第1章 概述
1.1 Hello简介
P2P:首先,程序员在Windows或Linux环境下,利用CodeBlocks、vim等编辑器敲入C语言代码(Program),然后利用gcc编译器对C语言程序执行编译命令:hello.c文件先经过预处理器cpp,生成hello.i文件,再经过编译器ccl生成hello.s汇编程序,然后经过汇编器as生成可重定位目标程序hello.o,最后通过链接器ld链接生成可执行文件hello。在Linux终端执行./hello命令,运行该可执行文件(Process)。
O2O:可执行文件hello执行后,shell通过execve函数和fork函数创建子进程并将hello载入,通过内存映射等为其分配自己的运行空间,CPU在流水线上依次执行每一条指令,内存管理系统利用虚拟内存地址从TLB或高速缓存中取出相应数据,如果出现不命中则继续从其下一级缓存中读取数据。最后程序运行结束后,shell接受到相应的信号,启动信号处理机制,对该进程进行回收处理,释放其所占的内存并删除有关进程上下文(context),hello程序重新回归0,挥一挥手不带走一片云彩,即From zero to zero。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows10 64位;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位;
开发工具:Visual Studio 2019 64位;GDB/OBJDUMP;GCC;EDB等
1.3 中间结果
hello.c:源代码文件
hello.i:预处理后生成的文件
hello.s:编译后生成的文件
hello.o:汇编后生成的可重定位目标程序
hello:链接之后生成的可执行程序
hello_o_asm.txt:hello.o的反汇编文件
hello_o_elf.txt:hello.o的ELF文件
hello_run_asm:hello的反汇编文件
hello_run_elf:hello的ELF文件

1.4 本章小结
本章进行了hello程序一生P2P,O2O的简要概述,列出了本实验的硬件环境、软件环境、开发工具和本实验中生成的中间结果文件的名字和作用。

第2章 预处理
2.1 预处理的概念与作用
预处理的概念是:在编译之前进行的处理。C语言的预处理主要有以下三个方面的内容:1. 宏定义;2. 文件包含;3. 条件编译,预处理命令以符号#开头,如#if, #endif, #define, #include等。
预处理的作用:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常以.i作为文件扩展名。
2.2在Ubuntu下预处理的命令
Ubuntu下预处理命令为:
gcc -E hello.c -o hello.i
截图如下:
在这里插入图片描述
2.3 Hello的预处理结果解析
hello.c文件共3000多行,可以看出前面绝大部分是将程序中#include后的库函数的代码复制上去。
在这里插入图片描述
程序的最后为hello.c中的main函数,可以看出,在3046行之后,和hello.c文件的#include之后的行完全相同,预处理阶段并没有对其做改动。
在这里插入图片描述
2.4 本章小结
本章介绍了预处理的概念和作用,同时介绍了Ubuntu环境下预处理的命令,并对预处理生成的hello.i文件进行了简要分析。

第3章 编译

3.1 编译的概念与作用
概念:将预处理完的.i文件通过一系列词法分析、语法分析和优化之后生成汇编文件。
作用:生成的汇编文件每条语句都以一种文本格式描述了一条低级机器指令,汇编语言为不同的高级语言的不同编译器提供了通用的输出语言,汇编语言相对于预处理文件更利于机器理解。

3.2 在Ubuntu下编译的命令
gcc -S hello.c -o hello.s
在这里插入图片描述
3.3 Hello的编译结果解析
3.3.1 数据
在这里插入图片描述
全局变量sleepsecs存放在.data段中,.align段标识要求4字节对齐,.size段标识该变量的长度为4字节,对应C语言代码中声明的int类型,而由于初值为2.5,经过强制类型转换成int之后值为2。
在这里插入图片描述
main函数的参数argc存在寄存器%edi中,并将其地址压入栈中,利用%rbp的偏移量来表示,由与立即数3的判断语句可知:

在这里插入图片描述
局部变量i存放在-4(%rbp)中,初始化为0,然后进入循环:
在这里插入图片描述
3.3.2 操作

  1. 赋值操作:赋值操作利用mov语句,如 表示 i = 0
    如果是局部变量不赋初值,在汇编代码里没有体现,只在用到并赋初值时才用寄存器等来存储;如果是全局或静态变量变量不赋初值,则会存放在.bss段;如果是已初始化的全局变量,会存放在.data段。
  2. 类型转换:在对全局变量sleepsecs进行赋值时进行了隐式类型转换,编译器会直接将转换后的数据送给变量,如3.3.1中图所示。
  3. 关系操作:利用cmp命令来进行关系操作,执行cmp命令后会设置标志位,然后后续利用对标志位的判断来完成其余操作。
    在这里插入图片描述
  4. 数组/指针/结构操作:数组地址存储在栈中,利用栈指针的偏移量来读取。
    在这里插入图片描述
  5. 控制转移:利用jmp指令和jxx一系列条件跳转指令来进行转移控制。
    在这里插入图片描述
  6. 函数操作:利用call指令来调用函数,将函数需要的参数存放在寄存器中或栈中来进行参数传递,函数调用结束之后,返回值保存在寄存器%rax中。
    在这里插入图片描述
    3.4 本章小结
    本章介绍了编译(指由.i文件生成.s文件)的概念和作用以及Ubuntu下进行编译的指令。然后对汇编代码进行解析,分别介绍了C语言中赋值操作、类型转换、关系操作、数组/指针操作、控制转移和函数操作等对应的汇编语言的指令。深入理解了编译生成的汇编代码。

第4章 汇编
4.1 汇编的概念与作用
概念:汇编器(as)将.s 文件翻译成机器语言指令,把这些指令打包成一种叫可重定位目标程序的格式,并将结果保存在目标文件(后缀为.o)中。
作用:将汇编代码转换成真正机器可以读懂的二进制代码。
4.2 在Ubuntu下汇编的命令
在这里插入图片描述
4.3 可重定位目标elf格式
在Ubuntu中利用readelf命令来查看ELF,并将其重定位为文本文件。
在这里插入图片描述
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF剩下的部分包含帮助连接器语法分析和解释目标文件的信息。包括ELF头的大小、目标文件的类型等。
一个典型的ELF可重定位目标文件包含下面几个节:
.txt:已编译程序的机器代码。
.rodata:只读数据。
.data:已初始化的全局和静态局部C变量。
.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。
.symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
.rel.text:一个.text节中位置的列表。
.rel.data:被模块引用或定义的所有全局变量的重定位信息。
.debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量以及原始的C源文件。
节头部表:
在这里插入图片描述
符号表:
在这里插入图片描述
可以从符号表中看出符号的大小,类型等信息。如main大小为133字节,类型为FUNC,Bind为GLOBAL,sleepsecs大小为4字节,类型为OBJECT等。
在这里插入图片描述
重定位条目常见有两种:

  1. R_X86_64_32:重定位绝对引用。重定位时使用一个32位的绝对地址的引用,通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。
  2. R_X86_64_PC32:重定位PC相对引用。重定位时使用一个32位PC相对地址的引用。一个PC相对地址就是据程序计数器的当前运行值的偏移量。
    通过重定位算法,计算出新的重定位地址。

4.4 Hello.o的结果解析
利用objdump -d -r hello.o对hello.o进行反汇编:
在这里插入图片描述
与第三章的hello.s对比,首先在指令前增加了其十六进制表示,即机器语言。其次在操作数上,hello.s中操作数为十进制,而hello.o的反汇编中操作数为十六进制。在条件跳转语句上,hello.o的反汇编文件用的是相对偏移量,而hello.s中是函数名:在这里插入图片描述
在调用函数方面,hello.o的反汇编文件中采用重定向的方式进行跳转,链接时根据重定位条目来获得地址信息。
在这里插入图片描述
全局变量的访问上,hello.s文件中对于全局变量的访问为LC0和sleepsecs(%rip),而在反汇编代码中是$0x0和0(%rip)

4.5 本章小结
本章介绍了汇编的概念,并且解析了可重定位目标elf格式,详细介绍了节头部表、符号表、重定位节等信息。通过生成的反汇编文件与hello.s内容进行对比,比较了两者之间的差异。

第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。
作用:链接可以使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以将它分解成更小、更好管理的模块,可以独立地修改和编译这些模块,当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
在这里插入图片描述
5.3 可执行目标文件hello的格式
首先用命令生成ELF文件:
在这里插入图片描述
查看各段的起始地址、大小等信息:可重定位条目:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
程序头表:
在这里插入图片描述
Dynamic section:
在这里插入图片描述
符号表:
在这里插入图片描述
版本信息:
在这里插入图片描述
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
可以看出虚拟地址空间起始地址为0x400000
在这里插入图片描述
由elf文件可看出.inerp偏移量为0x2e0,在edb对应位置找到:
在这里插入图片描述
同样可以在edb中找到.text段和.rodata 段等:
在这里插入图片描述
在这里插入图片描述
5.5 链接的重定位过程分析
对hello进行反汇编结果如下:
在这里插入图片描述
hello和hello.o相比,首先多了很多经过重定位之后的函数,如_init、puts@plt等,hello.o在.text段之后只有一个main函数;hello.o的地址是从0开始的,是相对地址,而hello的地址是从0x401000(_init的地址)开始的,是已经进行重定位之后的虚拟地址;在hello的main函数中,条件跳转指令和call指令后均为绝对地址,而hello.o中是相对于main函数的相对地址。
在这里插入图片描述
链接器完成的两个主要任务:符号解析和重定位。
重定位由两步组成:1. 重定位节和符号定义。 2. 重定位节中的符号引用。
如对puts函数的重定位:
在hello.o反汇编代码中,该行二进制编码为e8 00 00 00 00
addr(text)= 0x401105
refaddr = addr(text)+offset = 0x401126,即引用运行时的地址
addr(r.symbol) = addr(puts) = 0x401080
然后更新该引用,*refptr = (unsigned) (addr(r.symbol) + r.addend - refaddr)
= (unsigned) (0x401080 +(-4) – 0x401126) = (unsigned) (-aa) = ff ff ff 56
将其以小段序填入可得 56 ff ff ff ,与反汇编代码一致。

5.6 hello的执行流程
对hello进行gdb调试,用rbreak命令在每个函数入口处打上断点:
在这里插入图片描述
然后执行run和continue命令,查看整个程序进行的所有过程
在这里插入图片描述
在这里插入图片描述
5.7 Hello的动态链接分析
首先在elf文件中找到.got的地址0x403ff0
在这里插入图片描述
在edb找到相应地址处,并且在dl_init处设置断点,分析在dl_init前后该地址附近变化:
dl_init前:
在这里插入图片描述
dl_init后:
在这里插入图片描述
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,跳转到动态链接器中。每个条目都负责调用一个具体的函数。PLT[[1]]调用系统启动函数 (__libc_start_main)。从PLT[[2]]开始的条目调用用户代码调用的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[0]和GOT[[1]]包含动态链接器在解析函数地址时会使用的信息。GOT[[2]]是动态链接器在ld-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。
5.8 本章小结
本章主要介绍了链接的过程,利用edb、gdb、objdump等工具对链接的ELF文件、虚拟地址空间、重定位过程等进行详细分析,同时对hello程序的执行流程以及动态链接也做了简要介绍。至此,hello程序已经从程序代码转换成了一个可执行文件。

第6章 hello进程管理

6.1 进程的概念与作用
概念:进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
作用:进程为程序提供了一种假象,程序好像是独占的使用处理器和内存。处理器好像是无间断地一条接一条地执行我们程序中的指令。

6.2 简述壳Shell-bash的作用与处理流程
Shell是指为使用者提供操作界面的软件(命令解析器)。它接受用户命令,然后调用相应的应用程序。Linux系统中所有的可执行文件都可以作为Shell命令来执行。
处理流程:首先对用户输入的命令进行解析,判断命令是否为内置命令,如果为内置命令,调用内置命令处理函数;如果不是内置命令,就创建一个子进程,将程序在该子进程的上下文中运行。判断为前台程序还是后台程序,如果是前台程序则直接执行并等待执行结束,如果是后台程序则将其放入后台并返回。同时Shell对键盘输入的信号和其他信号有特定的处理。

6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的一份副本,包括代码和数据段、堆、共享库以及用户栈。父进程和子进程之间的最大区别是他们有不同的PID。在父进程中,fork返回子进程的PID。在子进程中,fork返回0,返回值提供一个明确的方法来分辨程序是父进程还是在子进程中执行。

6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个程序。
int execve(const char *filename, const char *argv[], const char envp[]);
execve函数加载并运行可执行文件
filename,且带参数列表argv和环境变量列表envp,execve调用一次并从不返回。
当main程序开始执行时,用户栈的组织结构如下图所示:
在这里插入图片描述
6.5 Hello的进程执行
进程为每个程序提供了一种假象,好像程序在独占的使用处理器。如图每个竖直的条表示一个进程的逻辑控制流的一部分。
在这里插入图片描述
一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流称为并发地运行。进程也为每个程序提供一种假象,好像它独占地使用系统地址空间。
处理器通常使用某个控制寄存器中的一个模式位来提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。该寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核模式中。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存的位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,也不允许直接引用地址空间中内核区的代码和数据。
运行应用程序的代码的进程开始处于用户模式中。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式改为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式改回用户模式。
内核为每个进程维持一个上下文。它由一些对象的值组成,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈等。在进程执行的某些时刻,内核可以决定抢占当前进程,并开始一个先前被抢占的进程,这种决策就叫调度。在内核调度了一个新的进程运行后,它他就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。
上下文切换:1. 保存当前进程的上下文 2. 恢复某个先前被抢占的进程被保存的上下文 3. 将控制权传递给这个新恢复的进程。
在这里插入图片描述
在hello中,程序执行sleep函数时,sleep显式请求让调用进程休眠,调度器抢占当前的进程,并且利用上下文切换转移到新进程。Sleep函数结束后,再通过上下文切换返回到hello函数中。

6.6 hello的异常与信号处理

  1. 不停乱按:
    如果乱按的过程中包括回车,那么乱按的内容将会在该程序结束之后作为命令输入。
    在这里插入图片描述
  2. Ctrl-C:
    在这里插入图片描述
    在键盘上按下Ctrl-C之后,会导致内核发送一个SIGINT信号到前台进程组中的每个进程,默认情况下结果是终止前台作业。
  3. Ctrl-Z:
    在这里插入图片描述
    在键盘下按下Ctrl-Z之后,会导致内核发送一个SIGTSTP信号到前台进程组的每个进程,默认情况下结果是停止(挂起)前台作业。
  4. ps命令:
    在这里插入图片描述
  5. jobs命令:
    在这里插入图片描述
  6. pstree命令:
    在这里插入图片描述
  7. fg命令:
    在这里插入图片描述
  8. kill命令:
    在这里插入图片描述
    6.7本章小结
    本章介绍了有关进程管理的多个概念。介绍了Shell的作用和处理流程,以及利用fork创建子进程、利用execve加载进程的方法。展示hello程序执行的具体过程,以及异常信号的处理机制。

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:
在有地址变换功能的计算机中,访问指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的物理地址。
线性地址:
线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
物理地址:
在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址,又叫实际地址或绝对地址。
虚拟地址:
程序访问存储器所使用的逻辑地址称为虚拟地址。
在hello_run_asm.txt文件中,显示出的地址为虚拟地址:
在这里插入图片描述
虚拟地址经过地址翻译得到物理地址。

7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理:指的是把一个程序分成若干个段进行存储,每个段都是一个逻辑实体。段式管理是通过段表进行的,包括段号(段名),段起点,装入位,段的长度等。程序通过分段划分为多个块,如代码段,数据段,共享段等。逻辑地址是程序源码编译后所形成的跟实际内存没有直接联系的地址,即在不同的机器上使用相同的编译器来编译同一个源程序则其逻辑地址是相同的,但在不同机器上生成的线性地址是不相同的。一个逻辑地址是两部分组成的,包括段标识符和段内偏移量。段标识符是由一个16位长的字段组成的,称为段选择符。前13位是一个索引号,后3位为一些硬件细节。索引号即是“段描述符”的索引,段描述符具体地址描述了一个段,很多个段描述符就组成了段描述符表。通过段标识符的前13位直接在段描述符表中找到一个具体的段描述符。
全局描述符表(GDT)整个系统只有一个,包含:1. 操作系统使用的代码段、数据段、堆栈段的描述符 2. 各任务、程序的LDT(局部描述符表)段
每个任务/程序有一个独立的LDT,包含:1. 对应任务/程序私有的代码段、数据段、堆栈段的描述符 2. 对应任务/程序使用的门描述符:任务门、调用门等。
在这里插入图片描述
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。VM系统将虚拟内存分割,称为虚拟页,类似地,物理内存也被分割成物理页。利用页表来管理虚拟页,页表就是一个页表条目(PTE)的数组,每个PTE由一个有效位和一个n位地址字段组成,有效位表明了该虚拟页当前是否被缓存在DRAM中,如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置。如果发生缺页,则从磁盘读取。
MMU利用页表来实现从虚拟地址到物理地址的翻译。
在这里插入图片描述
当页命中时,CPU硬件执行以下步骤:1. 处理器生成一个虚拟地址并把它传送给MMU 2. MMU生成PTEA,并从高速缓存/主存请求得到它 3. 高速缓存/主存向MMU返回PTE 4. MMU构造物理地址,并把它传送给高速缓存/主存 5.高速缓存/主存返回所请求的数据字给处理器

7.4 TLB与四级页表支持下的VA到PA的变换
下图给出了Core i7 MMU如何使用四级页表来将虚拟地址翻译成物理地址。
在这里插入图片描述
36位VPN被划分成四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1 PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2 PTE的偏移量,以此类推。

7.5 三级Cache支持下的物理内存访问
下图为通用的高速缓存存储器组织结构:
在这里插入图片描述
高速缓存的结构将m个地址位划分成了t个标记位,s个组索引位和b个块偏移位:
在这里插入图片描述
如果选中的组存在一行有效位为1,且标记位与地址中的标记位相匹配,我们就得到了一个缓存命中,否则就称为缓存不命中。如果缓存不命中,那么它需要从存储器层次结构的下一层中取出被请求的块,然后将新的块存储在组索引位指示组中的一个高速缓存行中,具体替换哪一行取决于替换策略,例如LRU策略会替换最后一次访问时间最久远的那一行。

7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任何一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
在这里插入图片描述
7.7 hello进程execve时的内存映射
execve函数加载并运行可执行目标文件需要以下几个步骤:

  1. 删除已存在的用户区域
  2. 映射私有区域:代码和数据区域被映射为.text区和.data区,bss区域是请求二进制零的,映射到匿名文件,栈和堆区域也是请求二进制零的,初始长度为零。
  3. 映射共享区域
  4. 设置程序计数器(PC):设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
    在这里插入图片描述

7.8 缺页故障与缺页中断处理
假设MMU在试图翻译某个虚拟地址A时触发了一个缺页,这个异常会导致控制转移到内核的缺页处理程序,执行以下步骤:

  1. 虚拟地址A是合法的吗?换句话说就是A在某个区域结构定义的区域内吗?如果这个指令不合法,那么缺页处理程序就会触发一个段错误,进而终止这个进程。
  2. 进程是否有读、写或者执行这个区域内页面的权限?例如这个缺页是不是因为一个运行在用户模式中的进程试图从内核虚拟内存中读取字造成的?如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,终止进程。
  3. 如果缺页是由于对合法的虚拟地址进行合法的操作造成的,内核会选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU。这次,MMU就能正常地翻译A,而不会缺页中断了。
    在这里插入图片描述

7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。对每个进程,内核维护一个变量brk,指向堆的顶部。分配器将堆视作一组不同大小的块的集合,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格:显式分配器和隐式分配器。显式分配器要求应用显式地释放任何已分配的块,隐式分配器要求分配器检测一个已分配块何时不再被程序所用,那么就释放这个块。隐式分配器又叫垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。
在这里插入图片描述
为实现动态内存分配器,可以使用隐式空闲链表。当一个应用请求k字节的块时,分配器搜索空闲链表,查找一个足够大可以放置所请求块的空闲块。分配器执行这种搜索的方式是由放置策略确定的。一些常见的策略是首次适配、下一次适配和最佳适配。一旦分配器找到一个匹配的空闲块,就需要决定分配这个空闲块中多少空间。一个选择是用整个空闲块,但这样会造成内部碎片。如果匹配不太好,那么分配器会将这个空闲块分割,第一部分变成分配块,剩下的变成一个新的空闲块。利用边界标记,可以允许在常数时间内进行对前面块的合并。这种思想是在每个块的结尾添加一个脚部,其中脚部就是头部的一个副本。这样分配器就可以通过检查它的脚部,判断前面一个块的起止位置和状态,这个脚部总是在距离当前块开始位置一个字的距离。但是这种方法也存在潜在缺陷,就是在应用程序操作许多个小块时,会产生显著的内存开销。
在这里插入图片描述
7.10本章小结
本章介绍了存储管理的有关内容。介绍了存储器的地址空间:物理地址、虚拟地址、逻辑地址、线性地址,然后对段式管理和页式管理进行了较为详细的描述,同时还讨论了VA到PA的变换、物理内存访问、fork和execve的内存映射、缺页故障和缺页处理、动态存储分配管理等内容。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的接口,称为Unix I/O,这使得所有的输入和输出能以一种统一且一致的方式来执行。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix I/O使得所有的输入和输出都以一种统一且一致的方式来执行:
· 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,他在后续对此文件的所有操作中标识这个文件。
· 改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0.这个文件位置是从文件开头起始的字节偏移量。
· 读写文件。一个读操作就是从文件复制n个字节到内存,从当前文件位置k开始,然后增加k到k+n。给定一个大小为m字节的文件,k>=m时执行型读操作会触发一个EOF的条件。
·关闭文件。内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符地址中。
函数:
·open函数:进程通过调用open函数来打开一个已存在的文件或者创建一个新文件。
·close函数:进程通过调用close函数来关闭一个打开的文件。
·应用程序通过调用read和write函数来分别进行输入和输出。
·调用stat和fstat函数检索到关于文件的信息(元数据)。
·应用程序可以用readdir系列函数来读取目录的内容。
8.3 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;
}
va_list是字符指针,而(char*)(&fmt + 4)表示fmt后的第一个参数的地址。vsprintf函数返回值是要打印出来的字符串的长度,其作用是格式化,产生格式化的输出并保存在buf中。最后的write函数即为写操作,把buf中的i个元素的值写到终端。
在write函数中,追踪之后的结果如下:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
一个int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call函数。在write函数中可以理解为其功能为显示格式化了的字符串。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 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函数中,首先声明了几个静态变量:buf表示缓冲区,BUFSIZ为缓冲区的最大长度,而bb指针指向缓冲区的首地址。
getchar调用read函数,将缓冲区读入到buf中,并将长度送给n,再重新令bb指针指向buf。最后返回buf中的第一个字符(如果长度n < 0,则报EOF错误)。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux I/O的设备管理方法以及Unix I/O接口的具体信息以及函数。最后分析了printf函数和getchar函数的实现。

结论
hello程序首先从hello.c的源代码文件开始,依次经过:
·预处理:对hello.c进行预处理,生成hello.i文件
·编译:将预处理完的hello.i文件通过一系列词法分析、语法分析和优化之后生成hello.s汇编文件
·汇编:将hello.s文件翻译成机器语言指令,把这些指令打包成一种叫可重定位目标程序的格式,并将结果保存在目标文件hello.o中
·链接:与动态库链接,生成可执行文件hello
hello文件运行时,首先shell通过fork创建子进程,将hello载入,为hello分配虚拟地址,并通过四级页表和TLB等结构,将虚拟地址翻译成物理地址,然后根据物理地址进行三级cache支持下的物理内存访问,取出相应信息。在程序运行过程中,还能够接受信号并进行异常处理。当hello程序运行结束后,shell父进程进行hello的回收操作,删除其对应的数据结构等信息。
通过本次实验,我感悟到计算机系统设计的精巧以及严谨性,每一种机制都能够恰到好处地完成应该完成的功能,并且消耗尽可能少的资源。几乎任何一种异常情况都能够得到合适地处理。可以说我们能有如今的计算机编程环境以及各种基于计算机的应用,都离不开这些底层系统的设计与实现。

附件
hello.c:源代码文件
hello.i:预处理后生成的文件
hello.s:编译后生成的文件
hello.o:汇编后生成的可重定位目标程序
hello:链接之后生成的可执行程序
hello_o_asm.txt:hello.o的反汇编文件
hello_o_elf.txt:hello.o的ELF文件
hello_run_asm:hello的反汇编文件
hello_run_elf:hello的ELF文件

参考文献
[1] Randal E.Bryant David R.O’Hallaron. 深入理解计算机系统(第三版). 机械工业出版社,2016.

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值