程序人生-Hello’s P2P

目录


第1章 概述 - 4 -
1.1 Hello简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 5 -
第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的编译结果解析 - 9 -
3.3.1数据 - 9 -
3.3.2 赋值 - 11 -
3.3.3 算术操作 - 11 -
3.3.4 关系操作 - 11 -
3.3.5 数组操作 - 12 -
3.3.6 控制转移 - 12 -
3.3.7 函数操作 - 13 -
3.3.8 类型转换 - 14 -
3.4 本章小结 - 14 -
第4章 汇编 - 15 -
4.1 汇编的概念与作用 - 15 -
4.2 在Ubuntu下汇编的命令 - 15 -
4.3 可重定位目标elf格式 - 15 -
4.4 Hello.o的结果解析 - 18 -
4.5 本章小结 - 20 -
第5章 链接 - 21 -
5.1 链接的概念与作用 - 21 -
5.2 在Ubuntu下链接的命令 - 21 -
5.3 可执行目标文件hello的格式 - 21 -
5.4 hello的虚拟地址空间 - 24 -
5.5 链接的重定位过程分析 - 25 -
5.6 hello的执行流程 - 29 -
5.7 Hello的动态链接分析 - 29 -
5.8 本章小结 - 30 -
第6章 hello进程管理 - 31 -
6.1 进程的概念与作用 - 31 -
6.2 简述壳Shell-bash的作用与处理流程 - 31 -
6.3 Hello的fork进程创建过程 - 32 -
6.4 Hello的execve过程 - 32 -
6.5 Hello的进程执行 - 33 -
6.6 hello的异常与信号处理 - 35 -
6.7本章小结 - 38 -
第7章 hello的存储管理 - 39 -
7.1 hello的存储器地址空间 - 39 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 39 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 41 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 42 -
7.5 三级Cache支持下的物理内存访问 - 43 -
7.6 hello进程fork时的内存映射 - 44 -
7.7 hello进程execve时的内存映射 - 45 -
7.8 缺页故障与缺页中断处理 - 46 -
7.9动态存储分配管理 - 46 -
7.10本章小结 - 49 -
第8章 hello的IO管理 - 50 -
8.1 Linux的IO设备管理方法 - 50 -
8.2 简述Unix IO接口及其函数 - 50 -
8.3 printf的实现分析 - 51 -
8.4 getchar的实现分析 - 52 -
8.5本章小结 - 53 -
附件 - 53 -
结论 - 53 -
参考文献 - 55 -

摘要

本次大作业从hello的自白入手,分析了hello的整个生命历程。从最初的的源程序hello.c开始,经过预处理、编译、汇编、链接,生成hello可执行目标程序。并简要分析了计算机系统如何对hello进行进程管理、存储管理以及I/O管理。从hello的整个生命周期,我们可以对计算机系统有更加深入的理解。

关键词

预处理 编译 汇编 链接 进程 存储 I/O

第1章 概述

1.1 Hello简介

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

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位
开发调试工具:gcc、edb、hexedit、gdb、readelf

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名称 文件作用
hello.i 源程序预处理之后的文本
hello.s hello.i经编译器生成的汇编文件
hello.o hello.s汇编生成的可重定位目标文件
hello hello.o链接生成的可执行目标文件
hello.elf hello.o的elf格式
hello.txt hello.o反汇编文件
hello1.elf hello的elf格式
hello1.txt hello反汇编文件

1.4 本章小结

本章描述了hello的P2P、020的过程,简要介绍了hello.c到生成可执行目标文件的中间过程以及生成的文件。
(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理:
预处理(预编译)工作也叫做宏展开:将宏名替换为字符串。预处理命令可以改变程序设计环境,提高编程效率,它们并不是 C 语言本身的组成部分,不能直接对 它们进行编译,必须在对程序进行编译之前,先对程序中这些特殊的命令进行“预处理”。经过预处理后,程序就不再包括预处理命令了,最后再由编译程序对预处理之后的源程序进行编译处理,得到可供执行的 目标代码。C 语言提供的预处理功能有三种,分别为宏定义、文件包含和条件编译。
文件包含命令的功能是把指定的文件插入该命令行位置取代该命令行,从而把指定的文件和当前的源 程序文件连成一个源文件。
预处理的作用:
1.将所有的#define删除,并展开所有的宏定义;
2.处理所有的预编译指令,例如:#if,#elif,#else,#endif;
3.处理#include预编译指令,将被包含的文件插入到预编译指令的位置;
4.添加行号信息文件名信息,便于调试;
5.删除所有的注释:// /**/;
6.保留所有的#pragma编译指令,因为在编写程序的时候,我们经常要用到#pragma指令来设定编译器的状态或者是指示编译器完成一些特定的动作。
7.生成.i文件。
包括(1)去注释 (2)宏替换 (3)头文件展开 (4)条件编译

2.2在Ubuntu下预处理的命令

gcc –E hello.c –o hello.i 截图如下:
在这里插入图片描述

2.3 Hello的预处理结果解析

经过cpp的预处理,生成hello.s文件,利用vim打开文件,发现文件内容有所增加,但还是文本形式,还是可以阅读的C程序。对原文件中的宏进行了宏展开,头文件中的内容被包含进该文件中、条件编译等。例如声明函数、定义结构体、定义变量、定义宏等内容。添加行号信息以及文件信息,并删除所有注释。

2.4 本章小结

本章介绍了预处理的概念,及其进行的一些处理,例如宏展开宏替换,引入头文件的信息。给出预处理的linux指令,并简要分析hello.i文件的内容。
(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译的概念:编译器ccl将文本文件 hello.i 翻译成文本文件 hello.s。 这个过程称为编译。编译会从词法、语法和语义上对文件进行分析,并进行汇编代码生成, 形成的还是文本文件------汇编语言文件。以.s作为文件扩展名。
作用:以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。生成一个.s的汇编文本文件。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s
截图
在这里插入图片描述

hello.s 部分代码截图如下:
在这里插入图片描述

.file 声明源文件
.section
.align 指令或数据地址对齐方式
.rodata 只读数据段
.text 代码段
.global声明全局变量
.type声明一个符号是数据类型还是函数类型

3.3 Hello的编译结果解析

3.3.1数据

1.字符串
在这里插入图片描述

程序中有两个字符串,存在只读数据段
在这里插入图片描述

分析汇编指令,一个作为参数传给puts,一个作为参数传给printf
2.局部变量
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

结合源代码以及汇编代码分析,可得知有一个局部变量i,存放在栈中%rbp-4的位置
argv作为用户传递给函数的参数,也存在栈中,argc为参数数量
3.全局变量
main函数为全局函数
4.各种立即数
在汇编代码中以$加数字体现
5.char* argv[]数组
作为唯一的数组,argv[]为参数列表,如果看成二维数组,数组的每一个元素都是一个字符串指针。数组的起始位置为栈中的%rbp-32的位置。
在这里插入图片描述

如图:两次调用%rbp-32,分别根据偏移找到argv[1]和argv[2],作为参数传递给printf

3.3.2 赋值

源程序hello.c有条赋值语句i=0,对应汇编代码表示为
在这里插入图片描述

根据赋值涉及的不同数据类型,对应mov指令有不同的后缀:
如movb(传送字节),movw(传送字),movl(传送双字),movq(传送四字)

3.3.3 算术操作

源程序hello.c中,循环语句中i++进行算数操作
在这里插入图片描述

对应汇编指令addl

3.3.4 关系操作

在这里插入图片描述

源程序hello.c中argc!=4,对应汇编编码为cmpl $4,-20(%rbp),同时这条cmpl的指令还有设置条件码的作用,当根据条件码来判断是否需要跳转到分支中。
在这里插入图片描述

源程序中i<8,在汇编代码被编译为:cmpl $7,-4(%rbp),计算 i-7然后设置条件码ZF,为下一步 jle 利用条件码进行跳转做准备。
在这里插入图片描述

3.3.5 数组操作

通过argv[1]和argv[2],分别指向main函数的第一、二个参数。并作为参数传递给printf函数。具体汇编代码见3.3.1数据中的char *argv[]数组部分。

3.3.6 控制转移

汇编语言中首先设置条件码,然后根据条件码来进行控制转移,在hello.c中,有以下控制转移指令:
(1)判断i是否为4,如果i等于3,则不执行.L2,否则执行.L2,对应的汇编代码为
在这里插入图片描述

(2) 循环条件判断:for(i=0;i<8;i++),通过每次判断i是否满足小于8来判断是否需要跳转至循环语句中,对应的汇编代码为:
在这里插入图片描述

.L2中,将%rbp-4位置存放的i赋值为0,.L3对应的红框中的内容将i与7比较,若小于等于,跳转执行.L4,否则向下执行调用getchar

3.3.7 函数操作

调用函数时有以下操作:(假设函数P调用函数Q)
(1)传递控制:进行过程 Q 的时候,程序计数器必须设置为 Q 的代码的起始 地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的 地址。
(2)传递数据:P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 中返回 一个值。
(3) 分配和释放内存:在开始时,Q 可能需要为局部变量分配空间,而在返回 前,又必须释放这些空间。
源程序hello.c的函数调用有:main函数,printf,exit,sleep ,getchar函数
main函数的参数是argc和argv;argv[1]、argv[2]为函数printf的参数
在这里插入图片描述
在这里插入图片描述

exit参数为1,sleep函数参数是atoi(argv[3])

3.3.8 类型转换

源程序hello.c调用atoi(argv[3]),对应汇编代码为:
在这里插入图片描述

3.4 本章小结

主要讲述了编译阶段编译器如何处理数据以及各种操作,怎样将其转为汇编指令。了解汇编器的编译原理,我们很容易进行逆向工程,将汇编代码还原为C语言程序。
(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

汇编器将hello.s转为可重定位目标文件hello.o,hello.o为二进制文件,包含程序的机器级指令编码。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

as hello.s –o hello.o 或 gcc –c hello.s –o hello.o
截图如下:
在这里插入图片描述

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
1.查看ELF文件头信息:
在这里插入图片描述

可知:ELF头以16字节的序列magic作为开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头的剩余部分帮助链接器语法分析和解释目标文件的信息。其中包括文件类型(REL)、机器类型(如x86-64)、节头表的偏移、节头表的表项大小以及表项个数。不同节的位置和大小是由节头表来描述的。
根据头文件的信息,可以知道该文件是可重定位目标文件,有13个节。
字符串表.strtab在节头表中的索引为10。
2.查看节头表的信息
节头部表,包含了文件中出现的各个节的语义,描述每个节的节名、在文件中的偏移、大小、访问属性、对齐方式等。由于是可重定位目标文件,所以每个可装入节的起始地址总为0,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小。同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,.data、.bss可读可写,.rodata只可读。

在这里插入图片描述

3.查看符号表:
.symtab:存放在程序中定义和引用的函数和全局变量的信息,不包含局部变量的条目。符号表是由汇编器构造的,使用编译器输出到汇编语言中的符号,.symtab节包含ELF符号表。这张表包含一个条目的数组。name是字符串表中的字节偏移,指向符号的以null结尾的字符串名字。value是符号的地址,对于可重定位的目标文件,value是距定义目标的的节的起始位置的偏移。
size是目标的大小,type要么是数据,要么是函数,binding字段表示符号串是本地的还是全局的。
在这里插入图片描述

4.可重定位节
重定位节:一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面调用本地函数的指令不需要修改。
重定位节.rel.text中各项符号的信息:
offset:需要被修改的引用节的偏移
symbol:标识被修改引用应该指向的符号,
type:告知链接器应该如何修改新的应用
attend:一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整Name:重定向到的目标的名称。
在这里插入图片描述

通过命令readelf -a hello.o > hello.elf,将所有elf文件内容输出到文件hello.elf
在这里插入图片描述

4.4 Hello.o的结果解析

objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
objdump -d -r hello.o >hello.txt
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
通过objdump反汇编的代码和hello.s进行比较,发现汇编语言的指令并没有什么不同的地方,只是反汇编代码所显示的不仅仅是汇编代码,还有机器代码。机器指令由操作码和操作数构成,汇编语言是人们比较熟悉的词句直接表述CPU动作形成的语言,是最接近CPU运行原理的语言。每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言(操作码和操作数)和二进制机器语言建立一一映射的关系,因此可以将汇编语言转化为机器语言,通过对机器代码的分析可以看出一下不同的地方。
(1)分支转移:反汇编的跳转指令用的不是段名称比如.L3,而是用的确定的地址,因为,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
(2)函数调用:在.s 汇编文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。这是因为 hello.c 中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。

4.5 本章小结

本章主要分析汇编器对hello.s进行汇编生成的hello.o可重定位目标文件,分析了可重定位目标文件的ELF头、节头表、符号表、以及可重定位信息,比较了hello.s汇编代码和hello.o反汇编代码的不同之处,简要分析了从汇编语言到机器语言的一一映射关系。

(第4章1分)

第5章 链接

5.1 链接的概念与作用

链接器是将各种代码和数据片段收集组合成为一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是程序被加载器加载到内存时;甚至执行于运行时,也就是由应用程序来执行。在现代系统中,链接是由叫做链接器的程序自动执行的。
可重定位目标文件hello.o由链接器生成hello可执行目标文件的过程,即链接的过程。

5.2 在Ubuntu下链接的命令

使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
在这里插入图片描述

如上图进行链接,发现目录下多了一个hello可执行目标文件
在这里插入图片描述

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
在终端命令行上输入:readelf -a hello >hello1.elf
在这里插入图片描述

1.分析ELF头
与hello.o的ELF相比,hello的ELF头中节头数量由13变成25个,文件类型变为可执行目标文件。由于文件已经重定位,程序的入口地址已经重定位到运行时的内存地址,并新增了程序头表。
在这里插入图片描述

2.节头表信息,包含各节头的大小、偏移量、对齐等等
在这里插入图片描述

3.符号表
在这里插入图片描述

4.段头表(程序头表):描述了可执行文件的连续的片与连续的内存段之间的映射关系。
在这里插入图片描述

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
通过edb查看,可得知hello的内存地址开始于0x400000,结束于0x400ff0
在这里插入图片描述
在这里插入图片描述

结合hello ELF文件中的节头表信息,如.text偏移量为0x520,大小为0x132,edb中位置如下:
在这里插入图片描述

5.5 链接的重定位过程分析

结合hello.o的重定位项目,分析hello中对其怎么重定位的。
在终端执行命令:objdump -d -r hello >hello1.txt
在这里插入图片描述

分析hello与hello.o的不同,说明链接的过程。可以发现有一下不同的地方:
(1)hello反汇编的代码有确定的运行时内存地址,即已经完成了重定位,而hello.o反汇编代码中代码的地址均为0,未完成重定位的过程
可重定位目标文件hello.o的反汇编如下,显示具体的重定位类型为PC相对引用还是可重定位绝对引用:
在这里插入图片描述

可执行目标文件hello的部分反汇编如下:
在这里插入图片描述

(2)hello反汇编的代码中多了很多的节以及很多函数的汇编代码,这些节都具有一定的功能和含义

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

hello的重定位过程:
(1)重定位节和符号定义:链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
(2)重定位节中的符号引用:链接器修改代码节和数据节中对每个符号的引用,使它们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。
(3)重定位条目:当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。代码的重定位条目放在.rel.txt,已初始化数据的重定位条目放在.rel.data节中
可重定位条目如下:
在这里插入图片描述

下图为ELF重定位条目的格式。
在这里插入图片描述

其中,offset是需要被修改的引用的节偏移,symbol标识被修改的引用指向的符号,type告知链接器如何修改新的引用,addend是一个有符号常数,一些类型的重定位信息需要使用它对被修改引用的值做偏移调整。
5.重定位符号引用
地址计算方法如下:
在这里插入图片描述

当算法运行时,链接器已经为每个节和每个符号都选择了运行时地址。在hello.o的反汇编代码中可以看到,为每个引用,汇编器产生一个重定位条目,显示在引用的后一行上。
举例说明:对exit函数的引用
在这里插入图片描述

查看hello反汇编,结果正确:
在这里插入图片描述

5.6 hello的执行流程

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
(1) 载入:_dl_start、_dl_init
(2)开始执行:_start、_libc_start_main
(3)执行main:_main、_printf、_exit、_sleep、
_getchar、_dl_runtime_resolve_xsave、_dl_fixup、_dl_lookup_symbol_x
(4)退出:exit
程序名称 地址
_init 0x400468
__libc_csu_init 0x4005e0
_start 0x400520
__libc_csu_init 0x4005e0
__libc_csu_fini 0x400650
_fini 0x400654
_IO_stdin_used 0x400660
main 0x40054a

5.7 Hello的动态链接分析

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。
延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。

5.8 本章小结

本章通过介绍连接的作用以及概念,详细介绍了一个可重定位目标文件如何变成可执行目标文件的过程,详细讲述hello的ELG各节的信息,分析了hello的虚拟地址空间、链接的重定位过程、hello的执行流程、hello的动态链接过程。历经艰辛,我们的hello终于诞生了。

(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用

进程的经典定义就是一个执行中程序的实例。在现代计算机系统上运行一个程序时,我们会得到一个假象,就好像我们的程序是当前运行的唯一的程序一样。我们的程序好像独占地使用处理器和内存。处理器就好像是无间断地一条接一条的执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统中唯一的对象。这些假象都是通过进程的概念提供给我们的。
进程给应用程序的关键抽象:
·一个独立的逻辑流,它提供一个假象,好像我们的程序独占地使用处理器。
·一个私有的地址空间,它提供一个假象,好像我们的程序独立地使用内存系统。
系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这些状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

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

shell:shell是一个用c语言编写的程序,它是用户使用Unix/Linux的桥梁,shell既是一种命令语言,又是一种程序设计语言,shell是一种应用程序。
功能:shell应用程序提供了一个界面,用户通过访问这个界面访问操作系统内核的服务。
处理流程:
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数
3)如果是内置命令则立即执行
4)否则通过fork创建一个新的子进程,执行相应的程序
5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
6)如果该任务是前台作业那么需要等到它运行结束才返回,即在命令行读入用户下一个输入
7)shell 应该接受键盘输入信号,并对这些信号进行相应处理

6.3 Hello的fork进程创建过程

父进程通过调用fork函数创建一个新的运行的子进程,子进程得到和父进程用户级虚拟地址空间相同的(但是独立的)份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork函数时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间最大的区别在于它们有不同的PID。
父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
以我们的hello为例,当我们输入 ./hello 1190200301 边子益 的时候,首先shell对我们输入的命令进行解析,由于我们输入的命令不是一个内置的shell命令,因此shell会调用fork()创建一个子进程。

6.4 Hello的execve过程

当创建了一个子进程,子进程在通过execve()在当前进程的上下文中加载并运行一个新程序。若没有出现错误,execve调用一次从不返回。
加载运行需要以下几个步骤:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
(2)映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。图6.1概括了私有区域的不同映射。
(3)映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
(4)设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到 CPU 引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存
图6.1 加载器是如何映射用户地址空间的区域的

图6.1 加载器是如何映射用户地址空间的区域的

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
hello进程的执行是依赖于进程所提供的抽象的基础上,下面阐述操作系统所提供的的进程抽象:
1.逻辑控制流::一系列程序计数器 PC 的值的序列叫做逻辑控制流,进程是轮流 使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占 (暂时挂起),然后轮到其他进程。
2.进程的上下文:上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
3.并发流:一个逻辑流的执行时间与另一个流重叠,成为并发流,这两个流成为并发的运行。多个流并发的执行的一般现象成为并发
4.进程时间片:一个进程执行它的控制流的每一部分的每一时间段叫做时间片。
5.一个进程和其他的进程轮流运行的概念成为多任务。多任务也叫时间分片。
6.私有地址空间:进程为每个流都提供一种假象,好像它是独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个内存字节是不能被其他进程读或者写的,在这个意义上,这个地址空间是私有的。
7.用户模式和内核模式:
处理器通常使用一个寄存器中的一个模式位提供两种模式的区分,该寄存器描述了进程当前享有的特权,设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置;当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据。
8.上下文切换:当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程:
1)保存以前进程的上下文
2)恢复新恢复进程被保存的上下文,
3)将控制传递给这 个新恢复的进程 ,来完成上下文切换。
现在分析一下hello进程执行,在进程调用execve函数之后,进程已经为hello程序分配了新的虚拟的地址空间,并且已经将hello的.txt和.data节分配为虚拟地址空间的代码区和数据区。最初hello运行在用户模式下,输出hello 1190200301 边子益,然后hello调用sleep函数之后进程陷入到内核。内核不会选择什么都不做,等待sleep函数调用结束,而是处理休眠请求,将当前进程挂起,即将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程B,当定时器到时发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。
整个上下文切换与图6.2类似,不同的是:read对应于hello程序中的sleep,磁盘中断为计时器中断,返回是从sleep返回。
图6.2 进程上下文切换的剖析
图6.2 进程上下文切换的剖析
程序hello运行效果如下图:
在这里插入图片描述

6.6 hello的异常与信号处理

程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
1.异常和信号异常可以分为四类:中断、陷阱、故障、终止,各自的属性如图
在这里插入图片描述

图6.6.1 异常的类别
hello程序出现的异常可能有:
中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。
陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
故障:在执行hello程序的时候,可能会发生缺页故障。
终止:终止是不可恢复的致命错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。
在发生异常时会发出信号,比如缺页故障会导致OS发生SIGSEGV信号给用户进程,而用户进程以段错误退出。常见信号种类如下图6.4所示
在这里插入图片描述

图6.6.2 信号种类
1.键盘输入造成的异常:
(1)正常执行效果如图6.6.3
在这里插入图片描述

图6.6.3

(2)按下 ctrl-z 的结果,输入ctrl-z默认结果是挂起前台的作业,hello进程并没有回收,而是运行在后台下,用ps命令可以看到,hello进程并没有被回收。此时他的后台 job 号是 1,调用 fg 1 将其调到前台,此时 shell 程序首先打印 hello 的命令行命令, hello 继续运行打印剩下的 8 条 info,之后输入字串,程序结束,同时进程被回收。如图6.6.4.

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

图6.6.4

(3)输入Ctrl+C,运行结果如图6.6.5,在键盘上输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业,如图6.6.6,用ps查看前台进程组发现没有hello进程。
在这里插入图片描述

图6.6.5

在这里插入图片描述

图6.6.6
(3)程序运行过程中按键盘,不停乱按,结果如图6.6.7,可以发现,乱按只是将屏幕的输入缓存到 stdin,当 getchar 的时候读出一个’\n’结尾的字串(作为一次输入),其他字串会当做 shell 命令行输入。
在这里插入图片描述

图6.6.7

6.7本章小结

在本章中,阐述进程的定义与作用,同时介绍了 shell 的一般处理流程和作用,并且重点分析了调用 fork 创建新进程,调用 execve函数执行hello,hello的进程执行,以及hello 的异常与信号处理,以及在shell进程运行时,对键盘输入给进程造成的影响进行分析。
(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址(Logical Address) 是指由程序产生的与段相关的偏移地址部分。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。

线性地址:也叫虚拟地址,和逻辑地址类似,也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件也是内存的转换前地址。

虚拟地址:也就是线性地址。

物理地址:用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大的逐字节编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。

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

机器语言指令中出现的内存地址,都是逻辑地址,需要转换成线性地址,再经过MMU(CPU中的内存管理单元)转换成物理地址才能够被访问到。
我们写个最简单的hello world程序,用gcc编译,再反汇编后会看到以下指令:
mov 0x80495b0, %eax
这里的内存地址0x80495b0 就是一个逻辑地址,必须加上隐含的DS 数据段的基地址,才能构成线性地址。也就是说 0x80495b0 是当前任务的DS数据段内的偏移。
在x86保护模式下,段的信息(段基线性地址、长度、权限等)即段描述符占8个字节,段信息无法直接存放在段寄存器中(段寄存器只有2字节)。Intel的设计是段描述符集中存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT内的索引值(index)。
Linux中逻辑地址等于线性地址。为什么这么说呢?因为Linux所有的段(用户代码段、用户数据段、内核代码段、内核数据段)的线性地址都是从 0x00000000 开始,长度4G,这样 线性地址=逻辑地址+ 0x00000000,也就是说逻辑地址等于线性地址了。
这样的情况下Linux只用到了GDT,不论是用户任务还是内核任务,都没有用到LDT。GDT的第12和13项段描述符是 __KERNEL_CS 和__KERNEL_DS,第14和15项段描述符是 __USER_CS 和__USER_DS。内核任务使用__KERNEL_CS 和__KERNEL_DS,所有的用户任务共用__USER_CS 和__USER_DS,也就是说不需要给每个任务再单独分配段描述符。内核段描述符和用户段描述符虽然起始线性地址和长度都一样,但DPL(描述符特权级)是不一样的。__KERNEL_CS 和__KERNEL_DS 的DPL值为0(最高特权),__USER_CS 和__USER_DS的DPL值为3。
用gdb调试程序的时候,用info reg 显示当前寄存器的值:
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
可以看到ds值为0x7b, 转换成二进制为 00000000 01111011,TI字段值为0,表示使用GDT,GDT索引值为 01111,即十进制15,对应的就是GDT内的__USER_DS用户数据段描述符。
从上面可以看到,Linux在x86的分段机制上运行,却通过一个巧妙的方式绕开了分段。
Linux主要以分页的方式实现内存管理。
在这里插入图片描述

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

线性地址(也就是虚拟地址 VA)到物理地址(PA)之间的转换通过分页机制完成。而分页机制是对虚拟地址内存空间进行分页。
这里不考虑TLB和多级页表,这将在下面探讨。
使用虚拟寻址,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存之前首先转换为适当的物理地址。将一个虚拟地址转换为物理地址叫做地址翻译,需要CPU硬件和操作系统之间的紧密合作。CPU芯片上叫做内存管理单元(MMU)的专用硬件,利用主存中的查询表来动态翻译虚拟地址。

虚拟地址作为到磁盘上存放字节的数组的索引,磁盘上的数组内容被缓存在主存中。同时,磁盘上的数据被分割成块,这些块作为磁盘和主存之间的传送单元。虚拟内存分割被成为虚拟页。物理内存被分割为物理页,物理页和虚拟页的大小时相同的。
任意时刻虚拟页都被分为三个不相交的子集:
未分配的:VM系统还未分配的页
缓存的:当前已经缓存在物理内存的已分配页
未缓存的:当前未缓存在物理内存的已分配页

每次将虚拟地址转换为物理地址,都会查询页表来判断一个虚拟页是否缓存在DRAM的某个地方,如果不在DRAM的某个地方,通过查询页表条目可以知道虚拟页在磁盘的位置。页表将虚拟页映射到物理页。如图7.3.1所示,页表就是一个页表条目的数组,每一个页表条目是由一个有效位和一个n为地址字段组成。有效位表明虚拟页是否缓存在DRAM中,n位地址字段是物理页的起始地址或者虚拟页在磁盘的起始地址。
在这里插入图片描述

图7.3.1页表

通过了解了上述虚拟地址转换为物理地址操作系统所提供的机制,现在我们来看一下到底是如何实现虚拟地址到物理地址的转换。如图7.3.2。

n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,例如VPN 0选择PTE 0。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。这里的VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。
在这里插入图片描述

图7.3.2

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

接下来我们将会研究在TLB与四级页表支持下, Intel Core i7 环境中研究 VA 到 PA 的地址翻译问题。
前提如下: 虚拟地址空间 48 位,物理地址空间 52 位,页表大小 4KB,4 级页表。TLB 4 路 16 组相联。CR3 指向第一级页表的起始位置(上下文的一部分,每次上下文切换时,CR3的值都会被恢复)。
分析前提条件:由一个页表大小 4KB,得出 VPO 12 位,VA共48位,所以VPN是36位,36位的VPN被划分为四个9位的片,每个片被作用到一个页表的偏移量;因为 TLB 共 16 组,所以 TLBI 需 4 位,因为 VPN 36 位,所以 TLBT 32 位。
在这里插入图片描述
在这里插入图片描述

如图 ,CPU 产生虚拟地址 VA,VA 传送给 MMU,MMU 使用前 36 位 VPN 作为 TLBT(前 32 位)+TLBI(后 4 位)向 TLB 中匹配,如果命中,则得到 PPN (40bit)与 VPO(12bit)组合成 PA(52bit)。 如果 TLB中没有命中,MMU 向页表查询,CR3 页表基址寄存器确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出 PTE(包含L2页表的基地址),如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目。如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。

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

当虚拟地址翻译成物理地址后,接下来我们讨论如何通过物理地址从内存读取内容。
我们只讨论cache1的物理寻址,cache2、cache3同理。
由于L1Cashe有S=64组,2^s=64,所以组索引(CI)位s为6,每组有8个高速缓存行,由于每个块的大小为64字节,所以块偏移(CO)为6为,物理地址PA共52位,因此标记位(CT)为40位。
L1Cache的物理访存大致过程如下:
(1) 组选择:取出物理地址中的组索引位CI,将二进制组索引转化为一个无符号整数,找到相应的组。
(2) 行匹配:把物理地址的标记为拿去和相应的组中所有行的标记位进行比较,当物理地址的标记位CT和高速缓存行的标记位匹配时,而且高速缓存行的有效位是1,则高速缓存命中。
(3) 字选择:一旦命中,我们就知道我们要找的字在这个块的某个地方。因此块偏移位CO提供了第一个字节的偏移。我们把块看成一个数组,字节偏移就是到数组的一个索引。把这个字的内容取出返回给CPU。
(4)不命中:如果高速缓存不命中,那么需要从存储层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,则采用最近最少使用策略 LRU 进行替换最后一次访问时间最久远的那个块。如图7.5所示 。
在这里插入图片描述

图7.5

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

当创建了一个子进程,子进程在通过execve()在当前进程的上下文中加载并运行一个新程序。若没有出现错误,execve调用一次从不返回。
加载运行需要以下几个步骤:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
(2)映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
(3)映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
(4)设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到 CPU 引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
在这里插入图片描述

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

缺页故障:当指令引用一个虚拟地址,而该虚拟地址对应的物理页面不在内存中时,MMU就会触发缺页故障。通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面 到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。CPU将引起缺页的虚拟地址再次发给MMU。因为虚拟页面现在缓存在物理内存中,就会命中,因此指令可以没有故障的运行完成。处理过程如下:

在这里插入图片描述

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显示地保留为供应用程序使用。空闲块可用来分配。空闲块可保持空闲,直到它显示地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显示执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格。两种风格都要求应用显示地分配块。他们的不同之处在于由哪个实体来负责释放已分配的块。
·显示分配器,要求应用显示地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显示分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。
·隐式分配器,另一方面,要求分配器检测一个已分配块何时不再被应用程序所使用,那么就释放这个块。隐式分配器也叫垃圾收集器,而自动释放未使用的已分配块的过程叫做垃圾收集。

1.隐式空闲链表:
隐式空闲链表区别块边界、已分配块、空闲块的方法如图所示:
在这里插入图片描述

在这种情况下,一个块是由一个字的头部、有效载荷,以及可能的一些额外的填充组成的。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。
头部后面就是调用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。需要填充有很多原因。比如,填充可能是分配器策略的一部分,用来对付外部碎片。或者也需要它来满足对齐要求。
我们可以将堆组织为一个连续的已分配块和空闲块的序列。因为空闲块是通过头部中大小字段隐含地连接着的,我们称这种结构为隐式空闲链表。
在这里插入图片描述

(1)放置已分配的块
当应用请求一个大小为k字节的块时,分配器搜索空闲链表,查找一个足够大可以放置所请求块的空闲块。分配器执行这种搜索的方式是由放置策略确定的。常见的放置策略有首次适配、下一次适配、最佳适配。
(2)分割空闲块
一旦分配器找到一个匹配的空闲块,它必须决定分配这个块中的多少空间。分配器通常选择将这个块分割为两部分。第一部分变成分配块,而剩下的一部分变为新的空闲块。
(3)获取额外的堆内存
如果分配器不能为请求块找到合适的空闲块,一个选择是通过合并那些在内存中物理上相邻的空闲块来创建一些更大的空闲块。如果这样还是不能生成一个足够大的块,或者空闲块已经最大程度地合并了,那么分配器就会通过调用sbrk函数,向内核请求额外的堆内存。分配器将额外的内存转化为一个大的空闲块,将这个块插入到空闲链表中,然后将被请求的块放置在新的空闲块中。
(4)合并空闲块
当分配器释放一个空闲块时,可能有其他空闲块与这个新释放的空闲块相邻。这些邻接的空闲块可能引起一种现象,叫假碎片,即有许多可用的空闲块被切割为小的、无法使用的空闲块。为解决假碎片问题,任何实际的分配器都必须合并相邻的空闲块,这个过程称为合并。
合并的情况一共有四种:前后都不空、前不空后空、前空后不空、前后都空。Knuth提出一种边界标记的技术,允许在常数时间内进行对前面块的合并。如图所示:
在这里插入图片描述

2.显示空闲链表:
显示空闲链表是将空闲块组织为某种形式的显示数据结构。堆可以组织为双向空闲链表,在每个空闲块里,包含一个前驱和后继指针。
在这里插入图片描述

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的时间减少到了空闲块数量的时间。不过,释放一个块的时间可以是线性的,也可能是某个常数,这取决于我们所选择的空闲链表中块的排序策略。
一种方法是用后进先出的顺序维护链表,将新释放的快放在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的快。这样情况下,释放一个快可以在常数时间内完成。如果使用了边界标记,那么合并也可以在常数时间内完成。
另一种方法是按照地址顺序来维护链表,其中链表中的每个块的地址都小于它后继块的地址。在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序的首次适配比LIFO排序的首次适配有更高的内存利用率,接近最佳适配的利用率。
一般而言,显示空闲链表的缺点是空闲块必须足够大,以包含所有需要的指针,以及头部和可能的脚部。这就导致了更大的最小块大小,也潜在提高了内部碎片的程度。

7.10本章小结

本章主要研究了hello的存储管理,包括hello的存储器地址空间,从逻辑地址到线性地址即虚拟地址的转化,从虚拟地址到物理地址的转化,详细分析了虚拟地址到物理地址的地址翻译过程,以及如何通过物理地址访问内存的机制,并探讨了hello进程fork与execve的内存映射,分析缺页故障以及处理机制,最后分析了动态存储器的分配管理。
(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

I/O设备的模型化:文件
设备管理:unix I/O接口
一个Linux文件就是一个m字节的序列:
B0,B1,B2……Bm
所有的 IO 设备(如网路、磁盘、终端)都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行。

8.2 简述Unix IO接口及其函数

Unix I/O输入输出的方式:
·打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个较小的非负整数值,叫做描述符,它在后续对此文件的所有所有操作中表示这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个文件描述符。
·Linux shell 创建的每个进程都有三个打开文件:标准输入(描述符为0)、标准输出(描述符为1)、标准错误(描述符为2)。
·改变当前文件的位置。对于每个打开文件,内核维护着一个文件位置k,初始值为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显示的设置当前文件位置为1。
·读写文件。一个读操作就是从文件复制 n>0 个字节到内存,从当前文件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的文件,当 k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。
·关闭文件。内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix I/O 函数:
(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的实现分析

https://www.cnblogs.com/pianist/p/3315801.html
1.研究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; 
    } 

在形参列表里有这么一个token:…
这个是可变形参的一种写法。
当传递参数的个数不确定时,就可以用这种方式来表示。
很显然,我们需要一种方法,来让函数体可以知道具体调用时参数的个数。
2.vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
3. 让我们追踪下write吧:

 write: 
     mov eax, _NR_write 
     mov ebx, [esp + 4] 
     mov ecx, [esp + 8] 
     int INT_VECTOR_SYS_CALL 

在printf中调用系统函数write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址
int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数
4.再来看看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 

一个call save,是为了保存中断前进程的状态。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求。键盘中断服务程序先从键盘接口取得按键的扫描码,然后根据其扫描码判断用户所按的键并作相应的处理,最后通知中断控制器本次中断结束并实现中断返回。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
函数getchar实现如下:

int getchar(void)
getchar{
static char buf[BUFSIZ];
static char *bb=buf;
static n=0;
if(n==0)
{
n=read(0,buf,BUFSIZ);
bb=buf;
}
return (--n>=0)?(unsigned char ) *bb++:EOF;
}

8.5本章小结

本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数的实现
(第8章1分)

附件

文件名称 文件作用
hello.i 源程序预处理之后的文本
hello.s hello.i经编译器生成的汇编文件
hello.o hello.s汇编生成的可重定位目标文件
hello hello.o链接生成的可执行目标文件
hello.elf hello.o的elf格式
hello.txt hello.o反汇编文件
hello1.elf hello的elf格式
hello1.txt hello反汇编文件

列出所有的中间产物的文件名,并予以说明起作用。
(附件0分,缺失 -1分)

结论

用计算机系统的语言,逐条总结hello所经历的过程。
源程序hello.c: ASCLL码文本文件。
修改了的源程序hello.i: hello.c经过cpp预处理,生成ASCLL码文本文件hello.i。
汇编文件hello.s: hello.i经过ccl的编译,生成汇编文件hello.s。
可重定位文本文件hello.o: hello.s经过汇编器的汇编,生成可重定位目标文件hello.o。
可执行目标文件hello:通过链接阶段,生成可执行目标文件hello。
运行:在终端输入./hello 1190200301 边子益,运行程序。
创建子进程:由于我们输入的命令不是内置的shell命令,shell会创建一个子进程来运行我们的程序。
加载:shell通过execve,在当前进程的上下文中加载运行hello程序。它会覆盖当前进程的地址空间,但没有创建一个新的进程。新的程序仍然有相同的PID,并且继承了调用execve函数时已打开的所有文件描述符。execve 调用启动加载器,映射到虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main 函数。
上下文切换:hello运行时,会调用一个sleep函数,陷入到内核,内核不会选择什么都不做,等待sleep函数调用结束,而是处理休眠请求,将当前进程挂起,上下文切换将当前进程的控制权交给其他进程。当定时器中断时,再将控制传递给当前进程。
异常处理:当我们的进程在运行时,可能会发生异常,这时,处理器会将控制传递给异常处理程序,处理的结果可能是返回到当前指令、返回到下一条指令、不返回即中止。
信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
终止:当子进程终止后,父进程会对子进程进行回收,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程,从此时开始,此进程就不存在了。

对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
通过本次大作业,我了解了hello从诞生到消亡的整个生命周期,理解了编译系统的构成部分。
计算机系统提供了很多抽象概念:例如文件是对I/O设备的抽象表示,虚拟内存是对主存和磁盘I/O设备的抽象表示,进程则是对处理器、主存、I/O设备的抽象表示。
计算机系统精巧的设计:高速缓存至关重要,针对处理器与主存之间的差异,采用了更小更快的存储设备,称为高速缓存存储器,大部分的内存操作都可以在快速的高速缓存中完成。
计算机系统全面的考虑:相应的信号处理程序及异常处理程序。
创新理念:引入虚拟内存,作为内存管理和内存保护的工具。
计算机还提供了一个工具:shell作为用户级交互程序,是面向用户的命令接口。用户通过访问这个界面访问操作系统内核的服务。

参考文献

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值