计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机
学 号 120L020314
班 级 2003009
学 生 张珑耀
指 导 教 师 郑贵滨
计算机科学与技术学院
2021年5月
本文主要阐述hello程序在Linux系统的生命周期,探讨hello程序从hello.c经过预处理、编译、汇编、链接生成可执行文件的全过程。通过本学期对计算机系统的学习并结合课本的知识详细阐述我们的计算机系统是如何对hello进行进程管
理、存储管理和I/O管理,通过对hello一生周期的探索,让我们对计算机系统有更深的了解。
关键词:Hello程序;预处理;编译;汇编;链接;进程;存储;虚拟内存;I/O ;
目 录
第1章 概述
1.1 Hello简介
P2P:在Linux中,hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork产生一个子进程,然后hello便从程序变为了进程。
020: shell为此子进程execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。
1.2 环境与工具
X64 CPU;2GHz;2G RAM;256GHD Disk 以上
Windows7/10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位 以上;
Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc
1.3 中间结果
hello.i | hello.c预处理之后文本文件 |
hello.s | hello.i编译后的汇编文件 |
hello.o | hello.s汇编之后的可重定位目标文件 |
hello | 链接之后的可执行目标文件 |
hello.out | hello反汇编之后的可重定位文件 |
1.4 本章小结
本章大致主要简单介绍了 hello 的 p2p,020 过程,列出了本次实验信息:环境、中间结果,并且大致简介了hello程序从c程序hello.c到可执行目标文件hello的大致经过的历程。
第2章 预处理
2.1 预处理的概念与作用
预处理概念:预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
预处理阶段作用:
1.处理宏定义指令预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。
2. 处理条件编译指令
条件编译指令如#ifdef,#ifndef,#else,#elif,#endif等。 这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
3.处理头文件包含指令头文件包含指令如#include "FileName"或者#include 等。 该指令将头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
4.处理特殊符号
预编译程序可以识别一些特殊的符号。 例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
2.2在Ubuntu下预处理的命令
命令:gcc hello.c -E -o hello.i
2.3 Hello的预处理结果解析
Hello.c
Hello.i
通过gcc命令将hello.c转换成hello.i文件,经过查看发现文件的内容增加,且仍为可以阅读的C语言程序文本文件。对原文件中的宏进行了宏展开,头文件中的内容被包含进该文件中。例如声明函数、定义结构体、定义变量、定义宏等内容。另外,如果代码中有#define命令还会对相应的符号进行替换。
2.4 本章小结
本章介绍了预处理的相关概念,通过对预处理与源文件的对比使我们能够更加了解这其中的原理。
第3章 编译
3.1 编译的概念与作用
编译的概念:编译器将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。其以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。 这个过程称为编译,同时也是编译的作用。
编译程序的基本功能是把源程序(高级语言)翻译成目标程序。除了基本功能之外,编译程序还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人际联系等重要功能。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1 处理变量
源程序中只有局部变量int i,对比汇编代码与源程序得:
hello.c:hello.i:
hello.c:hello.i:
可以看出局部变量存到了-4(%rbp)中了。
3.3.2处理控制转移语句
hello.c:
hello.i:
可以看出这里的if控制转移语句是通过jx汇编指令实现的。
3.3.3算术操作
1)加: x=x+y汇编语言是addq y,x
(2)减: x=x-y 汇编语言是subq y,x
(3)乘: x=x*y 汇编语言是imulq y,x
(4)除: z=x/y 汇编语言是
movq x, z
cqto
idivq y
复合语句就是上面的组合,或者也有复合的汇编语句:z=x+Ay+B(A,B都是立即数)的汇编语言是leaq B(x,y,A) z
而在本程序中,只出现了add指令
3.3.4处理数组、指针和结构体
- 数组:取数组头指针加上第i位偏移量来处理。
- (2)指针与数组类似,如果rax表示指针所存的寄存器,访问x指向的值就是(%rax)
- (3)结构体:通过结构体内部的偏移量来访问。
- 本程序中出现了数组,截图如下:
3.3.5函数操作
1.函数的调用通过call来实现,并跳转到指定函数,将下一条指令地址push入栈。
如:printf转换成了puts,把.L0段的立即值传入%rdi,然后call跳转到puts。
这里的exit是把立即数1传入到%edi中,然后call跳转到exit。
- 返回值:函数的返回值一般在寄存器%eax中,如果有返回值,则要先把返回值存到%eax中,再用ret返回。源程序中有主函数的return 0;就是先把返回值立即数0存到%eax中,再用ret返回。
3.3.6关系操作
汇编语言中的关系操作,通过设置条件码和cmp指令来实现
hello.c:hello.i:
这里汇编指令通过小于等于7来实现。
3.3.7 赋值
赋值语句在汇编代码中一般通过mov指令实现,其中赋值通常是立即数、寄存器与内存之间的交互。
hello.c:hello.i:
这里将1这个立即数赋值给了edi寄存器。
3.3.8类型转换
hello.c中涉及的类型转换是:atoi(argv[3]),将字符串类型转换为整数类型其他的类型转换还有int、float、double、short、char之间的转换。
3.4 本章小结
本章通过.c文件与.i文件的对比,更加深入的了解了c语言语句的汇编代码实现。让我们理解了为什么我们的c代码能在机器中运行以及机器是怎么实现的。
第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中,.o文件是一个二进制文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
命令:gcc hello.s -c -o hello.o
截图:
4.3 可重定位目标elf格式
命令:readelf -a hello.o > hello.elf
Elf文件信息:
(1)ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
- 节头:记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
(3).rela.text,保存的是.text节中需要被修正的信息;任何调用外部函数或者引用全局变量的指令都需要被修正;调用外部函数的指令需要重定位;引用全局变量的指令需要重定位; 调用局部函数的指令不需要重定位;在可执行目标文件中不存在重定位信息。本程序需要被重定位的是printf、puts、exit、sleepsecs、getchar、sleep和.rodata中的.L0和.L1。
.rela.eh_frame节是.eh_frame节重定位信息。
(4)符号表:.symtab,一个符号表,它存放在程序中定义和引用的函数和全局变量的信息,一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o > hello0.s
hello.s与hello0.s对比:
- 分支转移:在hello.s中分支转换跳转是通过L0等助记符实现,而在hello0.s中分支函数表示为主函数+段内偏移量。反汇编代码跳转指令的操作数使用的不是段名称,因为段名称知识在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
- 函数调用:在hello.s中是通过call+函数名称直接调用的,而在hello0.s中是通过call加main+偏移量,即具体地址实现的。
- 全局变量:在hello.s中通过 leaq .LC1(%rip), %rdi这种形式访问,而在hello0.s中通过lea 0x0(%rip),%rdi来访问。
4.5 本章小结
通过本章更加深入的了解了汇编和反汇编的机理,通过对elf文件的理解为后面链接奠定了基础,通过对汇编与反汇编文件的对比,明白了两种方式的异同点。
第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。链接是由叫做链接器的程序执行的。
作用:链接器在软件开发过程中扮演着一个关键的角色,因为它们使得分离编译(separate compilation)成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。
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.3 可执行目标文件hello的格式
命令:readelf -a hello > hello1.elf
- ELF头:节头数量有14个变为27个
(2)节头
(3)重定位节
- 符号表
5.4 hello的虚拟地址空间
使用edb加载hello,可以查看到本进程的虚拟地址空间各段信息。
从这张图可以看出hello的虚拟地址空间开始于0x400000。
而这里的每个地址同5.3节中节头部表的地址声明。
如:.data节开始于0x404048 大小为0x4
5.5 链接的重定位过程分析
命令:objdump -d -r hello > hello_objdump.s
1.函数变化:新增了exit、sleep等函数。
2.节变化:新增了一些节,见下图。
- 地址变化:在函数调用和控制转移部分,目标地址变成了确切的虚拟地址,见下图。
4.链接过程:将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
5.6 hello的执行流程
通过edb运行hello程序,查看所有Hello执行的过程、程序及地址。
(1)载入:_dl_start、_dl_init
(2)开始执行:_start、_libc_start_main
(3)执行main:_main、_printf、_exit、_sleep、
_getchar、_dl_runtime_resolve_xsave、_dl_fixup、_dl_lookup_symbol_x
(4)退出:exit
程序执行
ld-2.27.so!_dl_start 0x7f60c129ddf0
ld-2.27.so!_dl_init 0x7f60c12adc10
hello!_start 0x4010f0
lib-2.27.so!__libc_start_main 0x7fa84b958fc0
hello!puts@plt 0x401030
hello!exit@plt 0x401070
下面是一些edb调试过程截图:
5.7 Hello的动态链接分析
动态链接项目中,查看dl_init前后项目变化。动态链接采用了延迟加载的策略,即在调用函数时才进行符号的映射。使用偏移量表GOT+过程链接表PLT实现函数的动态链接。GOT中存放函数目标地址,为每个全局函数创建一个副本函数,并将对函数的调用转换成对副本函数调用。延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。这里我们来看.got.plt部分。
它的地址在0x404000.
调用前:
调用后:
由上面两张图的对比,我们可以发现.got.plt部分的内容因为动态链接发生了改变。plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址。plt就能跳转到正确的区域。
5.8 本章小结
本章让我们通过edb的使用以及多方面的对比,增加了对于链接指令以及过程的理解,深入的了解了它的虚拟地址空间、重定位、执行过程以及动态链接发生的变化。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
作用:进程为用户提供了以下假象:
(1) 我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。
(2) 处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
1.定义:在计算机科学中,Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(command interpreter,命令解析器)。它类似于DOS下的COMMAND.COM和后来的cmd.exe。它接收用户命令,然后调用相应的应用程序。
2.作用:shell实际上是一个命令解释器,将用户输入的命令传到内核。
3.处理流程:1.读入用户输入字符串2.切分字符串,获得参数3.检查是否为内部命令,是的话直接执行4.不是的话分配子进程执行5.shell来进行对应的处理
6.3 Hello的fork进程创建过程
父进程通过fork()函数创建一个新的运行的子进程。子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程结束时返回子进程pid,子进程结束返回0.
6.4 Hello的execve过程
Execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。所以,与fork一次调用返回两次不同,execve调用一次并从不返回。
栈结构如下:
6.5 Hello的进程执行
上下文切换:操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。内核为每一个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。
如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。比如,如果一个read系统调用需要访问磁盘,内核可以选择执行上下文切换,运行另外一个进程,而不是等待数据从磁盘到达。另一个示例是sleep系统调用,它显式地请求让调用进程休眠。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
开始hello程序处在用户模式,收到信号转换成内核模式,处理信号后,返回用户模式。在这个过程中,cpu不断切换上下文,切成时间片,交替占用cpu,实现进程的调度。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
异常分为四类:
- 中断 2.陷阱 3.故障 4.终止
信号提供了一种机制,通知用户进程发生了这些异常。
- 正常运行
- Ctrl+Z
由图中发现,但是我们不知道该进程的回收情况,所以我们调用ps指令查看一下。
通过ps我们发现hello进程只是从前台调到了后台,并没有终止回收。其实还在运行,我们可以通过kill指令将其杀死。
- ctrl+c
通过上图看出ctrl+c直接发送一个信号,将进程终止回收。进程中已经不存在hello。
- 不停乱按
由上图可以看出,运行过程中乱按,无关输入被缓存到stdin,并随着printf指令被输出到结果。这些乱码,被shell认为是命令。
6.7本章小结
通过本章的实验题目练习,更加了解了异常控制流中重要的信号机理,通过信号的控制以及内部对于进程的调控,让计算机更好的工作,有助于我们更好地理解并发思想。
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
- 逻辑地址:是在有地址变换功能的计算机中,访内指令给出的地址(操作数)叫逻辑地址,也叫相对地址,也就是是机器语言指令中,用来指定一个操作数或是一条指令的地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址即物理地址。一个逻辑地址由两部份组成,段标识符: 段内偏移量。
- 线性地址:是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
- 虚拟地址:CPU启动保护模式后,程序hello运行在虚拟地址空间中。注意,并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,Bootloader以及内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。
- 物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,表示具体的是代码段寄存器还是栈段寄存器抑或是数据段寄存器。
段描述符就是保存在全局描述符表或者局部描述符表中,当某个段寄存器试图通过自己的段选择符获取对于的段描述符时,会将获取到的段描述符放到自己的非编程寄存器中,这样就不用每次访问段都要跑到内存中的段描述符表中获取。
分段机制将逻辑地址转化为线性地址的步骤:
1)使用段选择符中的偏移值(段索引)在GDT或LDT表中定位相应的段描述符。(仅当一个新的段选择符加载到段寄存器中是才需要这一步)
2)利用段选择符检验段的访问权限和范围,以确保该段可访问。
3)把段描述符中取到的段基地址加到偏移量(也就是上述汇编语言汇中直接出现的操作地址)上,最后形成一个线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
根据csapp教材,虚拟内存被组织为一个有存放在磁盘上的N哥连续的字节大小单元组成的数组。每个字节有唯一的虚拟内存地址,作为到数组的索引。磁盘上数组的内容被缓存到主存中。VM系统通过将虚拟内存分割为称为虚拟页的大小固定的块来解决这个问题。每个虚拟页的大小为P=2^p字节。类似的物理内存被分割为物理页,大小也为P字节(物理页也被称为页帧)。
任意时刻虚拟页都被分为三个不相交的子集:
未分配的:VM系统还未分配的页
缓存的:当前已经缓存在物理内存的已分配页
未缓存的:当前未缓存在物理内存的已分配页
系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。
7.4 TLB与四级页表支持下的VA到PA的变换
这里我拿csapp书中的例子举例---core i7地址翻译,如图,首先说明一下VA和PA的构成。
VA由VPN(虚拟页号)和VPO(虚拟页面偏移)构成,这里VPN=36位,VPO=12位。
PA由PPN(物理页号)和PPO(物理页面偏移)构成,这里PPN=40位,PPO=12位=VPO。
VPN由标记位与组号构成用于取TLB中寻址。
PA由标记位、组号和块内偏移构成,用于取cache中寻数据。
过程是CPU发送一个虚拟地址,然后通过VPN去TLB中寻址,拿到PPN再跟VPO合并成为PA,最后到cache中拿数据。
当然这个过程中可能出现不命中,首先在去TLB取数据时如果发生不命中,那么VPN就回去页表中取ppn,如果取cache中取数据不命中,就从存储器系统中一级一级取数据。
7.5 三级Cache支持下的物理内存访问
这个上面7.4中已经说过大致的原理,细说的话要提及前面章节的内容就是存储器系统结构那一章节,由物理内存在存储系统中寻址,首先通过组号那些位找到对应的组,然后通过标记位,找到对应的行,再通过行中的标记位看是否有效,有效就根据块内偏移量,找到对应的数据,如果无效就会发生不命中,就会一级一级的向下找寻数据交互。
大致流程如下:
- 组选择取出虚拟地址的组索引位,将二进制组索引转化为一个无符号整数,找到相应的组
(2)行匹配把虚拟地址的标记为拿去和相应的组中所有行的标记位进行比较,当虚拟地址的标记位和高速缓存行的标记位匹配时,而且高速缓存行的有效位是1,则高速缓存命中。
(3)字选择一旦高速缓存命中,我们就知道我们要找的字节在这个块的某个地方。因此块偏移位提供了第一个字节的偏移。把这个字节的内容取出返回给CPU即可
(4)不命中如果高速缓存不命中,那么需要从存储层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU进行替换。
7.6 hello进程fork时的内存映射
在shell输入命令行后,内核调用fork创建子进程,为hello程序的运行创建上下文,并且分配一个与父进程不同的pid。通过fork创建的子进程拥有父进程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间。
7.7 hello进程execve时的内存映射
Execve函数在当前进程中加载并运行包含在可执行文件hello中的程序,用hello程序有效的替代了当前程序。加载并运行hello需要以下步骤:
1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存 在的区域结构。
2)映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
3)映射共享区域,hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4)设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
如书中所说:假设MMU在试图翻译某个虚拟地址A时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序随后就执行下面的步骤:
1)虚拟地址A是合法的吗?换句话说,A在某个区域结构定义的区域内吗?为了回答这个问题,缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。这个情况在图9-28中标识为“1”。
因为一个进程可以创建任意数量的新虚拟内存区域,所以顺序搜索区域结构的链表花销可能会很大。因此在实际中,Linux使用某些我们没有显示出来的字段,Linux在链表中构建了一棵树,并在这棵树上进行查找。
2)试图进行的内存访问是否合法?换句话说,进程是否有读、写或者执行这个区域内页面的权限?例如,这个缺页是不是由一条试图对这个代码段里的只读页面进行写操作的存储指令造成的?这个缺页是不是因为一个运行在用户模式中的进程试图从内核虚拟内存中读取字造成的?如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。这种情况在图9-28中标识为“2”。
3)此刻,内核知道了这个缺页是由于对合法的虚拟地址进行合法的操作造成的。它是这样来处理这个缺页的:选择一一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换人新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU.这次,MMU就能正常地翻译A,而不会再产生缺页中断了。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
堆中的块主要组织为两种形式:
1.隐式空闲链表(带边界标记)
在块的首尾的四个字节分别添加header和footer,负责维护当前块的信息(大小和是否分配)。由于每个块是对齐的,所以每个块的地址低位总是0,可以用该位标注当前块是否已经分配。可以利用header和footer中存放的块大小寻找当前块两侧的邻接块,方便进行空闲块的合并操作。
2.显式空闲链表
在未分配的块中添加两个指针,分别指向前一个空闲块和后一个空闲块。采用该策略,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。
7.10本章小结
在本章中学习了系统进程的存储空间情况,一个系统中的进程与其他进程共享cpu和主存资源的。虚拟内存一个非常重要的概念被引入,通过本章题目的练习,更加了解了虚拟内存的机理。并且从一个全新的角度再来看待fork等进程,使我对这些进程了解的更为深入。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
根据教材:一个Linux文件就是一个m字节的序列:
B0,B1,B2……Bm,所有的 IO 设备(如网路、磁盘、终端)都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行:
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix I/O使所有输入和输出以一种同一且一致的方式执行:
- 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
(2) Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
(3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
(4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
(5)关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix I/O函数:
- 打开和关闭文件:
- 读和写文件:
8.3 printf的实现分析
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
- 查看printf代码:
首先arg获得第二个不定长参数,即输出的时候格式化串对应的值。
- 查看vsprintf代码:
这样我们知道了vsprintf程序按照格式fmt结合参数args生成格式化之后的字符串,然后返回字串的长度。
- 接下来来看write函数的代码
在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址。
- 最后是系统调用
Syscall将字符串冲寄存器复制到显卡中,字符显示驱动子程序将点阵信息存储到vram中。显示芯片通过信号线向液晶显示器传输每一个点(RGB分量)。
最终实现打印出我们的信息。
8.4 getchar的实现分析
Getchar源代码:
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Unix操作系统中提供的基本I/O服务的一些基本概念与管理方法,包括以前c语言常用的文件开关读写操作,本次通过对此系统的学习,更加入的了解了这些函数内部的机理。并且分析了printf和getchar函数的实现。
结论
到此为止,我们就陪伴着hello走完了他的一生,在这一生中走过了很多阶段,让我们来回顾一下他的精彩人生吧。
- hello.c:编写c程序hello.c诞生。
- hello.i:通过预处理将c程序转为.i文本文件。
- hello.s:通过编译将hello.i变为hello.s。
- hello.o:hello.s经过汇编变为hello.o。
- hello:hello.o与可重定位目标文件和动态链接库链接成为可执行文件hello。至此可执行hello程序正式诞生。
- 运行:在shell中输入120L020314 张珑耀 0.5
- 创建子进程:调用fork()
- 加载:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。
- 上下文切换:hello调用sleep函数之后进程陷入内核模式,处理休眠请求主动释放当前进程,内核进行上下文切换将当前进程的控制权交给其他进程,当sleep函数调用完成时,内核执行上下文切换将控制传递给当前进程。
- 内存管理:动态分配内存。
- 信号处理:如果运行途中键入ctr-c ctr-z则调用shell的信号处理函数分别停止、挂起。
- 终止:父进程回收子进程,内核删除进程有关数据结构。
附件
如图:
参考文献
[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.