2022哈工大计算机系统大作业

  • 目录
    • 摘 要
    • 第1章 概述
      • 1.1 Hello简介
      • 1.2 环境与工具
      • 1.3 中间结果
      • 1.4 本章小结
    • 第2章 预处理
      • 2.1 预处理的概念与作用
      • 2.2在Ubuntu下预处理的命令
      • 2.3 Hello的预处理结果解析
      • 2.4 本章小结
    • 第3章 编译
      • 3.1 编译的概念与作用
      • 3.2 在Ubuntu下编译的命令
      • 3.3 Hello的编译结果解析
        • 3.3.0文件结构
        • 3.3.1数据
        • 3.3.2全局函数
        • 3.3.3算数操作
        • 3.3.4赋值操作
        • 3.3.5关系操作
        • 3.3.6控制转移
        • 3.3.7函数操作
        • 3.3.8类型转换
      • 3.4 本章小结
    • 第4章 汇编
      • 4.1 汇编的概念与作用
      • 4.2 在Ubuntu下汇编的命令
      • 4.3 可重定位目标elf格式
      • 4.4 Hello.o的结果解析
      • 4.5 本章小结
    • 第5章 链接
      • 5.1 链接的概念与作用
      • 5.2 在Ubuntu下链接的命令
      • 5.3 可执行目标文件hello的格式
      • 5.4 hello的虚拟地址空间
      • 5.5 链接的重定位过程分析
      • 5.6 hello的执行流程
      • 5.7 Hello的动态链接分析
      • 5.8 本章小结
    • 第6章 hello进程管理
      • 6.1 进程的概念与作用
      • 6.2 简述壳Shell-bash的作用与处理流程
      • 6.3 Hello的fork进程创建过程
      • 6.4 Hello的execve过程
      • 6.5 Hello的进程执行
      • 6.6 hello的异常与信号处理
      • 6.7本章小结
    • 第7章 hello的存储管理
      • 7.1 hello的存储器地址空间
      • 7.2 Intel逻辑地址到线性地址的变换-段式管理
      • 7.3 Hello的线性地址到物理地址的变换-页式管理
      • 7.4 TLB与四级页表支持下的VA到PA的变换
      • 7.5 三级Cache支持下的物理内存访问
      • 7.6 hello进程fork时的内存映射
      • 7.7 hello进程execve时的内存映射
      • 7.8 缺页故障与缺页中断处理
      • 7.9动态存储分配管理
      • 7.10本章小结
    • 第8章 hello的IO管理
      • 8.1 Linux的IO设备管理方法
      • 8.2 简述Unix IO接口及其函数
      • 8.3 printf的实现分析
      • 8.4 getchar的实现分析
      • 8.5本章小结
    • 结论
    • 附件
    • 参考文献

目录

摘 要

本文介绍了Hello程序在Linux系统下的整个生命历程,通过gcc,gdb,edb等各种工具研究了Hello程序经预处理、编译、汇编、链接生成可执行文件的全过程,并分析了其在运行过程中计算机系统对它进行的进程管理、存储管理和IO管理,直至最后被回收。通过对Hello程序整个生命周期的探索,我们对计算机系统形成了更深一步的理解。

关键词:HELLO;预处理;编译;汇编;链接;进程管理;存储管理;IO管理

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:在Linux系统中,hello.c经过cpp的预处理成为hello.i,再由ccl的编译变成汇编文件hello.s,然后汇编其as的汇编成为hello.o,最终经过ld的链接成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork产生一个子进程,hello程序就变为了进程。
020: shell为此子进程execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入主函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。

1.2 环境与工具

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位
开发与调试工具:gcc,vim,edb,readelf,HexEdit

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.i:hello.c预处理后的文本文件
hello.s:hello.i编译后的汇编文件
hello.o:hello.s汇编后得到的可重定位目标文件
hello:hello.o链接后得到的可执行目标文件
helloelf.txt:hello.o的elf格式,分析汇编器和链接器行为
hello.txt:可执行hello的elf格式,作用是重定位过程分析

1.4 本章小结

本章主要简单介绍了 Hello 的 p2p,020 过程,列出了本次实验的环境和工具,并给出了实验过程中的中间结果,并且大致简介了hello.c到可执行目标文件hello的大致经过的历程。

第2章 预处理

2.1 预处理的概念与作用

预处理:在对源程序进行编译之前,先对源程序中的预处理命令进行处理,然后再将处理的结果,和源程序一起进行编译,以得到目标代码。
作用:cpp根据以字符#开头的命令,修改原始的C程序。可以处理宏定义、编译指令、头文件和特殊符号。

2.2在Ubuntu下预处理的命令

命令:gcc -E hello.c -o hello.i

图 2.2

2.3 Hello的预处理结果解析

进行预处理后,我们发现在原文件夹内多了一个hello.i文件,打开后如下:

图2.3
经过预处理后,hello.c转化为hello.i文件,使得原来几行的代码增加至3000多行代码,而且仍然为可读的C语言文件。查看预处理产生的hello.i文件,可以发现main函数以及定义全局变量的代码没有任何改变,而原来前面的#include语句被替换成了大量的头文件中的内容,包括外部函数的声明、结构体等数据结构的定义、数据类型的定义等内容。而且源程序开头的注释也被删除了。

2.4 本章小结

本章介绍了预处理的概念和作用,然后在linux系统下使用预处理命令对hello.c文件进行预处理操作,得到hello.i文件,通过分析二者的差异,解析出预处理发挥的作用。

第3章 编译

3.1 编译的概念与作用

编译:编译器ccl将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。
作用:将源程序翻译成目标程序,同时具备语法检查、词法分析、调试手段、目标程序优化等功能。

3.2 在Ubuntu下编译的命令

命令:gcc -S hello.i -o hello.s

图3.2

3.3 Hello的编译结果解析

3.3.0文件结构

图3.3
.file:声明源文件
.text:声明代码段
.data:声明数据段
.rodata:只读数据
.align:数据或者指令的地址对其方式
.string:声明一个字符串
.global:声明全局变量
.type:指定类型

3.3.1数据

1.字符串
程序中的两个字符串都在只读数据段中,如图:

图3.3.1.1
2.局部变量
main函数声明了一个局部变量i,编译器进行编译的时候将局部变量i会放在堆栈中。如图所示,局部变量i放在栈上-4(%rbp)的位置,并通过相对栈顶(%rsp)的偏移量来访问。
其中movl为i赋初值0,addl在每次循环时对i增加1,cmpl比较i和9的大小来决定什么时候结束循环。

图3.3.1.2
hello.c中还有其他的局部变量,比如argc和argv,它们同样也都存放在栈上的,并通过相对栈顶(%rsp)的偏移量来访问。

3.数组
hello.c中唯一的数组是作为main函数的第二个参数,数组的每个元素都是一个指向字符类型的指针。数组的起始地址存放在栈中-32(%rbp)的位置,被两次调用找参数传给printf。

图3.3.3
4.各种立即数
立即数直接在代码中体现:

3.3.2全局函数

说明main函数为全局函数。

3.3.3算数操作

hello.c源程序中只包含一次算术操作,即i++,汇编代码用addl就能实现,

3.3.4赋值操作

赋值操作利用mov语句。如图,立即数0赋值给地址为%rbp-4的变量i,且其占4个字节。

3.3.5关系操作

hello.c源程序中出现了两次关系操作。
第一次:在if中判断argc的取值是否不等于3.
编译时使用cmpl指令将argc和3进行比较,依据跳转指令je根据条件码决定是否跳转。、

第二次:for循环中判断i是否小于8。
同样,编译时使用cmpl指令将i和7进行比较,依据跳转指令jle根据条件码决定是否跳转。不过进行比较的值是7却不是8,这是因为编译过程中进行了优化。

3.3.6控制转移

由之前用关系操作得到的条件码,实行跳转。

3.3.7函数操作

利用call指令调用函数,将函数需要的参数存放在栈中或寄存器中进行参数传递,在函数调用结束之后,将返回值保存在寄存器%rax中。

图3.3.7

3.3.8类型转换

hello.c中,调用atoi函数将字符型转为int整型变量。

图3.3.8

3.4 本章小结

本章主要介绍了编译的概念和作用,linux下的编译命令,并针对hello.s,详细地分析了编译器如何处理C语言的各种数据以及各类操作。

第4章 汇编

4.1 汇编的概念与作用

汇编:汇编器(as)将汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在hello.o 目标文件中,hello.o 文件是一个二进制文件,它包含程序的指令编码。
作用:将汇编代码转换成机器真正可以读懂的二进制代码。

4.2 在Ubuntu下汇编的命令

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

图4.2

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
1.在ubuntu中使用命令 readelf -a hello.o > helloelf.txt
查看hello.o的ELF格式,并将结果重定位为文本文件。
2.ELF头以一个16字节的目标序列开始,如图中的Magic所示,这个序列描述了生成该文件的系统的字的大小和字节顺序。以hello.o为例,这个16字节序列为7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00,描述了系统的字的大小为8字节,字节顺序为小端序。
ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。

图4.3.2
3.节头部表:负责记录各节名称、类型、地址、偏移量、大小、全体大小等信息。

图4.3.3
4.重定位节:包含偏移量、信息、类型符号值、符号名称等参数。

图4.3.4
5.符号表:记录程序中所定义的全局变量和函数的信息。

图4.3.5

4.4 Hello.o的结果解析

使用命令objdump -d -r hello.o,分析hello.o的反汇编,并与hello.s作比较。


图4.4
我们发现,反汇编代码不仅显示了汇编代码,还有机器代码,机器语言程序的是二进制机器指令的集合,是纯粹的二进制数据表示的语言,是电脑可以真正识别的语言。机器指令由操作码和操作数构成,汇编语言是人们比较熟悉的词句直接表述CPU动作形成的语言,是最接近CPU运行原理的语言。每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言(操作码和操作数)和二进制机器语言建立一一映射的关系,因此可以将汇编语言转化为机器语言,通过对机器代码的分析可以看出一下不同的地方。
接下来我们仔细对照反汇编代码和hello.s,发现它们大部分区别不大,而有些细微的差异。
1.函数调用
在hello.s文件中,call指令后通常为函数名称,而在反汇编文件hello.o中,call指令后为下一条指令的地址。而又因为在hello.c中调用的函数都是共享库中的函数,此时无法确认地址,所以所有的相对地址均设为0,然后在.rela.text节中为其加入重定位信息,等待静态链接的进一步确定。如图所示:

2.分支转移
在反汇编得到的代码中,跳转指令不再依靠段名称,而是直接通过地址跳转。这是因为这些段名称在汇编成机器语言后消失了,故反汇编后不会再出现。

4.5 本章小结

本章介绍了汇编的概念和作用,并在ubuntu里对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了该文件的ELF头、节头部表、重定位节和符号表,还比较了hello.s和hello.o反汇编代码的不同之处。

第5章 链接

5.1 链接的概念与作用

概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存中并执行。链接可以执行于编译时,也就是在源代码被编译成机器代 码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。
作用:链接由链接器执行,使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,然后可以独立地修改和编译这些模块。

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的格式

在ubuntu中使用命令 readelf -a hello.o > helloelf.txt
查看hello的ELF格式,并将结果重定位为文本文件。
1.ELF头:

图5.3.1
各类信息均能从图中得到,也可以看出可执行目标文件hello和可重定位目标文件的ELF头不同在于可执行文件的类型不再是REL而是EXEC,而且节头数量也不同,有27个。
2.节头部表:对 hello中所有的节信息进行了声明。

3.重定位节

图5.3.3
4.符号表

图5.3.4
可以发现,在可执行文件中多出了.dynym节,里面存放的是通过动态链接解析得到的符号,这些符号是程序引用的头文件中的函数。

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
1.使用edb打开hello。

图5.4.1
2.观察Data Dump窗口,发现虚拟地址从0x400000开始

接着我们可以发现这整个空间内之间的每一节都对应5.3的节头表中的内容,例如下图存放着.interp节,保存Linux动态共享库的路径

3…text节是从0x400550开始的,和ELF头中程序的入口一致。

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。

图5.5
可以看到的不同是文件中多了很多函数,这是因为链接的时候将头文件链接到了可执行文件中。

然后观察主函数,我们不难发现在hello.o反汇编代码中出现以main加上相对偏移的跳转已经全部被重写计算,这是因为在重定位后main函数有了全新的地址,使得这个计算成为可能。同时对子函数的call引用也在重定位后重写计算来了。
因此可知,重定位的大体过程是链接器ld将所有链接文件中相同的节合并,并按照要求计算新的偏移地址赋值给新的节。同时链接器按链接指令的顺序搜索符号表,查找符号引用。

5.6 hello的执行流程

图5.6

5.7 Hello的动态链接分析

在程序中动态链接是通过延迟绑定来实现的,延迟绑定的实现依赖全局偏移量表GOT和过程连接表PLT实现。GOT是数据段的一部分,PLT是代码段的一部分。
PLT数组中每个条目时16字节,PTL[0]是一个特殊的条目,他跳转到动态链接器中。每个可被执行程序调用的库函数都有自己的PLT条目。PLT[1]调用__libc_start_main函数负责初始化。
GOT数组中每个条目八个字节。GOT[0]和GOT[1]中包含动态链接器解析地址时会用的信息,GOT[2]时动态练级去在ld-linux.so模块的入口点。其余的每一个条目对应一个被调用的函数。
通过5.3.2的节头表我们可以找到.GOT.PLT的数据从0x601000开始。如图,这是未调用init前的GOT.PLT表。

如下图,这是调用init后的GOT.PLT表。经过初始化后,PLT和GOT表就可以协调工作,一同延迟解析库函数的地址了。

图5.7

5.8 本章小结

本章主要介绍了链接的概念和作用,以及生成链接的命令,分析了hello的elf格式文件,同时也分析了hello的虚拟地址空间以及重定位过程,遍历了整个hello的执行过程,并且比较了hello.o的反汇编和hello的反汇编。

第6章 hello进程管理

6.1 进程的概念与作用

进程:一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码,数据区域存储变量和进程执行期间使用的动态分配的内存,堆栈区域存储区着活动过程调用的指令和本地变量。
作用:进程为用户提供了以下假象:
(1) 我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。
(2) 处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

Shell-bash的作用:是一种交互型的应用级程序,是Linux的外壳,提供了一个界面,用户可以通过这界面访问操作系统内核。
处理流程:
1、从终端读入用户输入的命令
2、将输入字符串切分获得所有的参数
3、如果是内置命令则立即执行
4、否则则调用fork()创建新子进程,再调用execve()执行指定程序

6.3 Hello的fork进程创建过程

我们打开shell,使用 ./hello 120L021526 高岩1的命令来运行hello程序。

由于我们输入的不是一条内置命令,因此shell会调用fork函数创建一个子进程。这样,我们的hello子进程就被创建了。

6.4 Hello的execve过程

当创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,加载并运行需要以下几个步骤:
1.删除已经存在的用户区域,然后创建一组新的代码、数据、堆端,并初始化为0。
2.映射私有区域和共享区域,将新的代码和数据段初始化为可执行文件中的内容
3.设置程序计数器,使其指向代码区的入口,等到下一次调度这个进程时,将直接从入口点开始执行。

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
上下文的概念:上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
上下文切换的具体过程:
(1)保存当前进程的上下文
(2)恢复某个先前被抢占的进程被保存的上下文
(3)将控制传递给这个新恢复的进程。
进程时间安排片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
以hello为例,hello程序在调用了sleep程序后会陷入内核状态,内核可能会进行上下文切换。到程序运行到getchar的时候,内核也会进行上下文切换,让其他进程运行。除了这些,系统还会为hello程序分配时间片,即使没有执行到getchar或者sleep函数,只要hello时间片被用完,系统就会判断当前程序以及执行够久了,从而进行上下文切换,将处理器让给其他进程。

图6.5

6.6 hello的异常与信号处理

1.可能出现的异常:
(1)中断:来自I/O设备的信号。比如输入CTRL -C或者CTRL-Z
(2)陷阱:有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。
(3)故障:是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。
(4)终止:是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。
2.可能产生的信号:SIGINT,SIGSTP,SIGCONT,SIGWINCH等等
3.各类处理:
(1)按下回车,正常运行

程序正常运行,结束后正常被回收。
(2)按下ctrl-z


图6.6
当运行过程中按下ctrl-z,则会给进程发送SIGSTP信号,hello程序将被挂起,用ps命令可以看到hello进程并没有回收,其进程号为2977,再使用jobs命令可以查看job的ID为1,状态为已停止,最后用fg命令,即可让其回到前台继续运行。用命令pstree查看进程,找到hello进程的位置,最后输入kill -9 2977终止进程,再用jobs命令查看,可见已被终止,最后用ps命令查看,也再也没有hello进程了,说明进程已经彻底终止被回收。
(3)按下ctrl-c

按下ctrl-c后,hello进程运行终止。组合键ctrl-c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况下,结果是终止前台作业。利用ps指令可以看到,hello进程已经父进程回收,进程表中无hello进程。

6.7本章小结

本章介绍了进程的概念和作用,以及对shell的功能和处理流程,然后详细分析了hello程序从fork进程的创建,到execve函数执行,最后具体执行过程以及出现异常的处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:逻辑地址指的是在汇编代码中通过偏移量+段基址得到的地址,与物理地址不同。在hello反汇编代码中我们能够看到的就是逻辑地址。
线性地址:线性地址就是虚拟地址,具体见下。
虚拟地址:虚拟地址是逻辑地址计算后的结果,同样不能直接用来访存,需要通过MMU翻译得到物理地址来访存。在hello反汇编代码计算后就能得到虚拟地址。
物理地址:计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每个字节都有一个唯一的物理地址。第一个字节的地址为0,写下来的字节地址为1,再下一个为2,以此类推。虚拟地址通过MMU翻译后得到物理地址。在hello中通过翻译得到的物理地址来得到我们需要的数据。

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

逻辑地址的表示形式为[段标识符:段内偏移量],这个表示形式包含完成逻辑地址到虚拟地址(线性地址)映射的信息。
逻辑地址实际是由48位组成的,前16位是段选择符,后32位是段内偏移量。通过段选择符,我们可以获得段基地址,再与段内偏移量相加,即可获得最终的线性地址。
段标识符又名段选择符,是一个16位的字段,包括一个13位的索引字段,1位的TI字段和2位的RPL字段。

通过段标识符的前13位,可以直接在段描述符表中索引到具体的段描述符。每个段描述符中包含一个Base字段,它描述了一个段的开始位置的线性地址。将Base字段和逻辑地址中的段内偏移量连接起来就得到转换后的线性地址。全局的段描述符,放在全局段描述符表中,每个进程自己的段描述符,放在局部段描述符表中。全局段描述符表存放在gdtr控制寄存器中,而局部段描述符表存放在ldtr寄存器中。
逻辑地址到线性地址的变换过程为:给定逻辑地址,看段选择符的最后一位是0还是1,从而判断选择全局段描述符表还是局部段描述符表。通过段标识符的前13位,得到Base字段,和段内偏移量连接起来最终得到转换后的线性地址。

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

在页表的地址变换中MMU首先利用VPN选择PTE,将列表条目中的PPN和虚拟地址的VPO链接起来,这样便能得到对用的物理地址。

图7.3
当页面命中时CPU硬件执行的步骤:
1.处理器生成一个虚拟地址,并把它传送给MMU;
2.MMU生成PTE地址,并从高速缓存/主存请求得到它;
3.高速缓存/主存向MMU返回PTE;
4.MMU构造物理地址,并把它传送给高速缓存/主存;
5.高速缓存/主存返回所请求的数据字给处理器。

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

为了减少页表过大而导致的空间浪费,我们采用多级页表的方法来压缩大小。

在四级页表层次结构的地址翻译中,虚拟地址被划分为4个VPN和1个VPO。每个第i个VPN都是一个到第i级页表的索引,其中1<=i<=k。第j级页表中的每个PTE都指向第j+1级某个页表的基址,第四级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问四个PTE。(图中k取4即可)

图7.4

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

首先我们使用物理地址的CI进行组索引,对8个块分别对CT进行标志位的匹配。如果匹配成功且块的有效位为1,则成功命中。然后根据数据偏移量 CO取出相应的数据并返回。这里的数据保存在一级Cache。
如果没有命中,或者没找到相匹配的标志位,那么就会在下一级Cache中寻找,只要本级Cache中没找到就要去下一级的Cache中寻找数据,然后逐级写入Cache。
在更新Cache的时候,首先需要判断是否有有效位为0的块。若有,则直接写入;若不存在,则需要驱逐一个块(LRU策略),再进行写入。

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。
这一过程需要下面几步:
(1)删除已存在的用户区域
(2)映射私有区域
(3)映射共享区域
(4)设置程序计数器

图7.7

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

处理缺页要求硬件和操作系统内核协作完成:
(1)处理器生成一个虚拟地址,并把它传送给MMU。
(2)MMU生成PTE地址,并从高速缓存/主存请求得到它。
(3)高速缓存/主存向MMU返回PTE。
(4)PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
(5)缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。
(6)缺页处理程序页面调人新的页面,并更新内存中的PTE。
(7)缺页处理程序返回到原来的进程,再次执行导致缺页的指令,CPU将地址重新发送给MMU。因为虚拟页面现在已经缓存在物理内存中,所以会命中,主存将所请求字返回给处理器。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块:
·显式分配器:要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。显式分配器必须在严格的约束条件下工作,约束有:必须处理任意请求序列;立即响应请求;只使用堆;对齐块;不修改已分配的块。分配器的编写应该实现:吞吐率最大化;内存使用率最大化(两者相互冲突)。
·隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
分配器主要有以下三种具体实现方法:
·显式空闲链表:
堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。这样一来,会使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。显式空闲链表的缺点是空闲块必须足够大,以包含所有需要的指针,以及头部和可能的脚部。这就导致了更大的最小块大小,也潜在地提高了内部碎片的程度。
·隐式空闲链表:
隐式空闲链表的优点是简单。显著的缺点是任何操作的开销,例如放置分配的块,要求对空闲链表进行搜索,该搜索时间与堆中已分配块和空闲块的总数呈线性关系。
·带边界标记的隐式空闲链表:
这种方式可以允许在常数时间进行对前面块的合并,并且它对许多不同类型的分配器和空闲链表组织都是通用的。然而它也存在一个潜在的缺陷。它要求每个块都保持一个头部和一个脚部,在应用程序操作许多个小块时,会产生显著的内存开销。

7.10本章小结

本章先介绍了hello的存储器地址空间,然后分析了从逻辑地址到线性地址的变化(段式管理),以及从线性地址到物理地址的变化(页式管理)。再解析了TLB与四级页表支持下的VA到PA的变换详细分析了地址翻译的过程,分析了三级Cache支持下的物理内存访问,以及hello进程fork和execve时的内存映射,还有缺页故障与缺页中断处理的操作过程。最后讲述了动态存储分配管理的基本方法和策略,从而得到了一个较为完整的动态分配内存的过程。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,所有的输入输出都被当作对相应文件的读和写来执行。
设备管理:unix io接口
这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

Unix I/O接口:
(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想 要访问一个 I/O 设备
(2)Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。
(3)改变当前文件的位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。
(4)读写文件。一个读操作就是从文件复制n > 0个字符到内存,从当前文件位置k开始,然后k += n。对给定一个大小为m字节的文件,当k>=m时执行读操作会出发一个称为EOF的条件。
(5)关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。
Unix I/O函数:
(1)打开文件函数:int open(char *filename, int flags, mode_t mode);
flag参数为写提供一些额外的指示,mode指定了访问权限。
(2)关闭文件函数:int close(int fd);
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的实现分析

对于printf函数,

printf函数是格式化输出函数, 一般用于向标准输出设备按规定格式输出信息。printf中调用了两个函数,分别为vsprintf和write:
对于vsprintf函数,它根据格式串fmt,并结合args参数产生格式化之后的字符串结果保存在buf中,并返回结果字符串的长度。
对于write函数,

它将buf中的i个字符写到终端,由于i保存的是结果字符串的长度,因此write将格式化后的字符串结果写到终端。
对于sys_call函数,

它实现的功能就是把将要输出的字符串从总线复制到显卡的显存中。显存中存储的是字符的ASCII码。字符显示驱动子程序通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

对于getchar函数,

(1)getchar函数运行时,控制权会交给os,用户按键,输入的内容便会显示在屏幕上。按下回车键表示输入完成,这时控制权将被交还给程序。
(2)异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
(3)getchar等调用read系统函数,通过系统调用读取按键ASCII码,直到接受到回车键才返回。

8.5本章小结

本章介绍了Linux的IO设备管理方法,Unix IO接口及其函数,分析了如何实现printf和getchar函数,从而加深对文件操作的理解。

结论

hello经历的过程:
源程序编写:通过编写工具(文本编辑器、IDE等)编写出hello.c;
预处理:预处理器cpp读取需要的系统头文件内容,生成ASCII码的中间文件hello.i。
编译:编译器ccl将C语言代码翻译成汇编指令,生成hello.s。
汇编:汇编器as将hello.s翻译成机器语言指令,并生成重定位信息,将结果保存在可重定位目标文件hello.o中。
链接:链接器进行符号解析、重定位、动态链接等创建一个可执行目标文件hello,此时hello可以被执行。
运行阶段:当我们在shell键入./hello启动程序的时候,shell调用fork函数为其产生子进程,子进程中调用execve函数,加载hello程序,进入hello的程序入口点。
进程运行:内核负责调度进程,并对可能产生的异常及信号进行处理。内存的管理由MMU、TLB、多级页表、cache、DRAM内存、动态内存分配器共同完成,而Unix I/O的作用则是让程序与文件进行交互。
终止:hello最终被shell父进程回收,内核删除为hello进程创建的所有数据结构。

附件

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

hello.i:hello.c预处理后的文本文件
hello.s:hello.i编译后的汇编文件
hello.o:hello.s汇编后得到的可重定位目标文件
hello:hello.o链接后得到的可执行目标文件
helloelf.txt:hello.o的elf格式,分析汇编器和链接器行为
hello.txt:可执行hello的elf格式,作用是重定位过程分析。

参考文献

[1] https://www.cnblogs.com/diaohaiwei/p/5094959.html
[2] file:///C:/Users/m1777/Desktop/深入理解计算机系统原书第3版-文字版.pdf
[3]ANSIC标准定义的C语言预处理指令总结https://blog.csdn.net/zxnsirius/article/details/51158895?utm_source=itdadao&utm_medium=referral
[4]printf函数实现的深入剖析 https://www.cnblogs.com/pianist/p/3315801.html