计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 2+X人工智能
学 号 2021111342
班 级 21WL021
学 生 刘文青
指 导 教 师 史先俊
计算机科学与技术学院
2023年4月
HelloWorld是每个程序员的起点,本文通过跟踪一个名为Hello的程序的生命周期,详细描述了它从最初的C语言源代码开始,逐渐经历预处理、汇编、链接等一系列过程,最终成长为可执行文件。这也标志着Hello将迎来一个新的阶段。
在这个新的阶段中,Hello将与操作系统进行交互。操作系统就像Hello的伯乐一样,为它开辟进程,提供虚拟内存和独立的地址空间,为它分配时间片和控制流,以便让它在操作系统上自由运行。最终,随着进程的结束,Hello的辉煌生命周期也告一段落。
本文以一个名为hello.c的源代码为起点,追踪Hello的成长历程,并详细阐述了每一步变化的过程和意义
关键词:预处理,编译,汇编,链接,进程管理,虚拟地址,存储管理,IO操作
目 录
第1章 概述
1.1 Hello简介
HelloWorld想必是每一个程序员的启蒙小怪,当我们打开VSCode,输入一行行代码,按下编译运行后,我们便完成了第一次代码编写,向世界问好。这个看似非常简单的程序实则是早先第一个实现的P2P。
P2P:全称为From Program to Progress,从项程序到进程。Hello的P2P是指预处理(Preprocessing)、编译(Comcompling)和链接(Linking)的过程。具体来说,P2P的整个过程如下:
①预处理(Preprocessing):在这个阶段,Hello的源代码会被预处理器处理。预处理器会先处理预处理指令,例如#include和#define,然后将处理后的代码输出到一个.i文件中。这个.i文件是Hello的下一个阶段的输入文件。
②编译(Comcompling):在这个阶段,Hello的.i文件会被编译器处理。编译器会将.i文件中的代码翻译成汇编语言,生成一个.s汇编文件。这个.s汇编文件是下一个阶段的输入文件。
③汇编(Assembly)是编译器将预处理后的代码翻译成机器码的过程。具体来说,编译器会将.s汇编文件中的汇编代码翻译成机器指令,生成目标文件。目标文件包含了可执行代码和数据,但还没有被链接器链接成最终的可执行文件。
④链接(Linking):在这个阶段,Hello的.s汇编文件会被链接器处理。链接器会将汇编代码转化为可执行文件,并将代码中使用的函数和库链接到可执行文件中。最终生成的可执行文件就是Hello的运行文件。
020:由shell加载hello。映射虚拟内存,进入程序,载入物理内存,执行main函数。程序结束后,shell回收hello进程,删除hello的进程占用。
1.2 环境与工具
1.2.1 硬件环境
处理器 11th Gen Intel(R) Core(TM) i5-11400H @ 2.70GHz 2.69 GHz机带 RAM 16.0 GB (15.8 GB 可用)
系统类型:64 位操作系统, 基于 x64 的处理器
GPU:NVIDIA GeForce RTX 3050 Laptop GPU
1.2.2 软件环境
软件环境:Windows 10、Vmware16上的Ubuntu(64位)虚拟机
1.2.3 开发与调试工具
开发工具:vim文本编辑器、Visual Studio、EDB、Vscode、Gcc编译器
调试工具:GDB、EDB
1.3 中间结果
文件名称 | 作用 |
hello.c | 储存hello程序源代码 |
hello.i | 源代码经过预处理产生的文件(包含头文件等工作) |
hello.s | hello程序对应的汇编语言文件 |
hello.o | 可重定位目标文件 |
hello_o_s.txt | hello.o的反汇编语言文件 |
hello_o_elf.txt | hello.o的ELF文件格式 |
hello | 二进制可执行文件 |
hello_elf.txt | 可执行文件(hello)的ELF文件格式 |
hello_s.txt | 可执行文件(hello)的汇编语言文件 |
1.4 本章小结
本章简要介绍了hello的p2p以及020的过程,并简要的介绍了hello.c文本源文件到hello可执行目标文件的转变。并列举了实验进行过程中的硬件环境,软件环境,开发工具,调试工具。
第2章 预处理
2.1 预处理的概念与作用
预处理概念:处理c源文件中的预处理程序和注释。
预处理作用:预处理器(cpp)根据以字符#开头的预处理命令处理c源文件,修改原始的c源文件,预处理过程中,预处理器会分析预处理指令,依据#include进行库文件的插入,依据#define进行字符串的替换等,并且同时去除c源代码中的注释
2.2在Ubuntu下预处理的命令
Linux中使用如下指令进行预处理操作
图1 预处理操作指令
图2 hello.i文件生成
2.3 Hello的预处理结果解析
在Linux中我们用文本编辑器打开.i文件,程序如下图所示:
图3 hello.i文件内容展示
我们可以发现.i文件比源程序多了很多内容,也少了一些内容。多的内容包含对源文件中定义的宏进行了展开,将头文件中的内容包含到这个文件中,还定义了一些函数,以及一些结构体类型的声明;同时预处理操作去掉了原来的注释。
2.4 本章小结
本章执行了预处理指令,看了预处理后的文本文件的内容,看了预处理的方式与作用。
第3章 编译
3.1 编译的概念与作用
编译的概念:
编译是利用编译程序从源语言编写的源程序产生目标程序的过程,也是用编译程序产生目标程序的动作。也就是说编译器会将通过预处理产生的.i文件翻译成一个后缀为.s的汇编语言文件,编译就是从高级程序设计语言到机器能理解的汇编语言的转换过程。
编译的功能:
其实从概念中也可以直接提炼出,编译的功能就是产生汇编语言文件,并交给机器。除此之外,编译器还有一些其他功能,例如语法检查等。
3.2 在Ubuntu下编译的命令
Linux中使用如下指令进行编译操作
图4 hello.s编译指令
3.3 Hello的编译结果解析
图5 hello.s文件
3.3.1 数据
字符串:hello.c有两个字符串,字符串数据是以字符数组的形式存储在只读数据区的,如图7,这两个字符串在访问时都是通过其首地址访问的,如图6。string1中的汉字是以utf-8格式存的,超过了char表示的ASCII码范围,以十六进制的格式显示,因此是一堆大正数。
图6 字符串表示
图7 字符串访问指令
3.3.2 数组及操作
hello.c⾥⾯有⼀个参数的char型指针数组,数组⾸地址放在栈里,指针数组
⾥的每个元素指针里面放的是每个参数字符串所在地址。
图8 数组
图9为栈中-32(%rbp)处的内容,放的是指针数组首地址。可以看出整个指针
数组也在栈里,
图9 数组指针首地址
每次引用数组元素时,采用首地址加偏移的⽅式,先从-32(%rbp)处得到指针数组首地址,再加上偏移找到对应的指针,再将指针里的参数字符串的地址传递给寄存器,调用puts或printf函数打印字符串。
图10 参数字符串地址传递
3.2.3 局部变量
for循环里声明了⼀个局部变量i,编译器进⾏编译的时候将局部变量i放在栈中,即-4(%rbp)处。
图11 局部变量压栈
3.3.4 常量
⽤来做比较运算的常量4和5(比较时用的是4)等以立即数的形式存在。
图12 立即数用于跳转指令
3.3.5 赋值语句
在for循环中对i赋初值,直接用数据传输指令将立即数0传送给栈中-4(%rbp)
处,如图11所示。
3.3.6 类型转换语句
atoi(argv[3]),显式地将输⼊的最后一个参数由字符串类型转换为整数类型,
作为sleep的参数。
图13 类型转换语句
3.3.7 算术操作
i++涉及到加法运算,⽤addl命令实现。
图14 加法操作指令
3.3.8 关系操作及控制转移
控制转移往往是和关系操作结合在一起的,关系操作在比较中设置了条件码,控制转移根据条件码判断是否要进行跳转。
图15 关系操作及跳转指令
这里的if语句用来判断argc与4是不是相等,如果相等则跳转到.L2执行,如果不相等则顺序执行下一条指令。
类似的还有for循环,也被翻译成比较加跳转。
图16 for跳转指令源程序和汇编指令对应
3.3.9 循环语句
图17 循环语句
如3.3.8中所示,for循环语句也被翻译成比较加跳转,与if等跳转不同的是,条件满足时总是向之前的命令跳转,重复执行。
3.3.10 函数操作
主函数main,调用的函数有printf,exit,sleeep,getchar,atoi,全部都是共享库中的函数,命令只有直接call,如图18示例所示,返回值放在%rax里面。
图18 函数调用语句
3.4 本章小结
本章介绍了编译阶段编译器是如何将c程序翻译成汇编语⾔程序的,并进行了编译命令的演示,以及编译器是怎么处理C语言的各个数据类型以及各类操作的。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编器将.s文本文件翻译成机器语言。
汇编的作用:汇编器将.s中的汇编语言翻译成计算机能处理的机器指令语言,并输出可重定位文件(二进制文件)。
4.2 在Ubuntu下汇编的命令
图19 hello.o文件
4.3 可重定位目标elf格式
4.3.1 ELF头
.o文件为目标文件,相当于Windows中的.obj后缀文件,因此直接使用Vim或者其他文本编辑器查看会出现一大堆乱码。那么我们选择查看可重定位目标文件的elf形式,使用命令readelf -h hello.o查看ELF头,结果如图20所示。
ELF头以一个16字节的序列(Magic,魔数)开始,这个序列描述了生成文件的系统的字的大小和字节顺序。ELF头剩下部分的信息包含帮助连接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型和机器类型等。例如上图中,Data表示了系统采用小端法,文件类型Type为REL(可重定位文件),节头数量Number of section headers为13个等信息。
图20 hello.o的ELF文件
4.3.2 Section头
使用命令readelf -S hello.o查看节头,结果如图21。
图21 Section头信息
夹在ELF头和节头部表之间的都为节,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。各部分含义如下:
名称 | 包含内容含义 |
.text | 已编译程序的机器代码 |
.rodata | 只读数据 |
.data | 已初始化的全局和静态C变量 |
.bss | 未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量 |
.symtab | 一个符号表,存放一些在程序中定义和引用的函数和全局变量的信息 |
.rel.text | 一个.text节中位置的列表 |
.rel.data | 被模块引用或定义的所有全局变量的重定位信息 |
.debug | 一个调试符号表 |
.line | 原始C源程序中的行号和.text节中机器指令之间的映射 |
.strtab | 一个字符串表(包括.symtab和.debug节中的符号表) |
4.3.3 符号表
使用命令readelf -s hello.o查看节头,结果如图22
图22 符号表
这其中,Num为某个符号的编号,Name是符号的名称。Size表示他是一个位于.text节中偏移量为0处的146字节函数。Bind表示这个符号是本地的还是全局的,由上图可知ABS hello.c函数名称这个符号变量是本地的。
4.3.4 可重定位段信息
使用readelf -r hello.o查看可重定位段信息,结果如下
图23 可重定位节
offset是需要被修改的引用的节偏移,Sym.(符号)标识被修改引用应该指向的符号。Type告知连接器如何修改新的引用,Addend(加数)是一个有符号常数,一些类型的重定位要使用它对被修改的引用的值做偏移调整。
4.4 Hello.o的结果解析
使用objdump -d -r hello.o命令对hello.o可重定位文件进行反汇编,得到的反汇编结果如下图
图24 hello.o反汇编程序
看到hello.o的反汇编文件我们很是熟悉,它使用的汇编代码和hello.s汇编文件的汇编代码是一样的,但是在这反汇编文件的字里行间中,也混杂着一些我们相对陌生的面孔,也就是机器代码。
这些机器代码是二进制机器指令的集合,每一条机器代码都对应一条机器指令,到这儿才是机器真正能识别的语言。每一条汇编语言都可以用机器二进制数据来表示,汇编语言中的操作码和操作数以一种相当于映射的方式和机器语言进行对应,从而让机器能够真正理解代码的含义并且执行相应的功能。机器代码与汇编代码不同的地方在于:
(1)hello.s中的操作数和偏移均为10进制,反汇编所用均为16进制。
(2)hello.s中特有.cfi指令。
(3)反汇编中有二进制机器语言指令,hello.s里面只有汇编语言指令。
(4)hello.s 中跳转地址为.Lx,反汇编中为⽤偏移量表示的未重定位的内存地址。
(5)hello.s 中调⽤函数直接用函数名,⽽hello.o中用重定位条目来占位。
机器语言是二进制语言,是cpu可以识别的语言,由操作码和操作数构成。汇编语言是为了便于⼈们阅读机器语言而产生的一种文本表示。汇编语言可直接翻译成机器语言,和机器语言是⼀⼀对应的。
4.5 本章小结
本章介绍了汇编阶段的汇编器的作用,展示了汇编指令和生成的.o文件中ELF格式信息,进行了汇编语言程序.s⽂件和反汇编文件中内容的比较。
第5章 链接
5.1 链接的概念与作用
概念:链接(Linking)分为静态链接和动态链接,是指将多个目标文件或库文件组合成一个可执行文件或共享库的过程。
作用:链接的作用是将多个目标文件或库文件合并成一个可执行文件或共享库。通过链接,可以使得程序具有可执行性,并且可以使得不同程序间共享同一份库文件,提高系统资源利用率。链接还可以进行符号解析、地址重定向等操作,以确保程序在运行时能够正常访问所需的函数和变量。
5.2 在Ubuntu下链接的命令
Linux系统hello.c使用如图所示指令进行链接
5.3 可执行目标文件hello的格式
5.3.1 ELF头
使用命令readelf -h hello查看hello的ELF头
图26 ELF头
可以看到文件的Type发生了变化,从REL变成了EXEC(Executable file可执行文件),节头部数量也发生了变化,变为了27个。
5.3.2 Section头
使用命令readelf -S hello查看节头部表信息
图27 Section头信息
节头部表对hello中所有信息进行了声明,包括了大小(Size)、偏移量(Offset)、起始地址(Address)以及数据对齐方式(Align)等信息。根据始地址和大小,我们就可以计算节头部表中的每个节所在的区域。
5.3.3 符号表
使用命令readelf -s hello查看符号表信息
图28 符号表信息
与hello.o的符号表相比较,hello的符号数量剧增。这是因为经过连接之后引入了许多其他库函数的符号,一并加入到了符号表中。
5.4 hello的虚拟地址空间
在edb中打开可执行文件hello,可以看到hello虚拟地址空间的起始地址为0x401000,结束地址为0x401fff。
图29 EDB查看虚拟地址空间
根据5.3.2节里面的Section头部表,我们可以找到对应的节的其实空间对应位置,例如.interp节,offset为0,起始位置地址为0x401000在edb中有其对应位置,如图29所示。
图30 interp节与其对应
5.5 链接的重定位过程分析
使用命令objdump -d -r hello查看hello可执行文件的反汇编条目
图31 hello反汇编文件(截取)
分析 hello 与 hello.o 不同:
- hello.out反汇编有更多的节。hello.out从init部分开始反汇编,有许多节,如.text,.bss等,而hello.o中只有main函数部分。
- hello.out新增了许多加载时链接的共享库中的函数代码,如exit、printf、sleep、getchar等函数,是因为在加载时进行了符号解析加重定位再反汇编,所以会有这些函数代码。
- hello反汇编的代码有确定的虚拟地址,也就是说已经完成了重定位,main函数地址为 0x400637 开始。而hello.o反汇编代码中main函数地址为0,未完成可重定位的过程。
- 在调用函数和访问字符串⽅⾯,hello.out的反汇编有确定的call地址和字符串首地址,但是hello.o反汇编出来的这两个部分只有重定位条目占位,还没有进⾏全局符号和外部符号的解析和重定位(重定位条目和指令实际放在不同节,OBJDUMP工具将他们放在一起)。
图32 hello的反汇编函数调用
图33 hello.o的反汇编函数调用
5.6 hello的执行流程
hello程序先调用_init初始化,之后到_start,再到main,main执行之后执行_printf、_exit、_atoi、_sleep、_getchar,最后退出。
程序名称 | 程序地址 |
_init | 0x401000 |
main | 0x401125 |
_start | 0x4010f0 |
puts@plt | 0x401090 |
printf@plt | 0x4010a0 |
getchar@plt | 0x4010b0 |
atoi@plt | 0x4010c0 |
exit@plt | 0x4010d0 |
sleep@plt | 0x4010e0 |
5.7 Hello的动态链接分析
链接器在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到加载时再进行。在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU 编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,⽽PLT是代码段的一部分,由之前图27可知GOT.PLT地址为0x404000
图34 在调用_init之前
图35 在调用_init之后
可见其中个别字节发生了改变,是由于动态链接产生的结果。
5.8 本章小结
概括了链接的概念。分析了hello程序运行时的虚拟地址空间,hello的重定位过程和执行过程中地址的变化。阐述了动态链接的过程。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是⼀个执行中的程序的实例,系统中的每个程序都运⾏在某个进程的上下⽂中。
作⽤:提供两个抽象
(1)一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占的使用处理器。
(2)⼀个私有的地址空间,它提供一个假象,好像我们的程序独占的使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell是一个为用户提供界面访问操作系统的内核服务。它是用户使用linux的方式。
处理流程:
- 从终端输入程序
- 切分字符串获得参数
- 如果内置命令则立刻执行,否则调用相应的程序为其分配子进程并运行
6.3 Hello的fork进程创建过程
在Linux系统中,用户可以通过./指令来执行一个可执行目标文件。在程序运行时,Shell就会创建一个新的进程,并且新创建的进程更新上下文,在这个新建进程的上下文中便可以运行这个可执行目标文件。fork()函数拥有一个int型的返回值。子进程中fork返回0,在父进程中fork返回子进程的Pid。新创建的进程与父进程几乎相同但有细微的差别。子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本(代码、数据段、堆、共享库以及用户栈),并且紫禁城拥有与父进程不同的Pid。
6.4 Hello的execve过程
(1) execve函数调⽤loader加载器函数,loader删除子进程现有的虚拟内存段和映射表,清空和物理内存,硬盘上可执行文件的映射,栈、堆初始化为0。
- 将虚拟地址空间中的页映射到磁盘上所请求的可执行文件的页,运行时由缺页中断驱动可执行文件的页加载到物理内存,建李虚拟地址空间与物理地址空间的映射。
- 将IP设置为程序起始地址,
6.5 Hello的进程执行
1.逻辑控制流:程序计数器(PC)值的序列叫做逻辑控制流,简称为逻辑流。
2.上下文切换:上下文是内核重新启动一个被抢占进程所需的状态。内核可以决定抢占当前的进程,并重新开始一个先前被抢占的进程,这种决策成为调度,由调度器执行。在内核中调度一个新的程序运行,它会抢占当前的进程,该过程中使用的机制成为上下文切换,是一种较高层次的一场控制流来实现的。
3.时间片:进程执行它的控制流的一部分的每一个时间段称为时间片。
Shell调用fork为hello创建子进程,使用execve进行运行,开始hello运行在用户模式,收到信号后进入内核模式,运行信号的处理程序,之后回到用户模式。此时上下文切换,切分成时间片,并且也会切入到其他进程,形成多个程序共用处理器,但给人以独占的感觉。
6.6 hello的异常与信号处理
6.6.1 可能出现的异常:
① 中断:处理器外I/O设备信号的结果,异步异常,总是返回到下一条指令
② 陷阱:一种有意的异常,使指令执行的结果,其最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,称为系统调用。一般返回下一条指令。
③ 由错误的情况引起,非有意。如果被故障处理程序修正,则重新执行;若不能被修正,则终止程序。
④ 不可恢复的致命性错误。终止处理程序,并且不会控制返回给应用程序。
6.6.2 信号
图36 可能出现的信号及处理方法
6.6.3 hello的具体信号处理
① Ctrl+C结果:终止
图37 Ctrl+C信号
② 乱摁和回车:正常运行
图37 乱摁和回车
③ Ctrl+Z:停止
图38 Ctrl+Z
④ ps:显示进程
图39 ps
⑤ jobs:显示任务
图40 jobs
⑥ pstree:显示进程
图41 pstree
⑦ fg:后台停止程序变为前台运行程序
图42 fg
⑧ Kill:发送杀死信号:停止程序变为终止程序
图43 Kill
⑨ 发送SIGCONT:停止程序继续运行
图44 SIGCONT
6.7本章小结
本章概括了进程的概念,shell的作用,fork和execve的运行过程以及hello的异常和信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
在CPU中当然不是仅仅只有hello一个进程,而是许多进程共享CPU和主存资源。那么为了使内存管理更加高效,操作系统提出了一种十分重要的抽象,即虚拟内存。它有几处有点:
- 可以有效使用主存;
- 可以简化内存管理;
- 提供独立的地址空间。
下面介绍几个概念
① 逻辑地址:由程序产生的和段相关的偏移地址,格式为:段+偏移。
② 线性地址:逻辑地址加上相应段的基地址就生成了线性地址。
③ 虚拟地址:虚拟地址是一个抽象的地址空间,虚拟地址对应虚拟页,虚拟页会映射磁盘空间的一页,如果要使用该页上的数据,则会将该页载入内存,虚拟地址就对应了物理地址。
④ 物理地址:CPU外部地址总线上的物理内存的地址,可以将内存看成一个从0字节开始的数组,每个字节拥有单独的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
用段选择符去GDT(全局描述符表)里得到段基址(段基址)然后加上段内偏移,这就得到了线性地址。我们把这个过程称作段式内存管理。
图45 逻辑地址与线性地址映射
对于linux系统,为了保证可移植性,其简化了分段机制,仅使用IA-32的分页机制,把所有的段描述符的基址设为0,此时逻辑地址和线性地址基本没什么区别。
7.3 Hello的线性地址到物理地址的变换-页式管理
首先cpu的PC产生的是虚拟地址,也就是线性地址,需要经过MMU翻译成物理地址,最终cpu芯片发出的地址信号是物理地址。转换时需要用到一个重要的工具,就是存放在物理内存中叫做页表的数据结构,页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。页表就是一个页表条目的数组,虚拟地址空间的每一个页在页表中一个固定的偏移量处都有一个PTE,每个PTE是由一个有效位和一个n位的地址字段组成,如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置。CPU想要读取虚拟内存中的一个字时,地址翻译硬件将虚拟地址作为一个索引从页表中找到其所在物理页的起始位置,再构造这个字的物理地址传给地址总线。
图46 页式管理地址映射
7.4 TLB与四级页表支持下的VA到PA的变换
TLB:每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然⽽,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为快表(TLB)。TLB是一个⼩的,虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。地址翻译硬件每次想要读取页表时,先去访问TLB,若TLB中有想访问的PTE(即命中),翻译速度会很快。
多级页表:多级页表是一种压缩页表的方法,思想很简单,就是将虚拟地址划分为k个VPN和⼀个VPO,每个VPNi都是一个到第i级页表的索引,直到找到最后一级页表中的物理页起始地址,和VPO一起构造物理地址。
图47 多级页表
7.5 三级Cache支持下的物理内存访问
首先通过物理地址中的组索引位得到组号。先在L1高速缓存对比有效位和标志位,匹配成功则读取,失败则进入二级高速缓存中再次匹配,以此类推,并且进行缓存的更改,在k-1层存储中缓存该数据。
图48 三级Cache内存访问
7.6 hello进程fork时的内存映射
为新进程创建虚拟内存,创建当前进程的的mm_struct, vm_area_struct和页表的原样副本.,两个进程中的每个页面都标记为只读,两个进程中的每个区域结构都标记为私有的写时复制。在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存,随后的写操作通过写时复制机制创建新页面。
7.7 hello进程execve时的内存映射
为hello程序删除已存在的用户区域,创建新的区域结构,代码和初始化数据映射到.text和.data区(目标文件提供),bss和栈堆映射到匿名文件 ,栈堆的初始长度0,共享对象由动态链接映射到本进程共享区域,设置PC,指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页处理程序的主要步骤为:
1.检查虚拟地址是否合法,若不合法,则程序终止。
2.检查进程是否有读写或执行该区域权限,若不具有,则触发异常保护机制,程序终止。
3.内核选择一个牺牲页面,写入磁盘,重新更换新的页面。
4.控制权转移给hello进程,执行触发缺页的指令。
7.9动态存储分配管理
动态内存管理的基本方法;使用动态内存分配器。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。它紧接在未初始化的数据区域后面开始,并向下生长。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的,取决于分配器的不同风格。
策略:动态内存管理主要有两种策略:
- 用隐式空闲链表组织堆
首先了解几个概念:
首次适配(First fit):从头开始搜索空闲链表,选择第一个合适的空闲块:搜索时间与总块数(包括已分配和空闲块)成线性关系。在靠近链表起始处留下小空闲块的“碎片”。
下一次适配 (Next fit):和首次适配相似,只是从链表中上一次查询结束的地方开始。比首次适应更快:避免重复扫描那些无用块。一些研究表明,下一次适配的内存利用率要比首次适配低得多。
最佳适配 (Best fit):查询链表,选择一个最好的空闲块;适配,剩余最少空闲空间。保证碎片最小——提高内存利用率,通常运行速度会慢于首次适配。
在隐式空闲链表工作时,如果分配块比空闲块小,可以把空闲块分为两部分,一部分用来承装分配块,这样可以减少空闲部分无法使用而造成的浪费。隐式链表采用边界标记的方法进行双向合并。脚部与头部是相同的,均为4个字节,用来存储块的大小,以及表明这个块是已分配还是空闲块。同时定位头部和尾部,是为了能够以常数时间来进行块的合并。无论是与下一块还是与上一块合并,都可以通过他们的头部或尾部得知块大小,从而定位整个块,避免了从头遍历链表。但与此同时也显著的增加了额外的内存开销。他会根据每一个内存块的脚部边界标记来选择合并方式,如下图:
图49 隐式空闲链表中的块合并
- 用显式空闲链表组织堆
显式空闲链表只记录空闲块,而不是来记录所有块。它的思路是维护多个空闲链表,每个链表中的块有大致相等的大小,分配器维护着一个空闲链表数组,每个大小类一个空闲链表,当需要分配块时只需要在对应的空闲链表中搜索。
7.10本章小结
本章阐述了计算机中虚拟内存管理,物理地址,线性地址,逻辑地址以及他们的变换模式,段式,页式管理,在内存映射的基础上重新认识了fork和excave。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。
8.2 简述Unix IO接口及其函数
Unix IO 接口定义如 8.1 所述,下面简述其函数。
1. 打开⽂件,open 函数
int open(char* filename,int flags,mode_t mode);
进程通过调用open函数来打开一个存在的文件或是创建一个新文件。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件,也可以是⼀个或者更多位掩码的,为写提供⼀些额外的指示。mode参数指定了新文件的访问权限位。
2. 关闭文件,close函数
int close(int fd);
进程通过调用close函数关闭⼀个打开的文件,关闭一个已关闭的文件会出错。fd是要关闭的进程描述符,返回操作结果。
3. 读⽂件,read函数
ssize_t read(int fd,void *buf,size_t n)
进程调用read函数执行输入,read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
- 写文件,write 函数
ssize_t wirte(int fd,const void *buf,size_t n)
进程调用write函数执行输出,write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
- 显示地修改当前⽂件的位置,lseek函数
8.3 printf的实现分析
printf函数是C语言定义的标准I/O库中的函数,为程序员提供了UnixI/O的较高级别的替代,将write函数包装好供程序员方便地使用。
首先看printf函数的内容
图50 printf函数
printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。printf用了两个外部函数,一个是vsprintf,还有一个是write。
vsprintf函数作用是接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.字符显示驱动子程序:从ASCII到字模库到显示vram。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点。
8.4 getchar的实现分析
。
图51 getchar函数
当程序调用getchar时,程序等待用户按键,当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。当用户键入回车之后(回车也在缓冲区当中),getchar调用read系统函数,从缓冲区中每次读入一个字符,直到接受到回车键才结束。getchar函数的返回值是用户输⼊的第⼀个字符的ascii码,如出错返回-1,且将用户输⼊的字符回显到屏幕。如用户在按回车之前输⼊了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调⽤read系统函数,通过系统调用读取按键ascii码,直到接受到回⻋键才返回
8.5本章小结
本章介绍了Linux的I/O设备的基本概念和管理方法,展示了Unix I/O接口及其函数,最后分析了printf函数和getchar函数的工作过程。
结论
Hello的历程:
- hello.cC语言文本文件的编写
- 预处理过程:hello.c预处理为hello.i
- 编译:编译器(ccl)将文本文件hello.i(C语言)翻译成文本文件hello.s(汇编语言)。
- 汇编:汇编器将hello.s中的汇编语言翻译成计算机能处理的机器指令语言,并输出可重定位文件hello.o(二进制文件)
- 链接:将hello中使用的库函数文件与hello.o文件进行连接构成可执行目标文件。
- Shell为hello创建进程
- shell调用execve函数,execve函数会将新创建的子进程的区域结构删除,然后将其映射到hello程序的虚拟内存,然后设置当前进程上下文中的程序计数器,使其指向hello程序的入口点。
- 运行hello时,内存管理单元、TLB、多级页表机制、三级cache协同工作,完成对地址的翻译和请求。
- 当Hello运行到printf这一步时,操作系统会调用malloc函数从堆中申请内存。
- 当Hello执行时,可以通过IO输入等操作向进程发送信号。例如我们从键盘输入Ctrl-c,就会发送一个SIGINT信号,使当前前台进程的作业中断;同样哦们可以使用命令jobs来查看被抢占的进程,使用命令fg%<pid>来恢复对应ID的进程。
- 结束运行,shell回收hello进程。
感悟:
1. 执行一个程序真的太复杂了,计算机系统真的博大精深!
2. 在系统地分析hello程序的整个过程时,我越来越体会到计算机严密的逻辑和层出不穷的创新,这种严谨而积极的态度值得我们学习!
附件
文件名称 | 作用 |
hello.c | 储存hello程序源代码 |
hello.i | 源代码经过预处理产生的文件(包含头文件等工作) |
hello.s | hello程序对应的汇编语言文件 |
hello.o | 可重定位目标文件 |
hello_o_s.txt | hello.o的反汇编语言文件 |
hello_o_elf.txt | hello.o的ELF文件格式 |
hello | 二进制可执行文件 |
hello_elf.txt | 可执行文件(hello)的ELF文件格式 |
hello_s.txt | 可执行文件(hello)的汇编语言文件 |
参考文献
- Randal E.Bryant.深⼊理解计算机系统[M].北京:机械⼯业出版社,2016.7
[2] https://www.cnblogs.com/diaohaiwei/p/5094959.html