计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 人工智能
学 号 2021113513
班 级 大二
学 生 杨鹏
指 导 教 师 郑贵滨
计算机科学与技术学院
2023年5月
本文章研究了hello.c这个小巧程序在Linux系统下的生命周期。从原始程序开始,依次进行了预处理、编译、汇编、链接生成可执行文件,进而在shell中创建进程,使用虚拟空间等直至结束程序,从而对hello.c文件的“一生”有了更详细的认识。通过对hello.c程序的深入研究,把本学期计算机系统课程所学知识梳理与回顾了一遍,加深了对计算机系统原理的了解。
关键词:计算机系统;hello的一生。
目 录
第1章 概述
(0.5分)
1.1 Hello简介
指hello.c文件从可执行程序(Program)变为运行时进程(Process)的过程。以hello.c为例,hello.c文件先经过预处理器cpp,生成hello.i文件,再经过编译器ccl生成hello.s汇编程序,然后经过汇编器as生成可重定位目标程序hello.o,最后通过链接器ld链接生成可执行文件hello。在Linux终端执行./hello命令,运行该可执行文件(Process)。
1.2 环境与工具
1.2.1 硬件环境
CPU:11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz 2.30 GHz
RAM:16G
1.2.2 软件环境
Windows10 64位;Ubuntu 20.04 LTS
1.2.3 开发工具
Visual Studio 2021 64位;CodeBlocks 64位;gcc
1.3 中间结果
hello.i 预处理生成文件
hello.s 编译后的汇编语言文件
hello.o 可重定位目标文件
hello.elf hello.o的ELF格式可执行文件
hello 链接器生成的可执行文件
hello1.elf hello的ELF格式可执行文件
1.4 本章小结
本章对hello程序的p2p和o2o过程进行了简单介绍,同时列出了本次作业中所使用的软硬件及开发环境。此外,本章还展示了在hello程序执行过程中可能产生的各种中间文件,并解释了这些文件的作用。
第2章 预处理
(0.5分)
2.1 预处理的概念与作用
预处理是编译的第一个阶段,主要是在程序开始之前进行的一系列处理。预处理的主要目的是处理.c源文件中以#开头的指令,例如#define、#include等。这些指令在预处理期间会被解析和处理,最终形成.i类型的预处理生成文件。预处理的作用主要包括把#define的宏进行替换,将#include包含的文件插入到编译的源文件中等等。通过预处理,可以使得代码更加清晰、简洁,并且可以提高代码的重用性和可维护性。
2.2在Ubuntu下预处理的命令
预处理命令:gcc -E -o hello.i hello.c
经过预处理后生成了hello.i预处理产生文件。
2.3 Hello的预处理结果解析
原本去除注释不足二十行的hello.c居然被处理为了多达3860行的文件,究其原因我们发现该处理对源程序的宏定义进行了宏展开,而且把源程序中头文件的主体内容也涵盖了,包括大量的函数声明和少部分struct定义等,在文件的最后才是hello.c的真正内容,观看后发现几乎没有改动。
2.4 本章小结
本章介绍了预处理的概念及其作用,并且展示了在Linux系统下的操作实例,展示了hello.i文件的内容,发现预处理过程进行了大量的展开,扩充了许多内容。
第3章 编译
(2分)
3.1 编译的概念与作用
编译的概念:
把代码转化为汇编指令的过程,汇编指令只是CPU相关的,也就是说C代码和python代码,代码逻辑如果相同,编译完的结果其实是一样的。
编译的作用:将高级语言翻译为机器语言的过程:通常将其分为6步,包括扫描、词法分析、语义分析、源代码优化、代码生成、目标代码优化。编译完成后生成了汇编代码,然后编译器将汇编代码转化为机器码,即产生二进制文件。
3.2 在Ubuntu下编译的命令
编译命令:gcc -S hello.i -o hello.s
经过编译后产生了hello.s编译语言文件。
3.3 Hello的编译结果解析
3.3.1 初始部分
这一部分是在汇编文件.s中,对应hello.c中的main函数之前的初始部分。其中的.file指令用于声明源文件名为hello.c,而.text指令用于定义代码节。接下来是.section .rodata指令,代表只读数据,紧接着的.string指令定义了一个字符串,此后的.align指令指出对齐方式,这里设定为8字节对齐。最后,.globl表明main是一个全局变量,.type则说明main是一个函数类型。这部分的作用主要是对不同节的代码作出标记,包括只读数据和代码等,同时指定了各自的对齐方式和类型信息。这对于后续的汇编和链接过程非常重要,能够保证程序正常执行。3.3.2 数据部分
(1)字符串
hello.c中这两个字符串以字符串数组形式存储在只读存储区,并且第一个字符串中汉字是以UTF-8编码形式存储,超出了ASCLL码范围,因此表现为许多大的正数。(十六进制)
(2)局部变量
程序中局部变量只有i,i赋值为0,存放在-4(%rbp)的位置。
3.3.3 赋值操作
程序中赋值操作体现在for循环中i=0,需要用mov指令实现,i是int类型所以要用movl指令传递双字。
3.3.4 算术操作
程序中每次for循环结束后均需要i++,需要用add指令实现,i存储在-4(%rbp),每次执行该语句均会使i加一,由于i是32位所以使用addl指令。
3.3.5 关系操作
(1)条件语句
使用cmp指令比较立即数4和参数argc大小,如果相等则跳转到L2。(je指令)
(2)循环语句
使用cmp指令比较立即数4和参数i的大小,如果小于等于4则跳转到L4,这里是for循环中i<5的条件。
3.3.6 控制转移
同上述两个操作,先执行cmp判断条件,比较两个数的大小过后再根据跳转指令判断是否跳转。
3.3.7 函数操作
(1)main函数
传入参数argv、argc[],程序启动时便调用,设置返回值为0。(return 0)
在函数内部还调用了sleep、printf等函数。
(2)printf函数
传入参数argv[1]、argv[2],该函数在for循环中共被调用两次。
(3)exit函数
传入参数1,再调用函数。
(4)sleep函数
将参数从%eax传入到%edi中,再调用函数。
3.4 本章小结
本章简单说明了编译的含义和作用,并详细分析了hello.s文件中包含的汇编程序语句。在这些语句中,不同类型的汇编指令扮演了不同的角色,如实现各种计算机操作,包括关系运算、函数调用等等。
第4章 汇编
(2分)
4.1 汇编的概念与作用
汇编是指把汇编语言翻译成机器语言的过程。
汇编的作用:
汇编器as将hello.s翻译成机器语言指令,将这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在二进制目标文件hello.o中。
4.2 在Ubuntu下汇编的命令
汇编命令:gcc -c -o hello.o hello.s
4.3 可重定位目标elf格式
生成hello.o的elf文件的命令:readelf -a hello.o > hello.elf
(1)ELF头:
这个序列描述了系统信息、编码方式、系统字的大小和字节顺序等信息
(2)节头:
该序列描述了各节大小、名称、类型、地址、偏移量等信息。节头展示如下:
(3)重定位节:
链接器将目标文件和其他文件组合时,需要修改一些位置,例如调用外部函数指令就需要修改位置。需要重定位的内容展示如下:
(4)符号表:
该符号表存放着程序的定义、全局变量和函数的信息,但不包含局部变量的信息。符号表内容展示如下:
4.4 Hello.o的结果解析
生成hello.o的反汇编的命令:objdump -d -r hello.o
将其与hello.s对照发现有以下不同:
- 操作数进制
反汇编文件中所有操作数均为十六进制,而hello.s中操作数均为十进制。
- 分支转移
反汇编跳转指令的跳转位置表示为main函数+相对偏移量,而hello.s跳转位置都是模块。(例如.L3)
- 函数调用
反汇编文件中会调用函数的虚拟地址,而hello.s中会直接调用函数名称。
4.5 本章小结
该章节我们先阐述了汇编的概念和作用。之后以hello.o为例,详细分析得到的hello.o文件的内容。可看出汇编后产生了可重定位目标文件,这其中有可供计算机理解的机器代码,同时,之前很多不确定的函数调用和分支转移有了准确的地址偏移,以便于接下来链接器进行的链接过程。我们还对hello.o进行了反汇编产生了helloo.txt文件并与hello.s进行了比对,分析二者之间的区别和联系。
(1分)
5.1 链接的概念与作用
链接是一个将多个文件(主要是可重定位目标文件)组合在一起形成一个可执行文件的过程,可以在加载或运行时进行。这个过程主要是将各个目标文件进行地址修正,解决重定位问题,使得它们能够相互协调地运行。 在链接过程完成后,最终的可执行文件就能够在计算机上被加载到内存中并执行,如将hello.o可重定位目标文件链接成可执行文件hello。
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的格式
生成hello的ELF格式文件的命令:readelf -a hello > hello1.elf
打开查看文件内容:
(1)ELF头
ELF头描述了文件的总体格式,可以发现hello1.elf与hello.elf(hello.o的elf文件)的ELF头大致相同,区别在于hello的TYPE为REL,而hello1的TYPE变成了EXEC。
(2)节头
描述了各节的大小、偏移量等属性。
(3)程序头
程序头的主要作用是描述可执行文件如何映射到内存当中。
(4)段节
段节展示了hello各个节在段中的分布。
(5)动态节
(6)重定位节
重定位节包含偏移量、基址信息、链接器识别修改类型等信息。
(7)符号表
符号表包含hello的模块定义和引用符号的信息。
(7)版本信息
5.4 hello的虚拟地址空间
从edb打开hello后从窗口可以看到以下虚拟地址空间各段信息:
从输出信息可以看出,可执行文件hello被加载进虚拟地址空间起始地址为0x401000的位置。此外,根据5.3节中提到的节头部表,我们可以找到各个节在虚拟地址空间中对应的位置。例如,.interp节的虚拟地址空间起始位置为0x401000,在使用edb等反汇编工具时,我们也可以找到这个节在可执行文件中的具体位置,并查看其所包含的信息。
5.5 链接的重定位过程分析
对hello进行反汇编的命令: objdump -d -r hello
hello的反汇编内容如下:
hello与hello.o的不同如下:
- 新增函数
链接加入了一些hello.c中的库函数,比如printf,getchar等。
(2)地址改变
hello.o的地址是从0x000000开始的,而hello的地址是从0x401000(init的起始地址)开始的,这是经过重定位之后的虚拟地址。并且main函数中指令后的地址由之前的相对地址变成了实际地址。
重定位过程:
- 重定位节和符号定义
这一过程中链接器会将hello.o中所有类型相同的节合并成同一类型的聚合节,使程序中的每条指令都有唯一的运行时的内存地址。
- 重定位节中的符号引用
这一过程中链接器将修改代码节和数据节中符号的引用,将其指向正确的地址。
程序名 | 程序地址 |
init | 0x401000 |
start | 0x4010f0 |
main | 0x401125 |
printf | 0x4010a0 |
sleep | 0x4010e0 |
getchar | 0x4010b0 |
exit | 0x4010d0 |
5.7 Hello的动态链接分析
动态链接就是将程序按照模块拆分成各个相对独立的部分,在程序运行时再将其链接为一个完整的程序。这一机制是通过PLT表和GOT表实现的,根据hello1.elf可知GOT的起始位置为0x404000。
调用dl_init之前0x404008的后16个字节全部为0:
调用了dl_init之后字节发生了改变:
5.8 本章小结
本章主要介绍了链接的概念及其在程序开发中的作用。我们通过链接命令成功生成了可执行文件hello,接着使用分析工具查看了hello1.elf中各项内容的信息,以及hello在虚拟地址空间中的使用情况。在此基础上,我们深入分析了重定位过程、hello程序的执行流程以及动态链接的实现原理。
通过本章的学习,我们不仅掌握了如何使用链接命令生成可执行文件,还能够更好地理解程序连接和调试的相关技术。同时,通过对可执行文件中各个节的分析和对动态链接实现原理的探讨,我们也能更深入地理解可执行文件的组成结构及其在计算机系统中的运行机制。
第6章 hello进程管理
(1分)
6.1 进程的概念与作用
狭义定义:进程就是一段程序的执行过程。 广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
进程的作用:
进程是对正在运行的程序过程的抽象,进程可以抽象地提供给应用程序一个独立的逻辑控制流和一个私有的地址空间。它可以清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序。
6.2 简述壳Shell-bash的作用与处理流程
Shell是交互级的应用程序,它提供给用户一个命令行界面,可以读取用户的命令并执行命令,从而实现用户与计算机的交互。bash是Shell的一种实现,它被广泛应用于Linux和其他类Unix系统中。bash的处理流程是:从终端读取命令,对用户输入的命令进行解析,如果是内置命令就可以直接执行,否则要创建子进程,如果是前台程序就等待程序结束,后台程序就要将其放到后台并返回,并且Shell仍然可以执行命令。最终程序执行完毕后,Shell会回收子进程。
6.3 Hello的fork进程创建过程
用户在Shell界面输入./hello指令后,父进程调用fork函数创建一个新的子进程。该子进程与父进程几乎完全相同,他们有相同的代码段、栈段以及共享库,但他们拥有不同的PID。父进程中fork会返回子进程的PID,子进程中fork会返回0,可以通过返回值来区分程序在父进程中执行还是在子进程中执行。
6.4 Hello的execve过程
execve函数可以在当前进程下的上下文中加载并运行一个新程序。在hello执行execve时,系统会在当前进程的上下文中加载并运行可执行目标文件,带有参数列表和环境变量列表。在execve加载文件时会调用一个加载器,该加载器创建一组新的代码、数据、堆和栈段,并对其进行初始化,之后加载器会跳转到程序入口处设置用户,最后将控制权交给main函数。
6.5 Hello的进程执行
进程的正常进行依赖于上下文,上下文是操作系统重新启动一个被抢占进程所需的状态,这些状态包括存放在内存中的程序的代码、数据、栈、寄存器、所占用的资源等。在程序正常运行的时候,这些上下文状态绝不能被异常破坏。进程执行它的控制流的一部分的每一个时间段都称为时间片。Shell调用fork为hello创建子进程,然后使用execve函数运行它,开始hello运行在用户模式,在收到信号后进入内核,运行信号的处理程序后再回到用户模式。此时上下文切换,切分成时间片,并且也会切换到其他进程中,形成多个程序共用处理器,但给人一种每个进程独占处理器的感觉。
6.6 hello的异常与信号处理
原因 | 异步/同步 | 返回行为 | |
中断 | 来自I/O设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常 | 同步 | 总是返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
- 正常运行:
- 按下ctrl + z:
输入ctrl + z默认将前台任务挂起,但没有回收,仍然在后台运行,此时可用ps指令查看进程:
发现hello进程仍在执行。可用fg指令将其调回前台:
- 按下ctrl + c:
进程被终止。(使用ps指令发现后台没有hello进程)
(4)不停乱按:
不停乱按并不影响程序的输出。
(5)jobs命令(执行ctrl + z后):
(6)pstree命令(执行ctrl + z后):
(7)kill命令:
6.7本章小结
本章主要介绍了进程的概念和作用,展示了hello程序运行的流程机制,并对Shell的作用进行了详细介绍。我们还通过演示和分析了hello程序执行过程中的异常情况和运行结果,进一步深入了解了进程的运行机制。
在本章中,我们了解了进程作为操作系统对正在运行程序的抽象的概念及其在资源分配和管理程序状态方面的重要作用。同时,我们也学习了为什么Shell作为一个交互式程序对于用户操作非常重要。
通过本章的学习,我们深入掌握了进程在操作系统中的运行机制以及其与Shell之间的联系。同时,我们还了解了如何处理程序运行中的异常情况,这对于我们开发和调试程序非常有帮助。
第7章 hello的存储管理
( 2分)
7.1 hello的存储器地址空间
逻辑地址、线性地址、虚拟地址和物理地址是计算机系统中不同阶段所使用的不同地址形式,它们的含义如下:
逻辑地址:由程序产生的和段相关的偏移地址,格式为“段+偏移”。在内存管理中,逻辑地址需要经过地址翻译才能得到物理地址。
线性地址:位于虚拟地址和物理地址之间的中间层。CPU通过将逻辑地址中的段选择器和描述符中的信息组合在一起,可以计算出线性地址。在使用分页机制的系统中,线性地址需要通过页表转换为物理地址。
虚拟地址:程序所使用的逻辑地址。虚拟地址经过地址翻译后,才能得到物理地址,然后对内存中的实际数据进行访问。
物理地址:真实的内存地址,CPU可以直接将物理地址传送到与内存相连的地址信号线上,对内存中的实际数据进行访问。物理地址决定了数据在内存中真正存储的位置。在使用分页或分段机制的系统中,物理地址需要通过页表或描述符表转换得到。
这些地址形式在计算机系统中发挥着非常重要的作用,对于我们理解计算机系统的存储管理、内存地址翻译等方面的原理和实现都非常重要。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理是一种将程序划分为多个不同的段进行存储的管理方法。每个段都是一个独立的实体,具备单独的基地址、长度和属性等信息。在段式管理中,通常使用逻辑地址来访问内存中的数据,它由段选择器和段内偏移量两部分组成。其中,段选择器是一个16位长的字段,它的前13位用于定位全局或局部段描述符表中的一个具体描述符,描述了一个段的位置和大小等信息。
在段式管理中,全局描述符表 (Global Descriptor Table, GDT)用于存储全局的段描述符,而局部描述符表(Local Descriptor Table, LDT)则用于存储一些特定的局部描述符。这些描述符包含了关于段的基地址、长度、访问权限、特权级别等信息,操作系统可以通过这些信息来控制对段的访问。
通过段式管理,操作系统可以更加灵活、高效地管理内存。它允许不同的程序拥有不同的地址空间,因此相对于其他内存管理方案,例如基于页式管理的方案,段式管理更为灵活、多样化,能够满足更多的应用场景需要。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式内存管理是一种实现从虚拟地址到物理地址的映射的内存管理方式。在页式内存管理中,内存被划分为大小相等的页,每个页都有一个唯一的页号,而虚拟地址由页号和页内偏移量两部分组成。
页表是一种数据结构,用于记录每个页和物理地址之间的映射关系。它是一个页表条目(PTE)的数组,每个PTE包括标志位和物理地址等信息。操作系统通过操作页表,实现虚拟地址到物理地址的转换。转换过程由硬件内存管理单元(MMU)完成,它会根据页表中的信息,将虚拟地址转换为物理地址。
页式内存管理可以更加灵活地分配和管理内存,提高了内存的利用率。缺页中断处理也是页式内存管理的一个重要特点。当进程访问的内存页不存在于内存中时,会造成缺页中断,操作系统就会选择合适的方法将该页从磁盘中读入内存,并重新启用该页的页表。
下图展示了页式管理的示意图:
7.4 TLB与四级页表支持下的VA到PA的变换
当CPU要访问内存时,产生的虚拟地址需要通过内存管理单元(MMU)转换为物理地址。在转换的过程中,如果PTE在L1缓存中,则取数据的代价会降低一些,因此MMU中通常会设置一个小的缓存,称为快表(Translation Lookaside Buffer, TLB)。
快表中存储着最近使用的一些页表项,可以直接将虚拟地址映射为物理地址,从而加快访问速度。如果TLB中找不到对应的页表项,则需要访问页表来进行地址转换操作。
通常,虚拟地址被分为VPN(Virtual Page Number)和VPO(Virtual Page Offset)两部分。VPN的高位被分为TLBT和TLBI两部分,用于访问TLB。如果TLB中存在与VPN匹配的页表项,则可以直接找到物理页号(PPN)并获取相应的物理地址,完成地址转换。如果TLB中没有匹配的页表项,则需要从四级页表中逐级查找,找到对应的PPN,并将其转换为物理地址。
通过使用TLB和四级页表,操作系统可以实现虚拟地址到物理地址的转换,并为进程提供私有的地址空间,保障进程之间的内存独立性,提高了系统的安全和稳定性。
7.5 三级Cache支持下的物理内存访问
高速缓存存储器(Cache)是一种位于CPU寄存器和内存之间的高速存储设备,用于提高访问速度和效率。Cache分为L1、L2、L3三级,容量逐级递增。
当CPU发送一条虚拟地址时,MMU会将其转换为物理地址PA。物理地址PA被分为标记位、组索引和块偏移三部分。根据组索引CI,可以找到正确的组,然后比较该组中的每一行数据,如果有效位有效,且标记位一致,则表示命中Cache,可以直接返回数据。如果未命中,则需要在L2、L3等储存级别中进行类似的操作,直到找到对应的数据或最终确定数据不存在于Cache中。
如果命中Cache,需要同时更新各级Cache中储存的数据。如果不能在Cache中找到需要的数据,则需要访问内存。
通过使用高速缓存存储器,可以减少访问内存的次数,提高了系统的访问速度和效率。高速缓存存储器采用的是一种分级的缓存机制,使得存储元素的速度逐级递增,以满足CPU的高速读写需求。高速缓存存储器在计算机系统中发挥着重要的作用,可以有效地加速系统运行速度,并提升计算机系统的整体性能水平。
7.6 hello进程fork时的内存映射
在一个新进程调用fork函数时,操作系统内核会为这个新的子进程创建多种数据结构,并分配一个唯一的PID。此外,内核也会创建当前进程的内存映射结构体(mm_struct)及其所对应的虚拟内存区域结构(vm_area_struct)链表的副本,以及页表的快照,从而为新进程创建虚拟内存。在新进程中的每一个页面都会被标记为只读状态,以保证其不能修改已有的数据。同样,新进程中所有的虚拟内存区域都会被标记为可写的。
当fork在新的进程中返回时,这个进程的虚拟内存与调用fork函数时存在的虚拟内存完全相同。此时,这两个进程共享同一块物理内存,也就是说,这两个进程都可以访问相同的数据。但是,在该地址空间中的任意一个进程进行写入操作后,Copy-on-Write(写时复制)机制就会起作用,并为这个进程创建一个新的页面,将写入的数据存储在新页面中,而另一个进程仍然指向原来的页面。这样,操作系统可以实现进程之间的内存隔离,确保各个进程的内存空间互相独立,而又不会消耗过多的物理内存。
通过Copy-on-Write机制,操作系统可以避免在创建子进程时复制大量的内存数据,提高了进程的创建效率;同时,它也有效地降低了操作系统的系统资源开销,提高了系统的可靠性和稳定性。
7.7 hello进程execve时的内存映射
当execve函数加载并运行程序时,通常需要经历以下步骤:
1. 删除已存在的用户区域:首先,会删除当前进程用户部分已存在的区域,以便为新的程序让出足够的空间。
2. 映射私有区域:为新程序的代码创建新的区域结构,这些区域结构是私有的、写时复制的。这样,新程序的代码不会覆盖其他进程的内存空间,同时在需要修改时Copy-on-Write机制可以用于保护内存空间。
3. 映射共享区域:程序可能会与其它对象进行链接(例如,标准的C函数库),因此需要将这些共享对象映射到用户虚拟地址空间中的区域结构中。这样,多个进程可以共享同一块内存空间,避免重复消耗系统资源。
4. 设置程序计数器:最后,需要将程序计数器设置为指向新程序的代码区域入口,以开始执行程序的指令。这样,进程就可以开始执行新程序的代码了。
7.8 缺页故障与缺页中断处理
当DRAM缓存出现缺页时,操作系统需要进行缺页处理,主要步骤如下:
1. 检查虚拟地址是否合法:首先,操作系统会检查缺页的虚拟地址是否合法。如果地址非法,则会终止当前进程。
2. 检查进程权限:接着,操作系统会检查当前进程是否具有对内存数据的读写权限,如果没有,也会触发异常并终止当前进程。
3. 选择新页面:最后,操作系统会根据需要将一个物理页面调入内存,并更新页表以便能够访问到新页面的数据。如果内存空间已经满了,需要选择一个页面进行替换。通常情况下,操作系统会选择一个被修改过的页面,并将其进行交换出去,以便将新页面载入内存中,保证数据的一致性。
通过缺页处理程序的处理,操作系统能够在出现缺页时,动态地调整内存分配,并满足进程对内存数据的访问需求。操作系统会不断地对内存进行管理和维护,以确保整个系统能够高效的运行,并及时处理缓存不命中的情况。
7.9动态存储分配管理
动态存储分配管理是使用动态内存分配器(例如malloc函数)来完成的。动态内存分配器维护着进程的堆,它把堆视为一个由不同大小的块组成的集合,每个块是一个虚拟内存片段,可以是空闲的或已分配的。已分配的块供程序使用,而空闲的块则可以分配给程序以备将来使用。
动态内存分配器分为两种类型:显式分配器和隐式分配器。它们之间的区别在于由哪个实体来负责释放已分配的块。
隐式分配器会在块不再被调用时自动释放它。它把内存看作一个图,不可达的块就是无法被调用的块。
显式分配器会要求应用程序显式地释放已分配的块,例如C语言中的malloc函数。调用malloc函数会分配一个块,而调用free函数则会释放一个块。
动态内存分配器的作用是自动管理堆空间,即使在堆中经常进行分配和释放操作,也能保证程序不会出现内存泄漏或空间碎片化的问题,从而提高程序的稳定性和可靠性。
7.10本章小结
本章主要内容包括以下方面:
首先,介绍了hello进程的存储器地址空间,讨论了地址空间的组成以及不同地址空间对应的内存区域,包括代码段、数据段、堆和栈等区域。
接着,介绍了操作系统对内存的管理方式,包括段式管理、页式管理、TLB和四级页表支持下的虚拟地址到物理地址的转换过程,以及Cache的访问机制。
进一步分析了hello进程在fork时的内存映射和execve时的内存映射,包括内存结构体和虚拟内存区域等相关的数据结构,并讨论了这些操作的实现机制。
最后,对缺页故障处理和动态存储管理进行了介绍。缺页故障处理包括对虚拟地址是否合法、进程是否有权限访问和页面的选择等一系列步骤。而动态存储管理则使用动态内存分配器维护进程的虚拟内存区域堆,以避免出现内存泄漏或空间碎片化等问题。
结论
hello程序的执行分为以下几个步骤:
(1) 程序员编写hello.c源文件。
(2) hello.c经过预处理得到hello.i文件。
(3) hello.i经过编译得到一个包含汇编语言的文件hello.s。
(4) hello.s会被翻译得到可重定位目标文件hello.o。
(5) hello.o经过链接可以得到最终的可执行文件hello。
(6) 在shell中输入./hello命令运行hello。
(7) shell解析输入的命令,调用fork函数创建新的子进程。
(8) shell调用execve函数将程序载入内存并为其分配动态内存。
(9) CPU为进程分配时间片,hello将顺序执行自己的控制逻辑流。
(10) hello运行时会使用一个属于自己的虚拟地址空间。
(11) hello的输入输出与外界交互,与Linux I/O设备息息相关。
(12) hello执行完成后会变为僵死进程,最终被回收。
hello程序的执行经历了从源代码到可执行程序的编译、链接和载入过程,以及在进程运行时涉及的内存管理、进程调度、输入输出和进程状态管理等。在整个过程中,涉及了诸多计算机系统底层的细节和机制,需要理解并掌握这些底层知识,才能编写出高效、稳定、安全的程序。
通过对hello程序的分析,我们可以更深入地了解计算机系统的运行原理和内部结构,以及如何利用操作系统提供的API和函数来操纵计算机的硬件和软件资源。同时,我们也学会了如何使用动态内存管理、进程创建和管理、异常处理等技术来编写可靠的程序,并对计算机系统有了更深层次的理解。
总之,完成本次大作业让我们对计算机系统和底层原理有了更全面的认知和了解,也提升了我们的软件开发和计算机编程能力,为我们今后的学习和工作奠定了坚实的基础。
附件
hello.c hello源程序
hello.i 预处理生成文件
hello.s 编译后的汇编语言文件
hello.o 可重定位目标文件
hello.elf hello.o的ELF格式可执行文件
hello 链接器生成的可执行文件
hello1.elf hello的ELF格式可执行文件