程序人生-HIT计算机系统大作业-1190201027

 

摘  要

 

一个简单的hello程序,也将经历复杂精巧的处理过程才能在计算机中运行。本文跟随hello程序的整个生命过程,使用gcc,edb,objdump等工具分析其作为程序的预处理,编译,汇编,链接过程,并结合CSAPP对其作为进程运行时涉及的进程管理,内存管理及IO管理进行理论分析,从程序的角度系统梳理了计算机系统课程的知识体系。

 

关键词:计算机系统;编译;虚拟内存;系统级I/O                           

 

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

 

目  录

 

第1章 概述............................................................................................................. - 4 -

1.1 Hello简介...................................................................................................... - 4 -

1.2 环境与工具..................................................................................................... - 4 -

1.3 中间结果......................................................................................................... - 5 -

1.4 本章小结......................................................................................................... - 5 -

第2章 预处理......................................................................................................... - 6 -

2.1 预处理的概念与作用..................................................................................... - 6 -

2.2在Ubuntu下预处理的命令.......................................................................... - 6 -

2.3 Hello的预处理结果解析.............................................................................. - 7 -

2.4 本章小结......................................................................................................... - 7 -

第3章 编译............................................................................................................. - 9 -

3.1 编译的概念与作用......................................................................................... - 9 -

3.2 在Ubuntu下编译的命令............................................................................. - 9 -

3.3 Hello的编译结果解析................................................................................ - 10 -

3.4 本章小结....................................................................................................... - 14 -

第4章 汇编........................................................................................................... - 15 -

4.1 汇编的概念与作用....................................................................................... - 15 -

4.2 在Ubuntu下汇编的命令........................................................................... - 15 -

4.3 可重定位目标elf格式............................................................................... - 15 -

4.4 Hello.o的结果解析.................................................................................... - 17 -

4.5 本章小结....................................................................................................... - 19 -

第5章 链接........................................................................................................... - 20 -

5.1 链接的概念与作用....................................................................................... - 20 -

5.2 在Ubuntu下链接的命令........................................................................... - 20 -

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

5.4 hello的虚拟地址空间................................................................................ - 23 -

5.5 链接的重定位过程分析............................................................................... - 24 -

5.6 hello的执行流程........................................................................................ - 25 -

5.7 Hello的动态链接分析................................................................................ - 25 -

5.8 本章小结....................................................................................................... - 26 -

第6章 hello进程管理................................................................................... - 28 -

6.1 进程的概念与作用....................................................................................... - 28 -

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

6.3 Hello的fork进程创建过程..................................................................... - 29 -

6.4 Hello的execve过程................................................................................. - 29 -

6.5 Hello的进程执行........................................................................................ - 29 -

6.6 hello的异常与信号处理............................................................................ - 30 -

6.7本章小结....................................................................................................... - 32 -

第7章 hello的存储管理............................................................................... - 33 -

7.1 hello的存储器地址空间............................................................................ - 33 -

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

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

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

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

7.6 hello进程fork时的内存映射.................................................................. - 36 -

7.7 hello进程execve时的内存映射.............................................................. - 36 -

7.8 缺页故障与缺页中断处理........................................................................... - 37 -

7.9动态存储分配管理....................................................................................... - 37 -

7.10本章小结..................................................................................................... - 38 -

第8章 hello的IO管理................................................................................. - 39 -

8.1 Linux的IO设备管理方法.......................................................................... - 39 -

8.2 简述Unix IO接口及其函数....................................................................... - 39 -

8.3 printf的实现分析........................................................................................ - 39 -

8.4 getchar的实现分析.................................................................................... - 40 -

8.5本章小结....................................................................................................... - 40 -

结论......................................................................................................................... - 42 -

附件......................................................................................................................... - 43 -

参考文献................................................................................................................. - 44 -

 

 

 

第1章 概述

1.1 Hello简介

 

一个简单的hello程序,其整个生命过程(020)大致由以下部分组成。

       从零开始,程序员首先使用各种程序语言编写出hello程序,此时的程序还是可供人阅读的文本文件hello.c。在此之后,该文件经过一系列处理过程最终变为可以在机器上执行的可执行文件(program):

       预处理:hello.c中的各类预处理指令被执行,生成hello.i文本文件。

       编译:hello.i将被翻译成使用汇编语言描述的hello.s文本文件。

       汇编:hello.s进一步被翻译成可供机器识别的hello.o二进制可重定位目标文件。

       链接:hello.o中的重定位条目被处理,与其他可重定位目标文件组合最终生成可以在机器上运行的可执行文件hello。

      

       生成可执行文件hello后,程序被加载进内存开始由运行到终止回收的过程(process):

系统中所有程序依靠一个shell进程管理。当在shell中请求执行hello程序时(./hello),shell将通过fork创建一个与其具有相同代码及其他资源的拷贝子进程。子进程通过execve,将进程控制转给hello程序。程序正式开始运行。程序的运行过程也涉及多种机制的协调合作:

为了使多个进程“同时”在机器中运行,计算机系统设计了时间分片,上下文切换等机制;

为了使程序所在的内存被高效访问,使程序能够高效的运行,设计了多级高速缓存,页表等内存管理机制及流水线等指令处理机制;

为了实现程序与系统,程序之间的信息传递功能,设计了程序异常及信号的机制。

       通过这些机制,hello程序的代码被正确的从内存加载到处理器执行。所有代码运行完成后,hello程序返回。由shell对hello所在的子进程进行回收。程序的所有信息被移除,hello的生命过程结束。

 

1.2 环境与工具

 

硬件环境:

Intel Core i7-8750H CPU;2.20GHz;8G RAM;1024GHD Disk

 

软件环境:

Windows 10,Ubuntu 20.04.2.0,VMware-workstation 15.5.0

 

开发与调试工具:

Code::Blocks 64位,EDB

 

1.3 中间结果

 

中间结果文件名

作用

hello.i

预处理过程生成文件

hello.s

编译过程生成文件

hello.o

汇编过程生成文件

hello_o_elf.txt

readelf -a hello.o 输出文件

hello_elf.txt

readelf -a hello 输出文件

 

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

1.4 本章小结

       本章从宏观角度简要的介绍了hello程序从生成到执行到终止的整个生命过程。给出了实验的运行环境及工具,以及整个实验过程中生成的各种文件。

 

(第1章0.5分)

 

 

 

第2章 预处理

2.1 预处理的概念与作用

 

预处理的概念:

预处理是指程序源代码在被翻译为目标代码的过程中,编译过程执行之前进行的处理。预处理由CPP(C Preproccessor)预处理器完成。对于源代码中所有以#符号起始的行,预处理将会将其展开并试图解释为预处理指令。预处理指令一般用来使源代码在不同的执行环境中被方便的修改或者编译。

预处理指令一般包括以下三方面的内容:

1. 宏定义:格式为 #define ~ ~。对于宏定义预处理指令,预处理过程中将会使用宏名对应的具体代码或字符串等,对代码中全部的宏进行简单的替换。通过使用宏定义可以实现宏函数等功能。

2. 文件包含:格式为#include ~。对于文件包含预处理指令,预处理过程将尝试将对应的目标文件复制插入到代码所在的文件中。

3. 条件编译:有#if,#ifdef,#elif,#else等。用于实现有条件的预处理过程。

 

预处理的作用:

通过使用预处理,可以提高程序的通用性和易读性,减少输入错误并便于修改。同时使用文件包含预处理,有利于代码移植与服用,并便于模块化文件管理。

2.2在Ubuntu下预处理的命令

  

图3.3.4.1 关系操作

如图,关系比较操作由汇编指令组cmp实现。该指令将会对其后的两个参数进行比较,由比较的结果设置CPU寄存器中的标记位。另有一组junp指令实现条件跳转,如图中所示的je为相等时跳转,jle为小于等于时跳转,由此实现关系操作。

 

3.3.5 控制转移

Hello程序中包括了控制转移结构if条件判断和for循环。这些控制转移过程均使用条件判断和短转移实现。

条件判断if:

  

图3.3.6.1 函数调用及参数传递语句

如图,函数调用过程,一般使用call语句对相应地址中的函数进行调用,同时将该段代码的地址压栈。执行完调用函数后,函数使用ret指令返回,将栈中的地址弹出并返回到对应的地址下继续运行。汇编语言规定了参数传递由一系列CPU寄存器实现,将参数传入规定的寄存器中在函数间传递,此外还规定了函数调用过程中需要压栈保护的寄存器数据。如图中调用函数时,多使用了%rdi寄存器传参。

 

函数返回:

 

图4.3.2 可重定位目标文件重定位信息

       关于重定位条目的内容,可以使用readelf -r hello.o指令查看,也可以直接在以上的hello_o_elf.txt文本中查看。

       可以看到hello程序调用的各种库函数均需要重定位。文件暂时使用二进制0填充所有调用库函数的跳转地址,待执行下一步链接完成后,库函数的实际代码被加入文件,相应地址被重写后,程序就能够正常执行了。

 

    分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

4.4 Hello.o的结果解析

 

图4.4.1 对比.o文件反汇编文本与.s文件

使用了objdump -d -r hello.o  获得文件hello.o的反汇编文本,与第3章的 hello.s进行对照分析。

可以看到,机器语言全部由二进制数据表示。

 

图5.2.1 生成可执行文件

如图,使用以下命令将.o文件链接生成可执行文件。

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_elf.txt将hello程序的ELF文件信息导入hello_elf.txt,文件的具体信息可以查看该文件。

 

 

图5.3.1 可执行文件节头信息

 

图5.3.2 可执行文件节头信息(续)

       ELF文件的各节名字,大小,起始地址等信息如上图所示。

 

 

图5.4.1 通过地址及偏移推测ELF头地址

 

图5.4.2 在内存中查看ELF头

       在程序头部分查看虚拟地址及偏移,可以推测ELF头位于虚拟地址0x400000处。使用edb查看相应地址,可以发现hello文件头确实位于该地址处,有ELF标识,且Magic值也相符合。

 

        

图5.4.2 在内存中查看.text段代码

       使用readelf获取的段信息显示.text段起始地址为0x4010f0,在edb中查看也能发现对应地址下存放了_start函数的相应代码。其后为其余各个函数的代码,包括main函数。

       通过给readelf获取的段信息,在edb中查看其它段的虚拟地址下存储的内容,也与之一致。

 

5.5 链接的重定位过程分析

 

使用objdump -d -r hello 获取hello的反汇编代码,与hello.o的反汇编代码进行对比分析可以发现,两者有以下方面的区别:

链接过程中原本包含在其它文件中的所需的函数代码被加入进文件,如exit、printf、sleep、atoi、getchar等函数。

hello中增加了.init和.plt节,以及与这些节相关的函数。

hello中不再有hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。

hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于.rodata的访问,是$0x0,是因为它的地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。

 

根据hello和hello.o的不同,分析出链接的过程为:链接就是链接器ld将各个目标文件组装在一起,就是把.o文件中的各个函数段按照一定规则累积在一起,比如规则:解决符号依赖,库依赖关系,并生成可执行文件。

重定位:编译器和汇编器生成从0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。

对于hello来说,链接器把hello中的符号定义都与一个内存位置关联了起来,重定位了这些节,并在之后对符号的引用中把它们指向重定位后的地方。hello中每条指令都对应了一个虚拟地址,而且对每个函数,全局变量也都它关联到了一个虚拟地址,在函数调用,全局变量的引用,以及跳转等操作时都通过虚拟地址来进行,从而执行这些指令。

 

5.6 hello的执行流程

 

函数名称

函数地址

ld-2.27.so!_dl_start

0x7fce:8cc38ea0

ld-2.27.so!_dl_init

0x7fce:8cc47630

hello!_start

0x4011f0

libc-2.27.so!__libc_start_main

0x7fce:8c867ab0

-libc-2.27.so!__cxa_atexit

0x7fce:8c889430

-libc-2.27.so!__libc_csu_init

0x4011b0

hello!_init

0x401000

libc-2.27.so!_setjmp

0x7fce:8c884c10

hello!main

0x4011d6

hello!puts@plt

0x401090

hello!exit@plt

0x4010c0

hello!printf@plt

0x4010b0

hello!sleep@plt

0x4010d0

hello!getchar@plt

0x4010e0

ld-2.27.so!_dl_runtime_resolve_xsave

0x7fce:8cc4e680

-ld-2.27.so!_dl_fixup

0x7fce:8cc46df0

ld-2.27.so!_dl_lookup_symbol_x

0x7fce:8cc420b0

libc-2.27.so!exit

0x7fce:8c889128

使用edb执行hello,从加载hello到_start,到call main,以及程序终止的所有子程序名及地址如上表所示。

 

5.7 Hello的动态链接分析

  

动态链接库中的函数在程序执行的时候才会确定地址,所以编译器无法确定其地址,在汇编代码中也无法像静态库的函数那样体现。

动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。如果一个目标模块调用定义在共享库中的任何函数,那么就有自己的GOT和PLT。

PLT是一个数组,其中每个条目是16字节代码。每个库函数都有自己的PLT条目,PLT[0]是一个特殊的条目,跳转到动态链接器中。从PLT[2]开始的条目调用用户代码调用的函数。

GOT也是一个数组,每个条目是8字节的地址,和PLT联合使用时,GOT[2]是动态链接在ld-linux.so模块的入口点,其余条目对应于被调用的函数,在运行时被解析。每个条目都有匹配的PLT条目。

 

 

图5.7.1 .got段地址

 

图5.7.2 _init前内存状态

 

图5.7.3 _init后内存状态

       如图,根据readelf给出的段地址,在edb中查看,可以看到执行_init前后GOT表中的变化。

 

5.8 本章小结

本章首先介绍了链接的相关内容,通过对链接前后.o文件和可执行文件的比较,分析了链接中重定位和动态链接的意义及实现。同时通过在edb中调试测试,分析了可执行文件的虚拟地址空间。

 

(第51分)

 

 

6章 hello进程管理

6.1 进程的概念与作用

 

进程的概念:

狭义定义:进程是正在运行的程序的实例。

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

进程的概念主要有两点:第一,进程是一个实体。每一个进程都有独立的地址空间,一般情况下,包括文本区域、数据区域和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行),它才能成为一个活动的实体,称其为进程。

 

进程的作用:

进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个概念,所有多道程序设计操作系统都建立在进程的基础上。

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

 

Shell俗称壳,是指“为使用者提供操作界面”的软件(命令解析器),用户与操作系统内核之间的接口,起着协调用户与系统的一致性和在用户与系统之间进行交互的作用。它类似于DOS下的COMMAND.COM和后来的cmd.exe。

其主要的功能是命令解释,它接收用户命令,然后调用相应的应用程序。同时其携带了部分内置的命令及相应的处理过程。此外,Shell还包括了通配符,命令补全,重定向,管道等机制及功能。

其处理过程如下:当接受了一个命令行指令后,Shell首先判断它是否为内置命令,如果是就通过Shell内部的解释器将其解释为系统功能调用并转交给内核执行;若是外部命令或实用程序就试图在硬盘中查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行。在查找该命令时分为两种情况:(1)用户给出了命令的路径,Shell就沿着用户给出的路径进行查找,若找到则调入内存,若没找到则输出提示信息;(2)用户没有给出命令的路径,Shell就在环境变量PATH所制定的路径中依次进行查找,若找到则调入内存,若没找到则输出提示信息。

 

6.3 Hello的fork进程创建过程

 

父进程通过调用fork函数创建一个新的运行的子进程。新创建的子进程将会得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段,堆,共享库以及用户栈。子进程还获得与父进程任何打开的文件描述符相同的副本。父进程与新创建的子进程的最大区别在于他们有不同的PID。

fork被调用一次,返回两次。在父进程中fork返回子进程的pid,在子进程中fork返回0.父进程与子进程是并发运行的独立进程。

对于hello进程,父进程使用fork创建了hello所在的子进程后,还需要使用execve来将hello加载和运行。

 

6.4 Hello的execve过程

 

exceve函数在当前进程的上下文中加载并运行一个新程序。exceve函数加载并运行可执行目标文件,并带参数列表和环境变量列表。只有当出现错误时,exceve才会返回到调用程序,否则,exceve调用依一次且从不返回。在exceve加载了可执行目标文件后,他调用启动代码,启动代码设置栈,将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序,由此将控制传递给新程序的主函数。

 

6.5 Hello的进程执行

 

       系统中的每一个程序都运行在某个进程的上下文中。上下文是由程序正常运行所需的状态组成的。这个状态包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构的值。

      而系统中通常有许多程序在运行,为了使得进程为每个程序提供一个好像它在独占地使用处理器的假象,同时引入了进程时间片的概念。系统将会令多个进程轮流运行,每次执行单个进程的控制流的一部分,再在多个进程中轮流执行。一个进程执行它的控制流的一部分的每一时间段叫做时间片。

      使用时间片和上下文的概念,即可实现进程的并发执行。一个系统运行着多个进程,那么处理器的一个物理控制流就被分成了多个逻辑控制流。这些逻辑流的执行是交错的,它们轮流使用处理器。当一个时间片完成后,操作系统通过上下文切换机制,保存当前进程的上下文,恢复先前被抢占的进程所保存的上下文,并将控制传递给新恢复的进程。

       而为了使操作系统内核提供一个无懈可击的进程抽象,限制应用可以执行的指令以及它可以访问的地址空间范围,处理器同时引入了用户模式和内核模式的机制。通过实现某个控制寄存器的模式位标记,将进程分为用户模式和内核模式。没有设置模式位时,进程运行在用户模式中,它必须通过系统调用接口才可间接访问内核代码和数据;而设置模式位时,它运行在内核模式中,可以执行指令集中的任何指令,访问系统内存的任何位置。

 

6.6 hello的异常与信号处理

异常可以分为四类,分别为中断,陷阱,故障和终止。在hello按要求执行的过程中这些异常都可能发生:如键盘输入导致的中断,调试工具调试时使用的陷阱,缺页异常导致的故障,系统错误导致的终止等。

而在hello按要求执行的过程中,可能产生的信号有SIGSTOP,SIGCONT,SIGINT,SIGKILL等。

以下按照输入进行介绍:

 

 

图6.6.1 常规输入

在执行过程中随意输入,或者使用回车键,程序接受键盘输入中断异常,跳转到处理键盘输入的异常处理程序。处理完成后没有相关信号返回,跳转回hello程序继续执行。

 

 

图7.1.1 虚拟地址(左)与逻辑地址(中)

逻辑地址:指由程序产生的与段相关的偏移地址部分,由段选择符和偏移地址构成。如图中0xdff(%rip)即为一个典型的相对偏移地址,即逻辑地址。

线性地址:逻辑地址到物理地址变换的中间层,是处理器可寻址空间的地址。程序代码产生的逻辑地址加上段基地址就产生了线性地址。

虚拟地址:虚拟地址与线性地址定义相似,为线性地址的一个具体的有限子集。如上图中1200,1202等,即为机器代码的一个虚拟地址。

物理地址:CPU外部地址总线上的寻址信号,在实际上对应内存中的一个存储单元。hello程序存储在内存中时实际占有的地址即为物理地址。

 

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

 

图7.2.2 段描述符结构

       段式管理采用了描述符表等结构实现由逻辑地址到线性地址的映射管理。

如上图7.2.1为一个段选择符的基本结构,通过RPL表示CPU的当前特权级,通过TI表明选择的是全局描述符表还是局部描述符表。高位索引用于表示相对描述符表的索引位置。

       如上图7.2.2为段描述符表中的单个段描述符结构。段描述符存储有关于该段的各类信息,其中段基址即为该段的起始线性地址。段描述符表存储一个段描述符数组。

虚拟地址中的高位部分,按照段选择符格式解释,查找相应的段描述符,可以获得对应段的起始线性地址。虚拟地址中的低位部分,为相对于段起始的便宜地址。组合获得的两部分地址即可获得与该虚拟地址对应的线性地址。

      

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

 

图7.3.1 页式管理流程

页式管理使用了一个页表结构实现由虚拟地址(线性地址的有限子集)到物理地址的映射管理。

系统将地址空间划分为了一系列具有相同大小的内存块(通常为4kb),称为页,依此进行管理,通过页表建立映射。页表中存储了一系列页表条目,每个页表条目包含有效位和物理页号。通过有效位和物理页号内容可以判断虚拟内存是未分配,未缓存还是已缓存状态。

对于一个虚拟地址,可以将其分为两部分解释。高位作为虚拟页号,通过页表基址寄存器访问对应偏移下的页表条目,可以获取对应的物理页号。低位为虚拟页偏移量,直接映射为物理页偏移量。将两部分组合,即可得到与该虚拟地址对应的物理地址。

 

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

在7.3中描述的虚拟地址到物理地址的映射过程,由于页表存储于内存中,与数据,代码共同使用高速缓存,效率较低,发展出了使用快表(TLB)的处理方式。为了进一步减小页表大小,节约内存,又发展出了多级页表。

TLB与高速缓存Cache的实现相似。TLB中存储有一系列页表条目。原虚拟页号部分,被重新分为TLB标记和TLB索引两部分解释。通过TLB索引,在TLB中查找相应的组。将组中的每行标记与TLB标记比较,若存在标记相同,则TLB命中,该行中页表条目即为所需页表条目。若标记均不同,说明TLB不命中,此时需要TLB在内存中查找,加载(可能产生牺牲页)对应的页表条目。获取页表条目后,后续处理与7.3节内容相同。

多级页表技术,是在将7.3节中描述的虚拟页表段均分为多个分段,每一段对应一级页表的索引。使用一级页表基址与VPN1组合,获得VPN2所在页表的基址,该基址与VPN2组合,获得VPN3所在页表的基址……重复直至最后一级页表,获取物理页号,与虚拟页偏移组合获得实际的物理地址。

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

uploading.4e448015.gif转存失败重新上传取消

 

图7.5.1 Cache与物理地址结构

为了有效利用程序的局部性提高内存的读取速度,现代计算机使用了多级高速缓存技术。

       如图所示为一个高速缓存Cache结构。Cache的每一行中存储了部分内存块的实际内容,以及额外的有效位说明内容是否有效,额外的标记位用于查找比较。多个行被组织为一组,一级Cache中有多组。

经过7.3节及7.4节描述得到的物理地址,被按位解释为标记,组索引和块偏移。通过组索引可以查找Cache中相应的组,将该组中所有有效行的标记位与其标记进行比较,查找该部分地址对应内存是否被加载进Cache。对于比较成功的,说明Cache命中,根据块偏移查找基于对应地址的偏移内容即可。比较全部失败,说明Cache不命中。Cache向下一级存储器请求对应地址下的内容,将其加载(可能导致牺牲页)。

目前常见的处理器多使用三级高速缓存,作为内存与CPU间数据传递的过渡机制。

7.6 hello进程fork时的内存映射

 

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

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此也就为每个进程保持了私有地址空间的概念。

 

7.7 hello进程execve时的内存映射

 

execve函数在当前进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out有效替代当前程序。加载运行过程需要以下几个步骤:

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

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

(3)映射共享区域。如果a.out程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,在映射到用户虚拟地址空间中的共享区域内。

(4)设置程序技术器(PC)。execve做的最后一件事是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度该进程,将从入口点开始。

 

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

uploading.4e448015.gif转存失败重新上传取消

 

图7.8.1 缺页中断处理

如图,当处理器请求访问某个页面内容时,向MMU发送虚拟地址。MMU根据虚拟地址查找相应的页表条目,发现该条目标记的物理页处于未分配或未缓存状态,触发缺页故障。

缺页故障导致控制传递给内核态的缺页异常处理程序。该程序将负责把相应的物理内存加载进MMU和处理器,并处理可能的牺牲页。

处理完成后,控制将重新传递给用户态下的原程序,重新开始执行触发异常时所在的代码及之后的内容。

 

7.9动态存储分配管理

动态内存管理的基本方法:

虽然可以使用低级的mmap和munmap函数来创建和删除虚拟内存的区域,但是C程序员还是会觉得当运行时需要额外虚拟内存时,用动态内存分配器更方便,也有更好的可移植性。

动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk(读做“break"),它指向堆的顶部。

分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

动态内存管理的策略:

动态内存分配的过程需要处理空闲块组织,放置,分割,合并等多个方面的操作。按照实现方式,可以大致将动态内存管理分为以下几种实现策略:

隐式空闲链表。对于任何一个内存块,块的起始位置将存储一个头部。头部中包含有块大小,以及该块是否被分配的信息。通过头部中的大小字段,所有空闲块隐含的连接在一起,可以通过遍历所有块来遍历空闲块集合,进而实现内存分配。

显式空闲链表。在隐式空闲链表的基础上,每个块的结束位置额外存储一个尾部,尾部内容为其头部的拷贝,尾部被用于实现双向链表,可以逆向查找前一个块的信息。此外,每一个空闲块的头部之后额外添加了两个指向其前后空闲块的指针,显式的将所有空闲块连接在一起。

分离的空闲链表。将块按照大小划分为一些等价类,为每一个等价类单独维护一个链表。这种方法有众多具体实现,主要区别在于如何定义大小类,何时合并,何时请求额外堆内存,是否允许分割,等等。

 

7.10本章小结

本章介绍了与程序的存储管理相关的一系列内容。7.1至7.5各节,首先介绍了存储器地址空间,hello程序执行由逻辑地址,线性地址,虚拟地址到物理地址的地址转换过程及内存信息依靠地址的读取过程。7.6,7.7节介绍了进程执行时的内存映射情况。7.8节介绍了内存读取与异常,分析了缺页异常处理过程。7.9节介绍了动态内存分配过程的相关内容

 

(第7 2分)

 

8章 hello的IO管理

8.1 Linux的IO设备管理方法

 

Linux系统将所有I/O设备(例如网络,磁盘和终端)都模型化为文件。一个文件就是一个m字节的序列:

       B0,B1,…,Bk,…,Bm-1

而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅的映射为文件的方式,允许Linux内核引出一个简单,低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行:

打开文件。一个应用程序通过要求内核打开相应的文件,来宣告他想要访问一个I/O设备。

改变当前文件的位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量。

       读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。类似的,写操作就是从内存复制n>0个字节到文件,从当前文件位置k开始,然后更新k。

       关闭文件。当应用完成了对文件的访问之后,他就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。

 

8.2 简述Unix IO接口及其函数

 

如8.1节中描述的。通过将所有I/O设备模型化为文件,Linux就可以通过简单的应用接口对所有的设备输入输出进行统一执行。这一类应用接口被称为Unix I/O。Unix I/O大致包括以下几个函数:

open(),close():打开和关闭文件。

read(),write():读写文件。

lseek():改变当前文件位置。

 

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函数的实现如上所示。简单的说,printf函数先通过一个vsprintf函数,对调用函数时给出的输出格式及参数进行格式化,获得用于输出的实际字符串数组。代码中的i存储输出字符串数组长度,buf存储输出字符串的具体内容,arg存储了printf函数的参数数组。

    通过vsprintf格式化后,调用Unix I/O的write函数实现最终的输出。write函数传参后使用int INT_VECTOR_SYS_CAL 设置系统调用,将控制传递给内核态的sys_call函数。这个函数将按参数跳转到sys_call_table下对应的字符显示驱动子程序完成对输出设备的写操作。

字符显示驱动子程序根据子模库完成从ASCII码到显示器点阵每个点信息的映射,将信息传递给VRAM。显示芯片按照刷新频率逐行读取VRAM,并通过信号线向液晶显示器传输每一个点(RGB分量)。最终完成输出到屏幕的信息显示。

 

8.4 getchar的实现分析

 

getchar与8.3节中的printf实现细节有相似之处,均为经过包装Unix I/O得到的标准I/O函数,其依靠read函数实现。read函数传参后使用int INT_VECTOR_SYS_CAL 设置系统调用,将控制传递给内核态的sys_call函数。这个函数将按参数跳转到sys_call_table下对应的键盘中断处理子程序完成对输入设备的读操作。

键盘中断处理子程序负责讲接受按键扫描码转成ASCII码,保存到系统的键盘缓冲区。直到接受回车键后返回,读取相应字符的ASCII码,完成输入读操作。

 

8.5本章小结

       本章简要介绍了Linux对I/O设备的管理方式。首先介绍了Linux下的文件模型以及由此衍生出的Unix I/O接口,再printf,getchar函数具体分析了这些标准I/O函数涉及的底层I/O管理机制及实现方式

 

(第81分)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

结论

 

Hello程序从编写,编译,加载,执行,到终止回收的整个生命过程,经历了以下的重要过程:

代码编写,通过文本编辑器或更高级的编辑环境完成hello.c的代码编写。

预处理,经过预处理器cpp处理,得到hello.i文件。

编译,编译器将hello.i文件转换成使用汇编语言描述的hello.s文件。

汇编,汇编器将hello.s文件翻译成二进制机器指令描述的可重定位目标文件hello.o。

链接,链接器将hello.o文件与动态链接库链接生成可执行目标文件hello,此时hello程序终于能够被机器读取且正确执行。

开始运行,在shell中请求执行hello程序。shell调用fork为其创建子进程,并在子进程中调用execve加载hello程序。execve调用启动加载器,将hello程序所需的上下文加载后将控制传递给hello,hello程序中的代码正式开始执行。

运行过程中,内核为维护程序“单独执行”的假象,使用时间分片及上下文切换机制实现进程的并发。

内存访问,当处理器读取hello数据,或hello程序请求内存访问时,发出虚拟地址,MMU将虚拟地址映射为物理地址并通过三级高速缓存cache访问内存。

动态内存分配,malloc等函数会向动态内存分配器申请堆中的内存,涉及用户堆的实现及管理。

异常与信号,实现了程序间的相互通信,程序与内核的相互通信,hello运行过程中可能遇到各种异常和信号,将涉及各类处理机制。

程序结束。执行完全部代码后,shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。内存中不再有hello程序的相关痕迹,其生命周期最终结束。

 

       可以看出,即便是一个简单的hello程序,为了使其能够在机器上正确的执行,其中涉及到的过程也是极为复杂精细的。纵观整个过程,现代计算机实现了一个极其庞大的实现体系,对于每一个处理过程,都完成了细致巧妙的实现与优化,使得程序能够尽可能高效的在计算机上运行。从软件上的程序,内核,到硬件上的处理器,高速缓存,存储器与I/O设备,各个部分紧密协调,共同构成了计算机系统的整体结构。

       通过对计算机系统较为全面的了解,我越加惊叹于现代计算机的丰富内涵,对计算机有了更加本质深入的理解,同时也能够使用相应知识更好的解决程序与计算机的相关问题。

 

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

 

附件

hello.i

预处理过程生成文件

hello.s

编译过程生成文件

hello.o

汇编过程生成文件

hello_o_elf.txt

readelf -a hello.o 输出文件

hello_elf.txt

readelf -a hello 输出文件

 

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

 

参考文献

 

[1]  兰德尔 E.布莱恩特. 深入理解计算机系统. 龚奕利 译

[2]  printf 函数实现的深入剖析 - Pianistx

[3]  内存地址转换与分段_沈雷的专栏-CSDN博客

[4]  C语言open,read,write函数,及文件读写_centor的博客-CSDN博客

[5]  Linux系统调用详解(实现机制分析)_OSKernelLAB(gatieme)-CSDN博客

 

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

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值