计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机类
学 号 120L02xxxx
班 级 200300x
学 生 xxx
指 导 教 师 xxx
计算机科学与技术学院
2021年5月
本文先总体说明了hello从Program 到 Process的过程,以及hello从代码文件一步步被预处理,汇编,链接,最后回收的从无到无的过程。从第二章开始,本文对上述hello的一生(预处理,编译等过程)进行详细介绍与展示。
关键词:预处理;汇编;链接;进程;信号;
目 录
6.2 简述壳Shell-bash的作用与处理流程... - 23 -
6.3 Hello的fork进程创建过程... - 23 -
7.2 Intel逻辑地址到线性地址的变换-段式管理... - 28 -
7.3 Hello的线性地址到物理地址的变换-页式管理... - 28 -
7.4 TLB与四级页表支持下的VA到PA的变换... - 29 -
7.5 三级Cache支持下的物理内存访问... - 29 -
7.6 hello进程fork时的内存映射... - 29 -
7.7 hello进程execve时的内存映射... - 30 -
第1章 概述
1.1 Hello简介
1.1.1 hello.c的P2P(From Program to Process):
hello.c是一个在在linux环境下用c语言写成的一个高级c语言程序(源程序文件),即“Program”。具体代码如下图:
图1.1 hello.c源程序
程序指示用户,如果没有输入学号、姓名、秒数三个变量,就会打印该程序用法;而如果输入正确,就会按照用户的输入,每几秒打印用户的学号和姓名,一共执行八次。
在linux的命令行窗口中执行以下指令:
gcc -m64 -no-pie -fno-PIC hello.c -o hello
执行该指令后,系统会调用图1.2中的编译系统,生成名为hello的二进制可执行目标程序。而从hello.c到hello的“翻译”可分为以下四个阶段:
图1.2 编译系统
在shell中输入指令./hello 120L021925 刘馥淇 1,shell将获取argc、argv、envp三个参数,即argv[1]的内容是“120L021925”,argv[2]的内容是“刘馥淇”, argv[3]的内容是1。
经过cpp,ccl,as,ld的处理,逐步生成hello.i, hello.s, hello.o, hello文件,即Process,完成P2P的过程。
1.1.2 hello.c的O2O(From Zero-0 to Zero-0):
首先,调用fork()函数创建子进程,execve加载文件名后,调用启动代码设置栈,并将控制传给子进程。先删除当前虚拟地址的用户部分已存在的数据结构,为hello的代码、数据等创建新的区域结构,然后映射共享区域,设置程序计数器,使之指向代码区域的入口点,然后进入main函数,CPU为hello分配时间片执行逻辑控制流。hello通过Unix I/O管理来控制输出。在hello进程终止之后,hello所有的痕迹将被删除,操作系统将控制权传回给shell,shell将等待下一个命令行输入。至此,hello.c完成“从无到无”的O2O过程。
1.2 环境与工具
软件环境:Windows10 64位;
VirtualBox11;Ubuntu 20.04.4;
硬件环境:处理器Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz 2.30 GHz;
机带RAM 8.00 GB (7.80 GB 可用);
调试工具:CodeBlocks 17.12;gedit+gcc;
1.3 中间结果
hello.c:源程序文件;
hello.i:修改了的源程序;
hello.s:汇编程序文本文件;
hello.o:可重定位目标程序(二进制文件);
hello:可执行目标程序;
hello_asm.txt:hello的反汇编文件
1.4 本章小结
本章分别大致描述了hello.c的P2P和O2O的过程,并且列出了调试hello.c所用的软硬件环境以及调试工具,最后列出了编写本篇论文过程中所生成的所有文件名称与性质。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理阶段是指预处理器(cpp)根据以字符#开头的命令,修改原始的c程序的过程。比如hello.c中的#include <stdio.h>编译预处理指令就是告诉cpp读取系统头文件stdio.h的内容,并把它直接插入程序文本中。#include <unistd.h>与#include <stdlib.h>的作用同理。
作用:得到另外一个c程序文件,它通常以.i作为扩展名,即1.3中提及的hello.i。
2.2在Ubuntu下预处理的命令
预处理指令:cpp hello.c>hello.i 或gcc -E hello.c -o hello.i
如下图,运行该指令之后,图中出现了hello.i文件。
图2.2.1 预处理指令
2.3 Hello的预处理结果解析
图2.3.1 hello.i部分内容截图
打开生成的hello.i之后,如上图,hello.i仍是可以阅读的文本文件,但是通过观察行数可以发现,hello.i在hello.c的基础上增加了很多内容。在这里,预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中,于是得到hello.i。
2.4 本章小结
本章以对hello的预处理操作为实例,充分展现了.c文件在执行预处理指令之后经历的操作与预处理后生成的结果。
第3章 编译
3.1 编译的概念与作用
概念:编译是指编译器(ccl)将文本文件hello.i翻译成包含汇编语言程序的文本文件hello.s的过程,也就是将高级语言变成汇编指令的过程。
作用:生成汇编程序(文本文件)hello.s;词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。
3.2 在Ubuntu下编译的命令
如下图,输入编译指令:gcc -S hello.i -o hello.s
可以看到,执行该指令后,目录(这里是桌面)上出现了hello.s文件。
图3.2.1 汇编指令
3.3 Hello的编译结果解析
3.3.1 数据
(1)字符串常量
图3.3.1 出现的字符串
作为printf函数的参数出现。
(2)局部变量
图3.3.2 局部变量示例
通过mov等操作,将局部变量储存在%rax,%rbx等寄存器中。
(3)全局函数main
对主函数main的声明如下:
图3.3.3 全局函数main的声明
3.3.2 赋值
这里举几个通过汇编指令来实现的赋值操作:
图3.3.4 通过mov指令实现的赋值操作
图3.3.5 含有立即数的赋值指令
图3.3.6 通过add指令实现的赋值操作
3.3.3 类型转换
如下图,用call指令调用atoi函数,将字符串中的数字转化为int型;
图3.3.7 类型转换示例
3.3.4 算术操作
hello.s中存在着加法和减法操作,具体如下:
图3.3.8 加法操作
图3.3.9 减法操作
3.3.5 关系操作
在hello.s中,两个数值大小关系的比较是通过comp指令和j跳转指令完成的,具体示例如下:
图3.3.10 关系比较
在图中,判断寄存器中数值是否等于立即数4,若相等则跳转至L2.
3.3.6 数组/指针/结构体操作
由于hello中只涉及argv数组操作,所以这里只举出数组的例子。阅读hello.c代码,我们发现argv数组有四个成员,在hello.s中,argv[1],argv[2],argv[3]的存储情况分别如下:
图3.3.11 开辟栈空间,并使%rax指向栈顶
图3.3.12 argv[2]
图3.3.13 argv[1]
图3.3.14 argv[3]
3.3.7 控制转移
在hello.c中,完成控制转移的是if(-else)语句以及for循环。在hello.s中如下:
图3.3.15 if语句
图3.3.16 for循环
3.3.8 函数操作
以hello.s中的部分函数操作举例如下:
(1)函数参数传递
sleep函数的参数储存在%edi中,调用后传给sleep。
图3.3.17 sleep函数
(2)函数调用
hello.s中用call指令完成对函数的调用:
图3.3.18 函数调用
(3)函数返回
函数的返回值存储在寄存器%rax中。
图 3.3.19 atoi函数的返回值
3.4 本章小结
本章具体展示了编译的指令和结果,并且以截图的形式展示了一些.c文件中的操作对应到.s文件中具体是怎样完成的。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编是指,在编译之后,汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式。
作用:将汇编指令转化成机器语言指令;生成二进制文件hello.o。
4.2 在Ubuntu下汇编的命令
在终端中输入以下指令:gcc -c hello.s -o hello.o
如下图,桌面上出现了hello.o文件。注意到hello.o的图标与hello.s不同,说明它不再是一个文本文件,而是一个二进制文件。
图4.2.1 汇编指令
4.3 可重定位目标elf格式
4.3.1 elf头
图4.3.1 elf头
在终端中输入指令:readelf -h hello.o 查看elf头,如上图。
elf头描述程序的总体格式,包程序的入口点。其中,Magic表示一个确定的值,描述了生成该文件的系统的字的大小以及字节顺序,在程序执行时会检查它是否正确,如果不正确就拒绝加载文件;在elf文件中还能看到,文件类别为ELF64,数据为小段序,类型为REL(可重定位文件)等。
4.3.2 节头部表
图 4.3.2 节头部表
在终端窗口中输入指令readelf -S hello.o 查看节头部表。
节头部表描述目标文件的各个节的名称、类型、大小、读写权限等。没有进行链接的可重定位目标文件的每个节都是从0开始的。
4.3.3 符号表
在终端窗口中输入指令readelf -s hello.o 查看符号表。
符号表存放着程序中定义和引用的函数,以及全局变量的信息。其中value表示偏移量,由于未进行链接,所以value的值都是0。
图4.3.3 符号表
4.3.4 重定位节
在终端窗口中输入指令readelf -r hello.o查看重定位节。
重定位节描述了重定位时需要的一些信息,如偏移量,类型等,在链接时,这些信息将被修改。
图4.3.4 重定位节
4.4 Hello.o的结果解析
首先,输入指令objdump -d -r hello.o 对hello.o反汇编如下:
图4.4.1 hello.o的反汇编
机器语言的反汇编由几个分工明确的段构成。
下面分析机器语言与汇编语言的不一致:
(1)立即数不同:反汇编中,立即数用十六进制表示,即$0x;而汇编语言中,立即数用十进制表示,即$。
(2)分支转移函数的跳转指令不同:反汇编的跳转指令使用地址跳转,汇编则使用L2等记号标记跳要转到的段存储位置;
(3)函数调用不同:反汇编调用函数使用的是地址偏移量,而汇编则直接调用函数名;
4.5 本章小结
本章展示了在hello.s的基础上进行汇编,生成二进制文件hello.o的具体指令以及过程;并且利用readelf工具查看了可重定位目标文件的elf格式;最后通过反汇编,比较了机器语言与汇编语言在细节上的一些差异,通过完成本章的内容,笔者对于汇编的过程有了更加深刻、具体的认识。
第5章 链接
5.1 链接的概念与作用
(1)概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到到内存并执行。
(2)作用:链接使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
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
如下图,桌面生成了hello文件。
图5.2.1 ld链接指令
5.3 可执行目标文件hello的格式
5.3.1 elf头
在终端中输入指令:readelf -h hello 查看elf头:
图5.3.1 elf头
相比于4.3.1中链接前的elf头,Magic未发生变化;而类型由REL(可重定位文件)变成了EXEC(可执行文件);
5.3.2 节头
在终端输入指令readelf -S hello 查看各节头:
图5.3.2 节头
图5.3.3 节头
可以看到各节的大小、偏移量等信息;各节的地址即即为程序被载入虚拟地址后各段的起始地址。
5.4 hello的虚拟地址空间
使用edb加载hello文件如下:
图5.4.1 data dump
查图5.3.2知,.init节的起始地址位于0x401000节,偏移量为0x001000,大小为0x1b,类似地,可以查到其他节的位置。
5.5 链接的重定位过程分析
输入指令objdump -d -r hello > hello_asm.txt 将hello反汇编并将反汇编代码输出到hello_asm.txt中,查看该文件:
图5.5.1 hello_asm.txt,即hello的反汇编
hello.o的反汇编见图4.4.1
两个反汇编文件的不同:
(1)首先可以发现,hello的反汇编内容明显多于hello.o的反汇编;hello.o的反汇编只包含.text节,而hello的反汇编还包含.plt,_init等其他节;
(2)由于完成了重定位,hello的反汇编代码有确定的虚拟地址,而hello.o由于未完成可重定位的过程,其反汇编代码中代码的虚拟地址均为0。
下面查看hello.o的重定位项目:
图5.5.2 hello.s的重定位项目
下面分析hello的重定位过程:
在重定位这个步骤中,将合并输入模块,并为每个符号分配运行时地址。重定位由两步组成:
(1)重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自hello.s中所有输入模块的.data节被全部合并成一个节,这个节成为输出的hello的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量就都有唯一的运行时内存地址了。
(2)重定位节中的符号引用。在这一步中,链接器将修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。
5.6 hello的执行流程
子程序名 | 子程序地址 |
ld-2.31.so!_dl_start | 0x7f8e7cc34ed0 |
ld-2.31.so!_dl_init | 0x7f8e7cc486a0 |
hello!_start | 0x4010f0 |
libc-2.31.so!_libc_start_main | 0x7ff825425fc0 |
libc-2.31.so!_cxa_atexit | 0x7ff825448f60 |
hello!_libc_csu_int | 0x4011c0 |
libc-2.31.so!_setjmp | 0x7ff82fdb2e00 |
libc-2.27.so!exit | 0x7ff 82fdc3bd0 |
首先通过载入,将程序初始化。然后执行main函数,以及main函数调用的各个函数。
5.7 Hello的动态链接分析
对于动态共享链接库中PIC函数,编译器无法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表(PLT)+全局偏移量表(GOT)实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
图5.6.1 节头中的.got
然后用edb查看其地址:
图5.6.2 .got
图5.6.3 .got.plt
在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,将函数序号压栈,然后跳转到PLT[0],在 PLT[0]中将重定位表地址压栈,然后访问动态链接器。在动态链接器中使用函数序号和重定位 表确定函数运行时地址,重写 .got,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。
5.8 本章小结
本章通过具体展示重定位,动态执行等过程,具体地介绍了链接的相关内容。通过本章的撰写,笔者回忆了edb的使用方法,复习了重定位的相关知识。
第6章 hello进程管理
6.1 进程的概念与作用
(1)概念:
在现代系统上运行一个程序时,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通过进程的概念提供给我们的。
进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
(2)作用:
每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。
6.2 简述壳Shell-bash的作用与处理流程
(1)作用:在计算机科学中,Shell是指“为使用者提供操作界面”的软件,也成命令解析器。它类似于DOS下的COMMAND.COM和后来的cmd.exe。它接收用户命令,然后调用相应的应用程序,它只是系统的一个工具,我们可以使用它来操作计算机。
(2)处理流程:首先从shell终端读入命令,随后通过函数对shell命令进行解析,判断其是不是内置函数,若是则调用内置函数,等待当前进程结束之后调用该函数;若不是内置函数,则把它放到后台运行。
6.3 Hello的fork进程创建过程
当输入一个非内置shell命令时,比如./hello,shell会将hello识别为可执行程序,因此会调用某个驻留在存储器中被称为加载器的操作系统代码来运行它。
父进程通过调用fork函数来创建一个子进程。fork函数被调用一次,返回两次,一次是在父进程中返回子进程的PID,另外一次是在子进程中返回0。
图6.3.1 fork进程创建过程
6.4 Hello的execve过程
execve函数的作用是在当前进程的上下文中加载并运行一个新程序。
图6.4.1 execve函数的定义
首先,hello子进程通过execve()系统调用启动加载器。加载器删除子进程所有的虚拟地址段,并创建一组新的代码、数据、堆。通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器跳到_start地址,它最终调用hello的main 函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,此时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
操作系统内核使用一种称为上下文切换的较高层次形式的异常控制流来实现多任务。在进程执行过程中的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。这种决策即调度,是由内核中被称为调度器的代码处理的;
进程hello初始运行在用户模式中,直到它通过执行系统调用函数sleep()或者exit()时便进入到内核。内核中的处理程序完成对系统函数的调用,之后执行上下文切换,将控制返回给进程hello系统调用之后的那条语句。
图6.5.1 进程上下文切换的剖析
6.6 hello的异常与信号处理
hello中会出现的四类异常如下:
异常名称 | 产生原因 | 处理 |
中断 | 外部I/O设备引起的异常 | 将控制返回给应用程序控制流中的下一条指令 |
陷阱 | 执行某一条指令的结果 | 将控制返回给应用程序控制流中的下一条指令 |
故障 | 由错误情况引起 | 重新执行引起故障的指令;或者终止 |
终止 | 通常是硬件错误 | 返回给abort例程,由abort例程终止应用程序 |
(1)乱按回车
对程序运行没有什么影响。
图6.6.1 乱按回车
(2)乱按Ctrl-Z
内核会发送SIGSTP。SIGSTP默认挂起前台hello作业,但 hello进程并没有回收,而是运行在后台下。
图6.6.2 乱按Ctrl-Z
(3)乱按Ctrl-C
乱按Ctrl-C会导使内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业。
图6.6.3 乱按Ctrl-C
(4)Ctrl-Z运行ps
通过ps指令可以查看运行在后台的指令。
图6.6.4 Ctrl-Z运行ps
(5)Ctrl-Z运行jobs
图6.6.5 Ctrl-Z运行jobs
(6)Ctrl-z运行pstree
图6.6.6 Ctrl-z运行pstree
(7)Ctrl-z运行fg
图6.6.7 Ctrl-z运行fg
(8)Ctrl-z运行kill
图6.6.8 Ctrl-z运行kill
(9)其他乱按
其他乱按不会对输出有任何影响
图6.6.9 其他乱按
6.7本章小结
本章通过对hello的创建、加载和终止以及对异常及其信号的分析,使得对hello执行过程中产生信号和信号的处理过程有了更多的认识,通过本章展示与实践,笔者对进程这部分的概念有了更加深刻的掌握。
第7章 hello的存储管理
7.1 hello的存储器地址空间
(1)逻辑地址:由程序产生的与段相关的偏移地址部分。
(2)线性地址:逻辑地址经过段机制后转化为线性地址,分页机制中线性地址作为输入。
(3)虚拟地址:CPU启动保护模式后,程序运行在虚拟地址空间中。注意,并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,Bootloader以及内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。
(4)物理地址:物理地址(Physical Address)是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就直接成为物理地址了。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符和段内偏移量。可以通过段标识符的前13位地址,直接在段描述符表中相应地找到一个具体的段描述符,_Base字段表示包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。
(1)先检查段选择符中的TI字段,它决定着段描述符具体保存在哪一个描述符表中;
(2)由于一个段描述符有8字节长,因此它在GDT或LDT内的相对地址是段选择符的前13位的值*8;
(3)最后,Base + offset,得到要转换的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
计算机会利用页表,通过MMU来完成从虚拟地址到物理地址的转换。而页表是一个页表条目(PTE)的数组,将虚拟页地址映射到物理页地址。
线性地址即虚拟地址,用VA来表示。VA被分为虚拟页号(VPN)与虚拟页偏移量(VPO),CPU取出虚拟页号,通过页表基址寄存器(PTBR)来定位页表条目,当有效位为1时,从页表条目中取出信息物理页号(PPN),通过将物理页号与虚拟页偏移量(VPO)结合,得到由物理地址(PPN)和物理页偏移量(PPO)组合的物理地址。
图7.3.1 地址变换
7.4 TLB与四级页表支持下的VA到PA的变换
(1)将VA中的VPN分成三段,根据TLBT和TLBI,在TLB中寻找对应的PPN,如果没有找到,即判定为缺页,就需要到页表中去找。
(2)将VPN分成更多段(4段),CR3是对应的L1PT的物理地址,然后一步步递进往下寻址,越往下一层每个条目对应的区域越小,寻址越细致。
(3)在经过4层寻址之后,就能够找到相应的PPN,再和VPO拼接起来即得到PA。
7.5 三级Cache支持下的物理内存访问
(1)在得到物理地址VA后,使用物理地址的CI进行组索引(每组8路),对8路的各个块分别匹配CT进行标志位匹配。如果能够匹配,且各块的valid标志位为1,则为命中。然后根据数据偏移量CO取出数据并返回。
(2)如果没找到相匹配的CT,或者标志位为0,则为不命中。这时cache就会向下一级cache,比如二级cache或者三级cache中继续查询数据。最后把查到的数据按照顺序,逐级写入cache。
(3)最后在更新cache的时候,需要判断cache中是否有空闲(即有效位为0)的块。若有空闲块,则写入;若不存在,则驱逐。
7.6 hello进程fork时的内存映射
mm_struct(内存描述符):描述了一个进程的整个虚拟内存空间。
vm_area_struct(区域结构描述符):描述了进程的虚拟内存空间的一个区间。
在用fork()函数创建虚拟内存的时候,要经历以下步骤:
(1)创建父进程的mm_struct,vm_area_struct和页表的原样副本。
(2)把父子进程的每个页面都标记为只读页面。
(3)把父子进程的每个vm_area_struct都标记为私有,这样就只能在写入时复制。
7.7 hello进程execve时的内存映射
exceve()函数加载和执行程序hello,需要以下几个步骤:
(1)删除原有的用户区域。
(2)创建新的私有区域(如.malloc,.data,.bss,.text)。
(3)创建新的共享区域(如libc.so.data,libc.so.text)。
(4)更新程序计数器PC的值,使它指向程序的入口。
7.8 缺页故障与缺页中断处理
在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页。
处理方法:
(1)处理器生成一个虚拟地址,并把它传送给MMU;
(2)MMU生成PTE地址,并从高速缓存/主存请求得到它;
(3)高速缓存/主存向MMU返回PTE;
(4)PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序;
(5)缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘;
(6)缺页处理程序页面调入新的页面,并更新内存中的PTE;
(7)缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,在MMU执行了前三步之后,主存就会将所请求字返回给处理器。
图7.8.1 缺页的处理方法
7.9动态存储分配管理
动态储存分配管理使用动态内存分配器来执行。动态内存分配器维护着一个进程的虚拟内存区域,也就是堆。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留,为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
基本方法与策略:
(1)带边界标签的隐式空闲链表分配器管理:
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。
(2)显式空间链表管理:
显式空闲链表就是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。
7.10本章小结
本章通过对hello的存储空间分配,内存映射分析,动态存储分配管理等方面的阐述,具体地解析了hello的存储管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux下所有的I/O设备都被模型化为文件,而文件又是一个字节序列。故而,所有的输入和输出都被看作是读或者写该文件来执行,这种将设备映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(1)接口:Unix IO接口能够将设备映射成文件,从而统一输入输出。
(2)函数:打开文件:应用程序通过open函数令内核打开相应的文件;
关闭文件:当应用程序完成了对某个文件的访问之后,它就使用close函数通知内核关闭这个文件;
读写文件:读操作就是从文件复制n个字节到内存,从当前文件位置k开始,然后修改k为k+n;类似地,写操作就是从内存中复制n个字节到一个文件,从当前文件位置k开始,然后更新k;
改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量。
8.3 printf的实现分析
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
图8.3.1 printf函数实现
先看printf函数的实现,接收一个字符串fmt,然后把它作为参数传给vsprintf函数并输出。
图8.3.2 vsprintf函数
vsprintf接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
8.4 getchar的实现分析
用户输入命令后,getchar从stdio流中每次只读入一个字符。getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错则返回-1,且将用户输入的字符会显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在缓存区中,等待后续getchar读取。
8.5本章小结
本章解释了在Linux下IO设备的管理方法、IO接口及其函数,并且更加深刻地了解了printf和getchar函数。
结论
(1)预处理器cpp将hello.c文件扩展为hello.i文件;
(2)编译器将hello.i文件编译成汇编语言的hello.s文件;
(3)汇编器将汇编语言翻译成二进制机器语言,生成二进制的可重定位目标程序hello.o文件;
(4)链接器将所引用的目标文件进行符号解析,重定位后完全链接成可执行的目标文件hello;
(5)在shell中输入命令行指令,运行hello程序;如果不是内置指令,shell fork一个子进程;
(7)对子进程用命令行分析的参数execve加载,mmap映射虚拟内存。改变PC到_start,最后开始执行main函数;
(8)hello再运行时会调用一些函数,比如printf函数,这些函数与linux I/O的设备模拟化密切相关;
(9)hello最后被return或被exit后,hello的子进程终止,并被父进程shell回收。
附件
hello.c:源程序文件;
hello.i:修改了的源程序;
hello.s:汇编程序文本文件;
hello.o:可重定位目标程序(二进制文件);
hello:可执行目标程序;
hello_asm.txt:hello的反汇编文件
参考文献
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.