hello world的一生

计算机系统

计算机科学与技术学院
2019年12月
摘 要
本文通过对简单的程序hello进行p2p和o2o过程的分析,从进程创建到进程回收的整体过程分析,来阐述程序的生命周期,计算机的底层实现和整个计算机系统为实现程序运行的组织结构。
关键词:p2p, o2o,计算机系统;

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

目 录

第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 4 -
第2章 预处理 - 5 -
2.1 预处理的概念与作用 - 5 -
2.2在UBUNTU下预处理的命令 - 5 -
2.3 HELLO的预处理结果解析 - 5 -
2.4 本章小结 - 5 -
第3章 编译 - 6 -
3.1 编译的概念与作用 - 6 -
3.2 在UBUNTU下编译的命令 - 6 -
3.3 HELLO的编译结果解析 - 6 -
3.4 本章小结 - 6 -
第4章 汇编 - 7 -
4.1 汇编的概念与作用 - 7 -
4.2 在UBUNTU下汇编的命令 - 7 -
4.3 可重定位目标ELF格式 - 7 -
4.4 HELLO.O的结果解析 - 7 -
4.5 本章小结 - 7 -
第5章 链接 - 8 -
5.1 链接的概念与作用 - 8 -
5.2 在UBUNTU下链接的命令 - 8 -
5.3 可执行目标文件HELLO的格式 - 8 -
5.4 HELLO的虚拟地址空间 - 8 -
5.5 链接的重定位过程分析 - 8 -
5.6 HELLO的执行流程 - 8 -
5.7 HELLO的动态链接分析 - 8 -
5.8 本章小结 - 9 -
第6章 HELLO进程管理 - 10 -
6.1 进程的概念与作用 - 10 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 10 -
6.3 HELLO的FORK进程创建过程 - 10 -
6.4 HELLO的EXECVE过程 - 10 -
6.5 HELLO的进程执行 - 10 -
6.6 HELLO的异常与信号处理 - 10 -
6.7本章小结 - 10 -
第7章 HELLO的存储管理 - 11 -
7.1 HELLO的存储器地址空间 - 11 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.5 三级CACHE支持下的物理内存访问 - 11 -
7.6 HELLO进程FORK时的内存映射 - 11 -
7.7 HELLO进程EXECVE时的内存映射 - 11 -
7.8 缺页故障与缺页中断处理 - 11 -
7.9动态存储分配管理 - 11 -
7.10本章小结 - 12 -
第8章 HELLO的IO管理 - 13 -
8.1 LINUX的IO设备管理方法 - 13 -
8.2 简述UNIX IO接口及其函数 - 13 -
8.3 PRINTF的实现分析 - 13 -
8.4 GETCHAR的实现分析 - 13 -
8.5本章小结 - 13 -
结论 - 14 -
附件 - 15 -
参考文献 - 16 -

第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

P2P:用高级语言编写得到.c文件,然后经过cpp预处理得到.i文件,经过ccl的编译得到.s文件,再经过as的汇编将.s文件翻译成机器代码并打包成.o可重定位目标程序,最后再通过ld的链接得到可执行文件,在shell中键入启动命令,shell会为其fork产生子进程的进程,再调用execve加载进程。
O2O:操作系统调用execve后映射虚拟内存,进入程序入口后程序开始载入物理内存,再进入main函数执行代码,程序执行完成后,shell父进程负责回收hello进程,内核删除相关数据结构
1.2 环境与工具。
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:4.0GHz Intel Core i7
软件环境:win10 X64、Codeblocks、Ubuntu 18.04
开发工具:gdb、edb、hexcode
1.3 中间结果
hello.c :hello的c语言源代码
hello.i :预处理后的文本文件
hello.s :hello.i编译后的汇编文件
hello.o :hello.s汇编后的可重定位目标文件
hello.txt:hello.o的elf文件
hello_obj.txt:hello.o的反汇编结果文件
hello :helllo.o与预编译文件链接后的可执行文件
hello_e.txt:可执行文件hello的elf文件
1.4 本章小结
本章介绍了的过程,列举出本次实验的环境和工具,并且列出本次试验过程的中间结果文件
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
预处理概念:
又称为预编译,在程序编译之前,根据#开头的命令进行修改原始的程序,主要分为三方面的内容:1宏定义 2文件包含 3条件编译
预处理的作用:
1执行源文件中的#include指令,告诉cpp读取源程序所引用的系统源文件,并把源文件插入到程序之中。
2进行宏定义的替换
3条件编译,根据#if,#endif等条件进行需要编译的代码
2.2在Ubuntu下预处理的命令
Gcc –E hello.c –o hello.i

2.3 Hello的预处理结果解析
hello.c已经从527字节扩展至62.4KB,并且打开hello.i可以发现main函数前面被填充了大量的头文件文件

2.4 本章小结
本章对hello.c预处理阶段进行分析,以及操作的过程指令分析,并对于预处理得到的.i文件进行简要分析。
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译的概念:把预处理的文件进行一系列的语法分析并且进行优化后生成的相应的汇编指令
汇编的作用:将hello.i文件生成hello.s文件
3.2 在Ubuntu下编译的命令
Gcc –S hello.i –o hello.s
3.3 Hello的编译结果解析

文件声明:
.file 源文件
.text 代码段
.globel 全局变量
.align 对齐格式
.type 对象类型或函数类型
.size 数据空间大小
.section .rodata 只读代码段
.string 字符串

数据
.s文件中主要数据类型为:整形,字符串,指针数组
1整形
(1).main参数 int argc:
Argc是函数传入的第一个int的参数,存储在%edi中,表示终端输入的参数的个数多少。
(2).main函数中的局部变量 int i:
函数内部的局部变量存于堆栈中,其中用movl $0,-4(%rbp)进行数据在堆栈内的存储,并且由于-4所以可知i占用了4字节大小的栈空间

2字符串:
(1) 第一个字符串.LC0中包含汉字,其中汉字的编码方式为utf-8编码,汉字被编码为三个字节。
(2) 第二个字符串.LC1其中的两个%S对应与输入的两个参数:argv[1]和argv[2]。

3指针数组argv[]:
作为main函数的参数出现在栈帧中。
赋值:
1对局部变量的赋值:
通过movl指令,利用寄存器和$0, -4(%rbp)指令对局部变量进行赋初值0

算术操作
Hello.c中的主要的算术操作为循环遍历增加(i++)以及语句argc!=4
编译器将i++翻译为addl$1,-4(%rbp)

关系操作
Hello.c中的主要的算术操作为循环的控制(i<10)以及语句argc!=4
编译器将i<8翻译为$7,4(%rbp)

将argc!=4翻译为cmpl$4,-20(%rbp)
数组/指针/结构操作
指针数组(char *argv[]):
在argv数组中,argv[0]指向输入程序的路径和名称,argv[1]和argv[2]和argc[3]分别指向两个用户从终端输入的字符串分别对应学号姓名时间。根据图3-11,可知通过%rax+16和%rax+24%rax+8中存储的地址,(语句:addq $24,%rax addq $16,%rax 以及 addq $8,%rax)分别得到argv[1]和argc[2] 和argc[3]

控制转移
hello.c内部的控制转移主要有if语句以及for循环内部的控制转移
1.for(i=0;i<8;i++):
for循环的控制时比较cmpl $7, -4(%rbp) ,当i大于9时跳出循环,否则进入.L4循环体内部执行
2.if(argc != 4):
当argc不等于4时进行跳转。cmpl语句比较 -20(%rbp)和-4,设置条件码,然后根据ZF进行判断,如果最近的操作得出的结果为0,则跳到.L2中,否则顺序执行下一条语句。
函数操作
hello.c内部主要有5个函数:main(),printf(),exit(),atoi(),sleep(),getchar()
1.main():
参数传递:传入参数argc和argv,分别用寄存器%rdi和%rsi存储
函数调用:被系统启动函数调用,
函数返回:设置%eax为0并且返回
函数作用:作为程序运行的唯一入口
2.printf():
参数传递:call puts时只传入了字符串参数首地址;for循环中call printf时传入了 argv[1]和argc[2]的地址。
函数调用:在for循环中被调用
函数作用:用来打印信息
3.exit():
参数传递:传入一个布尔变量
函数调用:if判断条件满足后被调用
函数作用:如果传入的参数为1,则执行退出命令
4.atoi():
参数传递:传入参数argv[3]
传递控制:call atoi
函数调用:for循环下被调用
函数作用:将传入的参数从字符串形式转化为int整形
5.sleep():
参数传递:传入参数argv[3]经过atoi函数的输出值
传递控制:call sleep
函数调用:for循环下被调用
函数作用:使计算机程序(进程,任务或线程)进入休眠
6.getchar():
传递控制:call getchar
函数调用:在main中被调用
函数作用:用来读取字符串
3.4 本章小结
本讲分析了hello在编译过程中的一些变化,包括文件声明,数据类型分析,赋值,数据类型转换,算术操作,关系操作,数组操作,函数操作等。

(以下格式自行编排,编辑时删除)
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
汇编的概念:通过汇编器,把汇编语言转化为机器语言
汇编的作用:通过汇编器将汇编代码转化成为计算机能够理解的机器代码,这个代码在计算机中用二进制表示
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o

4.3 可重定位目标elf格式
Readelf -a hello.o >hello.txt

ELF头:
Elf头描述了生成该文件的系统的字的大小和字节顺序,并且包含帮助链接器语法分析和解释目标文件的信息。

节头:
节头描述不同的节的位置和大小,其中每个节都有一个固定大小的条目,描述每个节的名称,类型,地址和偏移量等。

重定位节:
连接器在处理目标文件时,须要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置。这些重定位的信息都记录在ELF文件表里面,对于每个须要重定位的代码段和数据段,都会有一个相应的重定位表,例如 .rel.text 表对应.text段。也就是说,重定位表记录了须要被重定位的地址都在相应段的哪些地方。

符号表:
.symtab符号表,用于存放程序中的定义和引用的全局变量和函数的信息。

4.4 Hello.o的结果解析
objdump -d -r hello.o > hello_obj.txt

操作数:
hello.s中的操作数是进制,hello.o反汇编代码中的操作数是十六进制。
分支转移:
跳转语句之后,hello.s中是.L2和.L3等段名称,而反汇编代码中跳转指令之后是相对偏移的地址。
函数调用:
hello.s中,call指令之后直接是函数名称,而反汇编代码中call指令之后是函数的相对偏移地址。因为函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
全局变量的访问:
在hello.s文件中,对于.rodata等全局变量的访问,是$.LC0,而在反汇编代码中是$0x0,是因为它们的地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。

4.5 本章小结
介绍汇编的概念和作用,以及分析了汇编产生的hello.o的可重定位文件的ELF格式的考察,同是将可重定位文件和反汇编文件进行对比,分析重定位过程中的过程。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
概念:将各种代码和数据片段收集并合成一个单一文件的过程,这个文件可以被记载到内存中并执行。
作用:在程序中的函数存在于一个单独预编译完成的目标文件当中,必须通过连接器将这个文件合并到程序中得到结果hello文件,生成一个可执行文件,可以被加载到内存当中,由系统执行。由此使得分离编译成为可能。
5.2 在Ubuntu下链接的命令
ld链接命令: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的格式
readelf -a hello > hello_e.txt
elf文件各阶段分析
在 ELF 格式文件中,Section Headers 对 hello 中所有的节信息进行了声明,其中包括大小 Size 以及在程序中的偏移量Offset,因此根据 Section Headers 中的信 息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。
它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。.text、.rodata和.data节与可重定位目标文件中的节是类似的,除了这些节已经被重定位到它们最终的运行时的内存地址外。.init节定义了一个小函数_init,程序初始化代码会调用它。因为可执行文件时完全连接的,所以无.rel节。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

可执行文件的程序头部表,展示了ELF可执行文件的连续的片被映射到连续的内存段的映射关系。其中展示了目标文件中的偏移,内存地址,对其要求,目标文件中的段大小,内存中的段大小,运行时访问权限等信息。图中橙色底的部分是只读代码段,白色底的部分是读/写段。

5.5 链接的重定位过程分析
objdump -d -r hello >hello_e_obj.txt
链接增加新的函数:在hello中链接加入了在hello.c中用到的函数,如exit、printf、sleep、getchar等函数。
增加的节:hello中增加了.init和.plt节,和一些节中定义的函数。
函数调用:hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
地址访问:hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于.rodata等全局变量的访问,是$0x0,是因为它们的地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。

5.6 hello的执行流程
程序名称 程序地址
ld-2.27.so!_dl_start 0x7f5d6118fea0
ld-2.27.so!_dl_init 0x7f5d6119e630
hello!_start 0x400500
hello!puts@plt 0x4004b0
hello!exit@plt 0x4004e0
在do_init前后global_offset表发生变化
在edb调试之后我们发现原先0x006008c0开始的global_offset表是全0的状态,在执行过_dl_init之后被赋上了相应的偏移量的值。这说明dl_init操作是给程序赋上当前执行的内存地址偏移量 。
5.8 本章小结
本章主要阐述了链接的概念与作用,并指出了在ubuntu下的链接指令,还通过分析hello.o与hello的elf文件以及反汇编文件的异同,更好的说明了链接这一个过程,尤其是其中的重定位部分
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程是一个具有一定的独立功能的程序关于某个数据集合的一次运行活动。
进程的作用:每次用户向shell输入一个可执行目标文件的名字,程序运行时就会创建一个新的进程,然后这个进程的上下文运行这个可执行的目标文件。应用程序创建新进程,并在新的进程中的上下文中运行自己的代码或其他应用程序。
进程提供给其他应用程序关键的抽象:一个独立的逻辑控制流,好像我们的程序独占的使用处理器,一个私有的地址空间,好像我们程序独占的使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
Shell的作用:shell是一个命令解释器,解释由用户输入的命令并且把他们送到内核。不仅如此,shell有自己的变成语言用于命令的编辑,它允许用户比那些由shell命令组成的的程序。Shell编程语言具有很多普通语言的特点,比如循环结构和分支控制结构等,用这种编程语言编写shell程序与其他的应用程序有相同的效果。
Shell的处理流程:shell首先检查命令是否是内部命令,若不是在检查是否是一个应用程序,然后再shell搜索路径里寻找这些应用程序(搜索路径为一个能够找到可执行的程序的目录列表),如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,就显示一条错误信息,如果能够找到命令,该内部应用程序将被分解为系统调用并传给linux内核。
6.3 Hello的fork进程创建过程
根据shell的处理流程,输入命令执行当前目录中的可执行文件hello,父进程通过fork函数创建一个新的运行的子程序进程hello,该进程几乎与父进程相同,hello进程与父进程的虚拟空间相同的一份副本,包括代码段,数据段,堆栈,共享库。Hello进程获得与父进程打开任何文件描述符相同的副本,在调用fork时,hello可以读取父进程中任何文件,但与父进程有不同的pid。Fork函数只被调用一次,却会返回两次,在父进程中,fork反汇hello进程的pid,在hello进程中,fork返回0.
6.4 Hello的execve过程
execve函数在加载并运行可执行目标文件Hello,且带列表argv和环境变量列表envp。只有当出现错误时,例如找不到Hello,execve才会返回到调用程序。在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:int main(int argc , char **argv , char *envp);
结合虚拟内存和内存映射过程,可以更详细地说明exceve函数实际上是如何加载和执行程序Hello,需要以下几个步骤:
删除已存在的用户区域。
映射私有区域。为Hello的代码、数据、bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时复制的。
映射共享区域。比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点
6.5 Hello的进程执行
逻辑控制流:一系列的程序计数器pc的值的序列叫做逻辑控制流,进程轮流使用处理器,在同一个处理器的核心中,每个进程执行他的流的一部分后被暂时挂起,然后轮到其他进程执行。
用户模式和内核模式:处理器通过某个控制寄存器中的亦歌模式位为限制一个应用可以执行的指令和它可以访问的地址空间范围。当设置了位模式时候进程就运行在内核模式中,可以执行指令集中的任何指令,并且可以访问内存中的任何位置,当没有设置位模式时候,进程处于用户模式,不允许进程执行特权指令,也不允许直接引用地址空间中的内核区的代码和数据。
用户模式和内核模式的转换:运行用户代码初始在用户模式中,进程从用户模式编程内核模式方法为通过中断,故障或陷入系统调用异常。当发生异常时,控制传递到异常处理程序,处理器将用户模式转变为内核模式,处理程序运行在内核模式之中,当返回时,处理前就把模式从内核模式改为用户模式。
上下文信息:内核为每个进程保留一个上下文,重启内核时启动一个被强占的进程所需要的状态,并通过上下文切换的机制控制转移到新的程序。
进程时间片:一个进程执行控制流的一部分时间段叫做时间片。
进程调度过程:hello初始运行在用户模式,在hello进程调用sleep之后陷入内核模式,计时器开始计时,内核通过上下文切换将当前进程将当前进程的控制权交给其他进程。当sleep函数时间到达时发送一个中断信号,此时进入内核状态执行中断处理,然后内核将进程控制权交还给hello进程,hello进程继续执行自己的控制逻辑流。
6.6 hello的异常与信号处理
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
可能出现的异常:
中断:信号SIGTSTP,默认为停止到下一个SIGCONT
终止:信号SIGINT,默认行为为终止

情况一:Hello运行什么都不按,程序结束后进程被回收,按回车键结束。

情况二:运行过程中按ctrl+c父进程收到SIGINT信号,终止hello进程,并回收hello进程

情况三:运行时乱按,发现并没有影响进程执行,当输入回车键时,getchar会读入,输入的字符串此时会被当做shell的命令行输入。
情况四:按下Ctrl+Z后运行ps命令。按下Ctrl+Z后,父进程收到SIGTSTP信号,将hello进程挂起,ps命令列出当前系统中的进程(包括僵死进程)

情况五:按下Ctrl+Z后运行pstree命令。pstree命令是以树状图显示进程间的关系。

情况六:使用fg命令将进程调到前台

情况七:使用kill给一个进程或多个进程发送信号,通过kill -9 9241杀死pid为9241的hello进程。

6.7本章小结
本章阐述了进程的概念和作用对于底层的抽象。简述了shell的作用原理和处理流程,然后分析了fork和execve函数功能,对hello进程的执行以及异常与信号的处理进行一系列的分析。结合hello在进程中的运行分析。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:包含在机器语言中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。就是hello.o里相对偏移地址。
线性地址:逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。是hello中的虚拟内存地址。
虚拟地址:一个带虚拟内存的系统中,CPU从一个有N=2^n个地址空间中生成虚拟地址。虚拟地址其实就是线性地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。地址翻译会将hello的一个虚拟地址转化为物理地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
分段功能在实模式和保护模式下不同
实模式:逻辑地址=线性地址=实际的物理地址。段寄存器存放真实段基址,同时给出32位地址偏移量,则可以访问真实物理内存。
保护模式:线性地址还需要经过分页机制才能够得到物理地址,线性地址也需要逻辑地址通过段机制来得到。
段寄存器用于存放段选择符,通过段选择符可以得到对应段的首地址。处理器在通过段式管理寻址时,首先通过段描述符得到段基址,然后与偏移量结合得到线性地址,从而得到了虚拟地址,具体步骤:
1看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
2拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样 Base,即基地址就知道了。
3把Base + offset,就是要转换的线性地址了。 还是挺简单的,对于软件来讲,原则上就需要把硬件转换所需的信息准备好,就可以让硬件来完成这个转换了。
7.3 Hello的线性地址到物理地址的变换-页式管理
CPU的页式内存管理单元,负责把一个线性地址,转换为物理地址。从管理和效率的角度出发,线性地址被分为以固定长度为单位的组,称为页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page[2^20]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址——对应的页的地址。 另一类“页”,我们称之为物理页,或者是页框(frame)、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。 这里注意到,这个total_page数组有2^20个成员,每个成员是一个地址(32位机,一个地址也就是4字节),那么要单单要表示这么一个数组,就要占去4MB的内存空间。为了节省空间,引入了一个二级管理模式的机器来组织分页单元。
分页单元中,页目录是唯一的,它的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。
每一个活动的进程,因为都有其独立的对应的虚似内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录地址放到cr3寄存器中,将别个的保存下来。
每一个32位的线性地址被划分为三部份,面目录索引(10位):页表索引(10位):偏移(12位) 依据以下步骤进行转换:
(1)从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
(2)根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
(3)根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
(4)将页的起始地址与线性地址中最后12位相加,得到最终的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7 MMU使用四级页表来将虚拟地址翻译成物理地址。36位的虚拟地址被分割成4个9位的片。CR3寄存器包含L1页表的物理地址。VPN1有一个到L1 PTE的偏移量,找到这个PTE以后又会包含到L2页表的基础地址;VPN2包含一个到L2PTE的偏移量,找到这个PTE以后又会包含到L3页表的基础地址;VPN3包含一个到L3PTE的偏移量,找到这个PTE以后又会包含到L4页表的基础地址;VPN4包含一个到L4PTE的偏移量,找到这个PTE以后就是相应的PPN(物理页号)。
7.5 三级Cache支持下的物理内存访问
要访问物理内存,需要以下几步:
1 CPU给出VA虚拟地址
2 MMU用VPN到TLB中找寻PTE,若命中,得到PA;若不命中,利用VPN(多级页表机制)到内存中找到对应的物理页面,得到PA。
3 PA分成PPN和PPO两部分。利用其中的PPO,将其分成CI和CO,CI作为cache组索引,CO作为块偏移,PPN作为tag。
先访问一级缓存,不命中时访问二级缓存,再不命中访问三级缓存,再不命中访问主存,如果主存缺页则访问硬盘。
L1 Cache是8路64组相联。块大小为64B。因此CO和CI都是6位,CT是40位。根据物理地址(PA),首先使用CI组索引,每组8路,分别匹配标记CT。如果匹配成功且块的有效位是1,则命中,根据块偏移CO返回数据。
如果没有匹配成功或者匹配成功但是标志位是1,则不命中,向下一级缓存中取出被请求的块,然后将新的块存储在组索引指示的组中的一个高速缓存行中。一般而言,如果映射到的组内有空闲块,则直接放置,否则必须驱逐出一个现存的块,一般采用最近最少被使用策略LRU进行替换。
7.6 hello进程fork时的内存映射
当fork被shell调用时候,内核为hello进程创建各种数据结构,并给它匹配唯一的PID,为了给hello进程创建虚拟内存,创建mm—struct,区域结构和页表原样副本,将两个进程中的每个页面都有标记为只读页面,并将每个区域结构都标记为私有的写时复制。当fork在进程中返回时现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当需要进行写操作的时候,写时复制机制会创建新的页面,为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
当调用函数execve函数加载hello程序时经历以下过程:首先删除已存在的用户区域,再将用户区域映射私有区域为Hello的代码、数据、bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时复制的。再映射共享区域。比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。最后设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页(page fault) 。图7-8展示了在缺页之前我们的示例页表的状态。CPU引用了VP3中的一个字,VP3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE3,从有效位推断出VP3未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,反映出VP4不再缓存在主存中这一事实。缺页处理程序从磁盘上用VP3的副本取代VP4,在缺页处理程序重新启动导致缺页的指令之后,该指令将从内存中正常地读取字,而不会再产生异常。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合,来维护,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
显式分配器:要求应用显式地释放任何已分配的块。例如C程序通过调用malloc函数来分配一个块,通过调用free函数来释放一个块。其中malloc采用的总体策略是:先系统调用sbrk一次,会得到一段较大的并且是连续的空间。进程把系统内核分配给自己的这段空间留着慢慢用。之后调用malloc时就从这段空间中分配,free回收时就再还回来(而不是还给系统内核)。只有当这段空间全部被分配掉时还不够用时,才再次系统调用sbrk。当然,这一次调用sbrk后内核分配给进程的空间和刚才的那块空间一般不会是相邻的。
隐式分配器:也叫做垃圾收集器,例如,诸如Lisp、ML、以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
放置策略:
首次适配:从头搜索,遇到第一个合适的块就停止
下次适配:从头搜索,遇到下一个合适的块停止
最佳适配:全部搜索,选择合适的块停止。
两种堆的数据结构组织形式
带标签的隐式空闲链表:
放置块时,分配器搜索空闲链表,常见有首次适配、下一次适配和最佳适配的放置策略。首次适配从头开始搜索空闲链表,下一次适配从链表的上一次查询结束的地方开始搜索,最佳适配检查所有空闲块,选择最小满足的。下一次适配运行最快,但利用率低得多;最佳适配最慢,利用率最高。
分配器找到匹配的空闲块后,根据情况可能分割它。如果没有合适的空闲块,合并空闲块来创建更大的空闲块。如果还是不能满足需要,分配器向内核请求额外的堆存储器,转成空闲块加入到空闲链表中。
分配器可以选择立即合并或推迟合并,一般为防止抖动,会采用某种形式的推迟合并。
合并需要在常数时间内完成,对于空闲链表来说,它是单链表,可以方便地查看后面的块是否空闲块,但前面的块则不行,一个好办法是在块的脚部使用边界标记,它是头部的副本,这样就可以在常数时间查看前后块的类型了。为了避免边界标记占用空间,可以只在空闲块中加边界标记。

分离的空闲链表:
分离的空闲链表利用分离存储来减少分配时间。分配器维护一个空闲链表数组,每个空闲链表为一个大小类。大小类的定义方式有很多,如2的幂。有简单分离存储和分离适配方法。
  简单分离存储的大小类的空闲链表包含大小相等的块,块大小为大小类中最大元素的大小。分配和释放块都是常数时间,不分割,不合并,已分配块不需要头部和脚部,空闲链表只需是单向的,因此最小块为单字大小。缺点是很容易造成内部和外部碎片。
分离适配的分配器维护一个空闲链表的数组,每个链表和一个大小类相关联,包含大小不同的块。分配块时,确定请求的大小类,对适当的空闲链表做首次适配。如果找到合适的块,可以分割它,将剩余的部分插入适当的空闲链表中;如果没找到合适的块,查找更大的大小类的空闲链表。分离适配方法比较常见,如GNU malloc包。这种方法既快、利用率也高。
7.10本章小结
本章着重介绍了被许多现代操作系统所采用的虚拟内存系统,即访存时地址需要从逻辑地址翻译到虚拟地址并进一步翻译成物理地址,并详细阐述了在TLB和四级页表支持下VA到PA的转换,以及得到了PA后,三级cache下的物理内存的访问过程,最后还讨论了malloc堆区的动态内存分配
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:
所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。
设备管理:unix io接口
设备管理方法:一个linux文件就是一个m个字节的序列:B0 , B1 , … , Bk , … , Bm-1所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix IO接口
1 打开文件,内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。
2 Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,他们可用来代替显式的描述符值。
3 改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。
4 读写文件,读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k
5 关闭文件。当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中
IO函数(UNIX)
open()函数
功能描述:用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。
函数原型:int open(const char *pathname,int flags,int perms)
参数:pathname:被打开的文件名(可包括路径名如"dev/ttyS0")flags:文件打开方式,
返回值:成功:返回文件描述符;失败:返回-1

close()函数
功能描述:用于关闭一个被打开的的文件
所需头文件: #include <unistd.h>
函数原型:int close(int fd)
参数:fd文件描述符
函数返回值:0成功,-1出错

read()函数
功能描述: 从文件读取数据。
所需头文件: #include <unistd.h>
函数原型:ssize_t read(int fd, void *buf, size_t count);
参数:fd:将要读取数据的文件描述词。buf:指缓冲区,即读取的数据会被放到这个缓冲区中去。count: 表示调用一次read操作,应该读多少数量的字符。
返回值:返回所读取的字节数;0(读到EOF);-1(出错)。

write()函数
功能描述: 向文件写入数据。
所需头文件: #include <unistd.h>
函数原型:ssize_t write(int fd, void *buf, size_t count);
返回值:写入文件的字节数(成功);-1(出错)

lseek()函数
功能描述: 用于在指定的文件描述符中将将文件指针定位到相应位置。
所需头文件:#include <unistd.h>,#include <sys/types.h>
函数原型:off_t lseek(int fd, off_t offset,int whence);
参数:fd;文件描述符。offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移)
返回值:成功:返回当前位移;失败:返回-1
8.3 printf的实现分析
调用printf函数的时候,先是最右边的参数入栈。fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素。fmt也是个变量,它的位置,是在栈上分配的,它也有地址。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar有一个int型的返回值.当程序调用getchar时.程序就等着用户按键.用户输入的字符被存放在键盘缓冲区中.直到用户按回车为止(回车字符也放在缓冲区中).当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符.getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕.如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取.也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了linux下IO设备的管理方法,简单阐述了Unix IO和Unix IO函数,并详细分析了printf函数和getchar函数的底层实现。
(第8章1分)
结论
1.用高级语言编写相应的hello程序(我们分析的是c语言程序hello.c),然后以文件的形式存储在主存中
2.hello.c经过预处理器转化为hello.i文件
3.hello.i文件经过编译器的编译操作转化为hello.s文件
4.hello.s经过汇编器被处理成可重定位的目标程序hello.o
5.链接器将hello.o与外部预编译文件进行链接,得到可执行文件hello
6.在shell中键入命令以及参数,内核为hello分配好运行程序所需要的堆、用户栈以及虚拟内存等系统资源
7. 通过调用execve函数加载并运行hello程序
8. hello的VA通过TLB和页表翻译为PA,然后在三级cache 支持下的hello物理地址访问,得到需要的数据以及代码
9. hello在运行过程中会有异常和信号等,当需要从外部对hello程序进行操控的时候,只需要在键盘上给一个相应的信号,程序就会按照指令来执行。
10. printf会调用malloc通过动态内存分配器申请堆中的内存
11. shell父进程回收hello子进程,内核删除为hello创建的所有数据结构,hello的一生结束
(结论0分,缺失 -1分,根据内容酌情加分)

附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c :hello的c语言源代码
hello.i :预处理后的文本文件
hello.s :hello.i编译后的汇编文件
hello.o :hello.s汇编后的可重定位目标文件
hello.txt:hello.o的elf文件
hello_obj.txt:hello.o的反汇编结果文件
hello :helllo.o与预编译文件链接后的可执行文件
hello_e.txt:可执行文件hello的elf文件
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
(参考文献0分,缺失 -1分)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值