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

计算机系统

大作业

题     目  程序人生-Hellos P2P  

专       业         计算学部               

学     号      120L022009                  

班   级        2003007                

学       生        李沛聪          

指 导 教 师         吴锐            

计算机科学与技术学院

2021年5月

摘  要

本论文研究了hello.c这一简单c语言文件在Linux系统下的整个生命周期,以其原始程序开始,依次深入研究了编译、链接、加载、运行、终止、回收的过程,从而了解hello.c文件的“一生”。文章将从计算机底层的角度一步一步分析操作系统是利用了什么原理,怎么实现这些过程的。帮助读者梳理漫游计算机系统。

关键词:计算机系统;hello程序;内存管理;IO管理;进程管理;P2P

                            

(摘要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简介

1.最初通过编辑器编写hello的程序建立.c文件,得到hello.c的源程序。

2.运行C预处理器(cpp)将其进行预处理生成hello.i文件。

3.运行C编译器(ccl)将其进行翻译生成汇编语言文件hello.s。

4.运行汇编器(as)将其翻译成一个可重定位目标文件hello.o。

5.运行链接器程序ld将hello.o和系统目标文件组合起来,创建了一个可执行目标文件hello。

6.通过shell输入./shell,shell通过fork函数创建了一个新的进程,之后调用execve映射虚拟内存,通过mmap为hello程序开创了一片空间。

7.CPU从虚拟内存中的.text,.data节取代码和数据,调度器为进程规划时间片,有异常时触发异常处理子程序。

8.程序运行结束时,父进程回收hello进程和它创建的子进程,内核删除相关数据结构。

1.2 环境与工具

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上

软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位

开发与调试工具:gcc,gedit,edb,readelf

1.3 中间结果

hello.c 源程序

hello.i hello.c预处理之后的文本文件

hello.s hello.i编译后的汇编文件

hello.o hello.s汇编之后的可重定位目标文件

hello hello.o与其他可重定位目标文件链接之后的可执行目标文件

hello.elf hello.o反汇编生成的文本文件

hello.out   hello反汇编生成的文本文件

hello1.elf  ELF格式下的hello

hello.asm   hello.o文本文件

1.4 本章小结

本章介绍了hello.c程序P2P和O2O的基本过程,介绍了实验的环境与工具以及实验生成的中间文件。

(第1章0.5分)


第2章 预处理

2.1 预处理的概念与作用

1.概念:预处理器根据以字符”#”开头的命令,处理hello.c源程序。

2.作用:根据源代码中的预处理指令修改源代码,预处理从系统头文件中将头文件的源码插入到目标文件中,最终生成hello.i文件。

2.在Ubuntu下预处理的命令

使用预处理命令:gcc hello.c -E -o hello.i 对hello.c进行预处理:

hello.c

hello.i

2.3 Hello的预处理结果解析

预处理后的.i文件打开后发现得到了扩展,从23行扩展到3060行。阅读文件发现是增加了三个头文件stdio、unistd、stdlib的源码。同时,我们还发现,源代码.c文件中的注释都被省去了。

2.4 本章小结

本章介绍了在预处理的概念与作用,并展示如何在ubuntu下用gcc对.c文件进行预处理,并分析了预处理后文本的变化,为文件后续的操作打下基础。


第3章 编译

3.1 编译的概念与作用

概念:编译器ccl将文本文件hello.i翻译成文本文件 hello.s,它包含一个汇编语言程序。它以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。        

作用:将预处理后的程序编译为更接近计算机语言,更容易让计算机理解的语言,相当于一个翻译的过程。除此之外,编译程序还具备语法检查、目标程序优化等功能。

3.2 在Ubuntu下编译的命令

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

hello.s:

3.3 Hello的编译结果解析

3.3.1汇编初始部分

节名称 作用

.file 声明源文件

.text 代码节

.section.rodata 只读数据段

.globl 声明全局变量

.type 声明一个符号是函数类型还是数据类型

.size 声明大小

.string 声明一个字符串

.align 声明对指令或者数据的存放地址进行对齐的方式

3.3.2数据类型

3.3.2.1整型

1.int i

main函数声明了一个局部变量i,编译器进行编译的时候将局部变量i会放在堆栈中。如图所示,局部变量i放在栈上-4(%rbp)的位置。

  1. int argc

参数 argc 作为用户传给main的参数。存放在寄存器edi上,并赋给-20(%rbp)

  1. 立即数3

立即数3在汇编语句中直接以$3的形式出现

3.3.2.2 数组

程序中涉及的数组为char *argv[],即函数的第二个参数。在hello.s中,数组的起始地址存放在栈中-32(%rbp)的位置,被两次调用传给printf。

3.3.2.3 字符串

程序中存在两个printf中的字符串,分别是"Usage: Hello 学号 姓名!\n"和
“Hello %s %s\n”,如图所示,字符串常量以uft-8格式编码并存储在.rodata段。

3.3.3 赋值操作

对循环变量i的赋值for(i=0;i<10;i++)对应汇编代码如下,通过mov指令完成。

3.3.4 类型转换

将第四个参数放在%rdi,之后调用atoi函数,将字符串化为整型。

3.3.5 算术操作

汇编语言中,算术操作指令包括:

1.对于i++操作,通过addl $1,-4(%rbp)把i=0进行不断加一,达到循环目的。

  1. 在数组中修改地址偏移量读取数组内容

  

3.3.6 关系操作

3.3.6.1  argc!=4:

使用cmpl语句设置条件码,je语句根据条件码做出是否需要跳转的选择,如图,将argc的值与3比较,如果相等,则跳转到.L2,否则顺序执行。

3.3.6.2  i<8:

对于i<8的循环中,每次与7比较,≥7则跳出循环。

3.3.7 数组操作

如前述3.3.2所述,hello.s中存在数组char *argv[],对其的访问操作通过寄存器寻址方式实现。首先对指针argv解引用,加16后对应argv[2],将argv[2]的值取出放入寄存器rdx中,再对指针argv解引用,加8后对应argv[1],将argv[1]的值取出放入寄存器rsi中,最后将字符串常量放入寄存器rdi中,调用printf函数进行输出。

3.3.8控制转移

3.3.8.1 if(argc!=4)

判断argc是否等于4,不等于则输出LC0中字符串,否则救跳转到L2,完成if判断。

3.3.8.2for(i=0;i<8;i++)

for循环在.L2处对循环变量i进行初始化,然后通过jmp跳转到.L3,在.L3中进行了循环终止条件的判断,如果i<=7,则跳转到循环体部分.L4,否则顺序执行。

3.3.9 函数操作

3.3.9.1 printf函数

(1)printf("用法: Hello 学号 姓名 秒数!\n");

只有一串字符调用puts函数

  1. printf("Hello %s %s\n",argv[1],argv[2]);

printf函数在调用之前,三个参数按顺序依次为.rodato节的.LC1的printf格式串的地址,argv[1],argv[2],分别存放在了寄存器rdi,rsi,rdx中。

3.3.9.2exit函数

exit函数调用之前,将立即数1存入rdi中,作为exit的第一参数进行传递。

3.3.9.3atoi函数

通过偏移把输入的第四个数储存在%rdi中作为参数。

3.3.9.4sleep函数

存储在%eax中的返回值转移到%edi中

3.3.9.5getchar函数

直接调用

3.4 本章小结

本章主要介绍编译操作的过程,主要将预处理后的hello.i文件编译为汇编代码文件hello.s,在此过程中,编译器将会对源文件进行语法分析、词法分析,得到汇编文件hello.s。通过理解了这些编译器编译的机制,我们可以很容易的将汇编语言翻译成c语言,提高了反向工程的能力。

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

1.汇编的概念

汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,将结果保存在目标文件hello.o中,hello.o文件是一个二进制文件,它包含函数main的指令编码,这个过程称为汇编。

2.汇编的作用

将汇编代码转换为机器指令,使其在链接后能被机器识别并执行.

4.2 在Ubuntu下汇编的命令

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

4.3 可重定位目标elf格式

命令为   readelf -a hello.o >hello.elf

分析.elf文件中的内容:

  1. ELF头:以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息:包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。

  1. 节头:描述了.o文件中每一个节出现的位置,大小,目标文件中的每一个节都有一个固定大小的条目。

  1. 重定位节:当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。

本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar这些符号同样需要与相应的地址进行重定位。

  1. 符号表

符号表是由汇编器构造的,使用编译器输出到汇编语言.s文件中的符号。.symtab节中包含ELF符号表。这张符号表包含一个条目的数组。

4.4 Hello.o的结果解析

objdump -d -r hello.o > hello.asm

4.4.1 结果对比

与hello.s相比,有以下区别:

  1. 分支转移跳转语句不同 hello.s中代码直接声明具体的段存储位置,通过形如L0、L1等助记符的段名称进行转移;而反汇编代码是计算出地址,依据地址跳转。

  1. 函数调用不同

在hello.s文件中,call之后直接跟着函数名称,而在反汇编得到的hello.asm中,call 的目标地址是当前指令的下一条指令。这是因为机器语言中调用的函数在共享库中,无法确定位置,所以相对位置为0,在重定位表中为其设置偏移量,等待进一步确认。

  1. 数的表示

hello.s中的操作表现为十进制,而反汇编代码中的操作为十六进制

4.4.2 机器语言的构成及与会变得映射关系

每一条汇编代码都可以用二进制机器指令表示,每一条机器指令由操作码和操作数构成,从而建立起一一对应关系。

4.5 本章小结

本章介绍了汇编的整个过程,从汇编语言到机器码,重点关注了生成文件hello.o可重定位这一特性,并且通过objdump反汇编得到的代码与hello.s进行比较,了解了其中的映射机制以及两者之间的区别。

(第4章1分)


5链接

5.1 链接的概念与作用

1.链接的概念:

链接是将各种代码和数据片段收集并合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。

2.链接的作用:

链接器在软件开发过程中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。

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

生成了可执行文件hello

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

命令为:readelf -a hello > hello1.elf

5.3.1 ELF头

Hello1.elf中的ELF头与hello.elf中的ELF头包含的信息种类基本相同,以 描述了生成该文件的系统的字的大小和字节顺序的16字节序列 Magic 开始,剩下的部分包含帮助链接器语法分析和解释目标文件的信息。type从REL变为EXEC,节点数量变为27个

5.3.2 节头部表

与hello.o的节头部表作对比,可以得出:①hello的节头部表增加了若干条目,下图中一共有26个节的信息,而hello.o的节头部表中只有13个节的信息。②所有节被分配了运行时的地址。可以看到下图中某些节地址有所不同,而hello.o的节头部表中所有节的Address为全0。

5.3.3 重定位表

基本与上一节一致。

5.3.4 符号表

可执行目标文件的符号表表项数目(51 entries)明显多于可重定位目标文件的表项数目(18 entries)。一方面,可执行目标文件中加入了与调试、加载、动态链接相关的节,使得表示节的符号数增多;另一方面,由于链接器对可重定位目标文件中的符号进行了进一步解析,加入了若干系统调用。

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息。

举例1:由上图可知main函数位于0x0000000000401125处

其在datadump中对应位置

举例2: 由节头部表可知.text节起始地址为0x4010d0结束于0x401215处

其对应位置

5.5 链接的重定位过程

命令:objdumo -d -r hello >hello.out

反汇编文件hello.out:

  1. 虚拟地址不同

hello反汇编中已经是确定的虚拟地址0x400000,而hello.o的反汇编中地址仍是0,未完成重定位。

  1. 反汇编节数不同hello.o只有.txt节,之中只有卖弄函数的反汇编代码;而hello在main函数之前填充有链接过程中重定位而加入进来各种函数、数据,增加了.init,.plt,.plt.sec等节的反汇编代码。

3.函数调用时,hello.o中call后面接的是下一条指令,不是函数的所在位置,而hello中完成了重定位,call直接指向函数所在的虚拟地址。

5.6 hello的执行流程

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。

载入:_dl_start、_dl_init

执行:_start、_libc_start_main
运行:_main,_printf,_exit,_sleep,_getchar,_dl_runtime_resolve_xsave,_dl_fixup,_dl_lookup_symbol_x

退出:exit

5.7 Hello的动态链接分析

对于动态共享链接库中PIC函数,编译器加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略,将过程地址的绑定推迟到第一次调用该过程。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。

GOT运行时地址为0x403ff0,PLT的运行时地址为0x404000。

查看调用前该处内容

调用后变为

5.8 本章小结

本章首先介绍了链接的概念及作用,详细分析了可执行目标文件hello的ELF格式,并且通过edb调试查看其虚拟地址空间,并分析了重定位过程、执行流程和整个动态链接过程。

(第5章1分)


6hello进程管理

6.1 进程的概念与作用

1.进程的概念:

经典定义就是一个执行中程序的实例。广义定义是进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

2.进程的作用:

进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

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

1.Shell-bash的作用:

Linux系统中,Shell是一个交互型应用级程序,为使用者提供操作界面,接收用户命令,然后调用相应的应用程序。

2.处理流程:

①从终端读入输入的命令。

②将输入字符串切分获得所有的参数。

③检查第一个命令行参数是否是一个内置的shell命令,如果是则立即执行。

④如果不是内部命令,调用fork( )创建新进程/子进程执行指定程序。

⑤shell应该接受键盘输入信号,并对这些信号进行相应处理。

6.3 Hello的fork进程创建过程

在终端中输入命令行./hello 120L022009 李沛聪 1后,首先shell对我们输入的命令进行解析,由于我们输入的命令不是一个内置的shell命令,因此shell会调用fork()创建一个子进程,子进程几乎但不完全与父进程相同。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是独立的一份副本,拥有不同的PID。

6.4 Hello的execve过程

调用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序hello。execve 函数从不返回,它将删除该进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序。将私有的区域映射进来,然后将公共的区域映射进来。后面加载器跳转到程序的入口点,即设置PC指向_start 地址。_start函数最终调用hello中的 main 函数。

6.5 Hello的进程执行

1.上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。

2.进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。

3.用户态和内核态:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

4.上下文切换:当一个进程正在执行时,内核调度了另一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。在进行上下文切换时,需要保存以前进程的上下文,恢复新恢复进程被保存的上下文,将控制传递给这个新恢复的进程来完成上下文切换。

hello的进程执行过程如下:

hello初始运行在用户模式,在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程以加载新的进程进行执行。同时将hello进程从运行队列中移入到等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程并执行,当定时器到时时发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。

对于hello的进程执行,具体过程如下:键盘输入./hello 120L022009 李沛聪 1,首先shell通过加载器加载可执行目标文件hello,操作系统进行上下文切换,切换到hello的进程中,此时为用户态,执行完相应函数后,调用sleep函数,进入内核态,当sleep的时间完成后时定时器发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,上下文切换再进入hello进程,回到用户态。

6.6 hello的异常与信号处理

1.程序正常运行,以回车为标志结束

  1. 执行过程中按回车。

4.按下Ctrl + C,Shell进程收到SIGINT信号,Shell结束并回收hello进程。

  1. 按下Ctrl + Z,Shell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。

键入jobs

键入pstree

键入fg

  1. 乱按

6.7本章小结

本章介绍了进程的概念和作用,以及壳Shell-bash的作用与处理流程,调用 fork 创建新进程,调用 execve函数执行 hello,最后介绍了执行过程中的异常与信号处理。

(第6章1分)


7hello的存储管理

7.1 hello的存储器地址空间

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

2线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式,分页机制中线性地址作为输入。

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

4物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。

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

段是对程序逻辑意义上的一种划分,一组完整逻辑意义的程序被划分为一段。段的长度不确定。段描述符用于描述一个段的详细信息。段选择符用于找到对应的段描述符。流程:通过段选择符的T1字段,确定是GDT中段还是LDT中的段。

查找段描述符,获得基地址,基地址+偏移,得到线性地址

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

线性地址即虚拟地址(VA)到物理地址(PA)之间的转换通过分页机制完成,而分页机制是对虚拟地址内存空间进行分页。

系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。

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

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

CPU产生虚拟地址,传递给内存管理单元,使用前36位虚拟页号向TLB中匹配。这其中36位虚拟页号被分成四片,每片用作一个页表的偏移量。如果上述匹配命中,则直接将相应物理页号和虚拟偏移量串联得到物理地址,如果匹配失败,内存管理单元向页表查询,通过寄存器确定第一级页表的起始地址通过第一片虚拟页号确定出在第一级页表中的偏移量,查找到页表条目,如果在物理内存中,开始确定第二级页表,以此类推,在第四级页表中查找到物理页号,再与虚拟偏移量组合成物理地址。

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

获得物理地址之后,先取出组索引对应位,在L1中寻找对应组。如果存在,则比较标志位,相等后检查有效位是否为1.如果都满足则命中取出值传给CPU,否则按顺序对L2cache、L3cache、内存进行相同操作,直到出现命中。然后再一级一级向上传,如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,将目标块放到被驱逐块的位置。

7.6 hello进程fork时的内存映射

当fork被调用后,内核为新进程创建数据结构并创建虚拟内存,创建当前进程的mm_struct、区域结构和页表的原样副本。将两个进程中的每个页面都标记为只读,将两个进程中的每个区域结构都标记为私有的写时复制。fork从新进程返回时,虚拟内存与刚创建时相同。

7.7 hello进程execve时的内存映射

execve函数加载并运行hello需要以下几个步骤:

1、删除已存在的用户区域

删除当前进程hello虚拟地址的用户部分中的已存在的区域结构。

2、映射私有区域

为新程序的代码、数据、bss和栈区域创建新的私有的、写时复制的区域结构。其中,代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。

3、映射共享区域

若hello程序与共享对象或目标(如标准C库libc.so)链接,则将这些对象动态链接到hello程序,然后再映射到用户虚拟地址空间中的共享区域内。

4、设置程序计数器

最后,execve设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

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

页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的,在指令请求一个虚拟地址时,MMU中查找页表,如果这时对应得物理地址没有存在主存的内部,我们必须要从磁盘中读出数据。在虚拟内存的习惯说法中,DRAM缓存不命中成为缺页。在发生缺页后系统会调用内核中的一个缺页处理程序,选择一个页面作为牺牲页面。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。对于每个进程,内核维护一个变量brk,它指向堆的顶部。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

分配器分为两种基本风格:显式分配器、隐式分配器。

显式分配器:要求应用显式地释放任何已分配的块。例如malloc。

隐式分配器:也叫做垃圾收集器,要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。

堆中的块主要组织为两种形式:

1.隐式空闲链表(带边界标记)

对比于显式空闲链表,代表并不直接对空闲块进行链接,而是将对内存空间中的所有块组织成一个大链表,在块的首尾的四个字节分别添加header和footer,负责维护当前块的信息(大小和是否分配)。由于每个块是对齐的,所以每个块的地址低位总是0,可以用该位标注当前块是否已经分配。可以利用header和footer中存放的块大小寻找当前块两侧的邻接块,方便进行空闲块的合并操作。优点是简单,缺点是搜索所需时间与堆中以分配块和空闲块的总数成线性关系。

2.显式空闲链表

堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱和succ(后继)指针。分别指向前一个空闲块和后一个空闲块。采用该策略,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。将空闲块组织成链表形式的数据结构。

7.10本章小结

本章介绍了在hello的内存管理,虚拟地址、物理地址、线性地址、逻辑地址的区别以及它们之间的变换模式,以及段式、页式的管理模式,基于内存映射重新认识fork和execve,同时介绍了动态存储分配的方法与原理。

(第7章 2分)


8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

Linux将文件所有的I/O设备都模型化为文件,甚至内核也被映射为文件。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。Linux就是基于Unix I/O实现对设备的管理

8.2 简述Unix IO接口及其函数

1、接口

1)打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息,应用程序只需要记住这个描述符。

  1. 改变当前的文件位置: 对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。

3) 读写文件:进行复制操作并改变文件位置k的值。

4) 关闭文件:内核释放相应数据结构,将描述符恢复到可用的描述符池中

2.函数

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 write(int fd, const void *buf,size_t)

write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置

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;

}

printf需要做的事情是:接受一个fmt的格式,然后将匹配到的参数按照fmt格式输出。printf用了两个外部函数,一个是vsprintf,还有一个是write。

vsprintf函数作用是接受确定输出格式的格式字符串fmt(输入)。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

write函数将buf中的i个元素写到终端。

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall。字符显示驱动子程序:从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有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。

当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

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

(第8章1分)

结论

hello的一生

一.hello出生于文本编译器,是由程序员用高级语言亲手写出的一个个字符,形成hello.c文件。

二.之后Hello要经过预处理操作,在这一步我们会对hello中带#的指令进行解析,生成hello.i文件。

三.经过预处理后的hello仍然无法被计算机识别,因此要进行编译操作,这一步将c语言程序编译成汇编语言程序,生成hello.s文件。

四.之后,hello.s还需要经过汇编阶段变成可重定位目标文件hello.o,成为机器可以读懂的东西。

五.链接:Hello.o文件和动态链接库共同链接成可执行目标程序hello。至此P2P过程结束。

六.接下来要对程序进行运行,用户在shell中输入./hello 120L022009 李沛聪 1后按下回车键运行Hello程序。

七.Shell调用fork和execve函数加载映射虚拟内存,为hello程序创建新的代码、数据、堆和栈段等,删除原来的进程内容,加载我们现在进程的代码,数据等到进程自己的虚拟内存空间。

八.CPU取指令并为进程分配时间片,加载器将计数器预置在程序入口点,hello顺序的执行进程的逻辑控制流。此时CPU会给出一个虚拟地址,通过MMU从页表里得到物理地址, 在通过这个物理地址去cache或者内存里可以获得需要的数据。

九.在程序执行过程中,如果从键盘输入Ctrl-C等命令,会给进程发送一个信号,然后通过信号处理函数对信号进行处理。

十.执行完成后父进程回收子进程,内核删除为该进程创建的数据结构,到此为止,hello运行结束。

通过详细全面的追踪Hello的一生,我深刻感受到计算机内部如此精妙,完美和谐的设计机制,这让我认识到每一个小小的程序在计算机中都有自己的完整的一生,要经历很多事情,也提醒我在设计一个程序时需要多方位取考虑,也需要多环节去考虑,想办法让让小小的机器和小小的代码发挥更加强大的力量。

(结论0分,缺失 -1分,根据内容酌情加分)


附件

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

(附件0分,缺失 -1分)


参考文献

为完成本次大作业你翻阅的书籍与网站等

[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分)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值