计算机系统2022 hello的一生

摘  要

本文是csapp课程的大作业,重点论述了hello程序从出生到死亡的完整人生。我们写的第一个程序都是 hello,它很简单但是我们没有去分析过hello 从源代码到可执行文件的过程。这里介绍了hello.c在Linux下的生命周期过程——从最初的预处理(预处理器根据以#字符开头的命令对原始的C语言代码进行转换),编译(指将一种语言翻译成汇编语言),汇编(将汇编语言程序翻译成机器语言指令,把这些指令打包成可重定位目标程序)和链接(将多个可重定位的.o 文件合并到一个可执行文件中)阶段进行分。最后我们对hello创建进程,存储管理,IO管理等方面结合理论知识进行研究,帮助我们更加深入的理解了计算机系统,类似于书本标题:深入理解了计算机系统——通过一个项目(hello)!

第1章 概述

1.1 Hello简介

P2P是From Program to Process的过程,经过预处理、编译、组装和链接过程,Hello文件成为我们需要的目标可执行文件。

020即From Zero to Zero,首先建立一个子进程,然后加载并执行程序,映射虚拟内存,输入程序条目后将程序加载到物理内存,输入执行目标代码的主函数,CPU为运行的程序分配时间片以执行逻辑控制流,这就是一个从“无”到“无”的一个过程。

1.2 环境与工具

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

软件环境:Windows11 64位;Vmware 11;Ubuntu 20.04  

开发工具:Visual Studio 2022 ;CodeBlocks2020 ;gcc

1.3 中间结果

hello.c 。。。。。。。。。。。。。hello的源文件

hello.i。。。。。。。。。。。。hello.c经过预处理后的文件

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

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

Hello。。。。。。。。。。。。。链接后的可执行文件

1.4 本章小结

    本章介绍了Hello的基本信息以及p2p和o2o的概念和理解,介绍了基本环境工具以及这个实验中间可能生成的一些中间结果。这就是我们这一章的全部内容了!

(第1章0.5分)


                 第2章 预处理

2.1 预处理的概念与作用

    概念:预处理器将#字符开头的代码进行一系列拓张,也就是将库函数直接代替这些代码,最后我们可以合并成一个新的文件,这个文件相比于源文件多了库函数。

作用:编译预处理是C语言编译器不可缺少的一部分。预处理器将#字符开头的代码进行一系列拓张,也就是将库函数直接代替这些代码,最后我们可以合并成一个新的文件,这个文件相比于源文件多了库函数。这在一定程度上增强了代码的可读性——由于编译器无法进行翻译编译前的处理指令,因为它不是语句,所以需要进行预处理在真正编译前,处理指令在解释并完成编译前,将预处理指令转换成相应的C程序段。

2.2在Ubuntu下预处理的命令

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

图 1 预处理hello.c

2.3 Hello的预处理结果解析

预处理器将#字符开头的代码进行一系列拓张,也就是将库函数直接代替这些代码,最后我们可以合并成一个新的文件,这个文件相比于源文件多了库函数。经过预处理后删除了注释,并将头文件中的.h拓张,将所有内容放入hello.i

图 2  预处理之后的文件们

图 3  hello.i 部分内容

2.4 本章小结

本章介绍了预处理这一过程,预处理器将#字符开头的代码进行一系列拓张,也就是将库函数直接代替这些代码,最后我们可以合并成一个新的文件,这个文件相比于源文件多了库函数。具体的预处理指令为命令:gcc -E hello.c -o hello.i从而生成hello.i,也就是说相比于hello.c预处理把三个头文件的内容包含了进来。这就是我们这一章的全部内容了!

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

编译的概念:编译说的是将我们要输入的一些语言变成汇编语言,这个是我们使用编译器的结果。编译器比较聪明,它不仅仅可以翻译语言,还可以优化代码,使得最后的汇编语言很高效。

编译功能:因为计算机不能直接识别的高级编程语言,所以编译器将我们要输入的一些语言变成汇编语言,这个是我们使用编译器的结果。编译器比较聪明,它不仅仅可以翻译语言,还可以优化代码,使得最后的汇编语言很高效。它还可以检查是否有语法错误在程序源代码中——只有没有语法错误时才能正常编译。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s,将hello.i编译得到 hello.s

图 4  运行编译命令

3.3 Hello的编译结果解析

3.3.1 数据

3.3.1.1常量

常量有字符串类型,观察 hello.c 可以发现这些字符串常量是"用法: Hello 学号 姓名 秒数!\n"。在 hello.s 中,常量类型被处理成如图所示。

图 5  hello.s中的字符串常量

3.3.1.2变量

  1. 局部变量

存在的局部变量有 int 型变量 i。通常来讲,在编译阶段,局部变量会被传递到寄存器,该变量会被存储在寄存器或栈中。针对 hello.c 中的局部变量 i,它在后续程序中被用作 for 循环的标志。

图 6 .hello.s 中的局部变量 i

    再比如函数的参数,main 函数的第一个参数是整形参数 argc,第二个参数是一个字符串数组argv。第一个参数被保存在寄存器%edi 中,在后续程序中,寄存器%edi 的内容被传递到-20(%rbp)中。

   

图 7 main 函数的参数

  1. 全局变量

全局变量sleepsecs,被赋初值为2。

图 8 全局变量

3.3.2 赋值

   程序中存在着变量赋值操作,即在前文中提到的将 0 赋给变量 i。之后 i 作为循环标志进入循环体中。

   

图 9  赋值操作

3.3.3 算术操作

  程序中存在算术运算即循环体中的 i++,如下图:

    

图 10 算术操作

3.3.4 关系操作

该程序中存在两处关系操作,均是比较操作,但本质不同,一个是 if 的条件判断,一个是 for 循环体的终止条件判断。

图 11 .if 条件判断

    

图 12  循环体的条件判断

3.3.5 控制转移

存在两种控制转移,一种是 if 的控制转移,一种是循环体中的控制转移。

1.比较 argc 和 4 的大小,若相等,则跳转.L2

图 13  if 中的控制转移

2.比较 i 7 的大小,若小于等于 7 则跳.L4

图 14  循环体中的控制转移

3.3.6 函数中的参数传递

   本小节将介绍函数中的参数传递是如何实现的

main 函数的两个参数 argc 存储在%edi 中,argv 存储在%rsi 中,在为函数分配栈空间后,会利用 mov 指令将参数传递到函数栈中

图 15  main函数的参数传递

atoi 和 sleep 两个函数的参数传递的操作与 main 函数类似。

3.3.7 函数调用 

函数调用的第一步是将函数参数从寄存器中取出并传递到函数堆栈,然后使用调用指令将函数地址推送到堆栈上,并将PC设置为函数的起始地址。 

图 16  printf 函数和 exit 函数的调用

3.4 本章小结

编译说的是将我们要输入的一些语言变成汇编语言,这个是我们使用编译器的结果。编译器比较聪明,它不仅仅可以翻译语言,还可以优化代码,使得最后的汇编语言很高效。C语言中简单的语句在汇编语言中变得复杂编程语言涉及寄存器的操作,使程序能够适应机器。我们分析了如何在虚拟机里面编译hello程序,并对这个编译结果里面出现的一些操作和数据进行分析,从而更加深入理解编译的过程。这就是我们这一章的全部内容了!

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:将汇编语言程序翻译成机器语言指令,把这些指令打包成可重定位目标程序。

汇编的作用:将汇编代码转换成计算机可识别的二进制文件。

4.2 在Ubuntu下汇编的命令

Linux 下汇编指令:gcc -c hello.s -o hello.o

图 17  汇编生成hello.o

4.3 可重定位目标elf格式

ELF格式如图所示:

图 18  ELF格式

使用readelf查看可得:

1.ELF头:

Elf头描述生成文件的系统的字长和字节顺序。

图 19  ELF头

2.节头部表

节头部表则描述了各节的名称、偏移量、地址、大小、读写属性等。

图 20  节头部表

3.重定位条目:

重定位条目包含重定位信息,也就是我们修改的位置信息。

图 21  重定位条目

4.符号表

    每个可重定位的目标模块都有一个符号表,包含了目标模块中定义和

引用的符号。主要有三种不同的符号:

(1)由目标模块定义并能被其他模块引用的全局符号

(2)由其他模块定义并被目标模块引用的全局符号

(3)只被目标模块定义和引用的局部符号。

图 22  符号表

4.4 Hello.o的结果解析

使用objdump得到hello.o的反汇编如下图:

图 23  hello.o的反汇编1

图 24  hello.o的反汇编2

经过对照分析,我们可以发现有下面的一些不同

A.操作数的进制一样

hello.o 中的操作数均是十六进制,而 hello.s 中则是十进制。

  1. 分支跳转的地址一样

hello.o 中的分支跳转是跳转到某个地址,而 hello.s 中则是跳转到 L2、L4 段等代码段。

  1. 函数调用格式一样

hello.o 中的函数调用时 call+一串地址,而 hello.s 中则是 call+函数名。

4.5 本章小结

我们先对汇编这个重要的中间的步骤进行分析,理解了它的概念和作用——将汇编语言程序翻译成机器语言指令,把这些指令打包成可重定位目标程序。之后我们在虚拟机中进行汇编实验,得到hello.o文件,之后我们查看并认真的分析它的elf文件格式,图文并茂地分析它的结果。最后我们将得到的汇编的结果进行解析,再反汇编对比分析,发现其中变化的奥妙!它们之间差异使我们对汇编和反汇编有了更深入的了解。这就是我们这一章的全部内容了!

(第4章1分)


5链接

5.1 链接的概念与作用

概念:链接器负责处理合并多个文件,如 printf.o,生成最终的 hello 可执行的目标文件。

作用:将多个可重定位的.o 文件合并到一个可执行文件中

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

得到:

图 25  输入链接命令

    

图 26  得到的可执行文件

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

可执行对象文件Hello的格式与可重定位对象文件的格式相似。ELF头描述文件的总体格式。它还包括程序要执行的第一条指令的地址。其他部分与可重定位对象文件中的部分类似,只是它们已重定位到其最终运行时内存地址。

图 27  可执行目标文件hello的格式

可以看到 hello 比hello.o 多了很多段,例如dynamic 段——保存了动态链接器的基本信息;dynsym 段——记录符号在动态符号表的偏移;hash 段——dynsym 段的辅助段,是一个哈希表,以加快符号查找过程;还有rel.dyn 段和rel.plt 段,用来修正数据引用。

5.4 hello的虚拟地址空间   

我们使用edb打开hello可执行文件

我们用Memory Regions调整地址空间,在Data Dump观察

如下图得到.ELF 的虚拟地址信息:

图 28  .ELF 的虚拟地址信息

根据节头中各段的地址,用Memory Regions调整地址空间,在Data Dump观察,得到

图 29  interp 段的虚拟地址信息

    

图 30  dynstr 段的虚拟地址信息

    

图 31  .text 段的虚拟地址信息

    

图 32  rodata 段的虚拟地址信息

    

5.5 链接的重定位过程分析

    我们使用objdump -d -r hello得到hello的反汇编结果

图 33 hello的反汇编结果main函数部分

对比可以发现存在一些不同之处——首先就是函数地址:main 函数地址在 hello 中起始地址为 401125 而在 hello.o 中起始地址则是 0;其次是 call 指令的不同——在 hello call 指令后接的是函数名,而不是 hello.o 中的相对偏移地址;最后是节头:hello 中多了很多的节。

原因是链接器在链接过程中必须完成两个任务,一个是重定位,另一个是符号解析。重定位使每个符号定义与一个内存位置相关联,这样就可以重新定位,然后修改所有符号引用,使其指向相应的内存位置。符号解析将每个符号引用与符号定义关联起来。

综上所述,可以得到hello中对其怎么重定位的

1. 重定位和符号解析:链接器将 hello.o 中所有类型相同的节形成一个新的节,并赋予新的内存地址,这使得 hello 中的每条指令和全局变量都有唯一的运行地址。

2. 符号引用:修改每个节中的符号引用,使得他们指向正确的地址。

5.6 hello的执行流程

我们使用edb执行之后,追踪每一步的子程序和地址,可以得到:

子程序名                                 地址

<dl_start>。。。。。。。。。。。。。。。0x7f2433995100

<_dl_init> 。。。。。。。。。。。。。。.0x72434008d00 

<_start>。。。。。。。。。。。。。。。。。4010f0  

<__libc_start_main>。。。。。。。。。。。401100

<__libc_csu_init>。。。。。。。。。。。。401150

<exit>。。。。。。。。。。。。。。。。。。0x7f24339acbb0

5.7 Hello的动态链接分析

我们从5.3中读取到.got.plt 段的地址为404000,在Data Dump中查看:

在调用 do_init 之前:很多部分为 0

图 34  调用 do_init 之前

在调用 do_init 之后:

图 35 在调用 do_init 之后

对于库函数而言,需要plt、got合作,plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。在 0x404000 处可以看到存在2个地址——0x7f3a61957190和0x7f3a61942200,这两个地址就是 got[1]和 got[2]的地址,其中got[2]就是动态链接ld-linux.so 的地址。

5.8 本章小结

我们介绍了链接的概念和功能,链接是将多个可重定位的.o 文件链接生成可执行文件。我们对对动态链接使用ld命令,分析了可执行文件的elf格式和段头信息,并与Hello进行了比较。然后分析hello的虚拟空间分布以及如何重新定位它,并得到一个从hello运行到结束的子程序。最后,我们分析如何执行hello以及它的动态链接。这就是我们这一章的全部内容了!

(第5章1分)

6hello进程管理

6.1 进程的概念与作用

进程的概念:

进程的就是一个执行中程序的实例。一个可执行程序的实例,给我们一种错觉:我们的项目似乎是唯一的程序当前运行的系统,我们的应用程序专用处理器和内存,处理器就像连续执行命令在我们的程序中,我们的程序代码和数据是唯一的对象在系统内存。这些假设是由过程的概念提供给我们的。

进程的作用:

它可以实现计算机可以并行实现不同任务的思想,使程序的模式切换更加自由、快捷、高效。进程提供给应用程序的关键抽象:独立的逻辑控制流就是我们的程序的专用处理器;专用地址空间就是程序的专用内存系统。

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

shell是一个交互型应用级程序,代表用户运行其他程序。shell应用程序提供了一个界面,用户通过访问这个界面访问操作系统内核的服务。  

处理流程:

1.读入输入的字符串。

2.将输入字符串切分获得所有的参数

3.如果是内置命令则立即执行,否则调用相应的程序执行

4.判断是不是后台进程,并执行对应操作

6.3 Hello的fork进程创建过程

我们可以使用fork函数,父进程调用fork函数创建一个新的运行的子进程,子进程得到和父进程用户及虚拟地址空间完全相同的一个副本,还获得与父进程任何打开文件描述符相同的副本。

例如我们在shell中输入./hello 120L020205 艾永亮 5,shell 会先判断第一个命令行参数是否是一个内置命令,判断不是后,会调用 fork()创建子进程,这时子进程与父进程具有相同的副本和虚拟空间。

 当这个进程由于某种原因终止时,进程保持在一种已经终止的状态,直到被他的父进程回收,当父进程回收他的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已经终止的进程,从此开始,该进程就不存在了。

6.4 Hello的execve过程

Hello 的子进程会在当前进程的上下文加载execve函数,且带列表argv和环境变量列表envp。只有当出现错误时,例如找不到Hello,execve才会返回到调用程序。argv[0]是可执行目标文件的名字。环境变量的列表是由一个类似的数据结构表示的。envp变量指向一个以NULL结尾的指针数组,其中每个指针指向一个环境变量字符串,每个串都是形如“name == value”的名字-值对。

6.5 Hello的进程执行

我们先介绍一下这几个名词的概念:(如下所示)

上下文: 上下文是内核运行进程所需的所有状态。它由通用寄存器、浮点寄存器、程序计数器、状态寄存器和各种内核数据结构组成。

时间片: 控制流只执行一个进程的时间段称为时间片。

调度: 内核决定进程切换,终止一个进程并启动另一个进程。由调度器代码处理的此类决策称为调度。当内核选择一个要运行的新进程时,我们说内核调度这个进程。在内核调度一个新进程运行后,它会抢占当前进程。一种称为上下文切换的机制用于将控制转移到一个新进程。

上下文切换: 首先保存当前流程的上下文,然后恢复先前被抢占的流程的上下文,最后将控制转移到新恢复的流程。

我们在shell中输入命令被shell了成功识别后,shell将时间片分配给所有进程,包括我们输入要求执行的hello程序进程。程序运行时,首先加载进程上下文信息,然后将控件从内核转换为程序。当一个片结束时,控件返回内核,保存进程上下文信息,并根据下一个片重新执行进程。

6.6 hello的异常与信号处理

首先,不妨说明一下信号的机制:一个信号就是一条消息,它通知进程系统中发生了一个某种类型的事件。每种信号类型都对应某种系统的事件。每种信号类型都对应于某种系统事件,底层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。信号提供了一种机制,通知用户进程发生了这些异常。

信号的发送方法有如下几种:1.用/bin/kill程序发送信号:/bin/kill程序可以向另外的进程发送任意的信号 2.从键盘发送信号:在键盘上输入Ctrl+C会导致内核发送一个SIGINT信号到前台进程中的每个进程。在键盘上输入Ctrl+Z会发送一个SIGTSTP信号到前台进程中的每个进程。 3.用kill函数发送信号:进程通过调用kill函数发送信号给其他进程。如果pid>0,则发给进程pid;如果pid=0,则发送给调用进程自己;如果pid<0,则发给进程组|pid|进程组的每个进程。 4. 使用alarm函数发送信号:进程可以通过调用 alarm 函数在指定 secs 秒后发送一个 SIGALRM 信号给调用进程。

当一个进程捕获了一个类型为k的信号时,就会调用为信号设置的处理程序, 一个整数参数被设置为k,这个参数允许同一个处理函数捕获不同类型的信号。当处理程序执行它的return 语句时,控制(通常)传递回控制流中进程被信号接收中断位置处的指令。我们说“通常”是因为在某些系统中,被中断的系统调用会立即返回一个错误。因为 signal 的语义各有不同,系统调用可以被中断。要解决这些问题,Posix 标准定义了sigaction 函数,它允许用户在设置信号处理时,明确指定他们想要的信号处理语义。

那么我们现在回到hello程序,我们使用以下命令或者按键,观察运行结果:

6.6.1不停乱按(乱按但不按下后面出现的命令)

图 36 不停乱按

我们可以发现乱按除了让屏幕输出按的字母,对程序没有其他的作用!

6.6.2乱按但是按回车

图 37  乱按但是按回车

我们可以发现按下的回车会在输出完之后起作用,让程序结束后,又将乱输入的字符作为命令在shell中执行!

6.6.3 按下 Ctrl-C

图 38  按下 Ctrl-C

我们可以发现按下Ctrl-C之后,程序结束了!这是因为按下Ctrl-C之后,进程收到终止信号,程序进程终止。

6.6.4 按下 Ctrl-Z

图 39  按下 Ctrl-Z

我们可以发现按下Ctrl-Z之后,程序停止了!这是因为按下 Ctrl-Z之后,进程收到停止信号,程序进程挂起。

6.6.5 按下Ctrl+Z 后输入 ps

图 40  按下Ctrl+Z 后输入 ps

我们可以发现按下Ctrl-Z之后,程序停止了,输入ps后输出了进程列表!这是因为按下 Ctrl-Z之后,进程收到停止信号,进程挂起,ps指令会输出进程列表到屏幕上。

6.6.6 Ctrl+Z 后输入jobs

图 41 按下Ctrl+Z 后输入 jobs

我们可以发现按下Ctrl-Z之后,程序停止了,输入jobs后输出了任务列表!这是因为按下 Ctrl-Z之后,进程收到停止信号,进程挂起,jobs指令会输出任务列表到屏幕上。

6.6.7 按下Ctrl+Z 后输入 pstree

图 42  按下Ctrl+Z 后输入 pstree

我们可以发现按下Ctrl-Z之后,程序停止了,输入pstree后输出了进程关系图!这是因为按下 Ctrl-Z之后,进程收到停止信号,进程挂起,pstree指令会输出进程关系图到屏幕上。

6.6.8按下 Ctrl+Z 后输入 fg

图 43  按下 Ctrl+Z 后输入 fg

我们可以发现按下Ctrl-Z之后,程序停止了,输入fg后恢复进程到前台!这是因为按下 Ctrl-Z之后,进程收到停止信号,进程挂起,fg指令会让进程在前台运行!

6.6.9按下 Ctrl+Z 后输入 kill

图 44 按下 Ctrl+Z 后输入 kill

我们可以发现按下Ctrl-Z之后,程序停止了,输入kill 后进程终止!这是因为按下 Ctrl-Z之后,进程收到停止信号,进程挂起,kill指令会杀死进程!

6.7本章小结

本章先是学习了进程的概念和作用,让我们了解了进程的相关知识。然后学习了shell的相关知识,知道了shell是怎么处理我们输入的指令的,以及如何创建并执行一个进程,也就是fork和exceve函数调用。还有如何切换进程,其中主要讲述了时间片、上下文、调度等概念。最后我们了解了shell的异常和信号处理情况,对异常和信号有了更深入的理解。这就是我们这一章的全部内容了!

(第6章1分)


7hello的存储管理

7.1 hello的存储器地址空间

图 45  hello反汇编部分代码

逻辑地址:逻辑地址是机器语言中用于指定操作数或指令的地址,如上图中的 4010c0  4010e0 等都是逻辑地址

线性地址:与逻辑地址类似,它是与硬件页内存相对应的非实地址上一个地址转换。

物理地址:计算机系统的主存被组织成一个连续字节大小的单元数组,每个单元都有一个唯一的物理地址。

虚拟地址:也就是线性地址。在hello程序中,每个部分的地址都是一个虚拟地址。在现代计算机中,系统是通过虚拟地址寻址的。

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

段式管理是将程序划分为多个分部。它的生成与程序的模块化直接相关。段管理通过段表实现,包括段编号或段名称、段起点、加载位和段长度。此外,还需要一个已占用主存区域表和一个可用主存区域表。段选择器分为索引、Ti和RPL。如果Ti为0,则在GDT全局描述符表中找到;如果Ti为1,则在LDT local descriptor表中找到它。RPL是段级别。它通过索引查找基址,并将偏移量添加到最后一个线性地址值。

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

计算机利用页表使用 MMU 来完成线性地址到物理地址的转换,如下图所示

虚拟地址VA分为P位VPO和n位VPN。MMU通过VPN选择合适的PTE,如pte0用于VPN 0,pte1用于VPN 1等。

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

与每个进程的虚拟地址空间中每个页的第一个地址相对应的物理地址存储在页表中。TLB缓存一些页表信息。如果相应的VPN条目缓存在TLB中,则可以直接从TLB获取PPN。如果没有缓存,请转到页表。如果页面已存储在主存中,请获取PPN。否则,请将页面的重要性设置为1并重复该过程。

图 46  TLB与四级页表

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

ALU基于VA获取PA后,CPU在缓存中找到对应的PA。访问缓存时,如果高级缓存中没有相应的缓存,则缓存会在低级缓存中搜索相应的缓存。搜索相应标签时,首先在相应的组中查找相同的标签,然后检查有效位是否为1。如果为1,则根据块偏移量获得相应位置的数据。

图 47  三级Cache

7.6 hello进程fork时的内存映射

我们可以使用fork函数,父进程调用fork函数创建一个新的运行的子进程,子进程得到和父进程用户及虚拟地址空间完全相同的一个副本,还获得与父进程任何打开文件描述符相同的副本。

当fork函数被当前进程调用时,内核将为新进程创建各种数据结构,并为其分配一个唯一的PID。它创建当前进程的mm结构体、区域结构体和页表的原始副本,为新进程创建虚拟内存。它将两个进程中的每个页面标记为只读,并在写时将两个进程中的每个区域结构标记为私有副本。

当fork在新进程中返回时,新进程的虚拟内存与调用fork时存在的虚拟内存完全相同。当两个进程中的任何一个稍后进行写操作时,写时复制机制将创建一个新页面。因此,私有地址空间的抽象概念是为每个进程维护的。

7.7 hello进程execve时的内存映射

Hello 的子进程会在当前进程的上下文加载execve函数,且带列表argv和环境变量列表envp。只有当出现错误时,例如找不到Hello,execve才会返回到调用程序。argv[0]是可执行目标文件的名字。环境变量的列表是由一个类似的数据结构表示的。envp变量指向一个以NULL结尾的指针数组,其中每个指针指向一个环境变量字符串,每个串都是形如“name == value”的名字-值对。

之后我们execve时的内存映射就会进行下列操作:

7.7.1删除已有用户区域。删除当前进程虚拟地址的user部分已有的zone结构。

7.7.2映射私有区域。所有这些新区域都是私人的,可以写或写。代码和数据区域被映射到。和文本。A. 0ut文件中的数据区。种植和堆字段也请求初始长度为零的二进制零。

7.7.3映射共享区域。如果a.out程序链接到共享对象,例如标准C库1ibc so,那么这些对象将被动态链接到程序,然后映射到用户虚拟地址空间中的共享区域。

7.7.4建立PC。Execve将当前进程上下文中的程序计数器设置为指向代码区域中的入口点,下一次调度进程时,它将从该入口点执行。

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

缺页:说的也就是DRAM内存丢失。CPU指向的是VP3中的未缓存在DRAM中的一个字。地址转换硬件从内存中提取PTE并触发缺页异常。此异常调用内核中的缺失页面异常处理程序删除一个页面。如果修改了VP4,内核会将其复制回磁盘。

图 48  缺页中断处理

缺页中断处理:内核将VP3从磁盘复制到内存中的PP3,更新PTE3,然后返回。当异常处理程序返回时,它将重新启动导致丢失页的指令,该指令将再次发送导致丢失页到地址转换硬件的虚拟地址。

7.9动态存储分配管理

方法A——隐式空闲链表的方法:

在每个块隐式自由链表中,除了存储块的相对空间大小外,块都需要存储在第一个块大小头中。这些自由链表块的遍历方法也分为优先级自适应和最优自适应。第一次适应意味着从头开始经历这些障碍——找到一个大于最初需要存储的大小的空闲块时被用作我们请求的地址。这种方法的优点是不会产生大量空间碎片和内部碎片。但缺点是会造成一定量的空间浪费和碎片。

方法B——显式空闲链表的方法:

此存储方法仅链接尚未分配给链表结构的可用块,如此可以大大提高访问效率,并且无需访问分配的块。然而种方法的缺点是需要在头中分配额外的空间来存储指向下一个块的空闲块位置的指针。

图 49  显式空闲链表

7.10本章小结

本章介绍了hello程序的存储管理的各级的结构。从hello中研究各种的地址空间,分析它们的作用。然后研究各种地址之间的关系和变化,比如我们对段的管理操作。之后结合图像发现了VA到PA是怎么进行变换的,这和四级页表和又TLB是相关的,没有它们的支持,我们无法进行这样的操作。再就是我们对cache的多级进行分析,思考它们是怎么去访问地址的。此时,我们研究原始的hello的进程,它是怎么创建并且执行的,这是一个复杂的问题,我们需要从调用的函数入手研究。其次,讲述了缺页的一些问题,这些再书上有比较详细的论述,我们查阅书记资料可以清楚了解到。最后,我们研究动态存储分配管理的两种方法,从不同的链表方式角度出发,分析它们的特点,这就是我们这一章的全部内容了!

(第7章 2分)


8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:所有的I/O设备都被模型化和映射为文件。

设备管理:unix io接口是由Linux内核支持的应用接口,是所有文件管理方法中最基础、安全、底层的方法。这使得再异步信号下也能安全运行。

8.2 简述Unix IO接口及其函数

Unix IO接口:

1 打开文件——我们调用内核来获取文件描述符,该描述符对文件执行所有操作,因此我们可以使用它来打开文件。

2 读/写文件——打开文件后读取文件,可以将字符读入内存;或者我们可以做相反的事情,将从内存中读取的字符放入当前文件,该文件正在写入该文件。

3 关闭文件——完成后,我们可以关闭它。我们通过内核来实现这一点,内核删除数据结构,然后恢复描述符。

Unix IO函数:

1 open——int open(const char *pathname,int flags,int perms)

在打开或创建文件,赋予文件各种属性和权限等

2 read——size_t read(int fd, void *buf, size_t count);

从文件读取数据,可以将字符读入内存

3 write——ssize_t write(int fd, void *buf, size_t count);

向文件写入数据,将从内存中读取的字符放入当前文件

4 lseek——off_t lseek(int fd, off_t offset,int whence);

定位文件指针到相应位置

5 close——int close(int fd)

关闭一个操作完全了的文件,删除数据结构,然后恢复描述符

8.3 printf的实现分析

以下格式自行编排,编辑时删除

我们先查看一下printf的源代码:

我们可以发现调用了两个函数——vsprintf和write

其中write是系统函数,

那么对于vsprintf函数——

 vsprintf的作用就是格式化——它接受确定输出格式的格式字符串,并且用格式字符串对个数变化的参数进行格式化,最后输出。

显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

8.4 getchar的实现分析

在getchar函数开始时先检查缓存的长度,以确定缓存中是否有数据。如果缓存中没有数据,调用写入缓存的函数,等待用户输入数据。当用户输入时,数据和Enter键都存储在缓存中。如果缓存中有数据,则直接使用该指针获取当前所指向的字符。如果数据指针被取出,它将指向下一个字符。将提取的字符赋给接收该字符的变量ch,并判断ch的值是否为结束符。如果有数据,则提取当前指向的数据,然后判断是否为终止符。

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

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

8.5本章小结

我们这里先研究了设备的模型化(所有的I/O设备都被模型化和映射为文件)和设备管理,理解了I/O设备的文件特性。之后从源代码出发理解了Unix IO接口和函数。之后我们结合计算机系统原理分析了printf和getchar的实现,对hello的IO管理有了更深入的理解!这就是我们这一章的全部内容了!

(第8章1分)

结论

这里介绍了hello.c在Linux下的生命周期过程——从最初的预处理(预处理器根据以#字符开头的命令对原始的C语言代码进行转换),编译(指将一种语言翻译成汇编语言),汇编(将汇编语言程序翻译成机器语言指令,把这些指令打包成可重定位目标程序)和链接(将多个可重定位的.o 文件合并到一个可执行文件中)阶段进行分。最后我们对hello创建进程,存储管理,IO管理等方面结合理论知识进行研究。在完成可执行程序之后,我们使用shell 读取指令(./hello 120L020205 艾永亮 5)调用fork生成子程序,子程序会调用函数 execve()加载 hello,为其分配时间片、虚拟空间以及PID,这样 hello 就成为了进程。当子进程收到信号时,hello 会根据不同的信号做出反应。当进程结束后,hello 也就结束了它的任务,它会被父进程回收,至此hello 过完了它的一生。

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


附件

hello.c 。。。。。。。。。。。。。hello的源文件

hello.i。。。。。。。。。。。。hello.c经过预处理后的文件

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

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

hello。。。。。。。。。。。。。链接后的可执行文件

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


参考文献

[2] 兰德尔 E.布莱恩特,大卫 R.奥哈拉伦. 《深入理解计算机系统(第三版)》.

机械工业出版社

  1. HIT ICS PPT
  2. 计算机组成与系统结构/袁春风编著.一北京:清华大学出版社,2010.4 (21世纪大学本科计算机专业系列教材)

(参考文献0分,缺失 -1分)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值