计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机
计算机科学与技术学院
2021年5月
摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息。摘要应包括本论文的目的、主要内容、方法、成果及其理论与实际意义。摘要中不宜使用公式、结构式、图表和非公知公用的符号与术语,不标注引用文献编号,同时避免将摘要写成目录式的内容介绍。
hello的一生不简单,我将在8各方面为hello撰写一本生命之书,剖析它的伟大。将我们的视角转入底层,掠过预处理,编译,汇编,链接,回收,看看hello不为人知的一面,这将有助于我们深入理解计算机系统。
关键词:hello;预处理;编译;汇编;链接;管理
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
返回值:子进程中返回0,父进程中返回子进程的ID,出错返回-1
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:hello.c原本是一个文本类型的文件,经过cpp预处理,ccl编译,as汇编,ld的链接生成了可执行文件,在shell中执行命令,将文件中的代码和数据从磁盘复制到内存,调用fork()创建一个新的子进程,通过execve调用加载器,创建hello进程。
020:hello成为进程后,系统为进程划分时间片,使他不断与其他进程上下文切换,并发执行。在hello执行完毕后,被shell回收,内存被释放,hello不再存在。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:X64 CPU; 2.3GHz; 16G RAM; 256GHD Disk以上;
软件环境:Windows10;VirtualBox6.1;Ubuntu20.04;
开发工具:Visual Studio2022; vi/vim/gedit+gcc
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c 给定的.c文件,文本类型,用于预处理
hello.i 预处理后的文件,用于编译生成.s文件
hello.s 汇编代码文件,用于汇编生成.o文件
hello.o 可重定位目标文件,用于查看elf信息,链接生成hello可执行文件
hello 可执行文件,用于运行。
1.4 本章小结
本章概述hello的P2P与020,列出环境和工具,写出中间结果,各个中间文件的名称和作用。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。
作用:C语言提供了·多种预处理命令,除文件包含(用#include),宏定义(#define M 10)外,还包括一种称为条件编译的预处理命令。经过预处理程序对源程序的预处理命令进行处理后,程序中就不再包含预处理命令了。合理的使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
2.2在Ubuntu下预处理的命令
应截图,展示预处理过程!
图2.2.1预处理
2.3 Hello的预处理结果解析
用文本编辑器打开hello.i文件,一共有3060行,按照代码顺序,将头文件展开,前面3000多行都是头文件的相关内容,后面的十几行是main函数。
图2.3.1 hello.i中的main
2.4 本章小结
预处理的概念和作用,hello.c预处理的过程和结果分析
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
概念:编译器将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。
作用:把源程序的C语言代码翻译成汇编代码。
3.2 在Ubuntu下编译的命令
应截图,展示编译过程!
图3.2.1编译
3.3 Hello的编译结果解析
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.3.1数据
- 字符串常量"用法: Hello 学号 姓名 秒数!\n"和"Hello %s %s\n"
引用时给出相应地址。
图3.3.1字符串数据
- 局部变量i:循环的条件,movl赋值0给i存到%rbp中
图3.3.2数据整型i
3.3.2赋值和逗号
- 局部变量i的赋值,movl赋值0给i存到%rbp中
图3.3.3 i=0
- 逗号,一般被逗号分隔开的都是直接执行的,比如 printf("Hello %s %s\n",argv[1],argv[2]);中argv[1],argv[2]的逗号,如图:
图3.3.4逗号分隔符
3.3.3类型转换
sleep函数需要int型,argv[3]是字符串型,用atoi强转
图3.3.5 类型转换
3.3.4算数操作
i++
图3.3.6算术操作
3.3.5关系操作
- Argc !=4,与四相等比较
图3.3.7 不等于
- i <8,因为i是从0开始++的,让i小于8,就让他停在等于7的时候
图3.3.8小于
3.3.6数组操作
1.传递参数argv[1]
图3.3.9参数argv[1]
2.传递参数argv[2]
图3.3.10参数argv[2]
3.传递参数argv[3]
图3.3.11参数argv[3]
3.3.7控制转移
- if,判断argc是否等于4之后控制转移,运用je
图3.3.12 if控制语句
- for,for(i=0;i<8;i++),判断i是否等于7,运用jle
图3.3.13 for控制语句
3.3.8函数操作
printf,先传参入%rdi,再用call调用
图3.3.14第一个printf 图3.3.15第二个printf
exit,先传1到%edi,再用call调用
图3.3.16exit
sleep,先传参到%edi,再用call调用
图3.3.17sleep
atoi,先传参到%rdi,再用call调用,观察sleep的调用可知,返回值存入%eax中
图3.3.18atoi
3.4 本章小结
本章说了编译的概念和作用,hello.c的编译过程和结果,并对结果进行了分析。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
概念:汇编器将.s文件中的汇编语言程序翻译成机器语言程序,打包成可重定位目标程序的格式,结果保存在.o文件中。
作用:将汇编语言翻译成机器可以识别的机器语言,便于机器执行
4.2 在Ubuntu下汇编的命令
应截图,展示汇编过程!
图4.2.1汇编过程
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
ELF头:Magic,16字节,包含着文件和操作系统信息。
文件类型是ELF64位,文件编码方式是小端法,运行的操作系统UNIX - System V,ABI版本,文件类型为可重定位文件,机器类型为Advanced Micro Devices X86-64文件版本为1,进程开始的虚拟地址是0,指向 程序头部表的开始是0,指向节头部表的开始是1240,文件头部的大 小是64b,程序头部大小0,条目数0,节头部表大小64b,条目数14, 节头部表的条目和其位置 (idx) 的对应关系.
图4.3.1 ELF头
节头:指明各个节的名称,类型,地址,偏移量
图4.3.2节头
图4.3.3访问属性
重定位节:偏移量表示要修改的位置在本重定位节的偏移量,类型表示重定位的类型,信息包括symbol和type两部分,其中symbol占前4个字节,type占后4个字节,symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的类,加数是重定位过程中需要加减的常量
类型R_X86_64_PC32 :公式:S+A-P,其中S:重定项中VALUE成员所指符号的内存地址,A:被重定位处原值,表示"被重定位处"与"下一条指令"的偏移,P:被重定位处的内存地址。
类型R_X86_64_PLT32 :公式:L+A-P,其中L:<重定项中VALUE成员所指符号@plt>的内存地址,A和P与上面相同。
图4.3.4重定位节
.symtab节:符号表,存放有关程序和函数的符号定义和引用信息。
图4.3.5符号表
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
hello.o的反汇编如下:
图4.4.1 hello.o反汇编
对比hello.s文件可知:
- hello.s中跳转语句接L1或者L2等,而hello.o接的是跳转的与main地址的相对位置,如图4.4.1的36
- hello.s中函数调用有他自己的名字,直接调用,而hello.o中,直接接重定向条目和下一个语句的地址,如图4.4.1的2a
- hello.s中的立即数是10进制的,而hello.o中的立即数是16进制的,如图4.4.1的58
- hello.s中的printf调用的字符串是全局变量中引用的,hello.o中则需要重定向,如图4.4.1的51
4.5 本章小结
本章介绍了汇编的概念和作用,展示汇编过程,分析了hello.o的elf格式,将hello.o的反汇编与hello.s进行了比较。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
注意:这儿的链接是指从 hello.o 到hello生成过程。
概念:链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全连接的、可以加载和运行的可执行文件作为输出。输入的可重定位目标文件有各种不同的代码和数据节组成,每一节都是一个连续的字节序列。指令在一节中,初始化了的全局变量在另一节中,而为初始的化的变量又在另外一节中。
作用:符号解析和重定位。前者将目标文件定义和引用的每个符号引用正好和一个符号定义关联起来,后者编译器和汇编器生成从地址0开始的代码和数据节,链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有引用,使他们指向这个内存位置。
5.2 在Ubuntu下链接的命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
图5.2.1链接过程
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
ELF头和节头相比hello.o增加了一些链接后的节,也有一部分节因hello是完全连接的消失了;比hello.o增加了程序头表,用来描述可执行文件的连续的片被映射到连续的内存片的关系;增加了动态链接库,动态符号表等相关信息
列出节头表,包括了各段的名称、大小、类型、偏移量、起始地址。
图5.3.1 节头表
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
进入edb,打开hello,可以在Data Dump处查看虚拟地址空间各段信息。
比如:
找图5.3.1中的[1].interp,请求 /lib64/ld-linux-x86-64.so.2来解释我们的 程序。
图5.4.1.interp
找图5.3.1中的[4].hash,存放哈希表。
图5.4.2.hash
找图5.3.1中的[7].dynstr,保存着我们调用外部字符串如函数名称。
图5.4.3.dynstr
找图5.3.1中的[12].init的地址,代表程序的开始,存放指令机器码。
图5.4.4.init
找图5.3.1中的[15].text的地址,存放已编译程序的机器代码
图5.4.5.text
找图5.3.1中的[16].fini,存放程序的终末。
图5.4.6.fini
找图5.3.1中的[17].rodata,存放着像printf中的格式串这样的只读内容。
图5.4.7.rodata
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
不同:hello.o中的相对偏移地址到了hello中变成了直接的虚拟内存地址。
图5.5.1相对变直接
hello中多出了外部库函数的代码。
图5.5.2外部库函数
hello中比hello.o多了很多如.init节。
图5.5.3多的节
链接过程:在使用ld命令链接的时候,指定了动态链接器为64的/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o中主要定义了程序入口_start、初始化函数_init,_start程序调用hello.c中的main函数,libc.so是动态链接共享库,其中定义了hello.c中用到的printf、sleep、getchar、exit函数和_start中调用的__libc_csu_init,__libc_csu_fini,__libc_start_main。链接器将上述函数加入。
重定位:拿exit举例
hello.o中的exit
图5.5.4exit
他的重定位信息
图5.5.5 重定位信息
这些信息告诉链接器修改开始于偏移量0x2b处的32位PLT相对引用,这样在运行时会指向exit的例程,接着链接器会按照0xR_X86_64_PLT32重定位类型具体的规定计算出相应的数值。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
0x7f9b2d360df0
0x7f9b2436c0a0
0x00401125
0x00401090
0x004010f0
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
动态链接库(共享库):可以在加载和运行时进行链接
①加载时链接:当可执行文件首次加载和运行时进行动态链接。
②运行时链接:在程序开始运行后(通过编程指令)进行动态链接。
通过图5.3.1中的.got.plt的位置可知,其起始位置为0x00404000
图5.7.1起始位置
在dl_init之前0x00404000后两个8个字节分别是
0x7fd8158ad190,0x7fd815896ae0
图5.7.2 dl_init之前
在dl_init之后,发生改变,0x00404000后两个8个字节分别变为 0x7fcadb4c9190、0x7fcadb2ae0e0, 其中GOT [0](对应0x00404000)和GOT [1] (对应0x7fcadb4c9190)包含动态链接器在解析函数地址时会使用的信息。 GOT [2](对应0x7fcadb2ae0e0)是动态链接器在1d-linux.so模块中的入口 点。其余的每个条目对应于一个被调用的函数。
图5.7.3dl_init之后
5.8 本章小结
本章介绍了链接的概念和作用,展示汇编过程,分析了elf格式,查看了虚拟空间信息,分析了重定位过程,执行流程,动态链接。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:个正在运行的程序或者软件就是一个进程,它是操作系统进行资源分配的基本单位,也就是说每启动一个进程,操作系统都会给其分配一定的运行资源(内存资源)保证进程的运行。
作用:每次用户向shell输入一个可执行目标文件的名字运行时,shell就会创建一个新的进程,然后在这个进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。
6.2 简述壳Shell-bash的作用与处理流程
作用:1.文件管理: 创建, 移动, 赋值, 删除, 编辑...2.权限管理: 不同用户的不同权限管理.3.用户管理: 创建以及删除用户.4.磁盘管理: 磁盘挂载.5.网络管理: IP的配置.6.软件管理: 下载及运行等等.
处理流程:Shell从终端读入输入命令,如果是内置命令则立即执行, 如果不是内部命令并且在列表里没有找到这个可执行文件,将会显示一条错误信息。
6.3 Hello的fork进程创建过程
一个现有进程可以调用fork函数创建一个新的进程。
#include<unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程中返回子进程的ID,出错返回-1
fork函数被调用一次,但返回两次。两次返回的唯一出别是child process的返回值是0,而父进程的返回值则是child process的进程ID。所以可以通过fork函数的返回值来进入父子进程独有的代码段(但是要借助ifelse(else if else )选择语句)。
6.4 Hello的execve过程
子进程调用execve函数(传入命令行参数)在当前进程的上下文中加载并运行一个新程序即hello程序,execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行hello程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
进程调度过程:当正在运行的进程用完了时间片后,即使此进程还要运行,操作系统也不让它继续运行,而是从就绪队列依次选择下一个处于就绪态的进程执行,而被剥夺CPU使用的进程返回到就绪队列的末尾,等待再次被调度。时间片可以静态设置好,也可根据系统当前负载状况和运行情况动态调整,时间片大小的动态调整需要考虑就绪态进程个数、进程上下文切换开销、系统吞吐量、系统响应时间等多方面因素。
用户态与核心态转换: 从用户态到内核态的转变,需要通过系统调用来完成。系统调用会将CPU从用户态切换到核心态,以便 CPU 访问受到保护的内核内存。系统调用的过程会发生 CPU 上下文的切换,CPU 寄存器里原来用户态的指令位置,需要先保存起来。接着,为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务。而系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
异常:中断,陷阱,故障,终止
处理:中断:硬件中断的异常处理程序被称为中断处理程序。
陷阱:陷阱处理程序将控制返回到下一条指令。
故障:当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。
终止:不可处理,终止程序 。
信号:SIGINT,SIGTSTP
处理:SIGINT:终止前台作业。
SIGTSTP:停止(挂起)前台作业.
各命令:
乱按键盘(包括回车,只会产生空行),正常输出
Ctrl-C,发送信号SIGINT,终止程序
Ctrl-Z,发送信号SIGTSTP,暂时挂起程序
在Ctrl-Z基础上运行ps jobs pstree fg kill命令
ps,显示进程信息
jobs, 显示作业状态
pstree,进程树
fg,将挂起的进程放到前台运行
kill,终止hello进程
6.7本章小结
本章介绍进程的概念和作用,简述壳的作用和处理流程,fork的创建过程,execve过程,进程执行的相关信息,异常与信号处理的种类和方法。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:hello.o里的相对偏移地址。由程序产生的与段相关的偏移地址部分。
线性地址:hello里面的虚拟内存地址。地址空间中的整数是连续的。
虚拟地址:hello里面的虚拟内存地址。在一个带虚拟内存的系统中,CPU从一个有N=2"个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间。
物理地址:hello运行时虚拟内存地址对应的物理地址。计算机系统的主存被组织成一个个由M个连续的字节大小的单元组成的数组。
7.2 Intel逻辑地址到线性地址的变换-段式管理
机器语言指令中出现的内存地址,都是逻辑地址,需要转换成线性地址,再经过MMU(CPU中的内存管理单元)转换成物理地址才能够被访问到。Linux中逻辑地址等于线性地址。所以Intel的设计是段描述符只存放在GDT,段寄存器存放的是段描述符在GDT内的索引值。
分段机制将逻辑地址转化为线性地址的步骤:
1.使用段选择符中的偏移值(段索引)在GDT或LDT表中定位相应的段描述符。
2.利用段选择符检验段的访问权限和范围,以确保该段可访问。
3.把段描述符中取到的段基地址加到偏移量上,最后形成一个线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
CPU通过地址来访问内存中的单元,地址有虚拟地址和物理地址之分,如果CPU没有MMU,或者有MMU但没有启用,CPU核在取指令或访问内存时发出的地址将直接传到CPU芯片的外部地址引脚上,直接被内存芯片(以下称为物理内存,以便与虚拟内存区分)接收,这称为物理地址。如果CPU启用了MMU,CPU核发出的地址将被MMU截获,从CPU到MMU的地址称为虚拟地址(Virtual Address,以下简称VA),而MMU将这个地址翻译成另一个地址发到CPU芯片的外部地址引脚上,也就是将虚拟地址映射成物理地址。
页式管理将逻辑地址转化为线性地址的步骤:
1.从cr3中取出进程的页目录地址
2.根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。
3.根据线性地址的中间十位,在页表中找到页的起始地址
4.将页的起始地址与线性地址中最后12位相加,得到最终我们想要的地址。
图7.3.1页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
VA到PA的变换即虚拟地址到物理地址的变换。
虚拟地址被划分成4个VPN和1个VPO。每个VPNi都是一个到第i级页表的索引,其中1≤i≤4.第j级页表中的每个PTE,1≤j≤k-1,都是指向第j+1级的某个页表的基址。第4级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问4个PTE,或者一个磁盘块地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问4个PTE.这里TLB能够起的作用,正是通过将不同层次上页表的PTE缓存起来。
图7.4.1 4级页表
7.5 三级Cache支持下的物理内存访问
通过地址解析出缓存的索引和偏移,对缓存进行访问,匹配标记查找是否含有相关的字,如果命中,则将数据发送给CPU,如果没有命中,则访问下一级缓存,取出这个字,存入高一级缓存,返回数据给CPU。
图7.5.1 三级cache下的物理地址访问
7.6 hello进程fork时的内存映射
当fork 函数被hello调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve 函数在shell中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
- 删除已存在的用户区域。删除shell虚拟地址的用户部分中的已存在的区域结构。
- 映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。
- 映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器(PC) 。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
图7.7.1映射用户地址空间的区域
7.8 缺页故障与缺页中断处理
缺页故障:虚拟内存的习惯的说法中,DRAM缓存不命中称为缺页。以图7.8.1为例,CPU引用VP3中的一个字,VP3并未缓存在 DRAM中。此时读取PTE3,从有效位可以判断VP3未被缓存,此时会触发一个缺页异常。
图7.8.1 缺页前
缺页中断处理:调用内核中的缺页异常处理程序,该程序会选中一个牺牲页此例中就是存放在PP3中的VP4,若该VP4被修改过,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,反映出VP4不再缓存再主存中这一事实,然后内核从磁盘复制VP3到内存中的PP3,更新PTE3,随后返回。当异常处理子程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重新发送到地址翻译硬件,但现在VP3已经在主存中,那么页命中也能由地址翻译硬件正常处理了。
图7.8.2 缺页后
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
基本方法:动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间的细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放。这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
printf中调用的malloc是用了动态内存分配器中的显示分配器。C标准库中提供了一个称为malloc程序包的显示分配器。程序通过调用malloc函数来从堆中分配块。而显示分配器必须在一些相当严格的约束条件下工作:
1.处理任意请求序列。2.立即响应请求。3.只使用堆。4.对其块。5.不修改已分配的块。
策略:明确两个相矛盾的性能目标,最大化吞吐率和最大化内存利用率。那么就要考虑好以下几个问题:空闲块组织、放置、分割、合并。需要用到隐式空闲链表、放置已分配的块、分割空闲块、获取额外的堆内存、合并空闲块和待边界标记的合并等方法。然而因为块分配与堆块的总数呈线性关系,所以对于通用的分配器,隐式空闲链表是不适合的,显示空闲链表是更好的方法。显示空闲链表包含的策略有:
放置策略:首次适配策略,下一次适配策略,最佳适配策略。
合并空闲块策略:立即合并策略和推迟合并策略。
7.10本章小结
本章说明了逻辑地址,物理地址,线性地址,虚拟地址的概念,介绍了段式管理和页式管理和四级页表下的VA到PA的转换,解释了三级cache下的物理地址的访问,分析了fork和execve时的内存映射,讲述了缺页故障和缺页中断管理,动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件,分为普通文件(包含任意数据),目录(包含一组链接的文件,其中每个链接都将一个文件名映射到一个文件,这个文件可以是另一个目录),套接字(用来与另一个进程进行跨网络通信的文件)。
设备管理:unix io接口,所有的I/O设备都被模型化为文件,而所有的输入和输出都被当作对相应文件的都和写来执行,并允许Linux内核引用出一个简单、低级的应用接口。
8.2 简述Unix IO接口及其函数
接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为K 。
4.读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号” 。
类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。
- 关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
函数:
打开文件:int open(char *filename,int flags,mode_t mode);
返回:若成功则为新文件的描述符,若出错为-1.
关闭文件:int close (int fd);
返回:若成功则为0,若出错则为-1.
读文件:ssize_t read (int fd, void *buf, size_t n);
返回:若成功则为读的字节数,若EOF则为0,若出错则为-1.
写文件:ssize_t write (int fd, const void *buf, size_t n);
返回:若成功则为写的字节数,若出错则为-1.
8.3 printf的实现分析
先看printf函数的函数体,它调用了vsprintf函数
图8.3.1 printf函数体
那再来看一下vsprintf函数的函数体,他返回了打印字符串的长度,作用就是格式化,它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
图8.3.2vsprintf函数体
再看write函数,我们应该找到INT_VECTOR_SYS_CALL的实现。
图8.3.3 追踪write
找到后发现他要通过系统来调用sys_call这个函数。
图8.3.4 INT_VECTOR_SYS_CALL
我们在看看sys_call的实现:
图8.3.5 sys_call的实现
可以看出代码里面的call是访问字库模板并且获取每一个点的RGB信息最后放入到eax也就是输出返回的应该是显示vram的值,然后系统显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量).
8.4 getchar的实现分析
先看一下getchar函数的函数体:
图8.4.1 getchar函数体
可以看到,他调用了read函数,read函数可以通过sys_call中断来调用内核中的系统函数。键盘中断处理子程序接受按键扫描码转成ASCII码,保存到系统的键盘缓冲区。然后通过系统调用读取按键ASCII码,直到接受到回车键才返回。
8.5本章小结
本章概述了Linux IO设备管理方法,简述Unix I/O接口及其函数,并对printf和getchar函数的实现进行了分析。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
hello的一生需要的经历如下:
预处理:由hello.c文件变成hello.i文件
编译:由hello.i文件变成hello.s文件
汇编:由hello.s文件变成hello.o文件
链接:由hello.o文件变成hello可执行文件
进程的创建:由fork创建子进程,
进程的执行:由execve启动加载器,映射虚拟内存,载入物理内存,载入main函数
执行指令:分配时间片,执行控制逻辑流
进程的异常与信号处理:对接受的信号进行处理,中断、故障、终止、陷阱。
回收:回收所有进程,删除数据结构
感悟:计算机系统博大精深,并非一朝一夕可以理解透彻,这门课也只是给我们打下了一个基础,我们要学习的还有很多,还是要坚持不懈努力前行!
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c 给定的.c文件,文本类型,用于预处理
hello.i 预处理后的文件,用于编译生成.s文件
hello.s 汇编代码文件,用于汇编生成.o文件
hello.o 可重定位目标文件,用于查看elf信息,链接生成hello可执行文件
hello 可执行文件,用于运行。
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[3] .dynsym .symtab_Farmwang的博客-CSDN博客_.dynsym
[4]https://chyyuu.gitbooks.io/simple_os_book/content/zh/chapter-3/implement_rand_mem.html
[5] 逻辑地址、线性地址和物理地址的转换_octopusHu的博客-CSDN博客_线性地址到物理地址的转换
[6] 深入理解计算机系统第三版
(参考文献0分,缺失 -1分)