计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机系统
学 号 1190201012
班 级 1903011
学 生 张中界
指 导 教 师 史先俊
计算机科学与技术学院
2021年5月
摘 要
本文主要阐述hello程序运行在linux系统中的生命历程,探讨hello程序经过预处理、编译、汇编、链接、运行的全过程,并结合课程知识阐述计算机系统的进程管理、存储管理及I/O管理的相关内容,通过对hello生命周期的探索,更加深入地理解计算机系统。
关键词:计算机系统;预处理;编译;汇编;链接;进程;存储;虚拟内存;I/O;
(CSDN链接:(4条消息) padisah的博客_CSDN博客-领域博主)
目 录
第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 -
P2P:hello.c经cpp预处理得到.i文件,ccl对其编译得到.s汇编语言文件。然后通过汇编器cs将汇编语言翻译成机器语言,再通过ld的链接生成可执行文件。Shell调用fork为其产生一个进程,于是hello便从程序变成了进程。
020:shell调用execve在进程上下文中加载hello,映射虚拟内存,为其创建新的区域结构,然后载入物理内存,进入main函数执行代码,CPU为其分配时间片执行逻辑控制流,程序执行完后,父进程shell回收子进程,内核更新相关的数据结构。
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位
开发与调试工具:gcc,gdb,gedit,edb,readelf等
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名称 | 文件描述 |
hello.c | 源文件 |
hello.i | 预处理之后的文件 |
hello.s | 编译之后的文件 |
hello.o | 汇编得到的可重定位目标文件 |
hello.out | 链接之后的可执行目标文件 |
helloelf.txt | hello.o的反汇编结果 |
本章主要介绍了hello的P2P、020过程及生成的中间文件。
预处理一般是指在程序源代码被翻译为目标代码的过程中,生成位进制代码之前的过程。
预处理指令的作用是使源代码在不同的执行环境中被方便地修改或编译。
gcc –m64 –no-pie –fno-PIC -E hello.c -o hello.i
图表
预处理中会展开以#起始的行,并解释为预处理指令。如源文件中对头文件的引用被替换为文件的内容,宏常量被替换成定义的值,等等。
图表 2
宏常量N被替换为它的值4:
图表 3
图表 4
本章介绍的预处理的概念及作用,展示并分析了hello.c文件预处理后发生的变化。
编译是指利用编译器从源文件产生汇编语言程序的过程。
编译的作用为检查源程序是否存在语法错误,并给出提示信息。
gcc –m64 –no-pie –fno-PIC -S hello.i -o hello.s
图表 5
原C语言程序被编译成了汇编语言程序,每一条高级语言指令都被解析为若干条汇编指令,描述了程序执行时机器做出的动作。
3.3.1汇编语言程序
汇编语言程序中包括汇编指令、伪指令、宏指令、数字、字符,还有处理器的通用寄存器和段寄存器。hello.s程序的开头为一些伪指令,用来定义程序模式、定义数据、分配寄存器等。
图表 6
3.3.2 符号及数据
1. 局部变量
C程序中定义的所有局部变量都通过寄存器或堆栈来存储,如int型变量i存储在了栈空间,地址为R[rbp]-4。
图表 6
2. 常量
C程序中的各种常量以立即数的形式出现在汇编程序中
图表 7
3. 数组
main函数中用到了char*型指针数组argv,由以下汇编代码可知其首址为R[rbp]-32。
图表 8
由下面的汇编代码可知,程序通过首址加偏移的寻址方式进行数组元素的寻址:
图表 9
3.3.3 汇编指令
1. 数据传送
程序中的赋值操作被翻译成数据传送指令,mov S,D 的执行结果为D←S。mov的后缀指示传送的数据大小,b表示字节传送,w表示字传送,l表示双字,q表示四字。
图表 10
2. 算数和逻辑操作
程序中的i++被翻译成了add指令,add S,D的执行结果为D←D+S。除此之外还有其它许多算数和逻辑操作的指令:
图表 11
3. 关系操作及控制转移
执行程序中的循环时要进行控制的转移,条件判断i<8的对应汇编指令中的cmp,用后面两个参数的比较结果设置条件码,作为跳转的依据。
图表 12
其次是跳转指令,根据条件码的状态跳转到指定代码处执行或执行下一条指令,也可以进行无条件跳转,通过jmp指令来实现。条件跳转含义如下图:
图表 13
3.3.4 函数调用
1. 转移控制:通过call指令实现函数的调用,该指令会把地址A压入栈中,并将PC设置为被调用函数的起始地址,压入的地址A被称为返回地址,是紧跟在call指令后面的那条指令的地址。
2. 传递数据:调用函数优先使用寄存器传参,参数个数大于6个时将使用栈空间传递参数;被调用函数将返回值存放在寄存器rax中。
3. 返回:被调用函数通过ret指令将控制返回给调用函数,在这之前应释放栈空间,恢复调用者函数的上下文,取出返回地址,然后才返回到调用函数。
以printf函数的调用为例分析其代码:
图表 14
寄存器edi存储第一个参数“Hello %s %s\n”,%rsi存储第二个参数argv[1],%rdx存储第三个参数argv[2],call指令将返回地址入栈,然后把控制转移到printf函数。
本章主要介绍了程序的编译阶段,编译器将高级语言翻译成汇编语言,简要阐述了汇编程序的结构以及一些汇编指令的意义与功能。
汇编指的是汇编器将汇编程序翻译成机器指令并打包成可重定位目标文件的过程,可重定位目标文件的后缀为.o,它是一个二进制文件,包含程序的指令编码。
gcc –m64 –no-pie –fno-PIC -c hello.s -o hello.o
图表 15
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.3.1 ELF头
命令:readelf -h hello.o
ELF头以一个16字节的序列考试,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
图表 16
4.3.2 节头部表
命令:readelf -S hello.o
节头部表中描述了不同节的位置和大小,其中目标文件中每个节都有一个固定大小的条目。每个节都是从0开始的,链接时将为每个节分配相应的内存空间。
图表 17
4.3.3 重定位节.rel.text与.rel.data
命令:readelf -a hello.o
当汇编器遇到对最终位置位置的目标引用时,它就会生成一个重定位条目,告诉链接器在将目标文件合并为可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text节中,已初始化数据的重定位条目放在.rel.data节中。
一个重定位条目包含的信息如下:
offset:需要被修改的引用的节偏移;
symbol:标识被修改引用应该指向的符号;
type:重定位类型;
addend:一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。
图表 18
4.3.4 符号表.symtab
命令:readelf -s hello.o
符号表包含程序定义和引用的符号的信息,它的每个条目指示一个符号的名称(Name)、类型(数据或函数)(Type)、所在节(Ndx)、距定义目标的节的起始位置的偏移(Value)、大小(Size)、本地还是全局(Bind)等。
图表 19
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
图表 20
图表 21
机器语言由以下三部分构成:
(1) 操作码,由指令码和功能码构成,表明指令类型
(2) 寄存器编号,指示源和目的寄存器
(3) 立即数。
hello.o和hello.s中的汇编代码的差别在于:
(1) 分支转移:在hello.s中,分支跳转的目的位置是直接用.L0等标记表示的,而反汇编代码中则表示为“函数名+偏移量”的形式;可以发现分支转移指令的机器码立即数部分并不等于汇编代码中的偏移量,这代表这条指令将通过运行时PC定位目标位置,机器码中的立即数是相对PC的偏移量。
(2) 函数调用:在hello.s中直接使用函数名,而反汇编代码使用“函数名+偏移量”的形式;可以发现函数调用的机器码中的立即数部分都为0,这将在链接阶段修改为正确的值。
本章主要介绍了汇编的概念和ELF文件格式,比较了汇编前后文件的变化,了解机器码的组成及其与汇编代码的映射关系。
注意:这儿的链接是指从 hello.o 到hello生成过程。
链接是将各种代码和数据片段手机并合并成为一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行与编译时,也可以执行与加载时,甚至执行与运行时,也就是由应用程序来执行。
连接器在软件开发过程中扮演着一个关键的角色,因为它是的分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
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
图表 22
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
命令:readelf -a hello
(1) ELF头,可以发现它与hello.o的文件头的不同之处在于它的Type项变为了EXEC,及可执行目标文件,共有25节。
图表 23-1
(2) 节头表,可以发现,Address项不再是全零,而是加载程序时对应节的虚拟地址。
图表 23-2
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
查看edb结果,发现hello的虚拟地址空间开始于0x400000。
图表 24-1
通过5.3中的节头表找到各个节所在位置,如代码节.text:
图表 24-2
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
命令:objdump -d -r hello
与hello.o的代码比较:
(1) hello的反汇编代码的地址不再从0开始,而是一个大于0x400000的值;
(2) 有些分支转移和函数调用的机器码变化了,这是重定位的结果。
重定位过程:
(1) 链接器将所有类型相同的节合并在一起,然后把运行时的内存地址赋给新的聚合节,当这一步完成时,程序中每条指令和全局变量都将有唯一的运行时地址。
(2) 链接器根据可重定位目标模块的重定位节解析代码节中对每个符号的引用,并将每一个符号和文件中的某个位置建立关联。使它们指向正确的运行时地址。
(3) 当链接器遇到最终位置未知的引用时就会生成一个重定位条目,放在.rel.text节中。
图表 25
Offset为需要被修改的引用的节偏移;Symbol表示需要被修改的引用的符号;Type告知链接器如何修改引用;Addend为偏移调整。
以函数调用为例说明重定位具体过程:
(1) 定位需要重定位的位置。通过重定位条目中的Offset得到引用在节中的位置refptr。
(2) 若Type为PC相对引用,则执行*refptr=(unsigned)(ADDR(r.symbol)+r.addend-refaddr),其中ADDR(r.symbol)为引用的地址,refaddr由Offset得到,代表引用的运行时地址。
(3) 若Type为绝对引用,则执行*refptr=(unsigned)(ADDR(r.symbol)+r.addend)。
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
ld-2.27.so!_dl_init |
hello!_start |
lib-2.27.so!__libc_start_main |
-libc-2.27.so!__cxa_atexit |
-libc-2.27.so!__libc_csu_init |
libc-2.27.so!_setjmp |
libc-2.27.so!exit |
图表 26
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
加载hello时,动态链接器对共享目标文件中的相应模块内的代码和数据进行重定位,加载共享库,生成完全链接的可执行目标文件。
动态链接采用延迟加载的策略,即在调用函数时才进行符号的映射,使用偏移量表GOT+过程链接动态表PLT实现函数的动态链接。GOT中存放函数目标地址,为每个全局函数创建一个副本,并将对函数的调用转换成对副本函数的调用。
调用dl_init之前的.got.plt内容:
图表 27
调用后:
图表 28
介绍了链接的概念和hello程序的虚拟地址空间,重点分析了重定位过程,简述了动态链接原理。
进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
作用:刻画系统内部出现的动态情况,描述系统内部各程序的活动规律。
Shell是一种交互型的应用程序,代表用户运行其它程序。
处理流程:
(1) 从终端读入命令
(2) 解析命令,构造argv和envp
(3) 若为内置命令则立即执行
(4) 否则调用fork函数创建一个子进程
(5) 调用execve函数在子进程的上下文中运行指定程序
(6) 接受键盘输入信号并进行相应处理
创建当前进程的区域结构、页表的原样副本等上下文信息,让子进程得到与父进程虚拟地址空间相同但是独立的一份副本,只是拥有不同的PID。
删除子进程现有的虚拟内存段,并创建新的代码、数据、堆栈,代码和数据被初始化为hello的代码和数据,堆栈被指控,然后加载器将PC指向hello程序的起始位置,执行hello程序。
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
进程提供给应用程序的抽象:
(1) 一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器;
(2) 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用CPU内存。
hello进程的执行是依赖于进程所提供的抽象的基础上:
(1) 逻辑控制流::一系列程序计数器 PC 的值的序列叫做逻辑控制流,进程是轮流 使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占 (暂时挂起),然后轮到其他进程。
(2) 并发流:一个逻辑流的执行时间与另一个流重叠,成为并发流,这两个流成为并发的运行。多个流并发的执行的一般现象成为并发。
(3) 时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
(4) 私有地址空间:进程为每个流都提供一种假象,好像它是独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个内存字节是不能被其他进程读或者写的,在这个意义上,这个地址空间是私有的。
(5) 用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的 代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
(6) 上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
(7) 上下文切换:当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用上下文切换的机制来将控制转移到新的进程。
图表 29
hello程序的执行:在shell调用execve函数之后,进程已经为hello程序分配了独立的虚拟内存空间,并将代码节和数据节分配到相应的区域。最初hello运行在用户模式下,一段时间后进程挂起hello的运行并切换到内核模式,进行上下文切换将控制交给其它进程,当定时器发送中断信号,进程进入内核模式处理中断,将控制给到hello程序,hello得以继续执行自己的控制逻辑流。
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.6.1 异常类型
(1) 中断:由来自I/O设备的信号导致,异步,执行处理程序后返回到下一条指令;
(2) 陷阱:有意的异常,同步,处理后返回到下一条指令;
(3) 故障:潜在的可恢复的错误,同步,可能返回到当前指令;
(4) 终止:不可恢复的错误,同步,不会返回。
hello执行过程中可能出现缺页故障、奇偶错误等异常。
6.6.2 信号
几种常见信号如下:
ID | 名称 | 默认行为 | 相应事件 |
2 | SIGINT | 终止 | 来自键盘的中断 |
9 | SIGKILL | 终止 | 由某进程发出 |
11 | SIGSEGV | 终止 | 无效的内存引用(段故障) |
14 | SIGALRM | 终止 | 来自alarm函数 |
17 | SIGCHLD | 忽略 | 一个子进程停止或终止 |
图表 30
6.6.3 键盘上的操作
(1) ctrl+c,向前台作业所在进程组中的每个进程发送SIGINT信号,默认行为是终止进程,可以看到控制转移到了shell,ps后发现没有hello进程,说明其执行了默认行为:
图表 31
图表 32
(2) ctrl+z,挂起前台作业,ps查看进程状态:
图表 33
通过kill向hello进程发生SIGINT信号:
图表 34
本章阐述了进程的定义和作用,介绍了shell的处理流程,分析了fork和execve的执行过程,以及异常与信号的处理。
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:段标识符加上一个段内偏移量,即.s文件中的表示形式
线性地址:即虚拟地址
虚拟地址:虚拟地址空间中的一个地址,需要MMU翻译为物理地址
物理地址:在内存中的真实地址。
逻辑地址由段标识符和段内偏移量两部分构成,解析逻辑地址时:
(1) 将段标识符解析为一个段描述符表的索引,取得段描述符;
(2) 由段描述符获得段的起始地址;
(3) 段基址加上偏移量的线性地址。
每个进程都维护着一个页表,记录每个虚拟页面的状态(未分配、已缓存或已分配未缓存),页表条目称为PTE。
(1) 处理器将VA发送给MMU
(2) MMU解析VA,分为VPN和VPO两部分
(3) 根据VPN向TLB请求PTE
(4) 若PTE未缓存则为不命中,需再次向内存请求
(5) 由PTE得物理页号
(6) 将物理页号和VPO组合成物理地址
(1) 处理器向MMU发送VA
(2) MMU解析出VPN,从VPN中解析出TLB的组索引TLBI和标记TLBT
(3) 在TLB中寻找匹配,若命中则得到PPN,和VPO组合成PA
(4) 若不命中,则根据一级页表的页号VPN1在一级页表中得到二级页表的首址,再根据VPN2在二级页表中得到三级页表的首址,以此类推,最终得到PPN,构造出PA
图表 35
(1) 提取出PA中的组索引、块索引及标志,在一级Cache的相应组中寻找有效位为1且标志相同的行
(2) 若找到,则根据块索引传送回请求的内容
(3) 若没有这样的行,则产生不命中,转而向二级Cache请求,进行相同的步骤,以此类推,直到产生命中,在上一级Cache中选择一个大小合适的空闲块或驱逐一个块,用目的块填充,最终一级Cache中包含请求的块,传送给程序。
fork函数为子进程创建上下文,为其分配一个不同于父进程的PID。通过fork函数创建的子进程拥有和父进程相同的用户级虚拟地址空间,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本。两个进程中的每个页面都标记为只读,每个区域结构都标记为私有的写时复制,当这两个进程中的人一个后来进行写操作时,写时复制机制就会创建新的页面,也就为每个进程保持了私有地址空间。
execve函数会调用驻留在内核区域的启动加载器代码,在当前进程上下文中加载并运行hello程序。
(1) 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构
(2) 映射私有区域。为新程序的代码、数据、bss和站区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello.out中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello.outt中。栈和堆区域也是请求二进制零的,初始长度为零
(3) 映射共享区域。hello.out链接的共享对象是动态链接到程序的,然后再映射到用户虚拟地址空间中
(4) 设置PC,使之指向代码区域的入口点。
缺页即DRAM缓存不命中。当CPU引用的地址对应的页面没有缓存在物理内存中时就会触发缺页异常。
缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,如果它已经被修改了,那么内核就会将它复制回磁盘,接下来内核从磁盘复制请求页到内存中的相应位置,更新PTE的有效位及物理页号,随后返回。重新启动导致缺页的指令,重新请求页面,这时该页面已经缓存在内存中了,产生页命中。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆,它紧接在未初始化的数据区域后开始,并向更高的地址生长。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块保持空闲,直到它显式地被应用所分配,一个已分配的块保持已分配状态直到它被释放。
分配器有两种基本风格:
(1) 显式分配器:要求应用显式地释放任何已分配的块;
(2) 隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。
分配器要考虑的几个基本问题包括:空闲块组织、放置、分割、合并等。下面依次阐述。
1. 块的组织方式:
(1) 隐式空闲链表
一个块由一个字的头部、有效载荷以及可能的一些额外的填充组成,头部编码了这个块的大小(包括头部和填充),以及这个块是已分配的还是空闲的。若双字对齐,则块大小的最低3位将不会使用到,就可以用来存储块的分配状态。
(2) 带边界标记的空闲链表
在隐式空闲链表块结构的基础上,添加一个尾部结构到块的末尾,它和头部保持相同,用来简化空闲链表的遍历
(3) 显式空闲链表
在带边界标记的基础上,为空闲块增加两个指针结构指示它在空闲链表中的祖先和后继,该策略能够使首次适配的分配时间从块综述的线性时间减少到空闲块数量的线性时间。
2. 放置策略
(1) 首次适配:从头开始搜索空闲链表,选择第一个合适的空闲块
(2) 下一次适配:从上一次查询结束的地方开始搜索
(3) 最佳适配:检查每个空闲块,选择合适所需请求大小的最小空闲块
3. 分割:分配器找到一个匹配的空闲块后将其分割为两部分,第一部分变成已分配块,剩下的变成一个新的空闲块。
4. 合并
(1) 立即合并:在每次一个块被释放时,就合并所有的相邻块
(2) 推迟合并:等到某个稍晚的时候再合并空闲块,如直到某个分配请求失败。
合并的性能还依赖于空闲块组织方式,带边界标记的空闲链表可以减少合并的时间。
5. 空闲链表中块的排序策略
(1) LIFO(后进先出):将新释放的块放置在链表的开始处,分配器会最先检查最近使用过的块。
(2) 按照地址顺序:链表中每个块的地址都小于它后继的地址。这种策略配合首次适配会得到更高的内存利用率。
本章简述了虚拟内存管理的基本内容,虚拟地址、逻辑地址和物理地址的关系及变换流程,阐述了动态内存分配的方法和原理。
设备的模型化:所有I/O设备(例如网络、磁盘、终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。
设备管理: Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式执行。
Unix IO接口的统一执行方式:
(1) 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个非负整数,叫做描述符,来标识这个文件,内核记录有关这个打开文件的所有信息,应用程序只需记住这个描述符。
(2) Linux shell创建的每个进程开始时都有三个打开的文件:标准输入、标准输出和标准错误。
(3) 改变当前文件的位置。对每个打开的文件,内核保持着一个文件位置,代表从文件开头起始的字节偏移量,应用程序能够通过执行seek操作显式地设置文件地当前位置。
(4) 读写文件:一个读操作就是从文件复制n(>0)个字节到内存,当前文件位置增加n。若文件中未读地字节数小于n,则触发一个称为EOF的条件,应用程序能够检测到这个条件。
(5) 关闭文件。内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。
Unix I/O函数:
(1) int open(char*filename,int flags,mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的,open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程如何访问这个文件,mode参数指定了新文件的访问权限。
(2) int close(fd)
fd是需要关闭的文件的描述符,返回操作结果。
(3) ssize_t read(int fd, void *buf, size_t n)
read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf,返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
(4) ssize_t write(int fd, const void*buf, size_t n)
write函数从内存位置buf处复制最多n个字节到描述符为fd的当前文件位置。
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
printf的函数体如下:
图表 36
printf函数按照格式fmt结合参数args生成字符串,返回字符串长度。
vsprintf函数接收一个格式化的命令,并把指定的匹配的参数和格式化输出。
write函数将buf中的i个元素打印到终端。
write函数的汇编如下:
图表 37
在write函数中,将栈中参数放入寄存器,%ecx为字符个数,%ebx为第一个字符的地址,最后一条语句代表通过系统来调用syscall。
sys_call函数体:
图表 38
syscall将字符串中的字节从寄存器通过总线复制到显卡的显存中,现存中存储的是字符的ASCII码,字符显示驱动子程序将通过ASCII码在子墨库中找到点阵信息将点阵信息存储到VRAM中,显示芯片会按照一定的刷新频率逐行读取VRAM,并通过信号线向液晶显示器传输每一个点,于是字符串就打印在了屏幕上。
getchar函数体:
图表 39
异步异常-键盘中断的处理:当用户按键时,键盘接口会受到一个代表该案件的键盘扫描码,同时产生一个中断请求,抢占当前进程,将控制传递给键盘中断子程序:先从键盘接口取得扫描码,然后将该扫描码转换成ASCII码,保存在系统的键盘缓冲区中。
getchar等调用read系统函数,通过系统调用读取缓冲区中的按键ascii码,直到接受到回车键才返回。
本章主要介绍了Linux的IO设备的管理方法,Unix IO接口及其系统调用,分析了printf和getchar的实现。
源文件:Hello程序以C源文件的最初形式诞生,其中每个字符都是用ASCII编码表示。
预处理:cpp解释hello.c中的宏及常量表达式,生成hello.i。
编译:hello.i经编译器ccl翻译为汇编文件hello.s。
汇编:hello.s经过汇编器as的处理变为二进制文件hello.o。
链接:链接器ld将hello.o及hello用到的其它模块静态链接为可执行目标文件hello
运行:在shell中输入./hello 1190201012 zzj 10执行hello程序。
创建子进程:shell调用fork函数为hello创建子进程。
加载:shell调用execve函数加载hello程序,映射虚拟页面,进入程序入口点载入物理内存。
动态内存申请:hello调用printf函数时会使用malloc申请堆内存。
上下文切换:hello调用sleep函数后进程切换到内核模式,将当前进程的控制转移到其它进程。
信号:hello运行过程中,可以在键盘上按下ctrl+c发送SIGINT信号来终止hello的运行,或ctrl+z发送SIGTSTP信号暂停其运行。
回收:当hello程序执行完毕后,内核发送SIGCHLD信号给hello的父进程,父进程通过waitpid系统调用回收hello,内核删除为hello创建的所有数据结构。
计算机系统犹如一个结构复杂、分化严重的城市,shell看似清朗的输入输出面板深处,却有着潜渊般晦暗难测的暗漩急流,一张张表单在高速缓存与DRAM的拥挤空间里星夜奔波,数以百计的进程正为停滞的逻辑控制流焦头烂额,当你在那无眠的永夜里仰望窗棂上街灯的昏黄晕影时,偶尔也会邂逅那些来自遥远殿堂的信使---往返于城市的隐秘角落、直属于最高权力的生死司。它们总是穿梭于最浓重的夜色里,化为生与死的界限间那道独一的霓虹。传说这座城市会为它的每个访客分派这样一个信使,一个只为你而存在的使者,来时挟风带雨,去时,只留下无尽的空寂。所以,城市中的居住者都很谦卑,虽生似蜉蝣,却能在生命的终末见证那份令人心惊的炫目光华,临了留下一句“来生可期”,便“自此尘寰音信断,山川风月永相思”---溘然长逝了。Hello也不过是这过客别墅的匆匆旅者,为着既定的宿命,一生奔波。每个在这座城市中流浪的旅人最终都会找到自己的归宿,生与死,不过一场轮回。而我们,又如何寻找自己的一席之地呢?
“火会在风中熄灭,
山峰会在黎明倒塌,
融进殡葬夜色的河”
----结语
文件名称 | 文件描述 |
hello.c | 源文件 |
hello.i | 预处理之后的文件 |
hello.s | 编译之后的文件 |
hello.o | 汇编得到的可重定位目标文件 |
hello.out | 链接之后的可执行目标文件 |
helloelf.txt | hello.o的反汇编结果 |
[1] 深入理解printf函数的实现_c1204611687的博客-CSDN博客_printf实现
[2] printf内部实现_积微致著-CSDN博客_printf函数内部结构