程序人生-Hello’s P2P

06-18 1304阅读

计算机系统

大作业

题     目  程序人生-Hellos P2P  

专       业       物联网工程           

学     号       2022111806           

班   级        2237301             

学       生         赵天一         

指 导 教 师          吴锐           

计算机科学与技术学院

2024年5月

摘  要

本文以hello.c这个程序为中心,阐述了Hello程序在Linux系统的生命周期,从hello.c依次深入研究了编译、链接、加载、运行、终止、回收的过程,从而了解hello.c文件的“一生”。并结合课程所学知识说明了Linux操作系统如何对Hello程序进行进程管理和存储管理等。本文通过在Ubuntu系统下对hello.c程序的深入研究,得以把计算机系统整个的体系串联在一起,回顾了学过的知识点,加深了对计算机系统的理解。

关键词:Hello程序;计算机系统;程序生命周期;计算机底层原理     

目  录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间

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

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

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

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

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

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

7.9动态存储分配管理

7.10本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献


第1章 概述

1.1 Hello简介

1.1.1  P2P过程

1.编辑:Hello以程序员的手指在键盘上的敲击,将自己以C语言的形式存储在hello.c文件中。

2.预处理:通过预处理器,处理hello.c文件,解析宏定义和条件编译等预处理指令,生成经过预处理的源代码。

3.编译:编译器将预处理后的源代码转换为汇编语言。

4.汇编:汇编器将汇编语言转换为机器可执行的二进制指令。

5.链接:连接器将汇编生成的可执行文件与所需的库文件链接在一起,生成最终的可执行文件。

6.进程创建:通过操作系统的进程管理,操作系统为Hello创建了进程,使其在系统中成为一个独立的执行实体。

1.1.2  O2O过程

1.编辑器:Hello作为编辑器中的文本内容存在,等待程序员的操作。

2.编译器:Hello被编译器解析、转换、优化,并生成可执行文件。

3.汇编器:Hello的指令被转换成机器语言。

4.链接器:Hello的可执行文件被连接成完整的程序。

5.操作系统:操作系统管理着Hello的运行环境,包括进程管理、存储管理、IO管理等。

6.CPU/RAM/IO等硬件:Hello在计算机硬件上执行,CPU执行指令,RAM存储数据,IO设备处理输入输出。

在这两个过程中,Hello经历了从程序到进程的转变,以及从零到整个系统的运行过程。通过操作系统和计算机系统的支持,Hello得以在计算机中运行、表演、完成任务,并最终被系统收尸,完成了自己的生命周期。

窗体顶端

窗体底端

1.2 环境与工具

硬件:12th Gen Intel(R) Core(TM) i7-12700H CPU @ 2.70GHz

   NVIDA GeForce RTX 3060 Laptop GPU

   32GB RAM

   1T 512GB SSD

软件:Windows11 23H2

Ubuntu 20.04.4 LTS 64位

调试工具:Visual Studio Code 64-bit;

gedit,gcc,notepad++,readelf, objdump, hexedit, edb

1.3 中间结果

表格 1 中间结果

文件名

功能

hello.i

预处理后得到的文本文件

hello.s

编译后得到的汇编语言文件

hello.o

汇编后得到的可重定位目标文件

hello.elf

用readelf读取hello.o得到的ELF格式信息

hello.asm

反汇编hello.o得到的反汇编文件

hello2.elf

由hello可执行文件生成的.elf文件

hello2.asm

反汇编hello可执行文件得到的反汇编文件

1.4 本章小结

本章简要介绍了hello 的P2P,020的具体含义,同时列出了论文研究时采用的具体软硬件环境和中间结果。

(第1章0.5分)


第2章 预处理

2.1 预处理的概念与作用

程序预处理是指在编译或解释源代码之前对其进行的一系列处理步骤。其主要目的是为了在实际编译或解释阶段之前对源代码进行一些必要的转换、修改或准备工作,以便于后续的编译或解释过程能够顺利进行。预处理器可以识别源代码中的宏,并将其替换为相应的文本。这使得程序员可以使用宏来定义常量、函数或代码片段,从而提高代码的可读性和灵活性。预处理器还可以根据条件指令来选择性地包含或排除源代码的部分内容。这使得程序员可以根据不同的编译选项或平台条件来编写灵活的代码,以实现跨平台或不同版本的兼容性。预处理器可以处理源代码中的头文件包含指令,将头文件的内容插入到源文件中。这样可以将程序分割成多个模块,提高代码的组织性和可维护性。

2.2在Ubuntu下预处理的命令

打开终端,输入命令cpp hello.c > hello.i

程序人生-Hello’s P2P

图1 预处理过程

2.3 Hello的预处理结果解析

打开预处理结果hello.i文件,结果被扩展为3000余行,行数大幅度增加。

程序人生-Hello’s P2P

图2 预处理部分结果

预处理器会处理#include指令,将指定的头文件内容包含到当前文件中。这样做的目的是为了将代码模块化,提高代码的可维护性和重用性,还会删除多余的空行和空格,以减小生成的预处理文件的大小。

2.4 本章小结

本章主要介绍了预处理的概念及作用、在Ubuntu系统下预处理hello.c文件并且对结果进行分析。

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

编译过程的概念和作用可以简要概括如下:

1.词法分析: 编译器首先会对.i文件进行词法分析,也称为扫描或标记化。在这个阶段,编译器会将源代码分割成一个个词法单元,比如关键字、标识符、操作符等。

2.语法分析: 接着,编译器会对词法单元进行语法分析,也称为解析(Parsing)。在这个阶段,编译器会根据编程语言的语法规则构建语法树或者其他中间表示形式。

3.语义分析: 在语法分析后,编译器会进行语义分析,检查代码是否符合语言规范的语义要求,比如类型检查、变量声明等。

4.优化: 接下来是编译器的优化阶段,编译器会尝试对中间表示的代码进行优化,以提高程序的性能或者减小生成的目标代码的大小。

5.代码生成: 最后一步是将优化后的中间表示形式翻译成目标代码,对于C语言而言,就是将中间表示形式翻译成汇编语言代码,生成.s文件。

整个编译过程的目的是将高级语言(如C语言)编写的代码转换成计算机能够执行的机器代码。编译过程中的各个阶段都有其特定的功能和作用,通过这些阶段的处理,可以保证最终生成的目标代码能够正确地执行所要求的功能,并且在性能和资源利用上达到一定的优化水平。

        

3.2 在Ubuntu下编译的命令

打开终端,输入命令 gcc -S hello.i -o hello.s

程序人生-Hello’s P2P

图3 编译过程

3.3 Hello的编译结果解析

3.3.1 数据与赋值

(1)常量数据

  1.printf字符串被保存在.rodata段

·源程序代码:

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

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

·汇编代码:

.section .rodata

.align 8

.LC0:

.string "\347\224\250\346\263\225:Hello \345\255\246\345\217\267 \345\247\223\345\220\215\346\211\213\346\234\272\345\217\267 \347\247\222\346\225\260\357\274\201"(第一个字符串)

.LC1:

.string "Hello %s %s %s\n"(第二个字符串)

  1. if判断值与for循环终止条件值在.text段

·源程序及对应汇编代码如下

if(argc!=5)  cmpl $5, -20(%rbp)

for(i=0;i hello.asm得到反汇编文件hello.asm。

程序人生-Hello’s P2P

图23 重定位过程

不同之处:

  1. hello.o的反汇编中只含有.text节,而hello的反汇编中还有.init,.plt,.plt.sec。
  2. 在hello中链接加入了exit、printf、sleep、getchar等在hello.c中用到的库函数。
  3. hello中不再存在hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。

程序人生-Hello’s P2P

图24 链接后的函数

5.6 hello的执行流程

用EDB打开hello。

根据symbol说明程序执行的函数以及其地址。

函数名

函数地址

.init

0x401000

.plt

0x401020

puts@plt

0x401030

printf@plt

0x401040

getchar@plt

0x401050

atoi@plt

0x401060

exit@plt

0x401070

sleep@plt

0x401080

_start

0x4010f0

main

0x401125

.fini

0x401248

程序人生-Hello’s P2P

图25查看调用程序界面

5.7 Hello的动态链接分析

   动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,在GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数,在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址,所以分析动态连接需要检查.got.plt的情况。

调用dl_init之前.got.plt段的内容:

程序人生-Hello’s P2P

图26 调用之前内容

调用dl_init之后.got.plt段的内容:

程序人生-Hello’s P2P

图27 调用之后内容

比较两张图可知GOT[1]和GOT[2]之间发生了变化,查询相关内容可知GOT[1]保存的是指向已经加载的共享库的链表地址。GOT[2]是动态链接器在ld-linux.so模块中的入口。这样,接下来执行程序的过程中,就可以使用过程链接表PLT和全局偏移量表GOT进行动态链接。

5.8 本章小结

本章首先介绍了链接的概念与作用,然后对文件进行链接,并对可执行文件hello的格式进行了分析,然后查看了hello的虚拟地址,并对链接的重定位过程进行了分析,概述了hello的执行流程,并对hello的动态链接进行了分析。

(第5章1分)


第6章 hello进程管理

6.1 进程的概念与作用

进程是程序执行的一个实例。一个程序在运行时会被操作系统加载到内存中,并分配一个独立的执行环境,这个执行环境就是一个进程。进程是操作系统进行资源分配和调度的基本单位,它具有以下特点和作用:

独立性: 每个进程都拥有独立的地址空间,使得它们彼此之间不会相互干扰。进程之间通常是隔离的,一个进程的崩溃不会影响其他进程的正常运行。

并发执行: 操作系统可以同时运行多个进程,每个进程都在自己的执行环境中独立执行。这种并发执行的方式使得计算机系统可以更有效地利用多核处理器和其他硬件资源。

资源分配: 操作系统为每个进程分配了一定的系统资源,包括内存空间、CPU时间、文件描述符等。进程可以通过操作系统提供的接口来请求和释放这些资源。

调度和管理: 操作系统负责对进程进行调度和管理,以确保系统资源被合理地分配和利用。这包括进程的创建、销毁、挂起、恢复以及切换等操作。

通信与同步: 进程之间可以通过各种机制进行通信和同步,包括共享内存、消息队列、管道、信号量、锁等。这使得不同进程之间可以进行数据交换和协作,实现复杂的任务分解和并发处理。

程序执行环境: 每个进程都有自己的程序执行环境,包括代码、数据、堆栈、寄存器状态等。操作系统负责管理这些执行环境,并在需要时进行切换和调度。

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

壳是用户与操作系统内核之间的接口,它接收用户输入的命令并将其转换成操作系统内核能够理解和执行的指令。

壳的主要作用是将我们的指令翻译给OS内核,让内核来进行处理,并把处理的结果反馈给用户。(Windows下的壳程序就是图形化界面)shell的存在使得用户不会直接操作OS,保证了OS的安全性。

壳的处理流程通常包括以下步骤: 壳从标准输入(通常是终端)读取用户输入的命令,对用户输入的命令进行解析,分析命令的结构和含义。根据解析后的命令调用相应的系统程序或应用程序进行执行。如果是内建命令(如cd、echo等),则直接在壳内部执行。根据命令中的I/O重定向符号(如、|等)对输入输出进行重定向处理。 如果命令中包含管道符号(|),壳将多个命令连接起来,形成管道,将前一个命令的输出作为后一个命令的输入。检测并处理命令执行过程中可能出现的错误,并将错误信息输出给用户。等待用户输入命令并执行,直到用户退出。

6.3 Hello的fork进程创建过程

输入命令后,shell会判断该命令不是内部指令,转而通过fork函数创建一个子进程hello。hello会得到一份包括数据段、代码、共享库、堆、用户栈等均与父进程相同且独立的副本。同时子进程还会获得与父进程打开任何文件描述符相同的副本,这表示当父进程调用fork时子进程可以读写父进程的内容。父进程和子进程只有PID不同,在父进程中,fork返回子进程的PID,在子进程中,fork返回0.

6.4 Hello的execve过程

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

6.5 Hello的进程执行

在程序运行时,Shell为hello fork了一个子进程,这个子进程与Shell有独立的逻辑控制流。在hello的运行过程中,若hello进程不被抢占,则正常执行;若被抢占,则进入内核模式,进行上下文切换,转入用户模式,调度其他进程。直到当hello调用sleep函数时,为了最大化利用处理器资源,sleep函数会向内核发送请求将hello挂起,并进行上下文切换,进入内核模式切换到其他进程,切换回用户模式运行抢占的进程。与此同时,将 hello 进程从运行队列加入等待队列,由用户模式变成内核模式,并开始计时。当计时结束时,sleep函数返回,触发一个中断,使得hello进程重新被调度,将其从等待队列中移出,并内核模式转为用户模式。此时 hello 进程就可以继续执行其逻辑控制流。

6.6 hello的异常与信号处理

(1)在程序正常运行时,打印十次提示信息,以输入回车为标志结束程序,并且回收进程。

程序人生-Hello’s P2P

图28 程序正常执行结果

(2)在程序运行时按回车,会多打印几处空行,程序可以正常结束,结束后也会多几行空指令。

程序人生-Hello’s P2P

图29 输入回车的情况

(3)按下ctrlc,Shell进程收到SIGINT信号^C,结束并回收hello进程。

程序人生-Hello’s P2P

图30 输入ctrl+c的情况

(4)按下ctrlz,Shell进程收到SIGSTP信号^Z,Shell显示屏幕提示信息[1]+  已停止,并挂起hello进程。

程序人生-Hello’s P2P

图31 输入ctrl+z的情况

  1. 对hello进程的挂起可由ps和jobs命令查看,可以发现hello进程确实被挂起而非被回收,且其job代号为1。

程序人生-Hello’s P2P

图32 ps与jobs指令

  1. Shell中输入pstree指令查看进程树

程序人生-Hello’s P2P

图33 pstree指令

6.7本章小结

本章主要介绍了进程的概念与作用,以及Shell-bash的基本概念。针对进程根据hello可执行文件中的具体情况分析了fork,execve函数的原理与执行过程,并且进行了hello程序带着各种参数情况下进行的各种结果。

(第6章1分)


第7章 hello的存储管理

7.1 hello的存储器地址空间

  1. 逻辑地址

逻辑地址是指由程序产生的与段相关的偏移地址部分,逻辑地址由选择符和偏移量两部分组成。具体而言,其为hello.asm中的相对偏移地址。

  1. 线性地址

逻辑地址经过段机制转化后为线性地址,其为处理器可寻址空间的地址,用于描述程序分页信息的地址。具体以hello而言,线性地址标志着 hello 应在内存上哪些具体数据块上运行。

  1. 虚拟地址

根据CSAPP教材,虚拟地址即为上述线性地址。

  1. 物理地址

CPU通过地址总线的寻址,找到真实的物理内存对应地址。

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

为了运用所有的内存空间,Intel 8086设定了四个段寄存器,专门用来保存段地址:CS(Code Segment):代码段寄存器;DS(Data Segment):数据段寄存器;SS(Stack Segment):堆栈段寄存器;ES(Extra Segment):附加段寄存器。

当一个程序要执行时,就要决定程序代码、数据和堆栈各要用到内存的哪些位置,通过设定段寄存器CS,DS,SS来指向这些起始位置。通常是将DS固定,而根据需要修改CS。所以,程序可以在可寻址空间小于64K的情况下被写成任意大小。所以,程序和其数据组合起来的大小,限制在DS所指的64K内,这就是COM文件不得大于64K的原因。

段寄存器是因为对内存的分段管理而设置的。

计算机需要对内存分段,以分配给不同的程序使用(类似于硬盘分页)。在描述内存分段时,需要有如下段的信息:1.段的大小;2.段的起始地址;3.段的管理属性(禁止写入/禁止执行/系统专用等)。

保护模式(如今大多数机器已经不再支持):

段寄存器的唯一目的是存放段选择符,其前13位是一个索引号,后面3位包含一些硬件细节(还有一些隐藏位,此处略)。

寻址方式为:以段选择符作为下标,到GDT/LDT表(全局段描述符表(GDT)和局部段描述符表(LDT))中查到段地址,段地址+偏移地址=线性地址。

实模式:

段寄存器含有段值,访问存储器形成物理地址时,处理器引用相应的某个段寄存器并将其值乘以16,形成20位的段基地址,段基地址·段偏移量=线性地址。

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

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

通过7.2节中的段式管理过程,可以得到了线性地址/虚拟地址,记为VA。虚拟地址可被分为两个部分:VPN(虚拟页号)和VPO(虚拟页偏移量),根据计算机系统的特性可以确定VPN与VPO的具体位数,由于虚拟内存与物理内存的页大小相同,因此VPO与PPO(物理页偏移量)一致。而PPN(物理页号)则需通过访问页表中的页表条目(PTE)获取,如下图所示。

程序人生-Hello’s P2P

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

若PTE的有效位为1,则发生页命中,可以直接获取到物理页号PPN,PPN与PPO共同组成物理地址。

若PTE的有效位为0,说明对应虚拟页没有缓存到物理内存中,产生缺页故障,调用操作系统的内核的缺页处理程序,确定牺牲页,并调入新的页面。再返回到原来的进程,再次调用导致缺页的指令。此时发生页命中,获取到PPN,与PPO共同组成物理地址。

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

针对Intel Core i7 CPU研究VA到PA的变换。

Intel Core i7 CPU的基本参数如下:

  1. 虚拟地址空间48位(n=48)
  2. 物理地址空间52位(m=52)
  3. TLB四路十六组相连
  4. L1,L2,L3块大小为64字节
  5. L1,L2八路组相连
  6. L3十六路组相连
  7. 页表大小4KB(P=4x1024=2^12),四级页表,页表条目(PTE)大小8字节

由上述信息可以得知,VPO与PPO有p=12位,故VPN为36位,PPN为40位。单个页表大小4KB,PTE大小8字节,则单个页表有512个页表条目,需要9位二进制进行索引,而四级页表则需要36位二进制进行索引,对应着36位的VPN。TLB有16组,故TLBI有t=4位,TLBT有36-4=32位。

程序人生-Hello’s P2P

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

如图所示, CPU产生虚拟地址VA,并将其传送至MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)在TLB中进行匹配,若命中,则得到PPN(40bit)与VPO(12bit)组合成物理地址PA(52bit)。若TLB没有命中,则MMU向页表中查询,由CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,则执行下一步确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并向TLB中添加条目。多级页表的工作原理展示如下:

程序人生-Hello’s P2P

s

若查询PTE的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。

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

因为三级Cache的工作原理基本相同,所以在这里以L1 Cache为例,介绍三级Cache支持下的物理内存访问。

L1 Cache的基本参数如下:

  1. 8路64组相连
  2. 块大小64字节

由L1 Cache的基本参数,可以分析知:

块大小64字节→需要6位二进制索引→块偏移6位

共64组→需要6位二进制索引→组索引6位

余下标记位→需要PPN+PPO-6-6=40位

故L1 Cache可被划分如下(从左到右):

CT(40bit)CI(6bit)CO(6bit)

在7.4中我们已经由虚拟地址VA转换得到了物理地址PA,首先使用CI进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO取出相应的数据后返回。

若没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中请求数据(请求顺序为L2 Cache→L3 Cache→主存,若仍不命中才继续向下一级请求)。查询到数据之后,需要对数据进行读入,一种简单的放置策略如下:若映射到的组内有空闲块,则直接放置在空闲块中,若当前组内没有空闲块,则产生冲突(evict),采用LFU策略进行替换。

7.6 hello进程fork时的内存映射

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

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

7.7 hello进程execve时的内存映射

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

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

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

  1. 映射私有区域

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

  1. 映射共享区域

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

  1. 设置程序计数器

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

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

在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页。缺页故障属于异常类别中的故障,是潜在可恢复的错误。

缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,如果牺牲页已经被修改了,内核会将其复制回磁盘。随后内核从磁盘复制引发缺页异常的页面至内存,更新对应的页表项指向这个页面,随后返回。

缺页异常处理程序返回后,内核会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件,此次页面会命中。

7.9动态存储分配管理

动态内存管理的基本方法与策略介绍如下:

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

具体而言,分配器分为两种基本风格:显式分配器、隐式分配器。

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

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

下面介绍动态存储分配管理中较为重要的概念:

  1. 隐式链表

堆中的空闲块通过头部中的大小字段隐含地连接,分配器通过遍历堆中所有的块,从而间接遍历整个空闲块的集合。

程序人生-Hello’s P2P

图36 隐式链表的结构

  1. 显式链表

在每个空闲块中,都包含一个前驱(pred)与后继(succ)指针,从而减少了搜索与适配的时间。

程序人生-Hello’s P2P

图37 显式链表的结构

  1. 带边界标记的合并

采取使用边界标记的堆块的格式,在堆块的末尾为其添加一个脚部,其为头部的副本。添加脚部之后,分配器就可以通过检查前面一个块的脚部,判断前面一个块的起始位置和状态。从而实现快速合并,减小性能消耗。

  1. 分离存储

维护多个空闲链表,其中,每个链表的块具有相同的大小。将所有可能的块大小分成一些等价类,从而进行分离存储。

7.10本章小结

本章主要进行了hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理的介绍。

(第7章 2分)


第8章 hello的IO管理

8.1 Linux的IO设备管理方法

1.设备的模型化——文件

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件。

例如:/dev/sda2文件是用户磁盘分区,/dev/tty2文件是终端。

2.设备管理——Unix IO接口

将设备模型化为文件的方式允许Linux内核引入一个简单、低级的应用接口,称为Unix IO,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

8.2.1Unix I/O接口:

  1. 打开文件

一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。对于Shell创建的每个进程,其都有三个打开的文件:标准输入,标准输出,标准错误。

  1. 改变当前的文件位置

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

  1. 读写文件

一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

  1. 关闭文件

内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

8.2.2Unix I/O函数:

  1. int open(char* filename,int flags,mode_t mode)

进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。

  1. int close(fd)

fd是需要关闭的文件的描述符,close返回操作结果。

  1. ssize_t read(int fd,void *buf,size_t n)

read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。

  1. ssize_t wirte(int fd,const void *buf,size_t n)

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

8.3 printf的实现分析

  1. printf函数体:

int printf(const char *fmt, ...)

{

    int i;

    va_list arg = (va_list)((char *)(&fmt) + 4);

    i = vsprintf(buf, fmt, arg);

    write(buf, i);

    return i;

}

分析:printf函数调用了vsprintf函数,最后通过系统调用函数write进行输出;va_list是字符指针类型;((char *)(&fmt) + 4)表示...中的第一个参数。

  1. printf调用的vsprintf函数:

int vsprintf(char *buf, const char *fmt, va_list args)

{

    char *p;

    chartmp[256];

    va_listp_next_arg = args;

    for (p = buf; *fmt; fmt++)

    {

        if (*fmt != '%')

        {

            *p++ = *fmt;

            continue;

        }

        fmt++;

        switch (*fmt)

        {

        case 'x':

            itoa(tmp, *((int *)p_next_arg));

            strcpy(p, tmp);

            p_next_arg += 4;

            p += strlen(tmp);

            break;

        case 's':

            break;

        /* 这里应该还有一些对于

        其他格式输出的处理 */

        default:

            break;

        }

        return (p - buf);

    }

}

分析:vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出写入buf供系统调用write输出时使用。

  1. write系统调用:

write: 

mov eax, _NR_write

mov ebx, [esp + 4]

mov ecx, [esp + 8]

int INT_VECTOR_SYS_CALL

分析:这里通过几个寄存器进行传参,随后调用中断门int INT_VECTOR_SYS_CALL即通过系统来调用sys_call实现输出这一系统服务。

  1. sys_call部分:

sys_call:

     /* 

      * ecx中是要打印出的元素个数

      * ebx中的是要打印的buf字符数组中的第一个元素

      * 这个函数的功能就是不断的打印出字符,直到遇到:'\0'

      * [gs:edi]对应的是0x80000h:0采用直接写显存的方法显示字符串

      */

     xor si,si

     mov ah,0Fh

     mov al,[ebx+si]

     cmp al,'\0'

     je .end

     mov [gs:edi],ax

     inc si

    loop:

     sys_call

   

    .end:

     ret 

分析:通过逐个字符直接写至显存,输出格式化的字符串。 

  1. 输出部分:

字符显示驱动子程序实现从ASCII到字模库到显示vram(即显存,存储 每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过 信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回;getchar调用系统函数read,发送一个中断信号,内核抢占这个进程,用户输入字符串,键入回车后(字符串和回车都保存在缓冲区内),再次发送信号,内核重新调度这个进程,getchar从缓冲区读入字符。

8.5本章小结

本章主要介绍了Linux的IO设备管理方法和及其接口和函数,对printf函数和getchar函数的底层实现有了基本了解,了解了Unix IO在Linux系统中的应用

(第8章1分)

结论

hello程序的一生经历了如下过程:

  1. 预处理

将hello.c中include的所有外部的头文件头文件内容直接插入程序文本中,完成字符串的替换,方便后续处理;

  1. 编译

通过词法分析和语法分析,将合法指令翻译成等价汇编代码。通过编译过程,编译器将hello.i 翻译成汇编语言文件 hello.s;

  1. 汇编

将hello.s汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在hello.o 目标文件中;

  1. 链接

通过链接器,将hello的程序编码与动态链接库等收集整理成为一个单一文件,生成完全链接的可执行的目标文件hello;

  1. 加载运行

打开Shell,在其中键入 ./hello 1190200208 李旻翀,终端为其fork新建进程,并通过execve把代码和数据加载入虚拟内存空间,程序开始执行;

  1. 执行指令

在该进程被调度时,CPU为hello其分配时间片,在一个时间片中,hello享有CPU全部资源,PC寄存器一步一步地更新,CPU不断地取指,顺序执行自己的控制逻辑流;

  1. 访存

内存管理单元MMU将逻辑地址,一步步映射成物理地址,进而通过三级高速缓存系统访问物理内存/磁盘中的数据;

  1. 动态申请内存

printf 会调用malloc 向动态内存分配器申请堆中的内存;

  1. 信号处理

进程时刻等待着信号,如果运行途中键入ctr-c ctr-z 则调用shell 的信号处理函数分别进行停止、挂起等操作,对于其他信号也有相应的操作;

  1. 终止并被回收

Shell父进程等待并回收hello子进程,内核删除为hello进程创建的所有数据结构。

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


附件

文件名

作用

hello.i

hello.c预处理后得到的文本文件

hello.s

hello.i编译后得到的汇编代码

hello.o

hello.s汇编得到的可重定位目标文件

helloo.elf

readelf读取hello.o得到的文本

helloo.asm

objdump反汇编hello.o得到的反汇编文件

hello

hello.o链接后得到的可执行文件

hello.elf

readelf读取hello得到的文本

hello.asm

objdump反汇编hello得到的反汇编文件

(附件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.

[7] Randal E.Bryant, David O'Hallaron. 深入理解计算机系统[M]. 机械工业出版社.2018.4 

[8] Pianistx.printf 函数实现的深入剖析[EB/OL].2013[2021-6-9].

https://www.cnblogs.com/pianist/p/3315801.html. 

[9] 梦想之家xiao_chen.ELF文件头更详细结构[EB/OL].2017[2021-6-10].

https://blog.csdn.net/qq_32014215/article/details/76618649.

[10] Florian.printf背后的故事[EB/OL].2014[2021-6-10].

https://www.cnblogs.com/fanzhidongyzby/p/3519838.html.

[11] printf函数实现的深入剖析. 博客园. 

https://www.cnblogs.com/pianist/p/3315801.html

[12] read和write系统调用以及getchar的实现. CSDN博客.

read和write系统调用以及getchar的实现_getchar 和read-CSDN博客

[13] 深入理解计算机系统(原书第三版).机械工业出版社, 2016.

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

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]