计算机系统大作业

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术学院
学   号 1180300425
班   级 1803004
学 生 王仝宇
指 导 教 师 史先俊

计算机科学与技术学院
2019年12月
摘 要
本文阐述了hello.c程序,从源代码到可执行目标文件,再到执行进程,最终被终止并回收的过程。具体描述了它的处理全过程,包括P2P和020.全面阐释了计算机的底层实现
关键词:预处理、编译、汇编语言、机器语言、可重定位目标文件ELF格式、链接、重定位、动态链接、shell、进程、Cache、页表、TLB

目 录

第1章 概述 - 4 -
1.1 Hello简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 4 -
第2章 预处理 - 5 -
2.1 预处理的概念与作用 - 5 -
2.2在Ubuntu下预处理的命令 - 5 -
2.3 Hello的预处理结果解析 - 5 -
2.4 本章小结 - 5 -
第3章 编译 - 6 -
3.1 编译的概念与作用 - 6 -
3.2 在Ubuntu下编译的命令 - 6 -
3.3 Hello的编译结果解析 - 6 -
3.4 本章小结 - 6 -
第4章 汇编 - 7 -
4.1 汇编的概念与作用 - 7 -
4.2 在Ubuntu下汇编的命令 - 7 -
4.3 可重定位目标elf格式 - 7 -
4.4 Hello.o的结果解析 - 7 -
4.5 本章小结 - 7 -
第5章 链接 - 8 -
5.1 链接的概念与作用 - 8 -
5.2 在Ubuntu下链接的命令 - 8 -
5.3 可执行目标文件hello的格式 - 8 -
5.4 hello的虚拟地址空间 - 8 -
5.5 链接的重定位过程分析 - 8 -
5.6 hello的执行流程 - 8 -
5.7 Hello的动态链接分析 - 8 -
5.8 本章小结 - 9 -
第6章 hello进程管理 - 10 -
6.1 进程的概念与作用 - 10 -
6.2 简述壳Shell-bash的作用与处理流程 - 10 -
6.3 Hello的fork进程创建过程 - 10 -
6.4 Hello的execve过程 - 10 -
6.5 Hello的进程执行 - 10 -
6.6 hello的异常与信号处理 - 10 -
6.7本章小结 - 10 -
第7章 hello的存储管理 - 11 -
7.1 hello的存储器地址空间 - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.5 三级Cache支持下的物理内存访问 - 11 -
7.6 hello进程fork时的内存映射 - 11 -
7.7 hello进程execve时的内存映射 - 11 -
7.8 缺页故障与缺页中断处理 - 11 -
7.9动态存储分配管理 - 11 -
7.10本章小结 - 12 -
第8章 hello的IO管理 - 13 -
8.1 Linux的IO设备管理方法 - 13 -
8.2 简述Unix IO接口及其函数 - 13 -
8.3 printf的实现分析 - 13 -
8.4 getchar的实现分析 - 13 -
8.5本章小结 - 13 -
结论 - 14 -
附件 - 15 -
参考文献 - 16 -

第1章 概述
1.1 Hello简介
P2P;process to process:
1、源程序hello.c被预处理器预处理成修改了的源程序hello.i
2、hello.i经编译器编译成汇编程序hello.s
3、hello.s经汇编器翻译成可重定位目标程序hello.o
4、hello.o经链接器与其调用的库函数链接成为可执行目标程序hello
hello被操作系统加载进入内存并运行,在运行过程中,shell使用内置fork函数为hello创建进程
020:hello的进程创建完毕后,由shell进程调用execve函数在上下文中加载运行,整个进程运行结束后,进程终止并被其父进程回收
1.2 环境与工具
X64CPU、Windows10操作系统 Ubuntu18.04 gdb、gcc、objdump等
1.3 中间结果
hello.c源程序文件
hello.i修改了的源程序文件
hello.s汇编程序文件
hello.o可重定位目标程序文件
hello可执行目标文件
1.4 本章小结
hello.c程序从编写、预处理、编译、汇编、链接再到执行,体现了计算机系统系统各部分的具体功能,以及它们之间的的协同合作。

第2章 预处理
2.1 预处理的概念与作用
预处理:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序
第六行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中
第七行的#include<unistd.h>命令告诉预处理器读取系统头文件unistd.h的内容,并把它直接插入程序文本中
第八行的#include<stdlib.h>命令告诉预处理器读取系统头文件stdlib.h的内容,并把它直接插入程序文本中
最终得到一个以.i为文件扩展名的修改后的源程序
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
在这里插入图片描述
在这里插入图片描述
2.3 Hello的预处理结果解析
hello.i增加至3113行,头文件stdio.h,unistd.h,stdlib.h依次读入并展开,main函数位于文件结尾
在这里插入图片描述
2.4 本章小结
本章展示了源程序hello.c进行预处理的命令、过程及结果

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
编译指的是编译器将修改后的源程序翻译为汇编程序的过程。在这个过程中,首先编译器汇集检查原文件中是否有语法错误。如果有,提示编译不通过;如果没有,则将修改后的源程序翻译为汇编程序。
编译最重要的意义是将高级语言源程序翻译成了汇编程序,从而消除了在不同编译器上编译的程序或用不同编程语言变异的程序的差别,从而为下一步汇编打下基础。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
在这里插入图片描述
在这里插入图片描述
3.3 Hello的编译结果解析
在这里插入图片描述
先介绍头部:
file:文件名:hello.c
text:代码段
.rodata:只读数据
align:8字节对齐
global:函数名,属强符号
type:指明强符号类型:函数类型
在这里插入图片描述
对main函数进行分析:
3.3.1、数据:
程序中数据主要包括全局变量、局部变量、指针数组
整型变量:1、argc由源程序可知:整型数据argc为第一变量,放在寄存器edi中
2、i,局部变量,存储在栈中,由rbp-4为其分配空间
在这里插入图片描述
由图可知,i占据4字节空间
字符串变量:程序中的字符串变量指函数的argv数组中的存储量,argv数组被作为一个指针存储在寄存器rsi中
在这里插入图片描述
由图可知,字符串被放在rodata段,编码如图所示
常量:常量以立即数形式存在,主要用于累加、赋值等操作
3.3.2、赋值
赋值:由源程序可知,赋值专指对局部变量i赋值,用mov操作实现
同时可以看出,此处采用rbp访问,利用rbp-4为i分配4字节空间
如图:在这里插入图片描述
3.3.3、算术操作:
在这里插入图片描述
用add操作实现i++
在这里插入图片描述
在这里插入图片描述

由图一可知,argv的地址作为参数二被保存于堆栈,图二中的mov操作实际上相当于运算,后部的加8、加16操作分别取argv[1]、argv[2]
3.3.4
关系操作:
在这里插入图片描述
argc!=4通过cmp操作实现,实际是不改变目的寄存器,只设置标志位的sub操作,je:相等时跳到L2块
在这里插入图片描述
源程序中i<8转换为i<=7,通过i与7比较实现,i<=7时跳转到L4
3.3.5数组/指针操作:
在这里插入图片描述
在这里插入图片描述
由图一可知,argv的地址作为参数二被保存于堆栈,图二中的mov操作实际上相当于
运算,将数组首地址赋给rax,后部的rax+8、rax+16操作分别取argv[1]、argv[2]
3.3.6跳转控制
关系操作:
在这里插入图片描述
argc!=4通过cmp操作实现,je:argc与4相等时跳到L2块执行,否则顺序执行
在这里插入图片描述
源程序中i<8转换为i<=7,通过i与7比较实现,i<=7时跳转到L4
3.3.8函数操作:
共调用函数7个
1、main函数
传入参数argc,argv返回值为0
2、puts函数
argc不等于4时调用,传参为输出字符串的地址,返回0
3、exit函数
argc不等于4时调用,传参为1,代表正确时退出,不返回
4、printf函数
argc=4且i<8时调用,传入了 argv[1]和argc[2]的地址
5、atoi函数
argc=4且i<8时调用,传入argv[3]的地址,返回argv[3]中字符转换成的整数值
6、sleep函数
argc=4且i<8时调用,传入atoi函数的返回值,返回进程暂停时剩下的时间片长度
7、getchar函数
main函数调用
3.4 本章小结
编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
通过对编译的结果进行解析,更深刻地理解了C语言的数据与操作,并且对C语言翻译成汇编语言有了更好的掌握。

(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
汇编是指汇编器将汇编语言源文件翻译成机器语言指令(二进制形式),同时将指令打包成可重定位目标程序并存在相应文件中的过程
作用:将汇编指令翻译成机器指令,使得程序第一次变为可以被机器识别的文件
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
在这里插入图片描述
在这里插入图片描述
4.3 可重定位目标elf格式
ELF头:
ELF头以一个16字节的序列开始,描述了生成该文件的系统的字的大小和字节顺序,剩余的部分包含帮助链接器进行语法分析和解释目标文件的信息,包括ELF头的大小、目标文件的类型、机器类型、节头表的文件偏移及表中条目的大小和数量
在这里插入图片描述
2.节头部表描述了不同节的位置和大小,其中目标文件中每个节都有一个固定大小的条目。具体的描述包括节的名称、类型、地址和偏移量等。
在这里插入图片描述
3、重定位表
两种定位形式:R_X86_64_PC32、R_X86_64_PLT32
在这里插入图片描述
在这里插入图片描述
4、symtab符号表
存放在程序中定义和引用的函数和全局变量的信息
在这里插入图片描述
4.4 Hello.o的结果解析
反汇编结果实际上就是main函数的汇编代码的变形
与第3章的差别及联系:
联系:汇编语言是反汇编结果的基础,反汇编结果的语句与汇编程序基本相同
差别:1、操作数:汇编语言是十进制数,反汇编结果是二进制数
2、操作符省去了对操作数类型的描述,如qwlb等
3、由于经过重定位,main函数的地址从零开始,同时需要main函数之前数据的操作也从零开始
4、汇编语言使用绝对跳转,反汇编结果使用相对跳转
4.5 本章小结
本章分析了汇编、反汇编及可重定位目标程序的信息,为下一步执行链接打下基础
(第4章1分)

第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,通过链接,可重定位目标文件就变成了可执行目标文件,为下一步创建进程加载运行打下基础,链接器主要是将main函数的可重定位目标文件与自定义函数、共享库函数各自的可重定位目标文件链接形成可执行目标文件
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.3 可执行目标文件hello的格式
可执行目标文件的格式类似于可重定位目标文件的格式,ELF头描述文件的总体格式,还包括程序的入口点
在这里插入图片描述
.text,.rodata,.data节与可重定位目标文件中的节相似,.init节定义了一个小函数_init,被程序的初始化代码调用
在这里插入图片描述
在这里插入图片描述
5.4 hello的虚拟地址空间
在这里插入图片描述
5.5 链接的重定位过程分析
hello与hello.o最大的不同在于地址,由反汇编结果可知,hello的反汇编结果与hello.o相比,链接了所有它调用的函数,不仅仅只有main函数,同时,hello经过链接,地址发生改变,从0x4000000开始,hello.o刚经过重定位,地址从0开始
在这里插入图片描述
在这里插入图片描述
hello.o反汇编结果
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

hello反汇编结果
根据hello和hello.o的不同,分析出链接的过程为:链接就是链接器ld将各个目标文件组装在一起,就是把自定义函数、主函数、共享函数的可重定位目标文件按照一定规则累积在一起,并生成可执行文件。
重定位过程:
1、重定位节和符号定义:在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。然后,链接器将运行时内存地址赋给新的聚合节、输入模块定义的每个节和每个符号。
2、重定位节中的符号引用:链接器修改代码节和数据节对每个符号的引用,使得它们指向正确的运行时地址。
5.6 hello的执行流程
0x4004c0 _init
0x4004e0 .plt
0x4004f0 puts@plt
0x400500 printf@plt
0x400510 getchar@plt
0x400520 atoi@plt
0x400530 exit@plt
0x400540 sleep@plt
0x400550 _start
0x400610 <__libc_csu_init>
0x400680 <__libc_csu_fini>
0x400684 <_fini>
5.7 Hello的动态链接分析

在这里插入图片描述
init执行前
在这里插入图片描述
Init执行后
5.8 本章小结
本章通过对重定位、虚拟地址和hello.o以及动态链接的分析,展现了hello程序的链接过程
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程的经典定义就是一个执行中程序的实例,是主存、I/O设备、CPU的共同抽象。
系统中的每个程序都运行在某个进程的上下文中,同时进程为应用程序提供了独立的逻辑控制流和私有的进程地址空间两大抽象,因此可以说,进程是程序在机器内运行的形式。
6.2 简述壳Shell-bash的作用与处理流程
shell程序是一个应用型交互级程序,代表用户运行其他程序,为用户提供操作界面。
处理流程:shell执行一系列读——写操作步骤,shell作用时,首先它读取用户的命令行,判断命令行的第一个单词是否是shell内置命令,若是则直接执行,若不是则假设它是一个可执行文件的名字再执行,代表用户运行命令
6.3 Hello的fork进程创建过程
父进程调用fork函数,创建子进程hello,内核为新进程hello创建各种数据结构,并分配给它一个唯一的PID,同时,将父进程的虚拟内存空间拷贝出一份副本给子进程,而且,内核也会将父进程所有的打开文件描述符复制一份副本给子进程。这样,子进程就几乎与父进程完全相同,它获得了父进程的栈、代码段、数据段、文件等。再根据时间片决定此时刻运行父进程还是子进程,并返回fork,父进程返回其PID,子进程返回0.
6.4 Hello的execve过程
execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。当出现错误时,execve返回调用程序,不出现错误时,execve正常加载运行hello,过程如下:
1、删除当前进程的虚拟地址的用户部分中的已存在的区域结构
2、为hello的代码、数据、bss和栈区域创建新的数据结构,并将其标记为只读,按私有写时复制方式向虚拟内存映射
3、将共享对象动态链接到hello程序,再映射到用户虚拟地址空间中的共享区域内
4、设置程序计数器(PC)为程序的入口地址
5、运行hello程序
6.5 Hello的进程执行
进程的上下文:进程的上下文指进程的物理实体和支持进程运行的环境,包括用户级上下文和系统级上下文,用户级上下文包括进程调用的堆栈、数据、代码及共享地址空间,系统级上下文包括进程控制信息、进程现场信息和进程进程标识信息。
进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式:用户模式属于低级用户,进程不允许执行特权指令
内核模式:内核模式也叫超级用户模式,一个运行在内核模式中的指令可以执行指令集中的任何指令。
执行过程:hello进程被创建并加载后开始运行,hello进程在时间片内按顺序执行每一条机器指令,当时间片已结束,hello进程被暂时挂起,将控制权转交给内核进程,内核进程继续执行,将控制权传递给其他与hello并发的进程。并发进程结束后,将控制权转交给内核进程,内核进程继续执行,将控制权传递给hello进程。当时间片未结束但hello进程出现异常或接收到需要中断进程执行的信号时,hello进程调用操作系统相应的处理函数进入内核代码,在内核代码中完成与处理函数进程的切换
6.6 hello的异常与信号处理
异常:中断、陷阱、故障、终止,其中除中断是由于I/O设备输入信号导致,属于异步异常外,其他均由指令执行引起,属于同步异常
由于hello进程中无错误,所以故障不会出现,但进程可以被人为终止,由于程序运行过程中可以按键盘,所以中断可以出现,由于hello没有向内核请求服务,所以陷阱亦不会出现,故仅有中断和终止可能出现
信号:中断:SIGSTP,默认行为:停止直到下一个SIGCONT
终止:SIGINT,来自键盘的终止信号
在这里插入图片描述
正常运行
在这里插入图片描述
按Ctrl+C,内核发送SIGINT信号给前台进程组中的每个进程,结果是终止前台作业
在这里插入图片描述
按Ctrl+Z,内核发送SIGSTP信号给前台进程组中的每个进程,结果是挂起前台作业
在这里插入图片描述
Ctrl+Z+ps显示正在运行的和已终止的进程
在这里插入图片描述
Ctrl+Z+jobs显示已启动的进程
在这里插入图片描述
在这里插入图片描述
Ctrl-Z+pstree,以树的形式显示进程间关系,其中,靠近根节点的是父进程
在这里插入图片描述
fg将进程跳到前台
在这里插入图片描述
用kill -9 2835 命令杀死pid为2835的进程

6.7本章小结
本章应用各种命令及理论分析,对hello程序在shell下的运行情况进行了解读
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
物理地址:主存(存储芯片)被组织成一个很大的数组,hello的各个段都被按单元存储在主存中,每个单元都有它自己的一个地址,即为物理地址
线性地址:非负整数地址的集合称为地址空间,对于hello来说,就是它的存储块地址的集合,如果这个集合中的整数连续,那么就说这些地址是线性地址
虚拟地址:物理地址在地址空间中的映射称为虚拟地址,对于hello来说,就是它的每个字节的物理地址对应的地址空间中的映射,由CPU产生
逻辑地址:由hello的每个段的段地址与段中各字节的偏移地址组成的地址称为逻辑地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段地址和偏移地址组成,计算机中共有4个段寄存器,用于存放数据、代码、堆栈、辅助4段的基地址,段选择符共计16位,前13位为索引位,用于确定段描述符在描述符表中的位置,第14位为Tl位,Tl=0时选择全局描述符表,Tl=1时选择局部描述符表,最后两位用于描述段的状态
逻辑地址向线性地址的转换:被选中的描述符先被送至描述符Cache,每次从描述符Cache中取32位基地址,与32位段内偏移量(有效地址)相加得到线性地址
7.3 Hello的线性地址到物理地址的变换-页式管理
从线性地址到物理地址需要通过页表,页表是页表条目的集合,每个页表条目包含有效位和存储物理页号的部分,一般来说,每个页被划分为4K大小。因此,理论来说,如果仅有一级页表则后12位负责页内选择,称为页内偏移量(VPO),前方各位共同决定虚拟页的选择(64位系统为36位),称为虚拟页号(VPN),具体过程如下:
1、CPU产生虚拟地址,通过地址总线传递给主存
2、主存进行翻译,截取VPN作为虚拟页号寻找对应的页
3、找到虚拟页后,取出其中的物理页号,与页内偏移量结合构成物理地址
如果有多级页表:以32位虚拟地址,2级页表为例:
第一级页表共有1024项,每一项都存储着一个二级页表的基地址,一个二级页表共有1024项,每项存储一个页,页内容同上,寻址过程如下:
1、CPU产生虚拟地址,通过地址总线传递给主存
2、主存进行翻译,取前十位作为一级寻址依据,寻找对应的一级页表项
3、找到一级页表项后,取中间十位作为二级寻址依据,寻找对应的二级页表项
4、找到二级页表项后,取出物理页号,与后十二位组合形成物理地址

7.4 TLB与四级页表支持下的VA到PA的变换
TLB又称为翻译后备缓冲器,实质上是一个虚拟寻址的高速缓存,其中每一行都保存着由一个PTE组成的块,若TLB有2的t次方个组,则VPN低t位构成组索引,高位构成标记,VPO构成块偏移。以高速缓存形式寻址,若寻址命中,即组索引、标记全部命中且状态位等于1,则找到PPN,并以块偏移为PPO构成物理地址,若不命中,则选择四级页表方式找物理地址,每一级页表号都蕴含在VPN中。物理地址共有48位,前36位为页表寻址区,最后12位为VPO,36位的虚拟地址被分割成4个9位的片。CR3寄存器包含L1页表的物理地址。VPN1有一个到L1 PTE的偏移量,找到这个PTE以后又会包含到L2页表的基础地址;VPN2包含一个到L2PTE的偏移量,找到这个PTE以后又会包含到L3页表的基础地址;VPN3包含一个到L3PTE的偏移量,找到这个PTE以后又会包含到L4页表的基础地址;VPN4包含一个到L4PTE的偏移量,找到这个PTE以后就能从中取出相应的PPN并与VPO组合形成物理地址。
7.5 三级Cache支持下的物理内存访问
得到物理地址后,就进入三级Cache支持下的物理内存访问阶段。PPN与VPO组成物理地址以后,以物理地址作为寻址依据,以L1 数据cache的介绍为例,L2和L3同理。
L1 数据Cache是8路64组相联。块大小为64B,则由于L1有64组,则它有6个组索引位,块大小为64B,则它有6个块偏移位,64位系统有52个地址位,故它有40个标记位。寻址时,以物理地址作为寻址依据,最后6位为块偏移位,前40位为标记位,中间六位为组索引位,访问时首先通过组索引位寻找对应的组号,找到组号后根据标记位寻找对应的块。如果该组中所有块的标记与给出的标记位均不相同或标记位相同但有效位为0,则不命中,需调用替换程序进行替换。一般而言,如果映射到的组内有空闲块,则直接放置,否则必须驱逐出一个现存的块,一般采用最近最少被使用策略LRU进行替换。反之则命中。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。同时将两个页面中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有写时复制。
当fork在hello中返回时,hello现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,私有写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。当出现错误时,execve返回调用程序,不出现错误时,execve正常加载运行hello,过程如下:
1、删除当前进程的虚拟地址的用户部分中的已存在的区域结构
2、为hello的代码、数据、bss和栈区域创建新的数据结构,并将其标记为只读,按私有写时复制方式向虚拟内存映射
3、将共享对象动态链接到hello程序,再映射到用户虚拟地址空间中的共享区域内
4、设置程序计数器(PC)为程序的入口地址
5、运行hello程序

7.8 缺页故障与缺页中断处理
一般来说,DRAM缓存不命中或虚拟内存中的字不在物理内存中称为缺页,缺页由于调用指令引起,属于同步异常,具体细分,属于同步异常中的故障。当发生缺页中断时,操作系统会调用缺页处理子程序。具体分析过程如下:
1、虚拟内存页的状态有三种情况:未分配、未缓存、已缓存,第二种情况会引起缺页中断。未分配是指物理内存中没有该虚拟页对应的页,未缓存是指物理内存中有该虚拟页对应的页,但页中并未存储内容。未分配时,虚拟页的有效位为0且内容为NULL;未缓存时,虚拟页的有效位为0且内容为-1.
2、缺页中断处理子程序被触发后,首先会检查物理内存中是否有空白页,若有,则将缺的页写入空白页,并更新PTE的存储内容为新页的物理页号,同时将PTE的有效位设置为1.若没有,则利用替换规则,选择一个牺牲页将其替换,替换前还要检查牺牲页是否被修改,若未被修改,则直接替换;若已被替换,则将其写回到磁盘的pagefile中去,而后进行替换,替换的内容来自于磁盘,为磁盘上要读取内容的副本。
3、完成上述操作后,缺页中断处理子程序返回触发缺页中断处理子程序的指令并再次执行该指令,消除缺页中断。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,成为。分配器将堆视为一组大小不同的块的集合进行维护,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的,已分配的块显式地保留为供应用程序使用,并且始终保留已分配状态直到它被释放,释放可以由应用程序显式执行,也可以由内存分配器隐式执行。空闲块可用来分配,并且保持空闲状态直到它被分配。
显式分配器:要求应用程序显式地释放任何已分配的块
隐式分配器:要求分配器检测到一个已分配块不再被使用后释放该块
隐式空闲链表:任何实际的分配器都需要一些数据结构,允许它区别块边界,区分已分配块和空闲块,大多数分配块将这些信息嵌入块本身。一般来说,在这种情况下,一个块由头部(一个字大小),有效载荷,以及一些可能的填充组成。头部描述字的大小和其他情况,其中,头部的最后一位就被用来描述块是否有效,同时,还可根据对齐情况,适当省略描述块大小部分的位数以增加精确度。
在上述情况下将堆组织为一个连续的已分配块和空闲块的序列。在这个序列中,空闲块通过头部中的大小字段隐含链接。这个序列就称为隐式空闲链表。
分配:首次适配:从头开始搜索空闲链表,选择第一个合适的空闲块。
下一次适配:每一次都从上一次搜索结束的地方开始搜索,选择第一个合适的空闲块。
最佳适配:对堆进行全面搜索,找到最合适的块。
增加堆的空间:
通过调用sbrk函数,申请额外的存储器空间,插入到空闲链表中 。
合并空闲块:目的:当分配器释放一个已分配块时,可能有其他空闲块与这个新释放的空闲块相邻,造成假碎片现象。即相邻的块被切割成了小块而无法使用。因此,合并空闲块的目的即为消除假碎片现象,提升内存利用率。
策略:1、立即合并:释放完内存就合并
2、推迟合并:直到发生某种情况需要合并时再合并
带边界标记的合并:在每个块的脚部添加一个字,这个字与头部的格式及作用相同,但是头部用于存储上一个块的大小和空闲情况,脚部用于存储本块的大小和存储情况;或头部用于存储本块的大小和空闲情况,脚部用于存储下一个块的大小和存储情况。在合并时,只需要根据头部或足部的空闲情况,选择向前合并或向后合并,在合并时,要保留被合并的第一个块的头部作为新块的头部,被合并的最后一个块的脚部作为新块的脚部,将其他块的头部和脚部全部去掉,以新块的大小作为头部或脚部的大小。
显式空闲链表:
根据定义,程序不需要一个空闲块的主体,所以实现空闲链表数据结构的指针可以存放在这些空闲块的主体里面。
显式空闲链表结构将堆组织成一个双向空闲链表,在每个空闲块的主体中,都包含一个pred(前驱)和succ(后继)指针。
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于空闲链表中块的排序策略。
一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。
另一种方法是按照地址顺序来维护链表,其中链表每个块的地址都小于它后继的地址。时间复杂度高,空间复杂度低。
分离的空闲链表:将块按大小分成等价类,将符合各个等价类条件的块放入各个等价类内。
分离适配:为了分配一个块,首先要按块的大小确定等价类,并对适当的空闲链表做首次试配,查找合适的块,如果找到了,就将其分割,一部分用于存储,另一部分插入合适的空闲块等价类内。如果找不到,就向更大的等价类申请,直到找到或最后找不到为止。若最后找不到,则执行增加堆空间操作。
7.10本章小结
虚拟内存是对主存的一个抽象。本章通过对虚拟内存的了解,学会了TLB和四级页表支持下VA到PA的转换,以及得到了PA后,三级cache下的物理内存的访问过程。通过本章内容,更深入掌握了fork函数和exceve函数和虚拟内存的种种联系,最后还学会了动态内存分配的管理。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m个字节的序列:
B0,B1,……,Bk,……,Bm
所有的I/O设备都被模型化为文件,而所有的输入和输出都被当成对相应文件的读和写来执行
8.2 简述Unix IO接口及其函数
I/O接口:
1、打开文件:内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。
注:Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,他们可用来代替显式的描述符值。
2、改变当前的文件位置:文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。
3、读写文件:读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k
4、关闭文件:当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中
函数:
open():
函数原型:int open(char *filename,int flags.mode_t mode)
参数:指针filename,flags,mode
函数作用:将filename转换为一个文件描述符,并且返回描述符数字。打开成功时返回的描述符总是在进程中当前没有打开的最小描述符。flags指明函数访问文件的方式:
O_RDONLY:只读
O_WRONLY:只写
O_RDWR:可读可写
打开失败时函数返回-1
close():
函数原型int close(int fd)
参数 fd文件描述符
返回值:关闭成功时返回0,关闭失败时返回-1
注:关闭一个已关闭的描述符会出错
read():
函数原型:ssize_t read(int fd,void *buf,size_t n)
参数:fd文件描述符,缓冲区buf,传送字节数n
返回值:read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf,返回值为-1表示错误,返回值为0表示EOF,否则返回实际传送的字节数量
write():
函数原型:ssize_t write(int fd,const void *buf,size_t n)
参数:fd文件描述符,缓冲区buf,传送字节数n
返回值:write函数从内存位置buf复制最多n个字节到描述符为fd的当前文件位置,返回值为-1表示错误,返回值为0表示EOF,否则返回实际传送的字节数量

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函数的参数个数不确定
printf调用了vsprintf和write
vsprintf:
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; 
} 

int vsprintf(char *buf, const char fmt, va_list args)
{
char
p;
char tmp[256];
va_list p_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); 

}
作用是实现格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
int getchar(void)
{
static char buf[BUFSIZ];
static char *bb=buf;
static int n=0;
if(n==0)
{
n=read(0,buf,BUFSIZ);
bb=buf;
}
return(–n>=0)?(unsigned char)*bb++:EOF;
}
getchar函数
getchar由宏实现:#define getchar() getc(stdin)。getchar有一个int型的返回值,当文件未结尾时,返回用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。当程序调用getchar时.程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才等待用户按键(这就是有些程序使用getchar函数消除回车的原理)。
由于getchar函数需要等待键盘输入,此为从I/O设备读入,为中断,属异步异常,系统会调用中断处理函数处理。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章学会了linux下IO设备的管理方法,了解了Unix IO和Unix IO函数,深入分析了printf函数和getchar函数的实现。
结论
hello.c源文件被预处理器进行预处理,写入头文件中的内容形成hello.i文件,编译器检查hello.i文件中是否有语法错误,若无错误,则将其翻译为汇编语言文件,而后,汇编器将其汇编为可重定位目标文件hello.o。可重定位目标文件经链接器与共享库函数链接生成可执行目标文件hello。可执行目标文件被shell进程调用execve函数加载运行。运行结束后,shell回收hello进程。过程中有VA通过TLB和页表翻译为PA的过程,以及进程的并发过程等过程。

附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.i修改后的源程序
hello.s汇编语言程序
hello.o可重定位目标程序
hello可执行目标程序
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1]深入理解计算机系统 Randal E.Bryant David R.O’Hallaron 机械工业出版社
[2] printf函数实现的深入剖析:https://www.cnblogs.com/pianist/p/3315801.html
[3]百度百科:段式管理
https://baike.baidu.com/item/%E6%AE%B5%E5%BC%8F%E5%AD%98%E5%82%A8%E7%AE%A1%E7%90%86/7291432?fr=aladdin
[4] 百度百科:getchar函数,https://baike.baidu.com/item/getchar/919709?fr=aladdin
[5] https://blog.csdn.net/luckyxiaoqiang/article/details/8669602 CSDN博客:动态内存分配(malloc/free)简单实现–隐式空闲链表
[6] https://blog.csdn.net/yfldyxl/article/details/81566279 CSDN博客: readelf命令使用说明

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值