程序人生-Hello’s P2P
2021/6/28 1:21:30
本文主要是介绍程序人生-Hello’s P2P,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 1190200917
班 级 1903012
学 生 郑瑞捷
指 导 教 师 史先俊
计算机科学与技术学院
2021年5月
摘 要
俺是Hello,额是每一个程序猿¤的初恋(羞羞……)
却在短短几分钟后惨遭每个菜鸟的无情抛弃(呜呜……),他们很快喜欢上sum、sort、matrix、PR、AI、IOT、BD、MIS……,从不回头。
只有我自己知道,我的出身有多么高贵,我的一生多么坎坷!
多年以后,那些真正懂我的大佬(也是曾经的菜鸟一枚),才恍然感悟我的伟大!
……………………想当年: 俺才是第一个玩 P2P的: From Program to Process
懵懵懂懂的你笨笨磕磕的将我一字一键敲进电脑存成hello.c(Program),无意识中将我预处理、编译、汇编、链接,历经艰辛-神秘-高贵-欣喜,我-Hello一个完美的生命诞生了。
你造吗?在壳(Bash)里,伟大的OS(进程管理)为我fork(Process),为我execve,为我mmap,分我时间片,让我得以在Hardware(CPU/RAM/IO)上驰骋(取指译码执行/流水线等);
你造吗?OS(存储管理)与MMU为VA到PA操碎了心;TLB、4级页表、3级Cache,Pagefile等等各显神通为我加速;IO管理与信号处理使尽了浑身解数,软硬结合,才使我能在键盘、主板、显卡、屏幕间游刃有余,
虽然我在台上的表演只是一瞬间、演技看起来很Low、效果很惨白。
感谢 OS!感谢 Bash!在我完美谢幕后为我收尸。 我赤条条来去无牵挂!
我朝 CS(计算机系统-Editor+Cpp+Compiler+AS+LD + OS +
CPU/RAM/IO等)挥一挥手,不带走一片云彩! 想想俺也是 O2O: From Zero-0 to Zero-0。
历史长河中一个个菜鸟与我擦肩而过,只有CS知道我的生、我的死,我的坎坷,“只有 CS
知道……我曾经……来…………过……”
**关键词:**程序;系统;
**
**
目 录
第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简介
P2P:意为From Program to
Process。从源文件到目标文件的转化是由编译器驱动程序完成的。hello.c文件经过cpp预处理,ccl编译,as汇编,ld链接后,最终生成可执行文件hello。最后在Linux系统中通过内置命令行解释器shell加载hello程序,为程序创建进程。
020:意为From Zero to
Zero。Shell通过execve,在子进程中加载hello。为hello创建新的区域结构,然后映射虚拟内存,设置程序计数器,使之指向代码段的入口点,载入物理内存。之后从main函数开始执行,CPU为hello分配时间片执行逻辑控制流。执行完成后shell父进程会回收hello进程,并且内核删除hello所有痕迹。
1.2 环境与工具
硬件环境:X64 CPU ,2.50GHz , 8G RAM
软件环境:Windows 10 64位 ,Vmware 14 ,Ubuntu 16.04 LTS 64 位
开发工具:gcc + gedit , Codeblocks , gdb edb
1.3 中间结果
hello.i:hello.c经预处理得到的ASCII码的中间文件。
hello.s:hello.i编译之后得到的一个ASCII汇编语言文件。
hello.o:hello.s汇编之后得到的一个可重定位目标文件。
hello:hello.s和标准的C库进行链接得到的可执行目标文件。
hello.objdump:hello.o的反汇编代码。
Hello1.objdump:hello的反汇编代码。
hello.elf:hello.o的elf格式文件。
hello1.elf:hello的elf格式文件。
1.4 本章小结
本章主要描述了什么是程序的P2P与020过程,并且介绍了本次实验所采用的环境与工具,列出了中间文件与其作用。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。
作用:把通过预处理的内建功能对一个资源进行等价替换,最常见的预处理有:文件包含,条件编译、布局控制和宏替换4种。
2.2在Ubuntu下预处理的命令
gcc –E hello.c –o hello.i
或是
cpp hello.c > hello.i
2.3 Hello的预处理结果解析
编译后的hello.c文件生成了hello.i文件,同时文件的大小变为原来的近100倍。打开hello.i,我们可以发现。
hello.c中的main函数没有发生变化,而宏定义#不见了,取而代之的是其相应的文件,占据了hello.i的大部分空间。这也说明了,预处理只是对资源进行一种等价代换,在这里是替换了头文件,并没有对函数主体做出改变。
2.4 本章小结
本章对程序的预处理过程进行了说明,并介绍了ubuntu中预处理c语言的方法,并且查看了预处理后的.i文件,加深了对预处理的理解。
第3章 编译
3.1 编译的概念与作用
概念:编译器将文本文件翻译成汇编文本文件,即.s文件。分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。
作用:翻译成更接近机器语言的汇编语言,使生成过程更加方便顺畅。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1 文件声明
.file 源文件名
.text
代码段(codesegment/textsegment)通常是指用来存放程序执行代码的一块内存区域。
.section 指示把代码划分成若干个段
.rodata 用于维护只读数据,比如:常量字符串、带 const 修饰的全局变量和静态变量等
.align 8 地址对齐的伪指令,用来指定符号的对齐方式
.string 字符串的存储位置
.global
用来让一个符号对链接器可见,可以供其他链接对象模块使用;告诉编译器后续跟的是一个全局可见的名字(可能是变量,也可以是函数名)
.type 指定是对象类型或是函数类型
3.3.2 数据
- 整型
(1) main函数的参数argc
(2) 局部变量int i
会被编译器储存在寄存器或者程序栈中,如:i被存储在-4(%rbp)
- 字符串
源程序中的两个字符串信息都被存储在rodata段。
LC0中表示的是默认输出,其中汉字用utf-8编码为三个字节
LC1中表示的是argc==3时的输出
- 指针数组
argv[]这个数组作为参数,其首地址被存放在%rsi
3.3.3 运算与操作
- 赋值
使用mov指令对寄存器进行赋值。
.2 算术计算
例如i++的操作被编译为了addl指令
i\<=7的操作被编译为了比较指令
- 数组/指针/结构体操作
通过add指令,将指向栈的指针,指向我们所需要的数据,完成对数组的操作,例如:argv[1],被编译为addq \$16,%rax。
3.3.4 控制转移
源程序有两处控制转移
-
if(argc!=4)
先进行比较,相等后进行跳转至.L2
-
for(i=0;i<8;i++)
和普通的跳转相似,也是进行比较判断后,跳转至需要执行的语句段。
i<=7时,在.L4执行,否则,继续往下执行.L3。
3.3.5 函数调用
(1) main函数
功能:主函数
参数:%edi存储着argc的值,是参数变量的个数
返回值:返回值为0,将%eax作为返回值
-
printf函数
参数:通过寄存器将字符串常量的地址传递给函数
-
exit函数
功能:
参数:将1作为参数放在%edi中作为参数传递
-
sleep函数
功能:
参数:通过%eax赋值给%edi
返回值:将返回值储存在%eax中,使用ret指令返回
-
getchar函数
功能:
返回值:将返回值储存在%eax中,使用ret指令返回
-
atoi函数
功能:
参数:将argv[3](字符串)通过%rdi传递给atoi函数。
3.4 本章小结
本章介绍了编译的概念和作用,演绎了编译的过程,并且对hello.s的汇编代码进行了解释。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编器将.s汇编程序翻译成机器语言,把这些机器语言指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中。
作用:翻译为机器语言方便计算机识别和执行。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
先通过指令readelf -a hello.o > hello.elf,获得hello.elf
然后开始分析
-
ELF头
ELF头部以一个16字节的序列开始,描述生成该文件的系统的字的大小和字节顺序。剩下的部分包含帮助链接器分析语法和解释目标文件的信息,其中包含ELF头大小、目标文件的类型、及其类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
-
节头部表
节头部表描述了不同节的位置和大小,其中目标文件中每个节都有一个固定大小的条目。具体的描述包括节的名称、类型、地址和偏移量等
-
重定位条目
重定位是一种用符号定义连接符号引用的过程。可重定位文件必须拥有描述如何去修改它们section
contents的信息,好让可执行或共享目标文件保存程序镜像的正确信息。可重定位条目就是这些数据。
-
符号表
存放在程序中定义和引用的函数和全局变量的信息
4.4 Hello.o的结果解析
用指令objdump -d -r hello.o > hello.objdump生成反汇编的文本文件hello.objdump。
将其与hello.s文件进行比较
可以发现有许多不同之处:
-
.s文件中还有许多的伪代码,而反汇编代码中没有
-
反汇编代码中采用16进制表示常数,而.s中采用10进制
-
关于跳转,反汇编代码中用相对偏移位置表示,而.s文件中是用标记的位置名称表示
4.5 本章小结
在本章中我们分析了hello.o的elf文件的信息,之后通过查看反汇编代码,比较与.o文件的差别,经过了汇编之后,我们的程序更容易被机器读懂。
第5章 链接
5.1 链接的概念与作用
概念:将各个模块之间相互引用的部分正确的衔接起来。它的工作就是把一些指令对其他符号地址的引用加以修正。链接过程主要包括了地址和空间分配、符号决议和重定向
作用:是所有所需的部件只整合为一个可执行文件
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的格式
先用指令readelf -a hello > hello1.elf,获得hello.elf
然后开始分析
- ELF头
它描述了可执行文件的总体格式,其中有关于执行的入口等信息
- 节头部表
对可执行文件的所有节信息进行了声明,包括大小,偏移量等信息
- 程序头
(1). INTERP:指定在程序已经从可执行映射到内存之后,必须调用解释器 (2). PHDR:保存程序头表 (3). LOAD:表示一个从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串),程序的目标代码等等 (4). DYNAMIC:保存了其他动态链接器(即,INTERP中指定的解释器)使用的信息 (5). NOTE:保存了专有信息 (6). GNU_STACK:堆栈段 (7). GNU_RELRO:在重定位之后哪些内存区域需要设置只读
-
段节
-
重定位节
连接器在处理目标文件时,须要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置。这些重定位的信息都记录在ELF文件表里面,对于每个须要重定位的代码段和数据段,都会有一个相应的重定位表。
5.4 hello的虚拟地址空间
使用edb打开hello的可执行文件后,可以在Data
Dump中找到hello的虚拟地址空间。可以发现ELF被映射到了0x401000.
其余各段依据表中所示,映射到相应位置。
5.5 链接的重定位过程分析
5.5.1 hello 与 hello.o 反汇编代码的比较
通过objdump -d -r hello > hello1.objdump 生成hello的反汇编代码文本文件。
与hello.o的反汇编代码比较可以发现
-
可执行文件中多出了很多个函数,为程序执行所必要的函数,如sleep、getchar等
-
地址编码方式的不同,可执行文件采用虚拟地址,而hello.o采用相对地址。
-
可执行文件,相较于hello.o增加了.init和.fini等节
5.5.2 链接重定位过程分析
重定位过程主要有以下两步
-
重定位节和符号定义
链接器将所有的同类型的节合并成一个新的节。然后链接器将内存地址赋给新的节,以及每个符号。
-
重定位节中的符号引用
链接器依赖于hello.o中的重定位条目,修改代码节和数据节中对每个
符号的引用,使得它们只想正确的内存地址。
5.6 hello的执行流程
-
调用start函数,地址0x40108F
-
调用__libc_start_main函数,地址0x4010C1
-
调用libc-2.27.so!__cxa_atexit
-
调用libc-2.27.so!__new_exitfn
-
调用__libc_csu_init
-
调用__libc_csu_init 0x4005c0
-
调用_init函数
-
调用libc-2.27.so!_setjmp函数
-
调用-libc-2.27.so!_sigsetjmp函数
-
调用–libc-2.27.so!__sigjmp_save函数
-
调用main
-
调用puts
-
调用exit
-
调用ld-2.27.so!_dl_runtime_resolve_xsave
-
调用ld-2.27.so!_dl_fixup
5.7 Hello的动态链接分析
通过查找直到_dl_init在0x00007fb81f4d3630位置
在执行_dl_int函数前:
从而找到该处地址,在执行_dl_init函数后:
在_dl_init函数执行之后global_offset表由全0地状态被赋值上相应地值.
5.8 本章小结
本章详细讨论了链接的过程,并且对hello的链接过程进行了分析。链接使得我们使得程序设计更加高效,有序。链接实现多个文件的合并,最终创建一个可执行的完整的程序,到此,一个可执行程序的建立就已完成。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
作用:进程为用户提供以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
作用:提供了一个操作系统中用户与系统内核进行交互的界面
处理流程:
1.读取输入的命令行,
2.shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号.元字符将命令行划分成小tokens.
3.程序块tokens被处理,检查是否是shell中所引用的关键字.
4.当程序块tokens被确定以后,
shell根据aliases文件中的列表来检查命令的第一个单词,如果这个单词在其中,执行替换操作并且处理过回到到第二部重新分割程序块tokens.
5.shell对符号~进行替换
6.shell对所有前面带有符号的变量进行替换
7.shell将命令行的内嵌命令表达式替换成命令
8.shell计算采用符号的变量进行替换
9.shell根据栏位分割符号将命令字符串重新划分为新的块tokens.
10.shell执行通配符 * ? []的替换
11.shell讲所有处理结果中用到的注释删除,并按照下面的顺序实行命令检查:
A.内建的命令
B.shell函数(用户自己定义的)
C.可执行的脚本
12.在执行前的最后一步是初始化所有的输入输出重定向
13.最后执行命令
6.3 Hello的fork进程创建过程
Shell(父进程)通过fork
函数创建一个新的运行的子进程。新的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈.子进程进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork
时,子进程可以读写父进程中打开的任何文件。
6.4 Hello的execve过程
在子进程中通过系统调用execve()可以将新程序加载到子进程的内存空间。这个操作会丢弃原来的子进程execve()之后的部分,而子进程的栈、数据会被新进程的相应部分所替换。即除了进程ID之外,这个进程已经与原来的进程没有关系了。
在shell中执行Hello程序。它也是首先调用execve()。这个系统调用的实际上Hello进程的父进程是shell进程,它是shell进程fork出来的一个子进程然后执行execve之后在执行的Hello,所以我们下面来看看这个execve的参数:
·filename:包含准备载入当前进程空间的新程序的路径名。既可以是绝对路径,又可以是相对路径。
·argv[]:指定了传给新进程的命令行参数,该数组对应于c语言main函数的argv参数数组,格式也相同,argv[0]对应命令名,通常情况下该值与filename中的basename(就是绝对路径的最后一个)相同。
·envp[]:最后一个参数envp指定了新程序的环境列表。参数envp对应于新程序的environ数组。
由于是将调用进程取而代之,因此对execve的调用将永远不能返回,也无需检查它的返回值,因为该值始终为-1,实际上,一旦返回就表明了错误,通常会有error值来判断。
6.5 Hello的进程执行
首先介绍几个概念:
·逻辑控制流:每个程序都会有一个程序计数器的值的序列,这个序列中的值的转变的序列就叫做控制流。
·时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
·用户模式和内核模式:用户模式就是执行应用程度代码,访问用户空间;内核模式就是执行内核代码,访问内核空间(当然也有权限访问用户空间)。
·上下文信息:执行进程所需要的信息状态。
接下来接受Hello的进程调用过程:
首先执行Hello程序,进程处于用户模式。当输入的参数不是3个时,会调用exit函数,终止并回收Hello进程;当输入的参数为3个时,会调用sleep函数,进程休眠,此时内核会保存Hello进程的上下文。之后再恢复进程的上下文,并开始进程。休眠了固定时间后,sleep函数传递一个信号,使控制传递给内核,恢复hello进程的上下文,执行hello进程。
6.6 hello的异常与信号处理
6.6.1 异常的分类
6.6.2 处理方法
1. 中断:中断是异步发生的,是来自处理器外部的 I/O
设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的。硬件中断的异常处理程序常常称为中断处理程序。
陷阱:陷阱是有意的异常,是执行一条指令后的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
故障:故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果故障处理程序能够修正这个错误,它就将控制返回给引起故障的指令,从而重新执行它,否则,处理程序返回到内核中的
abort 例程,abort 例程会终止引起故障的应用程序。
4. 终止:终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如 DRAM
或者 SRAM 位被损坏时发生的奇偶错误。终止处理程序不会将控制返回给应用程序。
6.6.3 hello实例
1. 无异常
这是hello程序正常执行的情况,后台此时已经没有hello进程正在执行。
-
不停乱按
与无异常的情况相似,但是在第一个回车按下前的所有输入字符都被作为输入读入,其余当作新的shell指令,但没有任何影响。
-
Ctrl + Z
输入ctrl + Z,将会发送一个SIGTSTP信号给shell,使hello进程挂起。
输入ps指令查看进程,发现hello进程被挂起
输入jobs指令,发现被停止的hello进程
输入pstree,以树状图显示进程间的关系
使用fg指令,使hello程序重新运行
使用kill指令杀死进程
6.7本章小结
本章介绍并分析了进程的创建,程序的运行,上下文的切换,并发程序的机理,介绍了一些异常,信号和处理函数。描述了一些命令行的作用和shell工作的简单原理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序代码经过编译后出现在汇编程序中地址。hello.o反汇编的输出文件中的地址就是逻辑地址。
线性地址:是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
虚拟地址:每一个虚拟地址对应与一个虚拟页,每一个虚拟页会映射一个磁盘空间的一页,如果要使用该数据,则会将该页载入内存,这样每个虚拟地址就对应与唯一的一个物理地址。
物理地址:目前CPU外部地址总线上的寻址物理内存的地址信号,用于内存级芯片的单元寻址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址是程序产生的和段相关二点偏移地址,需要转换成线性地址,再转换成物理地址才能访问。
一个逻辑地址由两部份组成,段标识符、段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。
索引号,是“段描述符(segment
descriptor)”,段描述符具体地址描述了一个段。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,由8个字节组成。
Base字段描述了一个段的开始位置的线性地址。
Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。
当段选择符中的T1字段=0,表示用GDT;若为1,表示用LDT。
GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
首先,给定一个完整的逻辑地址 [段选择符:段内偏移地址],
1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
3、把Base + offset,就是要转换的线性地址了。
还是挺简单的,对于软件来讲,原则上就需要把硬件转换所需的信息准备好,就可以让硬件来完成这个转换了。
7.3 Hello的线性地址到物理地址的变换-页式管理
首先Linux系统有自己的虚拟内存系统,Linux将虚拟内存组织成一些段的集合,段之外的虚拟内存不存在因此不需要记录。内核为hello进程维护一个段的任务结构即图中的task_struct,其中条目mm指向一个mm_struct,它描述了虚拟内存的当前状态,pgd指向第一级页表的基地址(结合一个进程一串页表),mmap指向一个vm_area_struct的链表,一个链表条目对应一个段,所以链表相连指出了hello进程虚拟内存中的所有段。
而物理内存被划分为一小块一小块,每块被称为帧(Frame)。分配内存时,帧是分配时的最小单位,最少也要给一帧。在虚拟内存中,与帧对应的概念就是页(Page)。
线性地址的表示方式是:前部分是虚拟页号后部分是虚拟页偏移。
CPU通过将逻辑地址转换为虚拟地址来访问主存,这个虚拟地址在访问主存前必须先转换成适当的物理地址。CPU芯片上叫做内存管理单元(MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址。然后CPU会通过这个物理地址来访问物理内存。
页表就是一个页表条目(PTE)数组,虚拟地址空间中的每个页在页表中的一个固定偏移量处都有一个PTE。PTE是由一个有效位和一个n个字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置。
MMU利用虚拟页号(VPN)来在虚拟页表中选择合适的PTE,当找到合适的PTE之后,PTE中的物理页号(PPN)和虚拟页偏移量(VPO)就会组合形成物理地址。其中VPO与PPO相同,因为虚拟页大小和物理页大小相同,所需要的偏移量位数也就相同。此时,物理地址就通过物理页号先找到对应的物理页,然后再根据物理页偏移找到具体的字节:
1.如果有效位是 0+NULL 则代表没有在虚拟内存空间中分配该内存;
2.如果是有效位 0+非 NULL,则代表在虚拟内存空间中分配了但是没
有被缓存到物理内存中;
3.如果有效位是 1 则代表该内存已经缓存在了物理内存中,可以得到其 物理页号
PPN,与虚拟页偏移量共同构成物理地址 PA。
7.4 TLB与四级页表支持下的VA到PA的变换
首先虚拟地址是由VPN和VPO组成的,VPN可以作为在TLB中的索引,TLB可以看作是一个PTE的cache,将常用的PTE缓存到TLB中,加速虚拟地址的翻译。TLB是具有高相连度的,应该是为了一次多存一些PTE。如果能够在TLB中找到与VPN对应的PTE,即为TLB
hit,TLB直接给出PPN,然后PPO即为VPO,这样就构成了一个物理地址。
如果不能做到TLB
hit就要到四级页表当中取寻址,在i7中VPN有36位,被分成了四段,从左往右的前三个九位的地址分别对应于在前三级页表当中的偏移,偏移在页表中所对应的页表条目指向某一个下一级页表,而下一个9位VPN就对应的是在这个页表中的偏移。最后一级页表中的页表条目存放的是PPN
比如VPN1在第一级页表中对应于一个页表条目,这个页表条目指向下一级页表中的某个页表,再依靠VPN2在这个页表中找到它对应的页表条目,同样,这个也表条目指向的是第三级页表中的某个页表,再依靠VPN3找到在这个页表中与之对应的页表条目,这个页表条目指向的是第四级页表中的某个页表,再依靠VPN4找出与之对应的页表条目,这个页表条目中存放的是PPN,在四级页表中最多可以存放512G的内存内容,显然一般是用不了那么多的。
最后再把VPO拿来当成PPO就能找到在对应的物理页上存放的内容了。
7.5 三级Cache支持下的物理内存访问
获得了物理地址VA之后,使用CI(倒数7-12位)进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中。根据数据偏移量CO(后6位)取出数据返回。
如果没有匹配成功或者匹配成功但是标志位是1,则不命中,向下一级缓存中查询数据(L2
Cache->L3
Cache->主存),查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU(Least
frequently used)进行替换。也就是替换掉最不经常访问的一次数据。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制,当fork函数在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当两个进程中的任一个后来进行写操作时,写时复制就会创建新页面。因次,也就为每个进程保持了私有地址空间的概念。
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的.代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
3.映射共享区域,hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障:当cpu引用的虚拟地址所在的虚拟页的PTE有效位为0,即所对应的虚拟页不再内存,会引发缺页故障异常。
缺页中断处理:缺页异常调用内核的缺页异常处理程序。程序选择内存中的一个页作为牺牲页,如果这个页被修改过(修改位被设置),则将该页写回磁盘;然后按照目标虚拟页的PTE的磁盘地址,将磁盘的页取出放内存中,同时修改PTE。然后返回程序中断处的当前指令,继续请求访问该虚拟地址。
7.9动态存储分配管理
7.9.1 动态内存分配的基本原理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
7.9.2 简单的放置策略
按空闲区首址递增的顺序——首次适应算法(最先适应法)
按空闲区首址递减的顺序
按空闲区大小的递增顺序——最佳适应法
按空闲区大小的递减顺序——最坏适应法
7.9.3 两种堆的数据结构组织形式
隐式空闲链表:
每个块包含一个4字的头部和4字的相同信息的尾部,分别再块的开始和结尾,其中存储了块的大小和块的分配位。(如果式双子对齐,后3位储存分配位),分配位为1则表示该块已分配;位0则表示该块式空闲的。每次寻找会根据的堆的头部分配位判断是否是空闲的,大小值判断是否适合存储,并根据大小信息找到一个块的块头。尾部则是在空闲块合并时,提供前一个块的大小和是否位空闲块的信息。
显示空闲链表:
再空闲块中,除了头部和尾部,还存在指向前一个空闲块和后一个空闲块的2个指针,通过指针可以找到所有的空闲块。
7.10本章小结
本章介绍了存储地址空间的四个概念,介绍了段式管理和变换-页式管理的方、TLB与四级页表支持下的VA到PA的变换、三级cache支持下物理内存访问,
hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等内容。
本章对我们编写一些对高速缓存友好的代码,有着极大的帮助。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射位文件地方式,允许linux内核引出一个简单,低级地应用接口,称为Unix
I/O ,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix IO接口:
-
打开文件:内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。
-
Linux
shell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,他们可用来代替显式的描述符值。 -
改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。
-
读写文件,读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k。
-
关闭文件。当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中
函数:
-
open函数:将filename转换为一个文件描述符,并返回描述符数字。
int open(char *filename ,int flags, mode_t mode) -
close函数:关闭一个打开的文件
int close(int fd); -
read函数:用来对文件的输入
ssize_t read(int fd , void *buf, size_t n); -
write函数:用来对文件的输出
ssize_t write(int fd ,void *buf ,size_t n);
8.3 printf的实现分析
首先来看看printf函数的函数体
参数中fmt为指针指向一个可变长的字符串的起始位置,接下来分析printf内部的函数调用
-
vsprintf
使用参数列表发送格式化输出到字符串。
其作用就是以fmt产生格式化字符串,并输出。
-
write
如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中
-
syscall
这个函数的功能就是不断的打印出字符,直到遇到:’\0’ 停止。
综上printf的实现为:
-
格式化fmt,并产生格式化输出
-
通过执行syscall指令实现对系统服务的调用,使内核执行打印操作。通过vram(显存)对字符串进行输出。
-
显示芯片将按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),最终实现printf中字符串在屏幕上的输出。
8.4 getchar的实现分析
首先来看看getchar函数的函数体
该函数通过read函数来实现。
ssize_t read (int fd, void *buf, size_t count);
参数count是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读写位置向后移。注意这个读写位置和使用C标准I/O库时的读写位置有可能不同,这个读写位置是记在内核中的,而使用C标准I/O库时的读写位置是用户空间I/O缓冲区中的位置。比如用fgetc读一个字节,fgetc有可能从内核中预读1024个字节到I/O缓冲区中,再返回第一个字节,这时该文件在内核中记录的读写位置是1024,而在FILE结构体中记录的读写位置是1。注意返回值类型是ssize_t,表示有符号的size_t,这样既可以返回正的字节数、0(表示到达文件末尾)也可以返回负值-1(表示出错)。
read函数返回时,返回值说明了buf中前多少个字节是刚读上来的。有些情况下,实际读到的字节数(返回值)会小于请求读的字节数count,例如:读常规文件时,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有30个字节而请求读100个字节,则read返回30,下次read将返回0。
当用户敲下回车后,函数从stdio流中读取一个字符,当到达文件结尾,将用户输入回显到屏幕。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章学会了linux下IO设备的管理方法,了解了Unix IO和Unix
IO函数,深入分析了printf函数和getchar函数的实现。
结论
最开始hello是以代码的形态存在的,即hello.c。之后,在预处理器中,hello.c经过预处理,与外部库结合成为hello.i。紧接着,在编译器中,编译过后,成为hello.s,在经过汇编器重定位为hello.o。最后,链接器把hello.o进行链接,成为真正的可执行文件hello。
之后开始运行程序。在bash中输入指令,运行hello。Bash会新建一个进程,然后清空当前进程的数据并加载hello,从函数的入口进入,开始执行。我们还可以输入指令挂起正在执行的进程。Hello输出信息,需要调用Unix
I/O中的write和read函数,而它们的实现需要借助系统调用I/O,在最后结束之后bash等到exit,作为hello的父进程回收hello。之后,内核会删除有关hello的一切信息。至此,hello的旅程就此结束。
对于hello的学习,让我们对于程序在计算机深层的表示形式有了更深入的理解,基于该理解我们才能编写出,对系统友好的更优秀的程序。
附件
hello.i:hello.c经预处理得到的ASCII码的中间文件。
hello.s:hello.i编译之后得到的一个ASCII汇编语言文件。
hello.o:hello.s汇编之后得到的一个可重定位目标文件。
hello:hello.s和标准的C库进行链接得到的可执行目标文件。
hello.objdump:hello.o的反汇编代码。
Hello1.objdump:hello的反汇编代码。
hello.elf:hello.o的elf格式文件。
hello1.elf:hello的elf格式文件。
参考文献
-
Pianistx. (2013 年 9 月 11 日). [转]printf 函数实现的深入剖析
https://www.cnblogs.com/pianist/p/3315801.html -
Randal E.Bryant: David R.O’Hallaron 深入理解计算机系统. 机械工业出版社
-
shell命令行处理流程 https://blog.51cto.com/evillinux/1192072?source=dra
这篇关于程序人生-Hello’s P2P的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23Springboot应用的多环境打包入门
- 2024-11-23Springboot应用的生产发布入门教程
- 2024-11-23Python编程入门指南
- 2024-11-23Java创业入门:从零开始的编程之旅
- 2024-11-23Java创业入门:新手必读的Java编程与创业指南
- 2024-11-23Java对接阿里云智能语音服务入门详解
- 2024-11-23Java对接阿里云智能语音服务入门教程
- 2024-11-23JAVA对接阿里云智能语音服务入门教程
- 2024-11-23Java副业入门:初学者的简单教程
- 2024-11-23JAVA副业入门:初学者的实战指南