CSAPP大作业

 

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业         计算机类              

学     号          200111502             

班     级          2003003             

学       生           肖博文          

指 导 教 师          史先俊             

摘  要

Hello.c是几乎每个程序员都要编写的程序,看似简单但从代码开始运行到得到可执行文件最后在终端输出“Hello World”的过程却十分复杂。我们的程序经过预处理,编译,汇编和链接生成可执行文件,我们将借助Hello.c来了解这个过程,深入理解计算机系统,对书本上的进程管理,存储管理加深理解,进而对这门CSAPP课程加深理解。

关键词:计算机系统;进程;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简介

  我们通过如codeblocks,vs等编辑器编写hello.c文件,.c文件经过一些列处理依次生成.i文件,.s文件,.o文件,可执行程序最后在shell中运行打印出“Hello World”,实现了from program to Process的过程。接下来shell 为 hello 进程 execve,映射虚拟内存,进入程序入口后程序开始载入物理内存。CPU 为hello可执行程序分配时间片执行逻辑控制流,程序运行结束后, shell 父进程回收 hello 进程,与此同时内核删除相关数据结构。实现了O2O—from Zero-0 to Zero-0的过程。

1.2 环境与工具

实验环境:Windows10 64位;Ubuntu 15

测试工具:VMware虚拟机,gcc,codeblocks,edb,vim等

1.3 中间结果

源文件hello.c,经过预处理得到的hello.i文件,经过编译得到的汇编文件hello.s,经过汇编得到的机器语言文件hello.o,从hello.o反汇编得到的汇编文件hello_edb.s,经过连接得到的可执行文件hello等

1.4 本章小结

本章对hello.c的执行过程进行简单描述,介绍了P2P、020的实现过程,介绍了作业中的实验环境和开发工具,最后简述了从.c文件到可执行文件中间经历的过程及每一步产生的可执行文件。

第2章 预处理

2.1 预处理的概念与作用

概念:预处理指的是程序在编译之前进行的处理,是计算机在处理一个程序时所进行的第一步处理,可以进行代码文本的替换工作,但是不做语法检查。预处理是为编译做的准备工作,能够对源程序.c文件中出现的以字符“#”开头的命令进行处理,包括宏定义#define,文件包含#include,条件编译#ifdef等,最后将修改之后的文本进行保存,生成.i文件,完成预处理。  

作用:CPP根据#后的不同命令执行不同的操作,宏定义在预处理的过程中会进行宏替换,即将实际值替换用#define定义的字符或字符串。#inculude指令能够告诉预处理器读取源程序中所引用的系统的源文件,并且将这一段代码直接插入到程序文件中,最终保存为.i文件中。条件编译指的是针对#ifdef,#ifndef等语句进行的处理。条件编译能够根据#if的不同条件决定需要进行编译的代码,#endif是结束这些语句的标志。使用条件编译可以使目标程序变小,在满足条件之后才会进行编译。

2.2在Ubuntu下预处理的命令

预处理命令:gcc -E hello.c -o hello.i

 

 

2.3 Hello的预处理结果解析

预处理阶段把.c变成.i文件的过程中,保留我们在.c文件中代码的同时,根据我们引用的头文件,为该文本文件添加了很多相关头文件的内容,包括各种变量的定义,函数的声明,结构体的声明,例如打开hello.i文件我们发现在main函数之前插入了各种头文件和函数声明,头文件stdio.h, unistd.h, stdlib.h依次展开。

 

2.4 本章小结

本章先介绍了预处理的概念和功能,并学习在Ubuntu下预处理的指令,对实际生成的hello.i文件与.c文件进行比较,深入了解预处理阶段所做的工作。

第3章 编译

3.1 编译的概念与作用

概念:简单地说,一个编译器就是一个程序,它可以通过某一种语言(源语言)编写的程序,并把该程序翻译成等价的,用另一种语言(目标语言)编写的程序。编译器所实现的这个过程我们将其理解成编译。

作用:编译器将输入的字符流通过语法分析器生成符号流,将符号流通过语法分析和语义分析生成语法树,接下来通过中间代码生成器,机器无关代码优化器,生成中间表示形式,最后通过代码生成器和机器相关代码优化器生成目标机器语言完成将.i文件编译生成.s文件的过程。

3.2 在Ubuntu下编译的命令

命令:gcc -S hello.i -o hello.s

 

3.3 Hello的编译结果解析

3.3.1数据

1.常量

常量是程序中不可改变的量,常量经常以字面量,或者宏定义的方式出现。

程序中的常量保存在.text,.rodata中,作为指令的一部分来执行。

2.变量

全局变量:

在所有函数外部定义的变量称为全局变量,它的作用域默认是整个程序,也就是所有的源文件,初始化的全局变量存储在.data文件中,全局变量的初始化直接完成不需要汇编语句。

局部变量:

定义在函数内部的变量称为局部变量,它的作用域仅限于函数内部, 离开该函数的内部就是无效的,再使用就会报错。

局部变量存储在栈或寄存器中,例如上图将程序中的局部变量存储在栈中并初始化为0。

3.3.2算数操作

 由于程序比较简单,.s文件中仅有addl   $1, -4(%rbp)算数操作,用于实现i++运算

3.3.3条件判断和状态转移

条件判断argc不等于4

 

 

 

上述指令实现循环功能:

 

3.3.4数组操作

主函数main中的参数有数组指针argv

 

 

3.3.5函数

1.main函数

 传入的参数存储在%edi和%rsi中,用call指令调用函数,将下一条指令压入栈中并将%设置为0返回。main函数的调用在系统启动时调用。

2.printf函数

 

Printf函数被编译成puts函数,将传入的参数保存在内存上并发送给寄存器%rdi,%rdi再作为参数传递给puts函数。

3.sleep函数

sleep(atoi(argv[3]));

汇编实现:

Sleep函数先通过call atoi得到参数值并将结果存到寄存器%rdi中,并将%rdi作为参数传给sleep函数。

4.exit函数

exit(1);

exit函数将常量1存储在寄存器%edi中,并将寄存器%rdi作为参数传递给exit函数。

5.getchar函数

因为源代码中的getchar没有参数传递,因此这里直接用call 命令调用getchar函数即可。

3.4 本章小结

本章首先介绍了编译器的工作原理,进而解释了编译的概念和作用并简单说明了编译过程中编译器所完成的工作。接下来通过Ubuntu下的命令将.i文件编译成.s文件,并通过展示.s文件的内容初步介绍了源程序中的常量,全局变量,局部变量,算数操作,条件控制和状态转移,各种函数在汇编语言下是如何实现其功能的。也对汇编语言的语法和使用有了初步掌握,对简单C语言实际运行时内存和寄存器的各种作用有了一定了解同时也对编译器在实际运行时对源代码的一些改变有了认知。

第4章 汇编

4.1 汇编的概念与作用

概念:汇编器将汇编语言翻译为机器语言的过程叫做汇编。汇编器将hello.s翻译成机器语言指令,然后把这些指令打包成可重定位目标程序的格式,并将结果输出为可重定位文件hello.o。

作用:将编译后的.s文件中的汇编语言指令转化为机器语言指令并将结果输出为.o可重定位目标程序文件。

4.2 在Ubuntu下汇编的命令

命令:gcc -hello.s -m64 -c -o hello.o

 

4.3 可重定位目标elf格式

ELF文件有ELF头,程序头表,节和节表头四部分构成。

ELF头是elf文件中的头部,存储这一些机器和ELF文件的基本信息,例如系统信息,编码方式,ELF头大小等。

 

节头表目,记录了.o文件中各个节的名称,类型,地址和偏移量等信息。

重定位节,记录了各个段引用的外部符号,在链接时需要通过重定位节对这些位置的地址进行修改。

符号表用于存放程序中调用的函数和全局变量的信息。

4.4 Hello.o的符号表结果解析

     hello.o文件的反汇编:

 

 

对某些命令的形式有了不同,例如在hello.s文件中mov命令通常后接位数例如movl,movq但在反汇编文件却将mov指令统一写成mov。

反汇编得到的汇编语言在分支转移的时候.s文件跳转语句后接的是段名称,而反汇编文件跳转指令使用的是相对偏移地址。

在hello.s文件call指令后接的是函数名称,而反汇编文件中的call命令后接是main函数的相对偏移地址。

在hello.s中操作数用10进制来表示,而反汇编文件中的操作数用16进制表示。

4.5 本章小结

本章先介绍了汇编的概念和作用,介绍了汇编器在汇编的过程中完成的任务。接下来在Ubuntu下用命令将.s文件汇编成.o可重定位目标程序文件。对可重定位目标ELF进行分析,初步了解了ELF文件的格式以及各部分的信息。最后我们用objjump对.o文件进行反编译,并将反编译文件与.s文件进行对比,了解了两者之间的差别。

5章 链接

5.1 链接的概念与作用

概念:在实际开发中,我们一定是多文件编程,所有文件在编译后,需要合在一起,合在一起的过程就是链接的过程。

作用:使得一个项目可以被分解成许多小模块,每个模块可单独进行修改,最后通过链接形成一个项目,实现了分离编译,节省了大量空间同时方便了程序员进行开发。

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的格式

ELF头:

 节头表目:

 

程序头:

 

可以看出一共有八个段,程序头存储着每段的信息,例如PHDR大小偏移0X40,大小是0X2a0,虚拟地址是0X400040,物理地址也是0X400040,是可读的,且为8字节对齐。

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。   

 

通过段名在edb的symbols view窗口中找到对应的虚拟内存地址,在通过Data Dump找到对应的节,右边就为其二进制形式。

 

5.5 链接的重定位过程分析

        生成的反汇编文件:

 

 

hello与hello.o的不同:

  1. 前面的编号有明显的不同,因为是放在了虚拟内存0X400000相应的位置,而hello.o反汇编是从0开始的。
  2. 在hello的连接中增加了源程序用到的库函数。
  3. hello增加了.init和.plt节直接反汇编前面有因为重定位加入进来各种函数,数据。
  4. 在hello.o用于定位的相对偏移地址到hello的反编译文件中都变味虚拟内存地址。

根据hello.o与hello的不同我们可以分析出链接的过程就是链接器将多个目标文件组合打包,函数段按次序排列的过程。

5.6 hello的执行流程

找到起始位置调用的子程序名<_init>

 

根据symbols view我们可以看到从加载加载hello到_start,到call main,以及程序终止调用的子程序名及其虚拟地址。

5.7 Hello的动态链接分析

初始化前:

 

初始化后:

 

Plt段初始存放的是一批代码,他们跳转到got所指示的位置,让后调用链接器。初始时got存放的是都是plt的第二条指令,当链接器修改got时下一次调用plt指向的就是正确的内存地址。

5.8 本章小结

本章先介绍了链接的概念和作用,学习在Ubuntu下使用命令执行链接生成hello可执行文件,并通过ELF文件了解hello的格式。接下来将hello.o文件与hello进行对比从而对链接的重定位过程简单了解。通过edb的使用我们也对实际运行中子程序的调用顺序以及初始化前后部分地址的改变初次了解。

6章 hello进程管理

6.1 进程的概念与作用

概念:进程简单来说就是在操作系统中运行的程序,它是操作系统资源管理的最小单位。但是进程是一个动态的实体,它是程序的一次执行过程。进程和程序的区别在于:进程是动态的,程序是静态的,进程是运行中的程序,而程序是一些保存在硬盘上的可执行代码

作用:从实现角度看,是一种数据结构,目的在于清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序。

6.2 简述壳Shell-bash的作用与处理流程

作用:Shell是一个命令行解释器,为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用shell来启动、挂起、停止甚至是编写一些程序。Shell还是一个功能相当强大的编程语言。Shell是解释执行的脚本语言,在shell中可以直接调用Linux命令。

处理流程:

1.终端进程读取用户由键盘输入的命令行。

2.分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量。

3. 检查第一个命令行参数是否是一个内置的shell命令。

4. 如果不是内部命令,则调用fork()创建新进程/子进程。

5. 在子进程中,用步骤2获取的参数调用execve()执行指定程序。

6.如果用户没要求后台运行(命令末尾没有&号),则shell使用waitpid(或wait...)等待作业终止后返回。

7. 如果用户要求后台运行(如果命令末尾有&号),那么shell返回并继续等待 用户的下一次输入。

6.3 Hello的fork进程创建过程

按照流程,终端执行hello后,shell分析命令行字符串并检查第一个命令行参数是否是一个内置的shell命令,因为不是内部命令shell会调用fork函数为其创建一个新的子进程,并得到一份与父进程相同的副本,但子进程的PID与父进程有所区别,父进程的fork函数返回子进程的PID,子进程的fork函数返回0。

6.4 Hello的execve过程

终端执行hello后,shell分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量。当子进程创建之后,利用获取的命令行参数调用execve执行指定程序。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。exceve做的最后一件事就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

6.5 Hello的进程执行

上下文切换:

在每个任务运行前,CPU 都需要知道任务从哪里加载、又从哪里开始运行,也就是说,需要系统事先帮它设置好 CPU 寄存器和程序计数器。上下文切换,就是先把前一个任务的 CPU 上下文保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。

进程时间片:

操作系统的任务调度是采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后便会被强制暂停并去执行下一个任务,每个任务轮流执行。任务执行的一小段时间就叫做时间片。

在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个过程称为调度。hello程序与操作系统其他进程通过操作系统的调度,切换上下文,拥有各自的时间片从而实现并发运行。

6.6 hello的异常与信号处理

正常运行:

 

执行异常:

  1. 异步异常:

中断,由处理器外部I/O设备引起。处理方法是由处理器的中断引脚指示,中断处理程序返回到下一条指令处。

2.同步异常:
                 陷阱,是有意的,执行指令的结果。处理方法:陷阱处理程序将控制返回到下一条指令。

故障,执行指令产生的结果,不是有意的但可能被修复。处理方法:处理程序要么重新执行引起故障的指令(已修复),要么终止。

 

终止,执行指令产生的结果,非故意,不可恢复的致命错误造成。处理方法:终止当前程序。

 

Hello执行过程输入ctrl+c的情况,发送SIGINT使其终止。

 

Hello执行过程输入ctrl+z的情况,发送SIGSTP使其终止。 

执行过程中乱摁:

 

运行ps命令:

 

运行jobs命令:

 

运行pstree命令:

 

运行fg命令:

 

运行kill命令,发送SIGKILL信号:

 

6.7本章小结

本章先介绍进程的概念以及作用,接下来介绍了如何在Linux下使用shell命令行解释器来创建子进程的过程。了解了内核是如何对hello的进程进行调度和管理,最后对进程执行过程中的异常信号处理简单介绍,并在hello运行时调用不同的命令去了解不同信号的处理机制。

 

7章 hello的存储管理

7.1 hello的存储器地址空间

1.逻辑地址:指由程序产生的段内偏移地址。

2.线性地址:指虚拟地址到物理地址变换的中间层,是处理器可寻址的内存空间(称为线性地址空间)中的地址。程序代码会产生逻辑地址,或者说段中的偏移地址,加上相应段基址就成了一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换产生物理地址。若是没有采用分页机制,那么线性地址就是物理地址。

3.虚拟地址:是由程序产生的由段选择符和段内偏移地址组成的地址。这2部分组成的地址并不能直接访问物理内存,而是要通过分段地址的变化处理后才会对应到相应的物理内存地址。

4.物理地址:指内存中物理单元的集合,他是地址转换的最终地址,进程在运行时执行指令和访问数据最后都要通过物理地址来存取主存。

7.2 Intel逻辑地址到线性地址的变换-段式管理

逻辑地址有段标识符和段内偏移量组成,段标识符又称为段选择符,段描述符是一种数据结构,实际上就是段表项,分两类:用户的代码段和数据段描述符,其中段选择符又包含一个索引号,用来在段描述符表中找到一个具体的段描述符,描述符表实际上就是段表,由段描述符(段表项)组成。有三种类型,全局描述符表GDT:只有一个,用来存放系统内每个任务都可能访问的描述符,例如,内核代码段、内核数据段、用户代码段、用户数据段以及TSS(任务状态段)等都属于GDT中描述的段,局部描述符表LDT:存放某任务(即用户进程)专用的描述符,中断描述符表IDT:包含256个中断门、陷阱门和任务门描述符。

给定一个逻辑地址段选择符和段内偏移地址根据段选择符T1位的值决定转换的是GDT中的段还是LDT中的段,根据相应的寄存器得到一个数组,此时通过索引号在这个数组中找到段描述符,这样Base基地址就知道了,最后将Base+offset就能得到这个转换的线性地址。

7.3 Hello的线性地址到物理地址的变换-页式管理

页式是一种内存空间存储管理技术,将各进程的虚拟空间划分成若干个相等长度的页,页式管理将内存空间按页的大小划分成片或页面并把页式虚拟地址与内存地址建立页表,页表是一个页表条目的数组将虚拟页地址映射到物理页地址。根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。根据线性地址的中间十位,在页表中找到页的起始地址;将页的起始地址与线性地址中最后12位相加,得到最终我们想要的物理地址。

7.4 TLB与四级页表支持下的VA到PA的变换

每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。多级页表:将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。获得VA首先通过它的VPN查找TLB。如果找到我们就得到了了我们的PPN物理页的信息。如果不命中我们就到页项里面寻找,将VPN划分为9位一份的四份然后对各级页表分别进行查找匹配,找到下一级页表的项,最后在第四级页表找到PPN。找到了就获得了PPN可能会更改TLB中的信息。如果找不到就会发生缺页。然后就会访问下一级存储获得里面的页,然后将页写入上一级快速缓存。依次循环。最后获得的PPN与VPO结合形成PA

7.5 三级Cache支持下的物理内存访问

我们得到VP后经过分配知道前六位为块偏移,中级6位是组索引,前面40位是标记。我们看到了整个数组的情况,依靠中间六位找到了L1中相应的组数。然后通过CT匹配相应的行数。当标记位为1且CT相同时命中。得到行然后再取块偏移处的数据即可。其他情况为不命中的情况,此时要在L2的寻找是否存在该数据有就将其加入L1,如果不空就要寻找到牺牲行。并且如果牺牲行有什么变化还要将其些回到我们的L2中。同理L2再次不命中就要找L3。并且将数据放入L2中,如果L2满了就要,选择牺牲的数据,有可能也会把牺牲的数据修改的写回给L3,然后再实行上面的步骤。同理L3也会不命中,就要和主存发生交换了

7.6 hello进程fork时的内存映射

Shell调用fork()时,内核为新进程创建各种数据结构,并分配给它一个唯一的P。为了给这个新进程创建虚拟内存,它创建了当前程序的mm_struct,区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中每个区域结构都标记为私有的写时复制。当fork在新进程中返回时,新进程现在的虚拟内存刚好的和调用fork时的虚拟内存相同。而当这两个进程中任何一个进行写操作时,就会触发一个保护故障。当故障处理程序注意到保护异常是由于进程试图写私有的写时复制区域中的一个页面而引起的,它就会在物理内存中创建这个页面的一个新副本,更新页表条目指向这个新的副本,然后恢复这个页面的可写权限。当故障处理程序返回时,CPU重新执行这个写操作,现在在新创建的页面上这个写操作就可以正常执行了。通过延迟私有对象中的副本直到最后可能的时刻,写时复制机制最充分地使用了稀有的物理内存,同时也为每个进程保持了其私有地址空间。

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:

1. 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。

2. 映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。

3. 映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

4. 设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

7.8 缺页故障与缺页中断处理

处理器生成一个虚拟地址,并将它传送给MMU。接着MMU生成PTE地址,并从高速缓存/主存请求得到它;由高速缓存/主存向MMU返回PTE,PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。缺页处理程序页面调入新的页面,并更新内存中的PTE,缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。

7.9动态存储分配管理

运行时需要额外的虚拟内存是就用动态内存分配器更为方便。内存分排气维护着一个进程的虚拟内存区域称为堆。堆进阶在为初始化的数据区域以后,并向上生长向更高的地址。对于每个进程,内核维护着一个变量brk他指向堆顶。分配器把堆视为一组大小不相等的块的集合。每个块都是一个连续的虚拟内存片,要么是已分配的要么是空闲的。已分配的块显示的保留为供应程序使用。空闲块可以用来分配。一个已分配的块保持已分配状态直到它被释放,这种释放要么是应用程序显示的执行要么是内存分配器自身隐式的执行。

两种分配器:

显式分配器(explicit allocator):要求应用显式地释放任何已分配的块。例如C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。

隐式分配器(implicit allocator):要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage collector),而自动释放未使用的已分配的块的过程叫做垃圾收集(garbage collection)。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

两种分配器的实现:

隐式空闲链表分配器:我们可以将堆组织为一个连续的已分配块和空闲块的序列,空闲块是通过头部中的大小字段隐含地连接着的,这种结构为隐式空闲表。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。一个块由一个字的头部、有效载荷、可能的填充和一个字的脚部组成,其中脚部就是头部的一个副本。头部编码了这个块的大小以及这个块是已分配还是空闲的。分配器就可以通过检查它的头部和脚部,判断前后块的起始位置和状态。

显示空闲链表分配器:将堆组成一个双向空闲链表,在每个空闲块中都包含一个pred(前驱)和succ(后继)指针。一种方法是用后进先出(LIFO)的顺序来维护链表,将新释放的块放置在链表开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块。在这种情况下释放一个块可以在常数时间内完成。若使用了边界标记,那么合并也可以在常数时间内完成。另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于其后继地址,在这种情况下释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址地址排序的首次适配比LIFO排序的首次适配有更高的内存利用率,接近最佳适配的利用。显式链表的缺点是空闲块必须足够大,以包含所有需要的指针,以及头部和可能的脚部。这就导致更大的最小块大小,亦潜在地提高了内部碎片的程度。

7.10本章小结

本章从各个方面介绍了内存方面的问题。从逻辑内存到虚拟内存,从虚拟内存到物理内存。然后又到了malloc动态内存分布。我们了解了程序运行时发生的种种问题。需要各个系统配合才能是运行的准备时间最短,运行性能最好。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:

所有的I/O设备都被模型化为文件,甚至内核也被映射为文件。

设备管理:unix io接口

这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。

8.2 简述Unix IO接口及其函数

打开和关闭文件函数:

open(char *filename, int flags, mode_t mode) 和 close(int fd)

open将filename转换成文件描述符。flags有O_CREAT(创建截断文件),O_TRUNC(截断文件),O_APPEND(设置文件位置在文件尾),close关闭文件。

读写文件函数:

read(int fd,void *buf,size_t n) and write(int fd,const *buf,size_t n)

read成功返回读的字节数,若EOF则为0,失败输出-1,write成功则为写的字节数,若出错则为-1。

改变当前的文件位置函数: (seek)lseek()指示文件要读写位置的偏移量

无缓冲的输入输出函数:rio_readn和 rio_writen

带缓冲的输入函数:rio_readlineb 和 rio_readnb

函数查看文件信息函数:

stat(const char *filename ,struct stat *buf)

Fstat(int fd ,struct stat *buf)

对目录的操作的函数:

打开目录:opender(const char *name)

读取目录:readdir(DIR *dirp)

关闭目录:closedir(DIR *dirp)

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;

    }

Printf调用的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);

   }

printf函数执行过程如下:

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用int 0x80或syscall。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf函数和getchar函数。hello实现与外接设备的交互,从外接设备读入数据,最后将信息输出到显示屏幕上等过程的实现均在本章被解析。

结论

Hello经历的过程:

1. hello被I/O设备编写,以文件的方式储存在主存(适当时刻存入磁盘)。

2. hello.c经预处理生成hello.i文件。

3. hello.i经编译生成hello.s汇编文件。

4. hello.s经汇编成可重定位目标文件hello.o 。

5. 链接器将hello.o和所需外部文件链接成可执行文件hello。

6. 在shell输入命令后,通过fork和exceve加载并运行hello。

7. hello在一个时间片中有自己的CPU资源,顺序执行逻辑控制流。

8. hello的VA通过TLB和页表翻译为PA。

9. 三级cache支持下hello的物理地址访问。

10. hello在执行过程中会有异常和信号需要处理。

11. hello在Unix为I/O的帮助下与用户交互。

计算机系统的设计与实现十分精巧,借助本次大作业让我深入理解了计算机系统。学会了从底层的设计思路去看一个程序而不光光只从代码部分理解。只有深入理解计算机系统的底层逻辑和具体实现才能对计算机的应用更加掌握。

附件

hello.i: hello.c经预处理生成hello.i文件。

hello.s: hello.s经汇编成可重定位目标文件hello.o 。

hello.o: hello.s经汇编成可重定位目标文件hello.o 。

hello: 链接器将hello.o和所需外部文件链接成可执行文件hello。

elf.txt:hello.o的ELF格式文件。

hello.elf:hello的ELF格式文件。

hello_objdump.s:hello的反汇编代码。

obj.txt:hello.o的反汇编代码。

参考文献

为完成本次大作业你翻阅的书籍与网站等

[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.

[7]pistni.printf函数的深入剖析[DB/OL].https://www.cnblogs.com/pianist/p/3315801.html

[8] 课程ppt

[9] DWJ-Blog. CSDN [DB/OL].https://blog.csdn.net/AGambler/article/details/817093

06

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值