计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 120L022023
班 级 2003012
学 生 谢扬
指 导 教 师 郑贵滨
计算机科学与技术学院
2021年5月
本文介绍了Hello程序的执行原理与过程,通过gcc,gdb,edb等各种工具研究了Hello程序如何经预处理、编译、汇编、链接生成可执行文件,并分析了其在运行过程中计算机系统对它进行的进程管理、存储管理和IO管理过程,以及最后被回收的过程。通过对Hello程序整个生命周期的探索,我们对计算机系统形成了更深一步的理解。
关键词:预处理;编译;汇编;链接;进程;虚拟内存;地址翻译;I/O;
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 5 -
3.2 在Ubuntu下编译的命令............................................................................. - 6 -
4.2 在Ubuntu下汇编的命令............................................................................. - 7 -
5.2 在Ubuntu下链接的命令............................................................................. - 8 -
5.3 可执行目标文件hello的格式.................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 10 -
6.3 Hello的fork进程创建过程..................................................................... - 10 -
6.6 hello的异常与信号处理............................................................................ - 10 -
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 -
8.1 Linux的IO设备管理方法.......................................................................... - 13 -
8.2 简述Unix IO接口及其函数....................................................................... - 13 -
第1章 概述
1.1 Hello简介
P2P(Program to Process):
在linux环境下,hello的 P2P的过程指从hello的源程序经过各种处理,生成可执行程序,转化为进程并运行的过程。具体过程包括:
- hello.c经过cpp的预处理得到hello.i文件
- 经过cc1编译生成hello.s的汇编文件
- 经过as的处理便为可重定位目标文件hello.o
- 最后由ld链接生成可执行文件hello。
- 用户通过shell键入./hello命令开始执行程序
- shell通过fork函数创建一个子进程
- 由子进程执行execve函数加载hello。
020(Zero-0 to Zero-0):
020过程指的是在execve执行hello程序后,内核为hello进程映射虚拟内存。在hello进入程序入口后,hello相关的数据就被内核加载到物理内存中,hello程序开始正式被执行。为了让hello正常执行,内核还需要为hello分配时间片、逻辑控制流。最后,当hello运行结束,终止成为僵尸进程后,由shell负责回收hello进程,删除与hello有关的数据内容。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;
Ubuntu 16.04 LTS 64位/优麒麟 64位
开发与调试工具:gcc,vim,edb,readelf,HexEdit
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.i:hello.c预处理后的文本文件
hello.s:hello.i编译后的汇编文件
hello.o:hello.s汇编后得到的可重定位目标文件
hello:hello.o链接后得到的可执行目标文件
helloelf.txt:hello.o的elf格式,分析汇编器和链接器行为
hello.txt:可执行hello的elf格式,作用是重定位过程分析
1.4 本章小结
本章概述了 hello.c程序P2P和O2O的基本过程,介绍了实验的环境与工具以及实验生成的中间文件。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器cpp会处理c语言程序中所以#开头为格式的命令,包括宏定义,指令头部文件,并且修改原始的c程序,将引用的c语言库合并为一个文本文件。
作用:在处理头文件时,预处理的作用是将其中的定义都添加至输出文件中供编译程序处理;在处理宏定义时,预处理器会根据编译命令将源程序的排除在外的语句转成空行;处理条件编译指令时,预处理器根据我们的需要将不必要的代码过滤;预编译程序还可以识别一些特殊的符号,在源程序中出现的这些字符将用合适的值进行替换。
2.2在Ubuntu下预处理的命令
命令:gcc hello.c -E -o hello.i
2.3 Hello的预处理结果解析
经过预处理之后,hello.c变为hello.i文件,打开该文件可以发现,文件变为3000多行内容大大增加,且仍为可以阅读的C语言程序文本文件。对原程序中的宏进行了宏展开,头文件中的内容被包含进该文件中。例如声明函数、定义结构体、定义变量、定义宏等内容,如果代码中有#define命令还会对相应的符号进行替换。同时,预处理删除了原来的程序主体段的注释信息,而保留了写入函数的主体部分。
2.4 本章小结
本章介绍了预处理概念与作用,以及在Linux下预处理的操作,简要分析了生成的hello.i文件。
第3章 编译
3.1 编译的概念与作用
编译:编译器ccl将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。
作用:将源程序翻译成目标程序,同时具备语法检查、词法分析、调试手段、目标程序优化等功能。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
通过对于hello.s文件内容的观察可以发现,其中的内容被按照汇编代码的转化格式,依一定结构进行了转化。
3.3.0:汇编指令介绍:
.file:声明源文件
.text:代码节
.rodata:只读代码段
.align:数据或者指令的地址对其方式
.string:声明一个字符串(.LC0,.LC1)
.global:声明全局变量(main)
.type:声明一个符号是数据还是函数类型
3.3.1数据
1.字符串
程序中的两个字符串都在只读数据段中。
2.局部变量
main函数声明了局部变量后,编译器进行编译的时候会将局部变量入栈存储,通过变量在栈中的偏移量访问变量。如图所示,hello.s中局部变量i放在栈上-4(%rbp)的位置。
hello.c中还有其他的局部变量,比如argc和argv,它们同样也都存放在栈上的,并通过相对栈顶(%rsp)的偏移量来访问。
3.数组
向main函数中传参int argc,char *argv[],可以看出argv首地址在栈中的位置为-32(%rbp),被多次调用传给printf。
4.立即数
立即数以$开头,直接在代码中体现:
3.3.2:赋值操作
Hello.c中的赋值操作在汇编代码中以mov指令体现,在hello.s中大量出现。如图:
3.3.3 类型转换
Hello.c中atoi()将字符串类型的argv[3]转换成整形,其他的类型转换还有int、float、double、short、char之间的转换。
3.3.4 算数操作
程序中有i的自增计算,在汇编文件中体现如下:
3.3.5关系操作及控制转移
其实现方式为,利用cmp语句对于两值进行比较,cmp后的跳转语句决定了其接收的情况。如图:
此为小于等于7时跳转,否则向下执行;
此为等于4时跳转,否则向下执行。
3.3.6:函数:
hello.c中涉及的函数操作有:
main,printf,exit,sleep,getchar,atoi
调用函数时会发生以下操作:
程序计数器会在该函数进行的时候设置为该函数代码的起始地址,返回时将计数器设置为调用该函数的下一条指令。且被调用的函数返回值存储在%rax中。在调用该函属实还要为局部变量分配空间,返回时释放空间,还原栈帧。即main函数调用结束时,调用leave指令,把栈的空间进行恢复,让栈的空间保持跟调用前一样的状态,然后调用ret返回。
3.4 本章小结
本章简单的描述了编译器初步将高级语言转化为汇编语言的过程,根据hello程序中使用的各种数据类型,跳转操作和函数调用等操作对hello.s程序进行分析,并分析编译后的汇编指令这样编写的理由。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编器(as)将汇编文件转换为二进制可重定位文件的过程。
作用: 将汇编代码转换为机器指令,并将其打包为重定位目标程序的格式,生成.o文件,为程序的链接与运行做准备。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
在linux下生成hello.o文件elf格式的命令:readelf -a hello.o > hello.elf
4.3.1 ELF头:
ELF头以16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。
通过上图信息可以得知程序的很多信息。
4.3.2节头表:
该部分包含了各节的名称、大小、类型、地址、偏移量。每个节有不同的读写权限、对齐大小。因为这是用来重定位的文件,所以每个节的起始地址都是0以方便重定位的进行。
4.3.3 符号表
符号表.symtab包含了程序中的函数、全局变量的名称、类型、大小、vis等信息。
其中size是目标的大小,type是数据或函数类型。Bind表示符号是否是全局的。
4.3.4:重定位节:
包含了text节中需要进行重定位的信息,链接器链接这些文件的时候需要修改位置。其中偏移量是需要修改的引用节的偏移量。加数是一个有符号int数,重定位是使用它对引用值做调整。
4.4 Hello.o的结果解析
在使用objdump -d -r hello.o反汇编之后,可以得到如下图的反汇编代码:
与汇编语言进行对比,可以发现:
1.在反汇编中立即数使用十六进制计数,而汇编代码则使用十进制。同时,在语句后的位数限制字符(如w,l,q)可能缺少或增加
2.在分支转移上,汇编语言跳转使用的是段名称,如L1、L2,而反汇编中则直接跳转到每一行最前面的逻辑地址,这是由于汇编后L1,L2这样的符号被替换成了相应的地址;
3.函数调用时,汇编语言是对函数的名称进行call操作,而反汇编中则是对于一个以主函数地址为基址的一个偏移地址进行call操作;
4.5 本章小结
本章介绍了汇编的概念与作用,生成了二进制可重定位目标文件,通过指令查看了ELF文件的相关内容,然后将反汇编文件与汇编程序对比,发现二者的联系。
第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的格式
使用如下命令:
5.3.1 ELF头
可以看到,其文件类型变为了EXEC(可执行文件),且section headers数变为27。
5.3.2 节头表
部分节头表如图所示:
5.3.2 程序头表
如图所示:
5.3.4 Dynamic section
5.3.5 重定位节
5.3.6 符号表
符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。部分符号表内容如下:
5.4 hello的虚拟地址空间
使用edb加载hello,由图知虚拟空间从0x401000开始,到0x401ff0结束。
同时可以通过查询节头表得到相应各个节的信息,如.text虚拟地址开始于0x4010f0,字节偏移为0x10f0。
在edb中查询如下:
5.5 链接的重定位过程分析
命令:objdump -d -r hello > hello2.txt ,获得反汇编代码。对hello与hello.o进行分析。
首先,我们观察hello.o的反汇编代码。可以观察到,hello.o的地址是0开始的,是相对地址,有许多地方正等待进行链接。而观察hello文件的反汇编代码, 会发现hello的地址是从0x401000(_init的地址)开始的,是已经经过重定位的虚拟地址。同时,hello文件中除了main函数以外,还多了很多经过重定位之后的函数,如_init、puts@plt。
。
接下来,对重定位节进行符号引用,在这一步中,链接器将修改对代码和数据中每个符号的引用,以指向正确的运行时地址。要执行这一步,链接器依赖可重定位模块中的可重定位条目的数据结构。
5.6 hello的执行流程
程序按以下顺序运行:
0x0000000000401000: hello!_init
0x0000000000401000: hello!_init
0x0000000000401020: hello!.plt
0x0000000000401030: hello!puts@plt
0x0000000000401040: hello!printf@plt
0x0000000000401050: hello!getchar@plt
0x0000000000401060: hello!atoi@plt
0x0000000000401070: hello!exit@plt
0x0000000000401080: hello!sleep@plt
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。
查看hello的ELF文件可以得到:
GOT表位置在调用dl_init之前:
在调用dl_init之后:
可以看到表中内容发生了变化。
5.8 本章小结
本章介绍了链接过程。解析了如何将.o文件生成可执行文件,分析了可执行文件的ELF表信息,据此分析了重定位的过程,查看了可执行文件的虚拟地址,最后还给出了hello的动态链接的分析。
第6章 hello进程管理
6.1 进程的概念与作用
进程是程序在计算机上的运行活动。系统中资源分配和调度的基本单位。以及操作系统结构的基础在早期的计算机结构中工艺设计进程是程序的基本执行实体。在面向线程设计的当代计算机体系结构中。进程是线程的存储库。程序是对指令、数据和组织的描述,而进程是程序的组成部分。
功能:
当像 hello 这样的程序在现代系统上运行时,操作系统可以给进程一种错觉:认为它是系统上运行的唯一程序,并且该程序似乎独占使用处理器、主存储器和 IO 设备, 处理器似乎是一个接一个地不间断地执行程序中的指令,即程序的代码和数据是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
shell是命令行界面的解析器,能够为用户提供操作界面,提供内核服务。shell能执行一系列的读、求操作,然后终止。读操作读取来自用户的一个命令行。求值操作解析命令并代表用户运行程序。
shell的处理流程为:
1.读取用户输入
2.解析用户输入
3.若要执行内部命令,直接执行
4.若要执行非内部命令,shell会fork子进程,在子进程中execve执行相关命令
5.根据&的有无,确定程序的前后台运行
6.3 Hello的fork进程创建过程
终端程序通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的 逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
6.4 Hello的execve过程
创建shell子进程后子进程调用 exceve 函数来加载和运行新程序。这就是当前子进程上下文中的 hello 程序。需要执行以下步骤:
1删除已有的用户空间。删除上一步的用户部分中的现有结构。
2生成新的代码、数据、堆和栈段。整个空间的结构是私有的。虚拟地址和数据空间代码空间映射到 hello 文件的 .txt 和 .data 区域。.bss 初始化为0。匿名文件映射以及 hello 文件中包含的文件的大小堆栈和堆空间也请求默认长度为零的二进制密码。
3共享空间映射 如果一个Hello 程序与libc.so 标准C 库等共享对象相关联,则这些对象与程序是动态关联的。然后将其映射到用户虚拟地址空间中的共享空间。
4 设置程序计数器(PC) exceve 做的最后一件事就是将当前进程上下文中的PC设置为指向代码区域的开头。
6.5 Hello的进程执行
1.逻辑控制流和时间片:
这个过程的操作本质上是CPU不断地从程序计数器PC指示的地址中取出指令并执行它们。值的序列称为控制的逻辑流。操作系统会调度进程的运行,执行进程A->上下文切换->执行进程B->上下文切换->执行进程A->...等等。在进程执行期间的某个时刻,内核可以决定抢占当前进程并重新启动先前被抢占的进程。这个决定称为调度,由内核中称为调度程序的代码处理。当内核选择一个新进程来运行时,我们说内核调度该进程。内核调度一个新进程运行后,它会抢占当前进程并使用上下文切换机制将控制权转移给新进程。当一个程序被调用并开始被另一个进程中断时,其间的时间就是运行时片。
2.用户模式和内核模式:
Hello通常在用户模式进行,访问权限受限,个别函数调用使其进入内核模式,有完全的权限
3.上下文切换:
如果系统调用在等待事件时被阻塞,内核可以让当前进程休眠并切换到另一个进程。上下文是内核重新启动抢占进程所需的状态,是一种比较高级的异常控制。流动。
4.调度:
在调度进程的过程中,操作系统主要做了两件事:加载保存的寄存器和切换虚拟地址空间。
6.6 hello的异常与信号处理
6.6.1.异常和信号异常种类:
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令或终止
终止 不可恢复的错误 同步 不会返回
6.6.2.实例:
- 正常运行
- 按下Ctrl-C
在键盘上输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业。
Ps进行查看,结果如图:
可见前台没有hello进程。
- 按下Ctrl-Z
输入ctrl-z默认结果是挂起前台的作业,hello进程并没有回收,而是运行在后台下。
Ps进行查看,结果如图:
可见hello进程仍然存在。
使用fg命令使其回到前台后如图:
可见其能够继续运行至正常结束。
使用jobs指令可以看到被停止进程:
使用kill命令可以正常终止之:
- 随意按键
易知程序运行不受到影响。
6.7本章小结
本章介绍了进程的概念与作用,以及Shell-bash的基本概念。针对进程,在这一章中根据hello可执行文件的具体示例研究了fork, execve函数的原理与执行过程。在hello运行过程中,内核有选择对其进行管理,决定何时进行上下文切换。并且当接受到不同的异常信号时,异常处理程序将对异常信号做出相应,执行相应的代码,每种信号都有不同的处理机制,对不同的异常信号,hello也有不同的处理结果。
第7章 hello的存储管理
7.1 hello的存储器地址空间
物理地址:加载到内存地址寄存器中的地址,内存单元的真实地址。 CPU通过地址总线的寻址找到真正物理内存对应的地址。
线性地址:地址空间中的整数是连续的,则称为线性地址空间。线性地址是逻辑地址到物理地址转换之间的中间层。在一个分段单元中,逻辑地址就是段中的偏移地址,再加上基地址就是线性地址。
逻辑地址:指程序生成的与段相关的偏移地址部分,以[段标识符:段内偏移]的形式表示,由段标识符加上相对地址在段内的偏移组成段。通过地址翻译器或映射函数可以把逻辑地址转化为物理地址。
虚拟地址:程序访问内存所使用的地址。其允许每个数据对象有多个独立的地址,其中每个地址都选自一个不同的地址空间。也即是线性地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,如图所示:
所有的段由段描述符描述,而多个段描述符能组成一个数组,我们称成功数组为段描述表。段描述符中的BASE字段对我们翻译线性地址至关重要的。
BASE字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。
为了得到BASE字段,我们利用索引号从GDT(全局段描述表)或LDT(局部段描述符表)中得到段描述符。选择GDT还是LDT取决于段选择符中的T1,若T1等于0则选择GDT,反之选择LDT。
这样我们就得到了BASE。最后通过BASE加上段偏移量就得到了线性地址。
-
- Hello的线性地址到物理地址的变换-页式管理
虚拟内存:存放在磁盘上、有N个连续字节的数组。
磁盘上,这个数组的内容被缓存在物理内存中,这些缓存块被称为页 (页面大小为P = 2p字节),此时物理页和虚拟页之间之间就会建立一个映射的关系,如图:
虚拟页面的三种状态:
(1)缓存:已经缓存在物理内存中的已分配页。
(2)未缓存:未缓存在物理内存中的已分配页。
(3)未分配/创建
当虚拟内存与物理内存的映射建立时,称作页表。页表是一个页表条目的数组,将虚拟页地址映射到物理页地址。DRAM中的每个进程都有自己的页表。
对地址进行翻译时,首先将n位虚拟地址拆分成p为的虚拟页面偏移VPO和n-p位的VPN,通过VPN找到页表,并通过页表来获得虚拟页号,将m-p位的物理页号和p位的虚拟页面偏移组合在一起(虚拟页面偏移等价于物理页面偏移,因为物理内存映射的是虚拟内存的一整页。)就得到了m位的物理地址。如图:
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7采用四级页表的层次结构。具体如下:
CPU产生虚拟地址VA,虚拟地址VA传送给MMU,MMU使用VPN高位作为TLBT和TLBI,向TLB中寻找匹配。
如果命中,则得到物理地址PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成物理地址PA,添加到PLT。
7.5三级Cache支持下的物理内存访问
对于一个虚拟地址请求,CPU首先将去TLB寻找,看是否已经在TLB中缓存。如果命中的话就直接MMU获取,没有命中的话就先在结合多级页表,得到物理地址PA,L1 Cache对PA进行分解,将其分解为标记(CT)、组索引(CI)、块偏移(CO),检测物理地址是否L1 cache命中,若命中,则直接将PA对应的数据内容取出返回给CPU,若不命中则在下一级中寻找,并重复L1 cache中的操作。如图
7.6 hello进程fork时的内存映射
当fork函数被当前进程hello调用时,内核为新进程hello创建各种数据结构,并分配给它一个唯一的PID。为了给这个新的hello创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当着两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行新程序a.out的步骤:
1.删除已有页表和结构体vm_area_struct链表 ;
2.创建新的页表和结构体vm_area_struct链表 ;
3.代码和初始化的数据映射到.text和.data区(目标文件提供);
4..bss和栈映射到匿名文件设置PC,指向代码区域的入口点 ;
5.Linux根据需要换入代码和数据页面.
如图:
7.8 缺页故障与缺页中断处理
当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。此时有可能会出现不同的情况,故需要缺页处理程序对与程序进行检查:
(1)地址是否合法? 程序搜索区域链表,确认地址是否在(合法的)某个区域内。
若不在,则判定非法,触发段错误 ,从而终止这个进程。
(2)访问是否合法? 检测程序是否有读、写或执行区域内页面的权限。
若没有,则违反许可,触发保护异常,从而终止这个进程。
如图:
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆,系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高地址)。对于每个进程,内核维护着一个变量brk,它指向对的顶部。
分配器将堆视为一组不同大小块(blocks)的集合,每个块要么是已分配的,要么是空闲的。
分配器的类型:
显式分配器: 要求应用显式地释放任何已分配的块,例如,C语言中的 malloc 和 free
使用边界标记的堆块的格式其中头部和脚部分别存放了当前内存块的大小与是否已分配的信息。通过这种结构,隐式动态内存分配器会对堆进行扫描,通过头部和脚部的结构实现查找。
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配和最佳适配。分配器在面对释放一个已分配块时,可以合并相邻的空闲块,其中一种简单的方式,是利用隐式空闲链表的边界标记来进行合并。
隐式分配器: 应用检测到已分配块不再被程序所使用,就释放这个块,比如Java,ML和Lisp等高级语言中的垃圾收集。
要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。在显式空闲链表中,可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
7.10本章小结
本章主要介绍了hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理的相关内容,对hello的存储管理有了较为深入的讲解。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件,所有的I/O设备都被模型化为文件,所有的输入和输出都能被当做相应文件的读和写来执行。
设备管理: unix io接口,Linux内核允许引出一个简单、低级的应用接口,称为Unix I/O。通过其所有的输入和输出都能以一种统一且一致的方式来执行。
对文件可行的操作:
·打开关闭操作open和close;
·读写操作read和write;
·改变当前文件位置lseek等
8.2 简述Unix IO接口及其函数
8.2.1 Unix I/O接口:
根据8.1中描述的Unix I/O接口的概念,可知I/O接口需要有如下结构功能:
打开文件:通过内核代开文件,内核返回非负整数,成为描述符。描述符表示这个文件。内核记录有关文件的所有信息。
文件位置。每个打开的文件,内核保持一个文件位置k,表示从文件开头起始的字节偏移量。
读写文件。进行复制操作并改变文件位置k的值。
关闭文件。内核释放相应数据结构,将描述符恢复到可用的描述符池中。
8.2.2 Unix I/O函数:
1、打开文件函数: int open(char *filename, int flags, mode_t mode);
flag参数为写提供一些额外的指示,mode指定了访问权限。
2、关闭文件函数: int close(int fd);
fd是打开文件时的返回值。
3、读文件函数: ssize_t read(int fd, void *buf, size_t n);
4、写文件函数: ssize_t write(int fd, const void *buf, size_t n);
8.3 printf的实现分析
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;
}
可以发现,其中调用了一个vsprintf(buf, fmt, arg)函数。这里查看其实现得:
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分量)。
这样最终便实现了printf的输出。
8.4 getchar的实现分析
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;
}