程序人生-Hello’s P2P
2021/6/28 1:20:45
本文主要是介绍程序人生-Hello’s P2P,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机类
学 号 1190202321
班 级 1903001
学 生 许凡楠
指 导 教 师 郑贵滨
计算机科学与技术学院
2021年6月
摘 要
借助edb,cpp,ccl,as,ld等工具的帮助,我们得到了一个hello可执行目标程序,通过跟随hello的一生,详细地介绍了在hello的一生中,计算机系统是如何借助硬件和软件的帮助实现hello的进程管理和内存管理的。
关键词:hello的一生;进程;内存;
目 录
第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 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P(From Program to Process):一开始我们在Linux中敲下了hello.c的代码,这就是hello.c文件(program),然后经过了ccp的预处理,生成hello.i文件,经过ccl的编译,生成了hello.s文件,经过as的汇编,生成了hello.o文件,然后通过ld和printf.o文件进行链接,生成了可执行目标程序hello,在shell中运行hello,shell就fork一个子进程,hello就变成了进程(process)。
020(From Zero-0 to Zero-0):从0开始,我们通过编辑器(Editor)在键盘上输入hello.c的源代码,然后经过Cpp预处理、Compiler编译器的编译、AS汇编和LD链接共享库生成可执行目标程序hello,在shell中,我们在键盘上输入./hello,操作系统便将磁盘上的hello程序载入到物理内存中,并将字符串存放到寄存器上,处理器CPU开始执行这个程序,将printf函数链接,初始化代码段和数据段等,加载器从start位置开始,然后来到main函数的地址,CPU按照一系列逻辑控制执行目标代码,最后输出字符串时,字符串内容从主存复制到寄存器文件,再从寄存器文件复制到显示设备,最终显示到屏幕上。最后程序执行完,进程终止,shell的父进程负责回收该进程,内核删除相关数据结构,恢复shell上下文并收回控制权等待用户的下一条指令,变回了shell一开始的状态,相当于又回到了0。
1.2 环境与工具
1.2.1 硬件环境
CPU:Intel® Core™ i7-8565U 2GHz
8G RAM
512GHD Disk
1.2.2 软件环境
Windows10
Vmware 16
Vmware虚拟机
Linux系统:Ubuntu 20.04
1.2.3 开发工具
Visual Stdio 2019
CodeBlocks
CLion
gcc
gdb
edb
vim
1.3 中间结果
hello.i:hello.c预处理生成的文件,查看预处理器的工作结果
hello.s:hello.i编译生成hello.s,查看编译器的工作结果
hello.o:hello.s经过as汇编生成hello.o,查看AS汇编的工作结果
hello:hello.o与其他.o文件链接生成可执行目标程序hello,查看LD链接器的工作结果
1.4 本章小结
本章主要是介绍了整个hello.c从编写到运行输出字符串的过程,介绍了什么是P2P,什么是020.
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:在对源代码进行编译之前的处理阶段,主要处理三个方面,分别是:1、头文件的扩展2、条件的编译、3、宏定义,以源代码中的#符号做为要进行预处理的标志,把以#开头的命令,做为依据修改源程序。
预处理的作用:
预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第一行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名,方便了接下来的编译器编译阶段。
2.2在Ubuntu下预处理的命令
在Ubuntu下预处理的命令为:gcc -E hello.c -o hello.i
图2-1 预处理
2.3 Hello的预处理结果解析
经过预处理后的hello.i文件相较于原来的hello.c文件扩展到了3065行,将hello.c文件中所需要的头文件都添加进去了。原始hello.c的代码放在了hello.i文件的最后。
图2-2 hello.c的代码放在了hello.i文件的最后
2.4 本章小结
介绍了预处理的概念与作用,并且了解了如何在ubuntu系统下进行hello.c的预处理操作,并通过观察预处理生成的hello.i文件,进一步对预处理的结果有了体会。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,hello.i是包含高级程序设计语言的文本文件,而hello.s是包含汇编语言的文本文件,编译的过程就是将以高级程序设计语言书写的源程序做为输入,然后生成以汇编语言表示的目标程序做为输出的过程。
编译的作用:编译能够将源程序转换为目标程序,还能进行语法检查、程序优化等功能,查看编译生成的hello.s文件,我们可以发现,文件中的每一行语句都描述了一条低级机器语言指令。汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。例如,C编译器和Fortran编译器产生的输出文件用的都是一样的汇编语言。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
如图,生成hello.s文件:
图3-1 编译
3.3 Hello的编译结果解析
3.3.1 局部变量:
查看hello.c文件,局部变量只有一个int i.
图3-2 局部变量i
在hello.s文件中,通过分析汇编代码可得变量i保存在了栈中,地址为-4(%rbp),%rbp是栈指针。
图3-3
图3-4
3.3.2 全局变量
查看hello.c文件,全局变量有一个,为int sleepsecs=2.5;
在hello.s中.global声明全局变量为sleepsecs。
图3-5全局变量为sleepsecs
3.3.3 常量
在hello.s中,常量就以立即数的形式来表示:
图3-6
3.3.4 参数argc
图3-7 参数argc
Main函数的第一个参数argc存放在%edi中。
3.3.5 参数argv[]
Main函数的第二个参数,这也是hello.c中唯一的一个数组参数。
图3-8
movq -32(%rbp),%rax获取数组argv的地址为-32(%rbp),将-32(%rbp)赋给%rax,对%rax进行addq加法来获得数组元素argv[1]和argv[2]。
3.3.6 字符串常量
Hello.c中的printf中的字符串存储在hello.s的.rodata数据段中
图3-9 hello.s的.rodata数据段
3.3.7赋值操作
例如在hello.c中对变量i进行赋值:
图3-10
在hello.s中对应movl语句:
图3-11
3.3.8算术操作
对于将变量i加1的操作,在hello.s中对应的是addl语句:
图3-12
3.3.9跳转指令
Jmp是直接跳转,对应的是for循环zhong对i赋初值为0后的指令
图3-13
变量i与9进行比较,若i<=9,边跳转到.L4
图3-14
3.3.10调用其他函数
调用printf与sleep函数:使用call指令来进行函数之间的调用:
图3-15
图3-16
3.4 本章小结
通过对hello.c文件进行编译,得到了hello.s文件,通过查看hello.s中的汇编指令,并与hello.c中的代码进行比较,对于编译器是如何将一个C语言程序翻译成汇编程序更加地了解,展示了一条C语言语句所对应的汇编指令大致是什么样的,以及展示汇编指令对于变量的表示和操作。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件hello.o中,hello.o是一个二进制文件。
汇编的作用:
-
根据汇编指令和特定的平台,把汇编指令翻译成机器代码。
-
合并各个section,合并符号表。
4.2 在Ubuntu下汇编的命令
gcc hello.s -c -o hello.o
图4-1 汇编
4.3 可重定位目标elf格式
4.3.1查看ELF头:命令:readelf -h hello.o
图4-2 ELF头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,剩下的部分表示了生成hello.o文件的这个系统的基本信息以及目标文件的相关信息等等,包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。
4.3.2 输入readelf -S hello.o命令查看Section Headers(节头部表)
表示了该ELF可重定位目标文件中包含的节有哪些:
图4-3 Section Headers(节头部表)
看上图,由于是可重定位目标文件,所以这所有的节的起始地址都为0,同时图中还显示了各个节的大小、名称、偏移量等基本信息。
输入readelf -a hello.o查看详细信息:
4.3.3重定位节.rela.test
图4-4 重定位节.rela.test
一个.test节中位置的列表,包含了需要重定位的所有符号信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。
4.3.4符号表.symtab
它存放在程序中定义和引用的函数和全局变量的信息。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。
图4-5 符号表.symtab 分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.4 Hello.o的结果解析
输入命令objdump -d -r hello.o >helloasm.txt分析hello.o的反汇编,并将结果输出到helloasm.txt文件中。
图4-6 分析hello.o的反汇编
查看helloasm.txt:
图4-7 查看helloasm.txt
与第3章的 hello.s进行对照分析:
图4-8 第3章的 hello.s
二者进行比较,发现通过objdump反汇编得到的代码,不仅仅包含了汇编指令,并且一行汇编指令对应了一行二进制的机器指令。二者对应的具体的汇编指令并没有多大的差别。
对于操作数的表示:机器语言中的操作数反汇编是以十六进制的形式表示的,而在hello.s中的汇编语言中是以十进制表示的。
图4-9
图4-10
分支转移函数的调用:在跳转指令上,二者也有一定的差别,机器语言反汇编得到的跳转指令与call指令是后面跟上具体要跳转到的指令地址,而在hello.s中的汇编语言中,跳转指令后跟的是标号.L4,而call指令后跟的是具体函数名。
图4-11
图4-12
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.5 本章小结
本章中,对与hello.s文件进行了汇编生成了hello.o文件,查看分析了hello
.o的ELF头和头部表等信息,最后对于hello.o反汇编得到的汇编代码与前面第三章hello.s中的汇编代码进行了比较,并说明了二者的部分不同之处。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接的概念:链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。在现代系统中,链接由链接器来自动执行。
链接的作用: 链接器(ld)将其他已经编译好的.o目标文件与我们的.o程序文件进行合并,链接生成新的可执行目标文件。
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-1链接
5.3 可执行目标文件hello的格式
5.3.1输入readelf -h hello查看hello的ELF头
图5-2 hello的ELF头
图中的类型为EXEC,表示hello是一个可执行文件,且有27个节。
5.3.2输入readelf -S hello查看节头部表
图5-3 查看节头部表
节头部表对hello中的每个节都做了说明,包含了每个节的名称、类型、大小、地址、偏移量等基本信息。
5.3.3输入readelf -a hello查看其他ELF信息
程序头描述了可执行文件连续的片被映射到连续的物理地址的相关信息。
图5-4 其他ELF信息
符号表存放了程序中定义和引用的函数和全局变量的信息。
图5-5 符号表
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息:
5.4.1edb打开hello
图5-6
5.4.2查看hello的虚拟地址空间
图5-7 hello的虚拟地址空间
5.4.3通过查看edb可得,hello的虚拟地址空间起始地址为0x400000,而结束地址为0x400ff0.
图5-8
图5-9
5.4.4与5.3中的节头部表进行对照:
图5-10
这里举两个例子:
图5-11
例如.interp,它的起始地址为0x4002e0,对照edb中:
图5-12
图5-13
例如.text,它的起始地址为0x4010d0,对照edb中:
图5-14
节头部表中的其余节同理。
5.5 链接的重定位过程分析
objdump -d -r hello将结果输出重定向到hello.txt文件中:
图5-15输出重定向到hello.txt文件中
将二者进行比较:
图5-16 比较hello与hello.o
(1) 第一处不同:地址的不同
在hello.o中:由于还没进行链接,还未涉及到虚拟内存地址,所以hello.o中的main函数的起始地址为0:
图5-17
而在hello中,由于已经链接完毕,已经是有具体的地址了:
图5-18
(2) 第二处不同:hello.o中仅仅只包含了hello.c中的main函数所对应的汇编指令,而hello中由于是经过了链接,所以多了很多节还包含了其他库中的函数,包含了其他库中的汇编指令。如下图:hello中还包含了__libc_csu_init函数。
图5-19
(3) 在call指令跳转上的不同:
例如在hello.o中的一处call跳转指令:
图5-20
机器指令e8后跟的是一串0,表示由于还没有完成链接,所以就还没有具体的跳转地址,所以暂时用一串0来填充。
而例如在hello中的一处call跳转指令:
图5-21
机器指令e8后跟的就是一串具体的二进制数,由于已经完成了链接,所以也就有了完整的跳转指令。
链接生成hello的过程:假设链接器读取一组可重定位目标文件,并把它们链接起来,形成一个输出的可执行文件。实际上,所有的编译系统都提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称为静态库,它可以用做链接器的输入。当链接器构造一个输出的可执行文件时,它只复制静态库里被应用程序引用的目标模块。现在回头去看一开始给出的链接命令:
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.o与这一组目标文件进行链接,复制需要的目标模块到可执行文件中,链接器从左到右按照它们在编译器驱动程序命令行上出现的顺序来扫描可重定位目标文件和存档文件,驱动程序自动将命令行中所有的.c文件翻译为.o文件。
在重定位步骤中,将合并输入模块,并为每个符号分配运行时地址。
重定位分为两步:(1)重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变星都有唯一的运行时内存地址了。
(2)重定位节中的符号引用。在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。
重定位hello.o的过程:
查看hello.o的重定位节的信息:如下图:
图5-22
图5-23
图中给出了hello.o需要重定位的节的名称,偏移量、重定位类型等信息。
图中有两种重定位的类型:
R_X86_64_PC32 :重定位一个使用32 位PC相对地址的引用。
R_X86_64_PLT32:与R_X86_64_PC32类似。
以第一条为例:
图5-24
图5-25
可得重定位条目信息为:
r.offset=0x1c;
r.symbol=radata;
r.type= R_X86_64_PC32;
r.addend=-4;
图5-26
链接器已确定:
ADDR(r.symbol)=0x402004;
ADDR(s)=0x401105;
重定位:
refaddr=ADDR(s)+r.offset=0x401105+0x1c=0x401121;
*refptr=(unsigned)(ADDR(r.symbol)+r.addend-refaddr)
=(unsigned)(0x402004-4-0x401121)
=(unsigned)(0xEDF);
转换为小端序为de 0e 00 00,结果正确:
图5-27
以第二条为例:
图5-28
图5-29
可得重定位条目信息为:
r.offset=0x21;
r.symbol=puts;
r.type= R_X86_64_PTL32;
r.addend=-4;
图5-30
链接器已确定:
ADDR(r.symbol)=0x401080;
ADDR(s)=0x401105;
重定位:
refaddr=ADDR(s)+r.offset=0x401105+0x21=0x401126;
*refptr=(unsigned)(ADDR(r.symbol)+r.addend-refaddr)
=(unsigned)(0x401080-4-0x401126)
=(unsigned)(0xff ff ff 56);
转换为小端序为56 ff ff ff,结果正确:
图5-31
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
结果如下所示:
Ld-2.31.so!_dl_start
图5-32
Ld-2.31.so!_dl_init
图5-33
Hello!_start
图5-34
Libc-2.27.so! libc start main
图5-35
Hello!printf@plt
图5-36
Hello!sleep@plt
图5-37
Hello!getchar@plt
图5-38
Libc-2.27.so!exit
图5-39
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
动态链接能够有效地解决内存资源的浪费,并且动态链接能够有效的解决传统静态库更新困难的问题。共享库是一个目标模块,在运行和加载时,可以加载到内存的任意位置,并和一个在内存中的程序链接,这个过程就被称为动态链接。
当hello程序调用一个由共享库定义的函数时,编译器没法预测这个函数运行时的地址,编译器为该引用生成一条重定位记录,然后动态链接器在程序加载时再解析它。GNU编译系统使用延迟绑定,将过程地址的绑定推迟到第一次调用该过程时。延迟绑定是通过GOT和PLT这两个数据结构之间复杂的交互来实现的,如果一个目标模块调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT,GOT是数据段的一部分,而PLT是代码段的一部分。
查看hello的ELF文件可得:
图5-40
.got段起始地址为0x403ff0,
.got.plt段起始地址为0x404000,进入edb调试
这是在还未调用dl_init前的.got段
图5-41
这是在还未调用dl_init前的.got.plt段
图5-42
调用dl_init后的.got段
图5-43
调用dl_init后的.got.plt段
图5-44
对比执行前后的结果,说明hello已经动态链接了共享库。
5.8 本章小结
本章介绍了链接的概念和作用,以及简要介绍了ELF的格式,介绍了在链接过程中是如何进行重定位的以及动态链接是怎么一回事。通过链接,使得我们成功地得到了一个完美的hello程序。
(以下格式自行编排,编辑时删除)
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:在现代系统上运行一个程序时,我们会得到一个假象,就好像我们的程序是系统当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通过进程的概念提供给我们的。
进程的作用:每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或者其他应用程序。
6.2 简述壳Shell-bash的作用与处理流程
Shell是一个交互型应用级程序,是操作系统与用户的中介,代表用户运行其他程序。
功能:shell应用程序提供了一个界面,用户通过访问这个界面访问操作系统内核的服务。
处理流程:
1、从终端读入输入的命令。
2、将输入字符串切分获得所有的参数
3、如果是内置命令则立即执行
4、否则调用相应的程序执行
5、shell 应该接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
打开shell,在shell中输入./hello,./hello不是shell的内置命令,输入的是一个可执行目标文件的名称,shell就会先在目录中找到hello这个可执行目标文件,shell就会通过调用fork()创建一个新的子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用 fork 时,子进程可以读写父进程中打开的任何文件。该子进程除了PID是最大的不同以外,其余与父进程几乎没有什么不同,shell会在这个新子进程的上下文中运行这个子进程。
6.4 Hello的execve过程
execve 函数在当前进程的上下文中加载并运行一个新程序。
execve 函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表 envp。只有当出现错误时,例如找不到hello, execve才会返回到调用程序。所以,与fork一次调用返回两次不同,execve 调用一次并从不返回。
execve函数用hello程序有效代替当前程序,加载并运行hello需要以下几个步骤:
1、 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2、 映射私有区域。为新程序hello的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。下图概括了私有区域的不同映射:
图6-1
3、映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库libc.
so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4、设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
6.5 Hello的进程执行
用户向shell输入可执行目标文件hello的名字,运行程序时,shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件hello。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或者其他应用程序。
Hello进程会提供给我们两个关键抽象:
(1) 一个独立的逻辑控制流,好像我们的程序独占地使用处理器。
(2) 一个私有的地址空间,好像我们的程序独占地使用内存系统。
下面更深入地查看这些抽象:
(1)逻辑控制流:
图6-2
一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC 值的序列叫做逻辑控制流,或者简称逻辑流。每个进程执行它的流的一部分,然后被抢占(暂时挂起),然后轮到其他进程。对于一个运行在这些进程之一的上下文中的程序,它看上去就像是在独占地使用处理器。hello进程就是做为这些进程中的其中一个进程,在逻辑控制流下进行hello的执行。
(2)并发流:
Hello的逻辑流在执行时间上与另一个流重叠,称为并发流,这两个流被称为并发地运行。多个流并发地执行的一般现象被称为并发。一个进程和其他进程轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
(3) 私有地址空间:
Hello进程为每个程序提供一种假象,好像hello进程独占地使用系统地址空间。Hello进程为每个程序提供它自己的私有地址空间。尽管和每个私有地址空间相关联的内存的内容一般是不同的,但是每个这样的空间都有相同的通用结果,如下图所示:
图6-3
(4) 用户模式和内核模式:
设置模式位时,进程运行在内核模式,内核模式下的进程能够执行指令集中的任何指令,并且可以访问系统中任何内存位置。没有设置模式位时,进程就运行在用户模式中,用户模式下的进程不允许执行特权指令,也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。
(5) 上下文切换:
操作系统内核使用上下文切换来实现多任务。内核为每个进程维持一个上下文。上下文就是内核重新启动先前一个被强占的进程所需要的状态。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,是由内核中称为调度器的代码处理的。
在hello进程的执行过程中:
当在shell中输入运行hello的命令后,便加载上下文并运行hello进程,等待内核调用,操作系统内核便会进行上下文切换,由内核来调度hello进程,内核决定抢占当前进程,当内核选择新进程hello运行时,我们就说内核调度了hello进程,并使用上下文切换的机制来将控制转移到当前hello进程。这时,刚开始运行hello程序,进程便从内核模式转移到用户模式,开始执行hello的指令,当运行到sleep时,切换到内核模式,挂起hello进程,通过上下文切换调度到其他的进程并保存hello进程的上下文,sleep结束后,回到用户模式,继续执行hello进程,通过上下文切换恢复hello进程的上下文,当hello运行到getchar()时,再次切换到内核模式,挂起hello进程,通过上下文切换调度到其他的进程并保存hello进程的上下文,从键盘输入结束后,发送信号,恢复hello进程,继续运行。
图6-4
6.6 hello的异常与信号处理
6.6.1hello运行过程中可能出现的异常:
图6-5
Hello执行过程中可能会出现以上这四种异常。
6.6.2 hello运行过程中会产生的信号:
Hello执行过程中可能产生的信号有SIGINT,SIGTSTP,SIGCHLD,SIGKILL,SIGALRM等信号,不同的信号类型对应着不同的行为。进程接收信号,触发控制转移到信号处理程序,信号处理程序运行结束返回到被中断的hello进程。
6.6.3程序运行过程中的各种操作与截图:
(1)乱按字符串后不接回车:
图6-6
输入的字符串会保存到缓冲区中,和Hello 1190202321 许凡楠一起输出。
(2) 乱按字符串接回车:
图6-7
getchar运行一次将输入的第一个回车前的所有字符串读入,而接下来的每一个回车前输入的字符串都会被当做命令被shell执行:例如在hello运行结束后,dsa与dsd做为命令行被shell执行,如下图:
图6-8
(3) 运行时输入Ctrl-Z:
图6-9
输入Ctrl-Z停止hello进程。
图6-10
输入ps查看当前所有进程和它们的PID。
图6-11
输入jobs查看当前作业,是hello,显示已停止。
图6-12
输入pstree将所有进程以树状图显示出来。
图6-13
输入fg让已停止的hello继续运行。
图6-14
通过ps命令得知hello进程的PID是3114,输入kill -9 3114杀死已停止的hello进程,再次输入ps,发现hello已被杀死。
(4)运行时输入Ctrl-C:
图6-15
先输入Ctrl-z停止hello,ps查看所有进程,fg继续运行hello,输入Ctrl-C,终止hello进程,再输入ps,发现hello进程已经消失。
6.7本章小结
本章介绍了hello是怎么变成一个进程的,以及我们的操作系统是如何管理hello进程的,好让hello进程与操作系统上其他的进程不产生冲突,好好执行,以及对hello进程运行中会产生的信号做了介绍,还有在shell下对hello进程运行时输入的各种命令做了详细介绍与截图。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1逻辑地址:
在计算机体系结构中是指应用程序角度看到的内存单元、存储单元、网络主机的地址。由程序产生的与段相关的偏移地址部分,hello中要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。
7.1.2线性地址:
线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址。如果没有启用分页机制,那么线性地址直接就是物理地址。
7.1.3虚拟地址:
CPU启动保护模式后,程序运行在虚拟地址空间中,虚拟地址此时与逻辑地址相同。
7.1.4物理地址:
真正的内存单元的地址,存储器中的每一个单元都对应着一个唯一的物理地址,又称为实际地址或绝对地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
图7-1逻辑地址到线性地址的变换
1、 给定一个逻辑地址包含了段选择符以及段内偏移地址。
2、 判断段选择符的T1的值,从而选择到全局段描述表GDT中还是局部段描述表LDT中寻找段。
3、 GDTR寄存器中包含GDT的地址和大小,LDTR包含LDT的地址和大小。
4、 再根据逻辑地址中的Index索引得到具体的段描述符。
5、 最后由得到的段描述符加上逻辑地址中的offset便得到了最后要得到的结果线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
形式上来说,地址翻译是一个元素的虚拟地址空间(VAS)中的元素和一个元素的物理地址空间(PAS)中元素之间的映射,
图7-2
下图展示MMU如何利用页表来实现这种映射。CPU中的一个控制寄存器,页表基址寄存器(PTBR)指向当前页表。n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO)和一个(n-p)位的虚拟页号(VPN)。MMU利用VPN来选择适当的 PTE。例如,VPN0选择PTE0, VPN1选择PTE1,以此类推。将页表条目中物理页号(PPN)和虚拟地址中的VPO串联起来,就得到相应的物理地址。注意,因为物理和虚拟页面都是P字节的,所以物理页面偏移(PPO)和VPO是相同的。
图7-3
地址翻译:页面命中:
图7-4
1、处理器生成一个虚拟地址,并把它传送给MMU。
2、MMU生成PTE地址(PTEA),并从高速缓存/主存请求得到PTE。
3、高速缓存/主存向MMU返回PTE。
4、MMU将物理地址传送给高速缓存/主存。
5、高速缓存/主存返回所请求的数据字给处理器。
地址翻译:缺页异常:
图7-5
1、处理器生成一个虚拟地址,并把它传送给MMU。
2、MMU生成PTE地址(PTEA),并从高速缓存/主存请求得到PTE。
3、高速缓存/主存向MMU返回PTE。
4、PTE中的有效位是0,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
5、缺页处理程序确定物理内存中的牺牲页。(若页面被修改,则换出到磁盘——写回策略)
6、缺页处理程序页面调入新的页面,并更新内存中的PTE。
7、缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中。
7.4 TLB与四级页表支持下的VA到PA的变换
图7-6
多级页表能够存储更多的存储空间,对于4级的多级页表结构,48位虚拟地址被分为4个9位VPN以及一个12位VPO。对于一级到三级页表,页表中的每个PTE都指向了下一级页表的基地址,而在第四级页表中,PTE对应了某个物理页面的地址或者是磁盘地址。每个页表4KB大小,每个PTE是8位。
图7-7
一开始虚拟地址VA的前36位VPN与TLB进行匹配,若命中,就得到PPN,加上PPO(即VPO)就得到物理地址;
若不命中,就到4级页表中查询,CR3确定一级页表的基地址,VPN1确定一级页表的PTE,得到第二级页表的基地址,以此类推,得到第4级页表的中对应的PTE,得到PPN,与PPO组成物理地址PA。
7.5 三级Cache支持下的物理内存访问
图7-8
在完成了7.4中的VA到PA的转换后,得到了一个52位的物理地址。首先图中的L1cache有64组,每组8行,每个块的大小为64B,所以块偏移为6,s=6,所以将这52位的地址分为3部分,分别是40位的CT高速缓存标记,6位的CI高速缓存索引,6位的CO缓冲块内的字节偏移量。
(1) 先根据CI得到需要查找的组序号
(2) 查找该组的8个行,根据CT与行中的标记位进行匹配,且行的有效为为1,则该高速缓存行命中。
(3) 命中后,由CO得到我们需要的字节起始位置,取出返回CPU即可。
(4) 若不命中,就需要到L2,L3,甚至于到主存中去寻找请求的块,然后将该请求的块放置或替换到组索引所对应的那组的某一个高速缓存行中,若有一行的有效位位0,就放置在该行,若所有行的有效位都为1,就选择一个最近不使用的一行进行替换。
7.6 hello进程fork时的内存映射
在shell上输入运行hello的命令,shell就会通过调用fork()创建一个新的子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用 fork 时,子进程可以读写父进程中打开的任何文件。该子进程除了PID是最大的不同以外,其余与父进程几乎没有什么不同,shell会在这个新子进程的上下文中运行这个子进程。
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。内核给新进程创建虚拟内存,创建当前进程的mm_struct、区域结构和页表的原样副本,将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve 函数在当前进程的上下文中加载并运行一个新程序hello。
execve 函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表 envp。只有当出现错误时,例如找不到hello, execve才会返回到调用程序。所以,与fork一次调用返回两次不同,execve 调用一次并从不返回。
execve函数用hello程序有效代替当前程序,加载并运行hello需要以下几个步骤:
1、 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2、映射私有区域。为新程序hello的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。下图概括了私有区域的不同映射:
图7-9
3、映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库libc.
so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4、设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
缺页故障:缺页故障常常发生,CPU通过地址总线可以访问连接在地址总线上的所有外设,包括物理内存、IO设备等等,但从CPU发出的访问地址并非是这些外设在地址总线上的物理地址,而是一个虚拟地址,由MMU将虚拟地址转换成物理地址再从地址总线上发出,MMU上的这种虚拟地址和物理地址的转换关系是需要创建的,并且MMU还可以设置这个物理页是否可以进行写操作,当没有创建一个虚拟地址到物理地址的映射,或者创建了这样的映射,但那个物理页不可写的时候,MMU将会通知CPU产生了一个缺页故障。或者由MMU获得了相应的PTE,而当PTE的有效位未设置时,这也会触发缺页故障。
缺页中断处理:由缺页故障,触发故障处理程序,
(1) 判断是否段错误,访问了一个不存在的页面,缺页处理程序搜索区域链表,检查是否合法。
(2) 若是正常缺页:从指定位置加载页面到物理内存中,更新PTE,再次申请该页面就不会导致缺页故障了。
(3) 判断是否是保护异常,例如违反许可,写一个只读的页面。
图7-10
7.9动态存储分配管理
7.9.1动态存储分配器
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制0的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
图7-11
分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
7.9.2 隐式空闲链表
首先,隐式空闲链表的格式为:
图7-12
带边界标签的隐式空闲链表的格式为:
图7-13
为了允许在常数时间内进行对前面块的合并,所以带边界标签的隐式空闲链表相较于之前的隐式空闲链表在每个块的结尾处添加个脚部 (footer, 边界标记),其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态。
(1)放置已分配的块:请求一个k字节的块时,分配器搜索空闲链表。查找一个足够大可以放置所请求的空闲块。分配器搜索方式的常见策略是首次适配、下一次适配和最佳适配。
(2)分割空闲块:一旦分配器找到一个匹配的空闲块,就要判断分配这个空闲块中多少空间。分配器通常将空闲块分割为两部分。第一部分变为了已分配块,第二部分变为了空闲块。
(3)获取额外的堆内存:若分配器不能找到合适的空闲块的话,一是选择合并那些在内存中物理上相邻的空闲块来创建一些更大的空闲块,若还不够大,分配器就会通过调用sbrk函数,向内核请求额外的堆内存。分配器将额外的内存转化成一个大的空闲块,将这个块插入到空闲链表中,然后将被请求的块放置在这个新的空闲块中。
(4)带边界标签的合并:
如图所示,分为四种情况:
-
前面的块和后面的块都是已分配的。
-
前面的块是已分配的,后面的块是空闲的
-
前面的块是空闲的,而后面的块是已分配的。
-
前面的和后面的块都是空闲的
图7-14
在情况1中,两个邻接的块都是已分配的,因此不可能进行合并。所以当前块的状态只是简单地从已分配变成空闲。在情况2中,当前块与后面的块合并,用当前块和后面块的大小的和来更新当前块的头部和后面块的脚部。在情况3中,前面的块和当前块合并。用两个块大小的和来更新前面块的头部和当前块的脚部。 在情况4中,要合并所有的三个块形成一个单独的空闲块,用三个块大小的和来更新前面块的头部和后面块的脚部。在每种情况中,合并都是在常数时间内完成的。
7.9.3显示空闲链表
隐式空闲链表为我们提供了一种介绍一些基本分配器概念的简单方法。然而,因为块分配与堆块的总数呈线性关系,所以对于通用的分配器,隐式空闲链表是不适合的(尽管对于堆块数量预先就知道是很小的特殊的分配器来说它是可以的)。
一种更好的方法是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针,如图所示:
图7-15
一般而言,显式链表的缺点是空闲块必须足够大,以包含所有需要的指针,以及头部和可能的脚部。这就导致了更大的最小块大小,也潜在地提高了内部碎片的程度。
7.10本章小结
本章对于hello运行过程中的内存分配管理,以及虚拟地址对物理地址借助TLB和多级页表的映射方法以及缺页处理等做了介绍。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个 Linux 文件就是一个m个字节的序列:B0,B1,B2,B3,…,Bm-1;
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
8.2.1 Unix I/O使得所有的输入和输出都能以一种统一且一致的方式来执行:
(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
(2)Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可用来代替显式的描述符值。
(3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
(4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k≥m时执行读操作会触发EOF条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
(5)关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
8.2.2 Unix IO的函数:
(1)int open(char* filename,int flags,mode_t mode)
根据文件名filename打开一个文件,并返回描述符,flags表示进程如何访问该文件,mode表示新文件的访问权限位。
(2)int close(fd)
fd表示需要关闭的文件的描述符,关闭该文件,返回操作结果。
(4) ssize_t read(int fd,void *buf,size_t n)
fd表示需要读取的文件的描述符,该函数表示从文件中最多读取n个字节赋值给内存位置buf。返回实际赋值的字节数,-1表示错误,0表示EOF。
(5) ssize_t wirte(int fd,const void *buf,size_t n)
fd表示需要写入的文件的描述符,该函数表示从内存位置buf复制最多n个字节写入到文件中。
8.3 printf的实现分析
研究printf的实现,首先来看看printf函数的源代码:
图8-1 printf函数的源代码
其中va_list arg = (va_list)((char*)(&fmt) + 4);
(char*)(&fmt) + 4) 表示的是第一个参数的地址。
对于vsprintf(buf, fmt, arg)返回要打印出来的字符串的长度,vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
对于write(buf, i),在printf中调用系统函数write(buf,i)将长度为i的buf输出到终端。
图8-2
这里是给几个寄存器传递了几个参数,然后一个int结束,一个int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。sys_call的功能就为显示格式化了的字符串。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar函数无参数,定义在头文件中,使用时需要调用头文件<stdio.h>。getchar()是最简单的一次读一个字符的函数,每次调用时从文本流中读入下一个字符,并将其ASCII码值作为结果值返回。在没有输入或者输入字符有错的时候,getchar()函数将返回一个特殊值EOF,EOF是定义在头文件中的。
当程序调用getchar()时,程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中,直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar()才开始从输入流中每次读入一个字符,getchar()函数的返回值是用户输入的第一个字符的ASCII码,如出错返回EOF。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,,待后续getchar()调用读取。也就是说,后续的getchar()调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。getchar函数还可由于取消’\n’的影响,如:while((c=getchar())!=’\n’),这里getchar();只是取得了’\n’但是并没有赋给任何字符变量,所以不会有影响,相当于清除了这个字符。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法以及Unix IO接口及其函数还介绍了printf函数和getchar函数是怎么实现的。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
Hello虽然不大,但它在计算机中的历程却是十分丰富的。
1、hello.c的诞生:我们在文本编辑器中书写hello.c的源代码
2、hello.c的预处理:借助cpp预处理器的帮助,hello.c变成了hello.i
3、hello.i的编译:借助ccl编译器的帮助,hello.i变成了hello.s
4、hello.s的汇编:借助as汇编器的帮助,hello.s变成了hello.o
5、hello.o与其他目标模块链接:借助链接器ld的帮助,hello.o和printf.o等链接成了hello,hello便诞生了。
6、shell的帮助:在shell中,我们运行hello并适当地输入参数,shell便会帮助我们让hello带着我们输入的参数跑起来。
7、子进程的创建fork:在shell中输入运行hello的命令,shell便会通过fork函数来创建一个子进程。
8、加载hello程序execve:shell在刚刚创建的子进程中调用execve来加载hello到这个子进程中运行,hello便成为了一个活着的进程。
9、hello的上下文:每个进程在运行时都有着自己的一个上下文,通过上下文切换来调用不同的进程。内核为每个进程划分时间片,在hello的时间片时,内核切换hello的上下文运行hello进程,hello的时间片结束后,内核会保存hello的上下文,然后切换到其他进程的上下文执行其他的进程。轮到hello时,就会恢复hello的上下文,执行hello。
10、hello中的指令执行:hello运行中,CPU不断地到内存中取指令,借助MMU和高速缓存的帮助,使得hello取指令的速度加快了很多。
11、hello申请动态内存:在hello的进程空间中,还存放着hello申请的堆,当hello调用printf函数时,hello便会申请堆中的内存。
12、hello接收信号:hello运行过程中,往往会接收到我们从键盘等IO设备发出的信号,这时hello会通过上下文切换来进入内核模式的异常处理程序。
13、hello的IO:hello的调用printf函数就是一个标准IO函数,通过这个函数,hello向我们展示了它向告诉我们的话。
14、hello的消失:当hello运行到结束时,其父进程便会负责hello的回收,内核回收hello的所有信息,hello的一生便结束了。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
对于计算机系统的设计与实现的深切感悟:
Hello的诞生是在计算机硬件,软件,操作系统的密切配合下完成的。
在我看来,计算机系统给我们提供了一个和硬件交流的桥梁,没有计算机系统的帮助,我们是没有办法这么高效地使用计算机的,计算机系统将硬件抽象,并且一个优秀的硬件设计会让我们的计算机系统工作地更加地高效,同时在软件方面,计算机系统也充分考虑到了硬件层面上的结构,使得硬件的性能发挥到最好,这一切设计的几乎严丝合缝,计算机系统实在让人惊叹。
创新理念:增大多级页表的级数,是否会提高能够管理地址空间的大小的同时而不对存储性能造成太大的不良影响。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
文件 作用
hello.c Hello的源代码
hello.i Hello.c预处理的文本文件
hello.s Hello.i编译得到的汇编文件
hello.o Hello.s汇编得到的可重定位目标文件
hello Hello.o和其他目标模块链接得到的可执行文件
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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.
(参考文献0分,缺失 -1分)
这篇关于程序人生-Hello’s P2P的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23Springboot应用的多环境打包入门
- 2024-11-23Springboot应用的生产发布入门教程
- 2024-11-23Python编程入门指南
- 2024-11-23Java创业入门:从零开始的编程之旅
- 2024-11-23Java创业入门:新手必读的Java编程与创业指南
- 2024-11-23Java对接阿里云智能语音服务入门详解
- 2024-11-23Java对接阿里云智能语音服务入门教程
- 2024-11-23JAVA对接阿里云智能语音服务入门教程
- 2024-11-23Java副业入门:初学者的简单教程
- 2024-11-23JAVA副业入门:初学者的实战指南