大作业
题 目 程序人生-Hello’s P2P
专 业 未来技术
学 号 2021112807
班 级 21WL021
学 生 马铭杨
指 导 教 师 史先俊
计算机科学与技术学院
2023年4月
本文主要由从深入理解计算机系统这本书的学习来对hello进程的在计算机当中的经历进行描述,由源程序变为可执行目标文件的具体流程以及在对可执行目标文件的进程管理、存储管理、IO管理的具体细节进行分析描述,以此来增强对自己对于计算机系统这门知识的掌握程度。
关键词:hello进程、进程管理、存储管理、IO管理、计算机系统
目 录
第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简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:预处理器将源程序hello.c修改为hello.i文件,之后将hello.i经过编译器的处理翻译为hello.s的汇编程序,然后将hello.s经过编译器再次翻译为机器语言指令,并打包为hello.o的可重定位目标程序的二进制文件,最后将这个可重定位目标程序文件调用printf函数并运用链接器处理得到hello.out的可执行目标程序。
020:在终端里输入./hello后系统调用fork函数创建子进程并利用execve函数加载并运行子进程,CPU为其分配进程时间片执行逻辑控制流,当程序结束后系统会回收hello进程并释放内存,删去为hello进程创建的一系列数据结构。
1.2 环境与工具
X64 CPU;2.1GHz;2.59GHz;8G RAM
Vmware 16。2;Ubuntu 16.04
Windows10 64位;Visual Studio 2022;CodeBlocks 64位
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
- hello.i 作用:修改了的预处理阶段程序
- hello.s 作用:汇编程序
- hello.o 作用:可重定位的目标程序
- hello.out 作用:可执行的目标程序
- hello.elf 作用:hello.o的ELF格式
- hello1.elf 作用:hello.out的ELF格式
1.4 本章小结
本章总结了hello.c文件在电脑里“诞生”、并经过一系列处理不断“成长”为一个完整的“生命”的过程,是所有程序的根本所在,并同时在本章中具体列出了电脑工作环境和工具种类。
第2章 预处理
2.1 预处理的概念与作用
预处理是指将源程序被处理为可执行目标程序的过程中,被翻译成汇编程序之前的过程,也就是经过预处理器根据字符#开头的命令进行修改的阶段。其作用是:使用预处理功能可以有利于程序的修改、阅读、调试等,提高程序编辑的效率。
2.2在Ubuntu下预处理的命令
图2.1 预处理命令
2.3 Hello的预处理结果解析
图2.2 预处理结果
经过预处理后形成的hello.i文件中没有以字符#开头的命令,在main函数之前的文字等也已删除,取而代之的是3000多行系统头文件中的内容。
2.4 本章小结
在本章中主要展开了程序编写中的预处理阶段,经过将hello.i文件与hello.c文件内容的对比,验证了预处理的作用。
第3章 编译
3.1 编译的概念与作用
编译是指利用编译器将预处理过后的程序翻译成汇编程序的过程,其作用是:由于计算机只能识别0、1,而无法直接识别我们编写程序所使用的高级语言,所以通过编译形成一个汇编语言程序,而汇编语言为不同高级语言的编译器提供了一种通用的输出语言,以供计算机识别。
3.2 在Ubuntu下编译的命令
图3.1 编译命令
3.3 Hello的编译结果解析
3.3.1 数据
图3.2.1 数据
图3.2.2 数据
在hello.s文件中可以得知,0与4为程序中出现的常量,计算机将程序中的局部变量存储到了栈内,其中:-20(%rbp)存储的是局部变量argc,-4(%rbp)存储的是局部变量i,
图3.2.3 数据
程序中全局变量只有main函数。
3.3.2 赋值
图3.2.4 赋值
此处将常量0赋值给局部变量i。
3.3.3 算术操作
图3.2.5 算术操作
此处是for循环中循环一次后对局部变量i进行加1的操作。
3.3.4 关系操作
图3.2.6 关系操作
此处是程序中对于局部变量argc是否等于4的判断,用于if的条件判断。
图3.2.7 关系操作
此处是程序中对于局部变量i是否小于等于4(也就是小于5)的判断,用来控制for循环的循环次数。
3.3.5 数组操作
图3.2.8 数组操作
此处是程序一开始建立一个栈来存储创建的数组。
图3.2.9 数组操作
此处%rax的值用来指向数组中不同的元素,当%rax中的值为8时,指向argv[1];值为16时,指向argv[2];值为24时,指向argv[3],然后再用printf、sleep函数进行操作。
3.3.6 控制转移
图3.2.10 控制转移
此处时程序中if的判断,argc的值与常量4的关系决定了是否会跳转。
图3.2.11 控制转移
此处时程序中for循环的内容及循环条件,若局部变量i的值小于5,则会跳转至L4,也就是执行循环。
3.3.7 函数操作
图3.2.12 函数
此处为参数传递,将数组的地址与局部变量存储到栈当中。
图3.2.13 函数调用
此处为函数调用,在程序当中调用了:puts()、exit()、printf()、atoi()、sleep()函数。
3.4 本章小结
本章主要对程序的编译阶段进行展开,具体分析了hello.c文件经过编译器处理形成的hello.s文件里的内容,理清了其中大部分机器语言指令的用意。
第4章 汇编
4.1 汇编的概念与作用
汇编是指利用汇编器将程序中的汇编指令翻译为等价的机器语言,其作用是:可以将翻译完的机器语言指令打包成可重定位目标程序,保存到一个二进制文件当中。
4.2 在Ubuntu下汇编的命令
图4.1 汇编命令
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
图4.2.1 hello.o的ELF格式
ELF头中以一个描述了生成该文件的系统的字大小和字节顺序的序列开始,其他部分包含帮助链接器语法分析和解释目标文件的信息,例如:机器类型、ELF头的大小等。
图4.2.2 节头
节头部表用来描述不同节的位置和大小等信息。
图4.2.3 重定位节
重定位节中偏移量是需要被修改的引用的节偏移;信息中包含symbol和type,symbol标识被修改引用所指向的符号,type告知链接器如何修改新的引用;而类型则是重定位条目的类型,目前只有R_X86_64PC32和R_X86_64_PLT32两种类型,而符号值则是用来对被修改的值做偏移调整;符号名称可用来计算被修改的位置。
4.4 Hello.o的结果解析
图4.3.1 hello.o
图4.3.2 hello.s
在hello.o的反汇编当中,通过这里与hello.s文件内容对比可以看出:在hello.o反汇编中常量使用的是十六进制,而hello.s文件当中则使用的是十进制;其次,在hello.o的反汇编中,分支转移时指向方式是间接寻址的方式,而hello.s当中则是以L2来指向其他分支。这是由于如同L2这样的方式都是汇编语言中的一种符号,在hello.o的机器语言中便不存在了,而是用确定的地址来指向不同的分支。
图4.3.3 hello.o
在hello.o的反汇编中还可以看出与hello.s不同的是在hello.o的反汇编中对于变量的访问是以间接寻址的方式访问,而且调用函数也是以间接寻址的方式来进行调用。这是由于在可重定位目标程序当中,这些共享库中的函数需要利用链接器才能进行调用,对于这些函数,机器语言都是这样来设置寻址方式,然后准备与静态库的链接。
4.5 本章小结
本章主要对程序的汇编阶段进行展开分析,通过将hello.o与hello.s文件内容的对比来分析机器语言与汇编语言的映射关系,并且具体分析了hello.o的ELF格式及其内容。
第5章 链接
5.1 链接的概念与作用
链接是指将程序各个部分的代码和数据整合到一个文件,形成一个可被加载到内存并执行的目标程序的过程。其作用是:可实现分离编译,将一个程序分解成更小且更好管理的模块,从而可以单独地编写其中的一个模块。
5.2 在Ubuntu下链接的命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
图5.1 链接命令
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
图5.2.1 ELF头
ELF头描述文件的总体格式,入口点地址是执行的第一条指令的地址。
图5.2.2 节头
节头部表用来描述不同节的位置和大小等信息。
图5.2.3 程序头
程序头表中介绍了目标文件中的偏移量,目标文件的类型,虚拟内存地址与物理内存地址。
图5.2.4 段节
这是可执行目标程序的ELF格式中的动态节。
图5.2.5 重定位节
重定位节中偏移量是需要被修改的引用的节偏移;信息中包含symbol和type,symbol标识被修改引用所指向的符号,type告知链接器如何修改新的引用;而类型则是重定位条目的类型,目前只有R_X86_64_GLOB_DAT和R_X86_64_JUMP_SLO两种类型,而符号值则是用来对被修改的值做偏移调整;符号名称可用来计算被修改的位置。
图5.2.6 符号表
符号表中存有符号的定义与函数的调用情况。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
图5.3 虚拟地址
从Data Dump中可以看到从0x400000开始便是虚拟地址空间,ELF头等信息都存储到其中。
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
图5.4.1和图5.4.2 hello的重定位
hello中新增加了init节和plt节,而hello.o中只有.text节。此处是通过输入的ld命令链接器所创建的.init节,程序初始化调用的代码就在其中,plt节是动态链接中的延迟绑定机制所形成的。hello的.text节中除了main函数之外还多了_start等内容。
图5.4.3和图5.4.4 hello与hello.o的区别
在这里可以看到hello中对使用的函数分配了虚拟地址空间,也就是链接器在完成符号解析后重定位节和符号定义,然后再根据重定位条目来修改代码节和数据节中对符号的引用,也就是后面调用函数时不再像hello.o一样而是使用下一条指令地址减去偏移量来得到目标函数地址的方式。
5.6 hello的执行流程
使用edb中step over功能查看从加载hello到_start,到call main,以及程序终止的所有过程,下面是列出的调用与跳转的各个子程序名或程序地址。
hello!_init <0x0000000000401000>
hello!puts@plt <0x0000000000401030>
hello!printf@plt <0x0000000000401040>
hello!getchar@plt <0x0000000000401050>
hello!atoi@plt <0x0000000000401060>
hello!exit@plt <0x0000000000401070>
hello!sleep@plt <0x0000000000401080>
hello!_start <0x0000000000401090>
hello!_dl_relocate_static_pie <0x00000000004010c0>
hello!main <0x00000000004010c1>
hello!__libc_csu_init <0x0000000000401150>
hello!__libc_csu_fini <0x00000000004011b0>
hello!_fini <0x00000000004011b4>
5.7 Hello的动态链接分析
在程序调用一个由共享库定义的函数时,会为该引用生成一个重定位记录,再利用动态链接器去解析它,而编译系统则时采用延迟绑定的技术来修改调用模块,这也需要靠GOT与PLT来实现。二者都是数组,GOT在数据段中,每个条目有8个字节,而PLT在代码段中,每个条目有16个字节,每个字节负责调用一个函数。
图5.5.1 动态链接项目
从hello的ELF格式中的节头表可以看到动态链接项目为.got、.got.plt及他们的虚拟地址0x403ff0与0x404000,使用edb可进行查看具体内容。
图5.5.2和图5.5.3 动态链接项目变化
由此可知,在dl_init前后,项目内容发生了变化。
5.8 本章小结
本章主要对程序中链接阶段进行展开,具体分析了可执行文件的ELF格式,通过与hello.o比较分析了链接重定位过程及不同,并使用edb查看了hello的虚拟地址空间与执行流程,最后分析了hello的动态链接,加深了对于程序链接阶段各方面的了解。
第6章 hello进程管理
6.1 进程的概念与作用
进程是指一个执行中程序的实例,程序都运行在某个进程的上下文当中。其作用是:进程会提供给应用程序两个假象:一个独立的逻辑控制流和一个私有的地址空间。
6.2 简述壳Shell-bash的作用与处理流程
shell是一种应用程序,是一个连接用户与Linux内核之间的一种桥梁,它不经能够解释用户输入的命令来发送给内核,让用户更加高效地使用Linux的系统内核,同时也能够保护内核;还能与程序之间互相调用,在程序之间传递数据。
处理流程:待用户在终端中输入命令行后,shell会读取输入的命令行,在命令行中找到特殊字符并翻译为间隔符号,通过这些间隔符号将命令行划分为几个部分,再处理这些被划分的部分查看是属于内部命令、用户自己定义的shell函数还是可执行目标文件,若是属于内部命令或是用户自己定义的shell函数,则会执行;若是属于可执行目标文件,则会创建一个新的进程并在创建的这个进程的上下文中运行这个文件。
6.3 Hello的fork进程创建过程
在终端输入命令行:./hello后,shell读取命令行后检测到属于可执行目标文件,利用fork函数创建了一个新的进程,也就是子进程。这个进程几乎但不完全与父进程相同,它们之间最大的区别就是它们的PID不同,子进程还可以读写父进程中打开的文件。fork函数调用一次会返回两次,待子进程终止后会由父进程或者init进程进行回收。
6.4 Hello的execve过程
execve函数会调用加载器去加载这个程序,将hello中的代码和数据从磁盘复制到内存后跳转到_start函数的地址设置一个栈后将控制传递给新程序的main函数去运行文件,还会带着参数列表argv和环境变量列表envp。argv变量与envp变量都分别指向一个以NULL结尾的指针数组,不同的是argv变量的指针指向参数字符串,而envp变量的指针指向环境变量字符串。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
用户在终端输入命令行运行可执行目标文件后,shell会利用fork函数创建一个子进程,再利用execve函数在这个子进程的上下文中去运行文件。上下文就是内核重新启动一个被抢占的进程所需的状态,上下文切换由内核进行调度;而进程时间片进程执行的的一小段时间。
在输入./hello后,shell会利用fork函数创建一个子进程。hello文件的进程执行过程中,会一直执行直到被内核决定调度进行上下文切换,此时会由用户模式转变为内核模式,保存当前进程的上下文并恢复其他先前被抢占的进程所保存的上下文后将控制传递给这个新恢复的进程,转换回用户模式去运行这个恢复上下文的进程。在hello的进程执行中,会遇到sleep系统调用,此时,sleep系统调用会请求让hello进程进入休眠状态,系统会暂停执行hello进程一段时间,由用户模式转换为内核模式,此时内核可以决定执行上下文切换,不把控制返回给hello进程,也可以直到休眠时间结束,由内核模式转换为用户模式继续执行hello进程。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
- 正常运行:正常五次打印信息后停下。
图6.1 正常运行
- 乱按:打印信息后不停乱按直到按到回车键程序结束。
图6.2 乱按
- Ctrl-Z:打印程序时按下之后程序被挂起
图6.3 Ctrl-Z
ps:显示进程信息,例如PID等。
图6.4 ps
jobs:显示已启动的作业状态。
图6.5 jobs
pstree:显示在树结构下进程与程序的关系
图6.6 pstree
fg:将挂起的指令转移到前台执行,发送信号SIGCONT给被挂起的进程使之继续执行。
图6.7 fg
kill:将挂起的hello进程结束。
图6.8 kill
- Ctrl-C:内核发送一个SIGINT信号到前台进程组中的进程。在默认情况下,结果是终止前台作业。通过ps可知hello已经被回收了。
图6.9 Ctrl-C
6.7本章小结
本章主要对程序的进程进行展开,理清了shell的作用和处理流程,fork函数创建进程和execve函数加载并运行进程的具体流程,通过这些结合上下文切换等内容具体分析了hello进程的执行,通过自己的操作来查看hello的异常与信号处理的情况,对于异常、进程等知识有了进一步的了解。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址是指从应用程序角度看到的内存单元、存储单元和网络主机的地址,也被称为绝对地址。例如在hello程序反汇编所得到的地址就是逻辑地址。
线性地址:线性地址是属于在逻辑地址与物理地址的中间层,通过将逻辑地址加上基地址便能得到,若计算机没有分页机制,那么线性地址也相当于是物理地址。
虚拟地址:CPU使用虚拟寻址,会生成一个虚拟地址去访问主存,虚拟地址被组织成存放在磁盘上的一个数组,在使用分页机制后,每一页都会映射到物理地址上。
物理地址:为了存储或获取信息,每个字节都被分配一个唯一的内存地址,就是物理地址。在hello中需要通过寻址方式去进行计算才能得到。
7.2 Intel逻辑地址到线性地址的变换-段式管理
计算机将程序分成了几段:代码段、栈段、数据段等等,以段为单位来分配内存。段式管理是通过段表进行的,段表包括段号、段名、段起点、装入位、段的长度等,段表分为三个类型:全局描述符表,局部描述符表和中断描述符表;段描述符分为用户的代码段和数据段描述符和系统控制段描述符,而在段选择符当中,TI的值用于选择全局描述符表还是局部描述符表,索引用来确定段描述符的位置,RPL用来表示CPU当前特权级。每次从描述符cache中取32位段基址与有效地址相加得到线性地址,下图为具体转换流程。
图7.1 段式管理
7.3 Hello的线性地址到物理地址的变换-页式管理
计算机储存的内存以段为单位划分,每个段又划分成若干个页。由于磁盘上的数据会被分割成块用来作为与主存之间的传输单元,虚拟内存也会被分割为一种叫“虚拟页”的块,而物理内存被分割成物理页。在虚拟内存系统中便利用页表这么一个数据结构来建议虚拟页与物理页之间的映射关系,将虚拟页映射到物理页上。虚拟内存包括虚拟页面偏移(VPO)和虚拟页号(VPN)两个部分,MMU通过虚拟页号来选择页表条目(PTE),并将物理页号(PPN)与虚拟页面偏移串联起来,从而得到物理地址,下图为具体流程。
图7.2 页式管理
根据页面的命中情况,会有以下两种处理方式:两者不同的是页面命中会由MMU构造物理地址并传给主存或者高速缓存,并返回请求的数据字给处理器;而页面不命中则会调用缺页处理程序后将缺页的虚拟地址重新发送给MMU。下图为具体流程:
图7.3 页命中与缺页情况
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是一个小的、虚拟寻址的缓存,其中每一行保存一个由单个PTE组成的块,包括TLB标记、TLB索引和VPO,TLB标记用来区别可能映射到同一个TLB组的不同VPN。TLB是通过利用VPN的位来进行虚拟寻址,当TLB命中后,MMU从TLB中取出与CPU产生的虚拟地址相应的PTE,MMU会将虚拟地址翻译成物理地址并发送给主存或者高速缓存,之后将请求的数据字返回给CPU,若TLB不命中,MMU则会从L1缓存取出相应PTE。下图为具体流程:
图7.4 TLB
计算机使用层次结构的页表来压缩页表,也就是多级页表。当使用k级页表进行地址翻译时,虚拟地址会被划分为k个VPN和1个VPO,第i个VPN时第i级页表的索引;而在k级页表之前的每一个页表的PTE都指向下一级别的某个页表基址,第k级页表的PTE则是包含某一个物理页面的PPN或磁盘块地址,下图为使用k级页表层次结构的地址翻译的具体流程图:
图7.5 多级页表
7.5 三级Cache支持下的物理内存访问
在通过将VA翻译为PA后,根据所得到的物理地址查看高速缓存组索引(CI)来在L1cache中进行查找,找到对应组后根据高速缓存标记(CT)来确定这个组里是否有对应的行,找到后再查看有效位是否为1,若有效位为1,则会根据高速缓存块偏移(CO)来查找需要的块;若不满足上述情况没有则属于L1不命中,会去L2、L3cache中进行查找,若都不命中,则会向主存里寻找块并进行行替换策略。
图7.6 组相联高速缓存
7.6 hello进程fork时的内存映射
当hello进程调用fork函数时,内核会为这个新的进程也就是子进程创建各种数据结构,并分配唯一的PID。同时,为了给子进程创建虚拟内存,内核创建了当前进程的mm_struct、区域结构和页表的原样副本,并将两个进程当中的每个页面标记为只读,每个区域结构标记为私有的写时复制。当fork函数在新进程中返回时,新进程的虚拟内存与调用fork时存在的虚拟内存相同,若hello这两个进程中的一个后来进行写操作,写时复制机制会创建新页面。
7.7 hello进程execve时的内存映射
当系统运用execve函数去加载并运行可执行文件hello时,会先删除已存在的用户区域,并为新程序的代码、数据、bss和栈区域创建新的私有、写时复制区域结构用来映射私有区域。其中:代码区、数据区会映射到.text和.data,bss区域会映射到匿名文件。之后会映射共享区域,若是与共享对象链接,那么会先动态链接到程序后再映射到用户虚拟地址空间的共享对象;最后会设置程序计数器来指向代码区域的入口点。
图7.7 execve内存映射
7.8 缺页故障与缺页中断处理
DRAM缓存不命中被称为缺页,缺页中断就是访问的页不在主存,需要将其调入主存再进行访问,此时会传递CPU中的控制到内核的缺页异常处理程序来确定出物理内存中的牺牲页,若被修改则会移出磁盘,之后会调入新的页面更新内存页表后返回到原来的进程。
7.9动态存储分配管理
堆是由动态内存分配器所维护着的一个进程的虚拟内存区域,它维护着brk变量用来指向堆的顶部。分配器将堆视作一组不同大小的块的集合来维护,每个块都是一个连续的虚拟内存片。其中,已分配的块显式地保留为应用程序使用,空闲块则可用来分配。一个已经被分配的块会一直保持已分配状态直到被释放。分配器有两种基本风格,一种是显式分配器,另一种是隐式分配器,它们都要求应用显式地分配块。
显式分配器有个典型的例子便是C标准库当中的malloc程序包,可以通过使用mmap和munmap函数来显式地分配和释放堆内存,或者使用sbrk函数来扩展和收缩堆,用free函数来释放已分配的堆块。
分配器利用隐式空闲链表来组织堆区别块边界以及区别已分配块和空闲块,当一个应用请求一个块时,分配器会以放置策略的方式搜索空闲链表来查找一个足够大可以放置这个块的空闲块,找到空闲块后会分割空闲块去分配空间。常见的方式由首次适配、下一次适配和最佳适配。其中:首次适配时从头开始搜索,选择第一个合适的空闲块;下一次适配时与之不同的是从上一次查询结束的地方开始搜索,而不是从头开始;最佳适配时 检查每个空闲块,选择适合所需大小的最小空闲块。对于合并来说分配器则是通过使用边界标记的方法去进行合并。
7.10本章小结
本章主要对程序的存储管理进行展开,结合之前的hello进程进一步对逻辑地址等四种地址有了更深的了解,通过描述段式管理、页式管理、TLB与多级页表来理清了在计算机中这四种地址之间的高效的转换方式,并利用这些来理解三级cache支持下的内存访问以及hello进程的内存映射后针对页试管理描述了缺页的情况与缺页中断处理,对其有了更多的认识。最后,通过描述计算机中动态存储分配管理的具体策略和基本方法,对于虚拟内存和存储管理的各方面有了进一步的认识。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
Linux文件式一个m个字节的序列,所有的I/O设备都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种方式允许Linux内核引出一个简单、低级的应用接口,这使得所有的输入和输出都能以统一的方式来执行。
8.2 简述Unix IO接口及其函数
Unix接口:
- 打开文件:一个应用程序通过要求内核打开相应文件来宣告它想要访问一个I/O设备,内核还会返回一个描述符来对此文件的所有操作中标识这个文件。Linux在创建进程开始时都会有三个打开的文件:标准输入、标准输出、标准错误。
- 改变当前文件位置:对于打开的文件,内核都会保持一个文件位置k(初始为0),而应用程序可以通过执行seek函数去设置文件的当前位置。
- 读写文件:读操作就是从文件中复制字节到内存,从当前文件位置k开始,增加到k+n。而当给定一个大小为m字节的文件,k≥m时执行读操作会触发EOF的条件。写操作则是从内存复制字节到一个文件后,从当前文件位置k开始更新k。
- 关闭文件:当完成访问后,应用会通知内核去关闭这个文件。内核会释放文件打开时创建的数据结构,并将描述符进行恢复。
Unix函数:
open函数:进程通过调用oepn函数去打开一个已存在的文件或创建一个新文件,open函数将filename转换为一个文件描述符,并返回描述符数字,这个返回的描述符时进程中当前没有打开的最小描述符,flag参数指明了进程对这个文件的访问方式:O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(可读可写)、O_CREAT(若文件不存在,则会创建一个截断的文件)、O_TRUNC(若文件已存在,则截断它)、O_APPEND(在每次写操作前,设置文件到文件结尾处)。
图8.1 open函数
close函数:进程通过调用此函数来关闭一个打开的文件。
read函数:应用程序调用此函数来执行输入。read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf,返回值若为-1,则表示一个错误;若为0则表示EOF,否则就会返回实际传送的字节数量。
write函数:应用程序通过调用此函数来执行输出。
lseek函数:应用程序通过调用此函数来显示地修改当前文件的位置。
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;
}
其中:fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素,而va_list arg则是指向输入字符的第一个参数。vsprintf函数的作用则是进行格式化,返回将要打印字符的长度。追踪vsprintf函数可知其函数体为:
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);
}
write函数的作用则是长度为i的buf进行输出,即将buf中的i个元素输出给终端。追踪一下write函数可得write函数体:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
其中:INT_VECTOR_SYS_CALL表示系统会调用sys_call函数,准确来说这个函数只有一个功能:显示格式化了的字符串。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
getchar函数并不是直接从键盘上读取数据,而是在键盘缓冲区中进行读取,getchar函数调用了read函数去读取储存到键盘缓冲区的ASCII码到buf中直到读取到回车键后并返回缓冲区长度,而getchar便利用read等函数去返回字符。
8.5本章小结
本章主要对hello的IO管理进行展开,通过对Linux的IO设备管理方法和Unix的IO接口及函数进行描述后对其的具体细节有了更深的印象,后来通过对printf函数和getchar函数的函数体和函数实现进行分析对程序的IO管理有了更深的理解。
结论
hello程序一个很简单的程序,但却包含了计算机中程序成长的一生。首先是hello在计算机中有源程序进行转换,变为hello的可执行目标程序。它一共要经过4个流程:
首先是预处理阶段,在这个阶段hello会被预处理器根据#开头的命令进行加工,将其翻译为有几千行的hello.i的程序;
之后会被编译器翻译为汇编程序,这个汇编程序里面含有汇编语言程序,这一步将我们使用的高级语言变为了一种通用的输出语言;
汇编程序会被汇编器翻译为机器语言指令,其实,在我看来,汇编语言与机器语言指令几乎一致,但不同之处却有些特殊,例如操作数等。这一步会将翻译之后的各个片段打包为一个可重定位目标程序;
最后便是链接阶段,它将一个巨大的源文件分成更小、更好管理的模块,修改时只需对特定模块修改即可,同时还有各种链接方式对程序的存储等提供了更高效的方法。
在hello程序变为可执行目标程序后,便是对其进程管理、存储管理、系统的IO管理进行分析。
计算机通过利用fork函数对hello去创建子进程后利用execve函数去加载运行这个新的进程,hello进程与其他很多的进程一同并发执行,在执行过程中,会根据抢占情况来进行用户与内核模式的切换,利用上下文切换去调度进程或是在键盘中输入Ctrl-C等信号后,在收到信号后进行信号处理程序。
在hello的存储当中,MMU将虚拟地址通过TLB、多级页表等方式将其翻译为物理地址。由于采用了段式管理、页式管理的方式,在访问的时候若是出现了缺页则会调用缺页中断处理程序。
hello程序在运行的时侯会调用printf函数与getchar函数,这些都会调用Unix I/O的函数。
hello进程在运行结束后会被父进程进行回收,系统会删去一系列与之有关的数据结构,也就是结束hello的一生。
在刚开始接触程序的时候由于是直接使用C语言这样的高级语言,并没有发现程序在计算机当中的门门道道,可当深入挖掘程序在计算机的一生后才发现一个程序的诞生竟会有这么多高深的学问,计算机系统无疑是一门高度浓缩了人类智慧结晶的产物,而我现在学习的也只是深入理解计算机系统这本书当中的一部分,这本书的厚度也侧面体现了这门学问的复杂度。通过这学期的学习才发现只是初窥门径而已,想要真正做到与计算机和程序进行对话还需要日后更多的学习。
附件
1.hello.i 作用:修改了的预处理阶段程序
2.hello.s 作用:汇编程序
3.hello.o 作用:可重定位的目标程序
4.hello.out 作用:可执行的目标程序
5.hello.elf 作用:hello.o的ELF格式
6.hello1.elf 作用:hello.out的ELF格式
参考文献
[1] Pianistx. printf 函数实现的深入剖析[EB/OL]. (2013-09-11)https://www.cnblogs.com/pianist/p/3315801.html.
[2] 西邮小菜机. 操作系统的内存管理——页式、段式管理、段页式管理[EB/OL]. (2022-05-08)https://blog.csdn.net/weixin_46199479/article/details/123438544?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168325291416800215068514%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=168325291416800215068514&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-1-123438544-null-null.142^v86^insert_down38v5,239^v2^insert_chatgpt&utm_term=%E9%A1%B5%E5%BC%8F%E7%AE%A1%E7%90%86&spm=1018.2226.3001.4187.
[3] Jay. 缺页异常(重要)[EB/OL]. (2022-09-18)https://blog.csdn.net/Jay112011/article/details/126914421.
[4] Hallaron Randal E. Bryant David. 深入理解计算机系统[M]. 原书第3版. 北京: 机械工业出版社, 2016.