计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 匿名
学 号 匿名
班 级 匿名
学 生 匿名
指 导 教 师 刘宏伟
计算机科学与技术学院
2023年5月
摘 要
本文从hello.c程序入手,深入分析了hello程序的执行过程中,从预处理到编译再到汇编、链接的编译器完成的过程,并对执行过程中的进程管理、存储管理以及IO管理进行探讨。本文对相关概念做出详细的解释,并对过程中的程序细节进行分析,将hello程序的一生完整地展示出来。
关键词:计算机系统;预处理;编译;汇编;链接;
目录
第1章 概述
1.1 Hello简介
P2P过程:即From Program to Process,从程序到进程。Hello程序是程序员编写的一个文本文件,经过预处理器的预处理阶段后,得到.i文件。之后通过编译器得到包含汇编语言程序的.s文件。再通过汇编器翻译成机器语言指令,并把这些指令打包成可重定位目标程序的格式,结果保存在.o文件中。最后通过链接器将若干文件链接,得到可执行目标文件,即Hello文件。在shell运行这个程序时,系统将这个程序加载至内存,由系统执行。
020过程:即From Zero-0 to Zero-0。开始时没有Hello程序的进程存在,经过P2P过程,得到ELF格式的可执行文件。shell在子进程中使用execve执行程序时,系统会把该程序加载到内存。进程结束时,父进程回收Hello,系统内核对内存进行清理,最后什么都没有留下。
1.2 环境与工具
硬件环境:X64 CPU;2.5GHz;16G RAM;512GHD Disk
软件环境:Windows 11 64位;VirtualBox/Vmware 11以上;Ubuntu 16.04
开发与调试工具:gcc; vim; edb; readelf; HexEdit
1.3 中间结果
文件名 | 文件作用 |
Hello.c | Hello程序的源代码 |
Hello.i | Hello.c经过预处理后的结果 |
Hello.s | Hello.i编译后生成的汇编程序 |
Hello.o | Hello.s汇编后生成的可重定位文件 |
Hello | Hello.o链接之后生成的可执行文件 |
a.out | 添加相关编译参数的可执行文件 |
1.4 本章小结
本章节简述了Hello的P2P过程和020过程,介绍了与Hello相关的软硬件环境及开发与调试工具,学习并了解了中间结果及其对应的作用,帮助我们理解程序的执行过程并发现潜在的问题,为后续编写高效优质的代码打下了坚实基础。
第2章 预处理
2.1 预处理的概念与作用
1.概念:
预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。预处理器(cpp)根据以#开头的命令,修改显示的C程序。
2.作用:
预处理会在编译前对源代码进行一些额外的处理和转换,以便在编译阶段能够更有效地进行编译和生成可执行代码。如通过宏展开简化代码的书写和重复使用,提高代码的可读性和可维护性;允许根据不同的条件编译不同的代码块;同时支持将其他源代码文件包含到当前的源文件中以便在编译时能正确引用和使用头文件中定义的函数等。
2.2在Ubuntu下预处理的命令
在Ubuntu下,预处理的命令通常由GNU C预处理器(cpp)执行。该预处理器是gcc编译套件的一部分,可以在终端中使用以下命令来进行预处理:
- cpp -E source.c -o output.c
`source.c`为源代码文件名,`output.c`为预处理生成的代码文件名。该命令将对源代码文件进行预处理,并将结果输出到指定的输出文件中。预处理器会执行宏展开、文件包含、条件编译等预处理操作,生成经过预处理的代码。
图2-1 预处理命令
图2-2 Hello.i代码内容
2.3 Hello的预处理结果解析
图2-3 源代码Hello.c的代码内容
对比两个文件的内容可知,预处理器完成了如下过程:首先,将stdio.h、unistd.h和stdlib.h中的内容读入到了hello.i中,然后将所有用#define定义的宏替换为了对应的值并且删除了注释内容。
2.4 本章小结
本章深入理解了预处理的概念与作用,学习并实践了Ubuntu下进行预处理的命令,并对Hello程序的预处理结果进行了分析,有助于我们理解程序运行中预处理阶段的工作过程,有利于今后编写代码时进行合理的优化和调试。
第3章 编译
3.1 编译的概念与作用
1.概念:
编译指的是程序从预处理文本文件(.i文件)经过编译器处理产生汇编程序(.s文件)的过程。
2.作用:
在编译阶段,编译器将源代码中的高级语言指令转化为等效的汇编语言指令,将高级语言的语法和语义转化为底层的操作。主要目的是将高级语言代码转换为更接近计算机硬件的汇编语言代码,为后续的汇编过程做准备。
3.2 在Ubuntu下编译的命令
- gcc -S Hello.i -o Hello.s
图3-1 编译命令
图3-2 Hello.s的代码内容
3.3 Hello的编译结果解析
1.数据类型:
(1)字符串常量:
(2)整型 int
局部变量:
立即数:
参数:
2.赋值操作:
3.类型转化:
将第四个参数放在%rdi,之后调用atoi函数,将字符串化为整型。
4.算术操作
也就是循环中的循环变量循环加1
5.关系操作
也就是比较argc != 4的汇编表达式
6.控制转移
当argc == 4 时,跳转到L2中
7.函数操作
调用printf函数,输出“用法: Hello 学号 姓名 秒数!\n”
3.4 本章小结
本章深入探讨了编译的概念与作用,学习了在Ubuntu下进行编译的命令,并对Hello程序的编译结果进行了解析,使我更加深入地理解了编译过程,并能够更好地掌握和应用编译技术来生成高效和可执行的代码。
第4章 汇编
4.1 汇编的概念与作用
1.概念:
汇编是将汇编语言程序文件(通常以.s扩展名表示)转换为机器语言二进制程序文件(通常以.o扩展名表示)的过程。
2.作用:
在汇编阶段,汇编器将汇编语言指令翻译为与特定硬件体系结构相关的机器语言指令,即计算机可以直接执行的机器语言表示形式,将程序的逻辑转化为底层的指令集。
4.2 在Ubuntu下汇编的命令
1. gcc Hello.s -c -o Hello.o
图4-1 汇编命令
图4-2 Hello.o的elf格式
4.3 可重定位目标elf格式
1. ELF头
图4-3 ELF头
分析:
ELF头的开始是一个16字节的魔数,其中前4个字节7f 45 4c 46描述了该文件是否满足ELF格式,第5个字节02表示该系统架构为x86-64,第6个字节01表示使用小端法,第7个字节01表示ELF的主版本号。
2.节头部表
图4-4 节头部表
分析:
程序的节头部表描述了每一个节的名称、大小、类型、地址、偏移量、旗标、链接、信息、对齐等信息。在节头部表中注明了本文件中没有节组、程序头和动态节。
3.重定位节
图4-5 重定位节
分析:
在类型部分,以PC32结尾的是静态链接的内容,而以PLT32结尾的是动态链接内容,最后有一个与重定位相关的异常处理单元.rela.eh_frame。
4. 符号表
图4-6 符号表
分析:
符号表中存放了所有引用的函数和全局变量的信息。
4.4 Hello.o的结果解析
图4-7 Hello.o的反汇编结果
经过对比,变化主要为以下三种:
一、条件分支变化
跳转指令由标签转变成了具体的相对偏移地址,可知标签在经过汇编过程时被删除,在实际机器指令中并不存在。
二、函数调用变化
在.s程序中,call后为函数名,而在反汇编程序中,call后为下一条指令的地址,因为该程序调用的函数是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址。对于这一类函数调用,call指令中的相对偏移量暂时编码为全0,然后在.rela.text节添加重定位条目,等待链接时的进一步确定。
三、数据访问变化
对于同一全局变量的访问,反汇编文件中需要通过链接时的重定位确定地址,而在.s文件中仍以.LC0占位符表示。
4.5 本章小结
本章深入研究了汇编的概念与作用,学习了在Ubuntu下进行汇编的命令,了解并分析了可重定位目标文件的elf格式,同时对Hello.o的反汇编结果进行了解析,以更好地理解汇编过程中的相关概念和技术,并能够更好地编写和调试汇编语言程序。
第5章 链接
5.1 链接的概念与作用
1.概念:
链接是将多个目标文件(.o文件)以及可能的库文件合并成一个可执行文件的过程。
2.作用:
(1)符号解析:链接器解析各个目标文件中引用的符号(函数、变量等),并将其与定义符号的位置进行关联,以便在程序运行时正确找到所引用的符号的实际位置。
(2)重定位:链接器将各个目标文件中的代码和数据段进行合并,并根据需要进行地址的重新定位,以确保程序中的所有内存引用都能正确映射到最终的内存位置。
(3)生成可执行文件:链接器最终生成一个可执行文件,其中包含了所有被链接目标文件的代码和数据。
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
图5-1 链接命令
5.3 可执行目标文件hello的格式
图5-2 可执行目标文件hello的elf格式
变化如下:
- ELF头:type从REL变为EXEC,节点数量变为27个
图5-3 ELF头
2.节头部表:
图5-4 节头部表
3.重定位表:
图5-5 重定位表
4.符号表:
图5-6 符号表
5.4 hello的虚拟地址空间
图5-7 hello的虚拟地址空间
一、用Memory Regions功能查看第一个部分:
图5-8 Memory Regions功能查看(1)
图5-9 Memory Regions功能查看(2)
由前16个字节可以看到,该部分即为ELF文件的头。
二、下一内存区域可读可执行,查看该区域可知:
图5-10 Memory Regions功能查看(3)
该部分为指令的装载地址。
三、第三个内存区域是只读数据域,查看这一部分:
图5-11 Memory Regions功能查看(4)
这一部分编码了代码中字符串常量等数据。
四、最后一部分是用户栈:
图5-12 Memory Regions功能查看(5)
5.5 链接的重定位过程分析
- objdump -d -r hello
变化如下:
- 含有汇编代码的段增多:
hello.o反汇编的代码只有.text段有汇编代码;
hello反汇编的代码除了.text段有汇编代码外,还有.init段、.plt段、.fini段。
- 增加了一些函数:
如main的入口函数_start、一些外部库函数。
- 进行重定位:
链接器把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使它们指向内存位置。
5.6 hello的执行流程
载入:_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的动态链接分析
调用共享库函数时,由于定义它的共享模块在运行时可以加载到任意位置,编译器无法预测函数的运行时地址,所以生成一条重定位记录,由动态链接器在程序加载时解析它。
由节头部表可知,.got.plt起始地址为0x404000。
图5-13 动态链接分析(1)
在调用前404008后的16个字节都是0,存放PLT中函数调用下一条指令:
图5-14 动态链接分析(2)
调用后变为7f5bc862d190、7f5bc8616bc0两个地址
图5-15 动态链接分析(3)
5.8 本章小结
本章回顾了链接的概念和作用,学习了在Ubuntu下进行链接的命令,深入分析了可执行目标文件hello的格式,更加深入地理解了可执行文件的结构和功能。此外,本章查看并了解了hello的虚拟地址空间,包括代码段、数据段、堆和栈等部分,也分析了链接的重定位过程、hello的动态链接过程,更加体会到链接过程的工作原理及重要作用。
第6章 hello进程管理
6.1 进程的概念与作用
1.概念:
进程是指在计算机系统中正在执行的程序的实例。
2.作用:
(1)资源分配:当一个程序被启动为一个进程时,操作系统为其分配必要的资源,如内存、处理器时间、文件和设备等。
(2)并发执行:操作系统可以同时运行多个进程,每个进程独立执行,使得计算机系统能够高效地利用资源,提高系统的吞吐量和响应能力。
(3)进程间通信:操作系统提供了进程间通信机制,使得进程能够相互交换数据和协调工作,有助于实现协作和数据共享,使多个进程能够协同完成复杂的任务。
6.2 简述壳Shell-bash的作用与处理流程
1.作用:壳Shell-bash是一个交互型应用级程序,代表用户运行其他程序,接收用户输入的命令并把它送入内核去执行。
2.处理流程:
(1)从终端读入命令行
(2)切分命令行字符串获得参数
(3)检查第一个命令行参数是否内置,是则执行,不是则fork创建子程序
(4)子程序中调用(2)获取参数,调用execve()执行
(5)前台作业等待作业终止后返回
(6)后台作业shell返回
6.3 Hello的fork进程创建过程
程序调用fork函数创建子进程,子进程得到与父进程用户级虚拟地址空间相同但彼此独立的一份副本,包括代码和数据段、堆、共享库以及用户栈。
在hello中,输入./hello 2021111933 LZS 1,运行程序发现hello不是内置的指令,分割后面字符串,作为参数。用fork创建出子程序,调用参数并执行,hello在子进程中执行至结束。
图6-1 hello的fork进程创建过程
6.4 Hello的execve过程
execve函数在新创建的子进程的上下文中加载并运行hello程序。execve函数的功能是加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有发生错误时execve才会返回到调用程序。所以,execve调用一次且从不返回。
6.5 Hello的进程执行
(1)逻辑控制流:
一系列程序计数器PC的值的序列叫做逻辑控制流,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。
(2)用户模式与内核模式:
处理器通过某个控制寄存器中的一个模式位来提供限制应用可执行的指令以及可访问的地址空间范围的功能。该寄存器描述了当前进程运行的权限,当设置了模式位时,进程就运行在内核模式中,否则,进程就运行在用户模式中。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存;而用户模式的进程不允许和执行特权指令、也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。
(3)上下文切换:
内核为每个进程维持一个上下文。上下文就是内核重新启动的一个进程所需的状态。它由一些对象的值组成,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈等。在进程执行的某些时刻,内核可以决定抢占当前进程,并开始一个先前被抢占的进程,这种决策就叫调度。在内核调度了一个新的进程运行后,它他就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。上下文切换的步骤:(1) 保存当前进程的上下文;(2) 恢复某个先前被抢占的进程被保存的上下文;(3) 将控制权传递给这个新恢复的进程。
(4)进程时间片:
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
6.6 hello的异常与信号处理
1.异常种类:
hello执行过程中出现的异常种类可能会有:中断、陷阱、故障、终止。
(1)中断:中断是来自处理器外部的I/O设备的信号的结果,硬件中断的异常处理程序被称为中断处理程序。
(2)陷阱:陷阱是执行一条指令的结果,是有意的异常。陷阱处理程序将控制返回到下一条指令。
(3)故障:故障由潜在可恢复的错误情况引起。当故障发生时,处理器将控制转移给故障处理程序,如果处理程序能够修正,则将控制返回到引起故障的指令,从而重新执行它,否则,处理程序返回到内核中的abort()终止引起故障的应用程序。
(4)终止:终止是由不可恢复的错误引起的。终止处理程序将控制返回给abort()终止该应用程序。
2.运行结果
hello正常执行的状态:
图6-2 程序正常执行
在hello运行时按下Ctrl + z:
图6-3 运行时按下Ctrl + z
进程将发送SIGTSTP信号,将该进程暂时挂起,运行ps可以看到该进程仍然存在:
图6-4 运行ps
运行jobs可以看到该进程暂时终止:
图6-5 运行jobs
运行fg可将该任务转为前台执行:
图6-6 运行fg
输入kill指令,将SIGKILL发送到对应进程,直接杀死该进程:
图6-7 运行kill指令
输入pstree,将所有进程以树的形式显示:
图6-8 运行pstree
在运行时输入Ctrl^c,进程将发送SIGINT信号,结束进程:
图6-8 运行Ctrl^c
6.7本章小结
本章复习了进程的概念与作用,壳(Shell)-bash的作用与处理流程,Hello进程的创建过程如fork和execve,Hello进程的执行过程及Hello进程的异常与信号处理,更加深入地理解了进程管理的工作原理及其重要作用。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1.线性地址:指虚拟地址到物理地址变换的中间层,是处理器可寻址的内存空间(称为线性地址空间)中的地址。程序代码会产生逻辑地址,或者说段中的偏移地址,加上相应段基址就成了一个线性地址。
2.虚拟地址:程序访问存储器所使用的逻辑地址就是虚拟地址。
3.物理地址:指内存中物理单元的集合,是地址转换的最终地址,进程在运行时执行指令和访问数据最后都要通过物理地址来存取主存。
4.逻辑地址:由程序产生的段内偏移地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段选择符由索引、TI和RPL组成。其中TI决定描述符表的选取,若TI为0,则选择全局描述符表GDT;TI为1,则选择局部描述符表LDT。RPL表示CPU当前的特权级,若RPL为00,则为最高级的内核态;若RPL为11,则为最低级的用户态。 索引部分为13位,它表示当前使用的段描述符在对应的描述符表中的位置。
逻辑地址到线性地址的转换:首先从段寄存器中取出段选择符,由段选择符来确定要使用的段描述符,然后将被选中的段描述符送到描述符cache中,从cache中取出32位段基址,与32位偏移量(有效地址)相加得到32位线性地址。
图7-1 逻辑地址转线性地址
7.3 Hello的线性地址到物理地址的变换-页式管理
页表是一个页表条目(PTE)的数组,虚拟地址空间的每个页在页表中一个固定偏移量处都有一个PTE。PTE是由一个有效位和一个n位地址字段组成的。有效位表明了该虚拟页当前是否被缓存在主存中,若有效位为1,那么地址字段就表示DRAM中对应的物理页的起始位置,这个物理页中缓存了该虚拟页;若有效位为0,如果地址字段为一个空地址,那么表示这个虚拟页没有被分配,如果地址字段指向该虚拟页在磁盘上的起始位置,就说明虚拟页被分配但未被缓存。
地址翻译是由CPU中的内存管理单元MMU实现的。CPU中的一个控制寄存器,页表基址寄存器指向页表的首地址虚拟地址由n位组成,对于一个大小为2^p的页面而言,虚拟地址可以分为p位的虚拟页偏移量(VPO)和(n-p)位的虚拟页号(VPN)。
地址翻译:首先给定一个要访问的虚拟地址,从虚拟地址中取出虚拟页号,MMU会利用VPN和页表基址寄存器来选择正确的PTE,若该PTE对应的虚拟页已被缓存到主存中,即可直接读出其地址字段;否则,需要缺页处理程序更新PTE,将其缓存入主存中,再读取其地址字段。然后将虚拟地址中的VPO当作PPO直接填写到PPN后即可得到一个完整的物理内存。
图7-2 页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
1.TLB
TLB是MMU中所包含的一个关于PTE的小缓存,称为翻译后备缓冲器。TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着由单个PTE组成的块,TLB通常有高度的相连度。TLB不命中引发了额外的内存访问,但是一般而言,由于局部性的存在,TLB不命中很少发生。
2.VA到PA的变换
CPU产生虚拟地址,传递给内存管理单元,使用前36位虚拟页号向TLB中匹配。这其中36位虚拟页号被分成四片,每片用作一个页表的偏移量。如果上述匹配命中,则直接将相应物理页号和虚拟偏移量串联得到物理地址,如果匹配失败,内存管理单元向页表查询,通过寄存器确定第一级页表的起始地址通过第一片虚拟页号确定出在第一级页表中的偏移量,查找到页表条目,如果在物理内存中,开始确定第二级页表,以此类推,在第四级页表中查找到物理页号,再与虚拟偏移量组合成物理地址。
图7-3 TLB与四级页表支持下的VA到PA的变换
7.5 三级Cache支持下的物理内存访问
在三级 Cache 支持下的物理内存访问,可以分为三级缓存、主存两个级别:
L1缓存:CPU 内部一个小型的高速缓存,用于存储 CPU 最常用的数据和指令,通常在 CPU 内部;L2缓存:一个更大的高速缓存,通常集成在 CPU 包内,尽可能贴近 CPU;L3缓存:各个 CPU 核心之间共享的高速缓存,通常位于 CPU 包的顶部或侧面,距离 CPU 芯片略有距离。主存:最慢的存储器,通常用于存储大量的数据和代码。
在程序执行过程中,CPU 会首先访问 L1 缓存,如果不命中,则会到 L2 缓存中查找,如果还未命中,则会到 L3 缓存查找,最终如果不能从 CPU 内部缓存或 L3 缓存中获取需要的数据,就会去主存中访问需要的数据。当程序访问物理内存时,如果在三级 Cache 中未能发现所需的数据,会触发缓存失效,并在较慢的主内存中访问所需的数据。当数据被放入缓存时,会优先放入 L1 缓存,然后才是L2和L3缓存。三级 Cache 提供了多级高速缓存,可以有效减少 CPU 对主存的访问次数,提高了内存访问的效率。
7.6 hello进程fork时的内存映射
进程hello在 fork() 时,操作系统会在新进程的地址空间中创建一个与父进程相同的副本,包括了父进程中所有已经加载到内存中的代码段、数据段和堆栈段。此时,子进程与父进程共享同样的物理空间,即它们将映射到相同的物理页面。子进程中会有一份与父进程相同的虚拟地址空间,但它们对应的物理页有可能不同。新进程所拥有的内存空间是由操作系统所分配并分配的,并且新进程可以根据需要重新解释这些数据结构。
图7-4 一个私有的写时复制对象
7.7 hello进程execve时的内存映射
execve 函数在shell中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域。删除shell虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。.bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障是指当程序访问的页面(页)不在当前内存中时,发生的一种异常情况。当程序引用一个尚未分配或者未在内存中的页面时,操作系统会触发缺页中断,并进行相应的缺页中断处理。
缺页中断处理:
当发生缺页故障时,操作系统会检测到这一异常情况,并触发缺页中断。缺页中断是一种硬件中断,用于通知操作系统当前程序发生了缺页故障,需要进行相应的处理。缺页中断处理包括以下步骤:
1. 中断处理程序的执行:当发生缺页中断时,操作系统会暂停当前进程的执行,并跳转到缺页中断处理程序的执行位置。这个处理程序位于操作系统内核中,负责处理缺页中断。
2. 缺页处理程序的执行:缺页中断处理程序会根据缺页的详细信息进行处理。这包括确定缺页的原因、查找所需的页面是否在辅存中等。
3. 页面调入:如果发现所需的页面不在内存中,操作系统会从辅存(如硬盘)中加载该页面到内存中,以满足程序的访问需求。这可能涉及页面置换算法,选择要替换的页面,腾出空间来存放所需的页面。
4. 更新页表:在页面调入后,操作系统会更新进程的页表,将该页面映射到正确的内存地址。这样,当程序再次访问该页面时,就能够直接在内存中找到它,而不会再触发缺页故障。
5. 恢复执行:缺页中断处理程序完成后,操作系统会将控制权返回给中断发生时的进程,并恢复其执行。这样,程序可以继续执行,访问所需的页面。
7.9动态存储分配管理
动态内存管理是指在程序运行过程中,根据程序的需要,动态地分配和释放内存空间。以下是动态内存管理的基本方法和策略的简述:
1. 基本方法:
动态内存分配:根据程序运行时的需求,通过特定的内存分配函数从操作系统的堆中申请一块连续的内存空间。
内存释放:在不再需要使用某块内存时,通过相应的内存释放函数将其归还给操作系统,以便重新分配给其他程序使用。
2. 策略:
首次适应算法:按照内存地址从低到高的顺序搜索空闲内存块,找到第一个满足大小要求的空闲块进行分配。
最佳适应算法:搜索所有空闲内存块,找到大小最接近需求的空闲块进行分配。目标是找到最小的足够容纳所需内存大小的空闲块。
最差适应算法:搜索所有空闲内存块,找到大小最大的空闲块进行分配。目标是为了留下较大的连续空闲块,以便后续分配较大的内存需求。
快速适应算法:将内存分割成不同大小的块,并使用不同的空闲链表管理每个大小的块。根据所需大小直接在相应的链表中搜索合适的块进行分配,提高查找效率。
分区算法:将可用内存划分为固定大小的分区,每个分区可以分配给一个进程。分区可以等大小,也可以根据需求进行动态调整。
内存回收与合并:释放内存时,可以尝试将相邻的空闲内存块合并成更大的块,以减少内存碎片化问题。
垃圾回收:自动检测和释放不再使用的内存,减轻程序员手动管理内存的负担。
7.10本章小结
本章复习了hello程序的存储器地址空间,学习了Intel架构下的地址变换机制、TLB和四级页表的支持、三级Cache对物理内存访问的支持、hello进程在fork和execve过程中的内存映射、缺页故障和缺页中断处理、动态存储分配管理,更加深入地理解程序在计算机系统中的存储管理机制和运行过程。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
在Linux中,IO设备管理方法主要基于以下两个核心概念:设备的模型化和设备管理。
1. 设备的模型化:Linux将IO设备抽象为文件,并使用文件系统来管理这些设备。这种模型化的概念称为"一切皆文件"。每个IO设备都被表示为文件以便可以通过标准的文件操作接口来进行设备的访问和控制。
2. 设备管理:Linux提供了设备驱动程序来管理各种IO设备。设备驱动程序是连接IO设备和操作系统的接口,负责处理设备的输入输出、设备的状态管理等任务。每个IO设备都有相应的设备驱动程序,它们以模块的形式存在于内核中,可以动态加载和卸载。设备驱动程序通过与设备进行通信,将设备的功能暴露给上层应用程序使用。
在Linux中,设备的管理和访问是通过文件操作接口来实现的。应用程序可以通过打开设备文件来获取对应设备的文件描述符,然后使用read、write等文件操作函数来进行数据的读取和写入。设备驱动程序负责解析这些文件操作,并将其转化为对设备的实际操作。
8.2 简述Unix IO接口及其函数
Unix IO接口是一组定义在 `<unistd.h>` 头文件中、用于进行输入和输出操作的函数,提供了对文件、设备和网络套接字的访问和操作。以下是一些常用的Unix IO函数:
1. `open()`:打开文件或创建新文件,并返回文件描述符。可以指定文件的名称、访问模式和权限等参数。
2. `close()`:关闭文件描述符,释放与文件相关的资源。
3. `read()`:从文件描述符中读取数据,并将数据存储到指定的缓冲区中。
4. `write()`:将数据从指定的缓冲区写入到文件描述符中。
5. `lseek()`:设置文件偏移量,用于定位文件中的读写位置。
6. `fcntl()`:用于对文件描述符进行各种控制操作,如修改文件状态标志、获取文件描述符状态等。
8.3 printf的实现分析
printf函数是C语言中常用的输出函数,用于将格式化的数据输出到标准输出设备或其他文件。Printf函数通过格式解析和参数处理将数据格式化为字符串,并通过内核调用write系统函数将数据输出到标准输出设备。内核使用字符显示驱动和显示设备操作将字符转换为最终的显示信号,实现在屏幕上显示输出的功能。
8.4 getchar的实现分析
getchar函数是C语言中常用的输入函数,用于从标准输入设备(通常是键盘)获取一个字符。getchar函数通过键盘中断处理程序将用户按下的键转换为ASCII码,并存储到键盘缓冲区中。当应用程序调用getchar函数时,会触发read系统函数的调用,从键盘缓冲区读取数据,直到接收到回车键才返回获取到的字符的ASCII码。这样,getchar函数实现了从标准输入设备获取字符的功能。
8.5本章小结
本章学习了hello的IO管理,包括Linux的设备管理方法、Unix IO接口及其函数、printf函数和getchar函数的实现分析等重要基础知识,帮助我们更加深入地理解和应用IO操作,为开发和编写程序提供了基础。
结论
从使用C语言编写hello.c程序以ASCII码的形式保存,到预处理器进行预处理,将hello.c文件补全生成hello.i文件,再到编译器将hello.i文件编译为包含汇编语言程序的hello.s文件,再到汇编器将hello.s文件中的汇编指令转化为计算机可以理解的机器指令,得到可重定位的hello.o文件,再到链接器进行符号解析和重定位,最终生成可执行的hello程序。
通过对hello程序的一生的分析,我认识到麻雀虽小五脏俱全,即使是一个再简单不过的程序,它的运行也包含了众多操作,需要软硬件配合、内核与操作系统协作。计算机系统的设计与实现蕴含了多年以来众多技术人员的经验与智慧,通过不断地完善发展,才有了现在比较完备的体系,我们应该继续深入学习《计算机系统》这一课程,继续完善计算机体系。
附件
hello | 链接得到的可执行文件 |
Hello.c | 源文件 |
Hello.i | 预处理后得到的文件 |
Hello.o | 汇编后得到的文件 |
Hello.s | 编译后得到的文件 |
Hello.elf | Hello.o的elf格式 |
Hello1.elf | Hello的elf格式 |
Hello_compare | 反汇编得到的文件 |
参考文献
[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] printf 函数实现的深入剖析. [转]printf 函数实现的深入剖析 - Pianistx - 博客园