程序人生-Hello’s P2P

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术

计算机科学与技术学院
2019年12月
摘 要
本文通过对hello程序P2P和020的整体介绍,从C文件到可执行文件,从程序到进程,涉及到预处理、编译、汇编、链接、进程管理、存储管理、I/O管理等,并且hello进程创建直到回收的全部过程。以这些过程的分析为例,我们更好地说明了计算机的底层实现,并且更深地阐明了整个程序的生命周期。。

关键词:P2P;020;进程;计算机系统;hello;

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

目 录

第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简介
P2P: From Program to Process 。linux中,hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork产生子进程的过程。

020: shell通过execve加载并执行hello,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。
1.2 环境与工具
硬件环境:X64 CPU ,2.50GHz , 8G RAM
软件环境:Windows 10 64位 ,Vmware 14 ,Ubuntu 16.04 LTS 64 位
开发工具:gcc + gedit , Codeblocks , gdb edb

1.3 中间结果
hello.c :hello源代码
hello.i :预处理后的文本文件
hello.s :hello.i编译后的汇编文件
hello.o :hello.s汇编后的可重定位目标文件
hello_objdump :hello的反汇编代码
hello.0_objdump :hello.o的反汇编代码
hello :链接后的可执行文件
1.4 本章小结
本章对hello进行了简单的介绍,分析了其P2P和020的过程,列出了本次任务的环境和工具,并且阐明了任务过程中出现的中间产物及其作用。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
预处理又叫做预编译,是指在对C源代码文件进行词法扫描和语法分析之前所做的工作。预处理器cpp根据以字符#开头的命令,修改原始的C程序。;例如hello.c中第一行的#include<stdio.h>命令告诉预处理器读取系统头文件,通常是以.i作为文件扩展名。
预处理的作用:将源文件中以“include”格式包含的文件复制到编译的源文件中,用实际值替换用“#define”定义的字符串。根据“#if”后面的条件决定需要编译的代码。
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析
修改得到的C程序hello.i已经从原来hello.c的534个字节增加到63364个字节,并且增加到3110行。再用gedit打开hello.i,发现main函数在文件的最后部分。

而在main函数之前,预处理器(cpp)读取头文件stdio.h 、stdlib.h 、和unistd.h中的内容,三个系统头文件依次展开。比如stdio.h的展开,打开usr/include/stdio.h发现了其中还含有#开头的宏定义等,预处理器会对此继续递归展开,最终的.i程序中没有#define,并且针对#开头的条件编译语句,cpp根据#if后面的条件决定需要编译的代码。
2.4 本章小结
进行hello.c的预处理,生成hello.i,将c文件改变成了统一格式,并且对hello.i程序进行了预处理结果解析,理解了预处理器读取系统头文件中内容,并把它插入程序文本中的过程。
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
概念:编译器ccl将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。该程序包含函数main的定义。
作用:以一种低级机器语言指令为不同高级语言的不同编译器提供了通用的输出语言
在这个阶段,编译器首先要检查代码的规范性,是否有语法错误等,以确定代码的实际要做的工作,再检查无误后,编译器(ccl)见文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。该程序包含函数main的定义,语句以一种文本格式描述了一条低级机器语言指令。汇编语言位不同高级语言的不同编译器提供了通用的输出语言。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析
先介绍hello.s文件开头的部分:

.file:源文件名
.globl:全局变量
.data:数据段
.align:对齐方式
.type:指定是对象类型或是函数类型
.size:大小
.long:长整型
.section .rodata:下面是.rodata节
.string:字符串
.text:代码段

3.3.1 数据:
hello.s中C语言的数据类型主要有:全局变量,局部变量,指针数组
整型变量:

  1. hello.c定义了一个全局变量int型sleepsecs,且赋初值为2,将sleepsecs存放在.data段,.data段4字对齐,sleepsecs变为long型,数值为2,经过编译之后存放在.rodata段。
    根据下图可知sleepsecs被定义为全局变量globl,在.data段中,sleepsecs被设置为对齐方式是4,类型是object,大小是4字节。

  2. int argc;argc是函数传入的第一个int型参数,存储在%edi中。

  3. int i;局部变量,通常保存在寄存器或是栈中。根据movl $0, -4(%rbp)操作可知i的数据类型占用了4字节的栈空间。i存储在-4(%rbp)的地址上,初始值为0,每一次循环+1,跳出循环的条件为i>9。
    常量:在hello.s中一些如3和9的常量以立即数的形式出现。
    字符串,argv[1]和argv[2]都声明在.rodata只能读数据段中,并且给出了字符串的编码。

3.3.2 赋值:
首先是对全局变量sleepsecs的赋值,赋初值为2:

其次是对局部变量i的赋值:使用movl语句,对应于C程序中i=0 。由movl $0, -4(%rbp)完成。

3.3.3 类型转换:
因为2.5是浮点数类型,因为sleepsecs是int型,当给它赋值为2.5时,值会向零舍入,为2。

3.3.4 算术操作:
编译器将i++编译为

3.3.5 关系操作:

  1. i<10的关系操作编译为:

  2. argc!=3的关系操作编译为:

3.3.6 函数操作:
函数操作对应汇编语言中call指令。函数要提供对过程的机器支持,需要处理许多不同的属性。假设过程P调用过程Q,Q执行后返回P,这些动作包括以下机制:
传递控制 :在进入过程Q的时候,程序计数器必须被设置为Q的代码的起始地址,然后返回时,要把程序计数器设置为P中调用Q后面那条指令的地址。
传递数据 :P必须能够向Q提供一个或多个参数,Q必须能够向P返回一个值。
分配和释放内存 :在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些存储空间
程序中调用函数的有:

  1. main函数:
    参数传递:传入参数argc和argv,分别用寄存器%rdi和%rsi存储。
    函数调用:被系统启动函数调用。
    函数返回:设置%eax为0并且返回,对应return 0 。
    2.printf函数:
    hello.c中使用了两个printf函数,其中第一个printf函数打印常量字符串,该字符串存储在.LC0中,当判断条件argc!=3满足时,将LC0的值赋给%rdi,其中%rip是下一条指令的地址,然后调用puts函数输出字符串。第二个printf函数打印的是从命令行写入的字符串,分别为argv[0],argv[1],还有.LC1。编译器使用多次movq指令,将argv[0]存储在%rdx,argv[1]存储在%rsi,然后leaq指令将.LC1存储在%rdi,最后调用printf函数输出字符串。
    3.exit函数:
    参数传递:传入的参数为1,再执行退出命令
    函数调用:if判断条件满足后被调用
    4.sleep函数:
    参数传递:传入参数sleepsecs,传递控制call sleep
    函数调用:for循环下被调用
    5.getchar
    传递控制:call getchar
    函数调用:在main中被调用
    3.3.7 数组/指针/结构操作:
    通过对传入的字符串数组argv进行寻址来读取参数。argv是从命令行键入的字符串的地址数组,里面按顺序存放着命令行输入的字符串在内存中的存放地址。
    argv[2]
    argv[1]

3.3.8 控制转移:
hello.c中使用到了比较运算和控制转移,比较运算分别为argc!=3和i<=9,若argc!=3,跳转到.L2执行,若i<=9,跳转到.L4执行。
if(argc!=3) 。当argc不等于3时进行跳转。cmpl语句比较 -20(%rbp)和-3,设置条件码,判断ZF零标志,如果最近的操作得出的结果为0,则跳到.L2中,否则顺序执行下一条语句。

for(i=0;i<10;i++) :for循环里面的比较和转移过程。for循环的控制时比较cmpl $9, -4(%rbp),当i大于9时跳出循环,否则进入.L4循环体内部执行。

编译器通过cmp指令进行比较,然后调用je,jle等条件跳转指令(设置CF,ZE,SF,OF条件码,条件码不同,条件不同)跳转到某一地址执行,执行完之后返回下一条指令,这是控制转移,常常和关系运算一起使用。在这里只使用了cmp指令。其实还有test指令,和cmp指令类似。。
3.4 本章小结
在该阶段,编译器将hello.i文件编译成更抽象更低级的hello.s汇编语言文件,为汇编阶段产生机器可识别的机器语言指令做准备。
通过对编译的结果进行解析,更深刻地理解了C语言的数据与操作,并且对C语言翻译成汇编语言有了更好的掌握。

(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编器把汇编语言转换为相对应的机器语言,再把生成的机器语言变成可重定位目标文件,即由.s文件生成.o文件,.o文件是一个二进制文件,它包含17个字节是函数main的指令编码。其中,可重定位目标文件包含二进制代码和数据。
汇编的作用:将汇编语言翻译成一条条机器语言方便机器执行该段代,由.s文件生成.o文件。
4.2 在Ubuntu下汇编的命令
命令:gcc -c hello.s -o hello.o

4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
ELF头
.text
.rodata
.data
.bss
.symtab
.rel.txt
.rel.data
.debug
.line
.strtab
节头部表
1.ELF头描述了ELF头的字节大小,文件的类型,节头的数量,字符串表索引节头,入口地址等信息,并且包含帮助链接器语法分析和解释目标文件的信息。

  1. 节头部表描述了不同节的位置和大小,其中目标文件中每个节都有一个固定大小的条目。具体的描述包括节的名称、类型、地址和偏移量等。

  2. 重定位节中包括所有需要重定位的符号的信息,包括偏移量、信息、类型、符号值和符号名称+加数。当链接器把这个可重定位目标文件与其他文件相结合时,需要修改这些符号的位置。
    当汇编器生成一个目标模块是,它并不知道数据和代码最终将放在内存中的什么位置,它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行目标文件时如何修改这个引用。代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。
    ELF重定位条目的数据结构如下:
    typedef struct
    {
    long offset; /需要被修改的引用的节偏移/
    long type:32, /重定位类型/
    symbol:32; /标识被修改引用应该指向的符号/
    long attend; /符号常数,对修改引用的值做偏移调整/
    }Elf64_Rela;
    两种最基本的重定位类型:
    R_X86_64_PC32 :重定位一个使用32位PC相对地址的引用。
    R_X86_64_32 :为位置无关代码,即无需重定位的代码。重定位一个使用32位PC绝对地址的引用。
    可以看出8条重定位信息的详细情况,分别对符号.rodata,函数puts,exit等,加数也在符号名称之后。

4…symtab是一个符号表,它记录了hello.c中调用的函数和全局变量的的名称,类型,地址等信息,value地址信息,在可重定位文件中是起始位置的偏移量。bind表示符号是全局的还是本地的。UND表示为在本文件中定义的符号。
4.4 Hello.o的结果解析
反汇编命令:objdump -d -r hello.o

可重定位目标文件与汇编代码的区别:

  1. 操作数:hello.s中的操作数时十进制,hello.o反汇编代码中的操作数是十六进制。
  2. 分支转移:跳转语句之后,hello.s中是.L2和.L3等段名称,而反汇编代码中跳转指令之后是相对偏移的地址。
  3. 函数调用:hello.s中,call指令之后直接是函数名称,而反汇编代码中call指令之后是函数的相对偏移地址。因为函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
  4. 全局变量的访问:在hello.s文件中,对于.rodata和sleepsecs等全局变量的访问,是$.LC0和sleepsecs(%rip),而在反汇编代码中是$0x0和0(%rip),是因为它们的地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。

4.5 本章小结
通过汇编操作,汇编语言转化为机器语言,hello.o可重定位目标文件为后面的链接做了准备。分析了ELF格式下可重定位文件的组成和各个节存储的内容,使用readelf看具体查看。。通过对比hello.s和反汇编代码的区别,更深刻地理解了汇编语言到机器语言实现地转变,和这过程中为链接做出的准备(设置重定位条目等)。

(第4章1分)

第5章 链接
5.1 链接的概念与作用
概念:链接将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行与编译时,也就是源代码被翻译成机器代码时;也可以执行于加载时,也就是程序被加载器加载到内存并执行时;甚至可以执行与运行时,也就是由应用程序来执行。
作用:当程序调用函数库(如标准C库)中的一个函数printf,printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个函数必须通过链接器(ld)将这个文件合并到hello.o程序中,结果得到hello文件,它是一个可执行目标文件,可以被加载到内存中,由系统执行。另外,链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。
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得到了hello的ELF 格式,hello共由24个段组成,从.interp到.shstrtab。在地址这一栏中我们可以看到各段的起始地址,在大小一栏中即可得到各段的大小。
5.4 hello的虚拟地址空间
在edb中打开hello,通过Data Dump查看hello程序的虚拟地址空间各段信息。在Memory Regions选择View in Dump可以分别在Data Dump中看到只读内存段和读写内存段的信息。

可执行文件中加载的信息从0x400000处开始存放。Section Headers节中的信息可以知道这些虚拟内存中相应位置存储的信息。
5.5 链接的重定位过程分析
反汇编命令:objdump -d -r hello

区别:
hello可执行目标文件中多出了.init段和.plt段。.init段用于初始化程序执行环境,.plt段是程序执行时的动态链接。所有的重定位条目都被修改为了确定的运行时内存地址。
程序添加了许多动态链接库中的函数。使用ld链接,定义了函数入口,初始化函数,动态链接器与动态链接共享库定义hello.o中的各种函数,将上述共享函数加入。
hello.o中的相对偏移地址到了hello中变成了虚拟内存地址,hello中使用的跳转地址和函数调用地址均为虚拟内存地址。
观察hello和hello.o,可以发现在汇编代码上没有发生实质性的变化,主要是地址发生了改变,链接之前.o文件中main函数的反汇编代码从地址0开始往下,可以认为是相对偏移地址,而在链接之后,在main函数之前还链接上了其他的库文件。另外,hello和hello.o在ELF格式下的段的个数种类发生了变化,hello增加了如.interp, .hash等段。
链接过程:链接主要包括解析符号和重定位两步。在重定位之前,汇编器在hello.o文件的重定位段记录了需要重定位的符号和相应的类型和偏移量。链接器通过对符号的解析(包括局部符号和全局符号),将每个符号的引用和符号的定义相关联。

根据hello.o中的重定位项目,分析hello重定位过程:
重定位过程合并输入模块,并为每个符号分配运行时地址,主要有以下两步:
1.重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。包括hello.o在内的所有可重定位目标文件中的.data节被全部合并成一个节,这个节成为输出的可执行目标文件hello中的.data节。然后,连接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,hello中每条指令和包括.rodata、sleepsecs等全局变量都有唯一的运行时内存地址了。
2.重定位节中的符号引用。链接器依赖于hello.o中的重定位条目,修改代码节和数据节中对每个符号的引用,使得它们指向正确运行时的地址。
5.6 hello的执行流程
hello调用与跳转的各个子程序名或程序地址如下:
0x400430 init;
0x400460 puts@plt;
0x400470 printf@plt;
0x400480 __libc_start_main@plt;
0x400490 getchar@plt;
0x4004a0 exit@plt;
0x4004b0 sleep@plt;
0x4004d0 _start;
0x4004fe main;
0x400580 __libc_csu_init;
0x4005f0 __libc_csu_fini;
0x4005f4 _fini;
5.7 Hello的动态链接分析
编译器对于PIC使用了GOT来确定他的位置,每个GOT条目中生成了一个重定位记录,在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标正确的绝对地址,每个引用全局目标都有自己的GOT。在PIC函数调用时,编译器是无法预测这个函数运行时地址,因为定义它的共享模块在运行时可以被加载到任意位置,对此GUN编译器用延时绑定技术来解决这一问题,把函数地址的解析推迟到它实际被调用的地方。延迟绑定是通过两个数据结构之间的交互来实现,即通过GOT和PLT,如果一个目标模块调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT。GOT是数据段的一部分,PLT是代码段的一部分。

接下来通过edb调试,观察在dl_init前后,动态链接项目的变化。
.got.plt的起始地址为0x601000,在datadump中找到该位置。

图do_init前

图do_init后

如图,在do_init前后global_offset表发生变化。
在edb调试之后我们发现原先0x006008c0开始的global_offset表是全0的状态,在执行过_dl_init之后被赋上了相应的偏移量的值。这说明dl_init操作是给程序赋上当前执行的内存地址偏移量 。

相应位置信息:
通过ELF文件知道GOT表的存储位置:
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。
dl_init函数执行后:
原先0x006008c0开始的global_offset表是全0的状态,在执行过_dl_init之后被赋上了相应的偏移量的值 。
5.8 本章小结
本章主要理解了Ubuntu下链接的过程,链接就是是将各种代码和数据片段收集并组合成一个单一文件的过程。通过查看hello的虚拟地址空间,并且对比hello.o和hello的反汇编代码,更好地掌握了链接尤其是重定位的过程,但是我们知道链接并不止于此,hello会在它运行时要求动态链接器加载和链接某个共享库,而无需在编译时将那些库链接到应用中。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:狭义上:一个执行中的程序。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序的正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器
环境变量以及打开文件描述符的集合。广义上:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。
进程的作用:
1.每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。
2.进程提供给应用程序的关键抽象:一个独立的逻辑控制流,好像我们的程序独占地使用处理器;一个私有的地址空间,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
Shell是一个交互型的应用级程序,它代表用户运行其他程序。Shell应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。Shell编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的Shell程序与其他应用程序具有同样的效果。

处理流程:shell首先检查命令是否是内部命令,若不是再检查是否是一个应用程序(这里的应用程序可以是Linux本身的实用程序,如ls和rm,也可以是购买的商业程序,如xv,或者是自由软件,如emacs)。然后shell在搜索路径里寻找这些应用程序(搜索路径就是一个能找到可执行程序的目录列表)。在解析命令行之后,调用函数检查第一个命令行参数是否是一个内置的shell命令。如果是,就立即解释。如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。如果能够成功找到命令,该内部命令或应用程序将被分解为系统调用并传给Linux内核。
6.3 Hello的fork进程创建过程
在终端输入./hello使得hello执行,shell检查该命令是否为内置命令,显然这不是内置命令。接着shell会执行fork函数。fork函数会创建与当前进程平行运行的子进程,系统将父进程的上下文包括代码,数据段,堆,共享库以及用户栈都创建一份副本,然后利用这个副本执行子进程,这就意味着当父进程调用fork时,Hello进程可以读写父进程中打开的任何文件。父进程和Hello进程最大的区别在于它们有不同的PID。fork函数只被调用一次,却会返回两次。在父进程中,fork返回Hello进程的PID,在Hello进程中,fork返回0 。即当shell调用fork 时,hello可以读写shell中打开的任何文件。shell和hello进程之间最大的区别在于它们有不同的PID。fork在执行时被调用一次,但是却返回两次,一次是返回到父进程,一次是返回到新创建的子进程。
6.4 Hello的execve过程
execve在当前进程中载入并运行程序,execve 函数加载并运行可执行目标文件hello, 且带参数列表argv 和环境变量列表envp。只有当出现错误时,例如找不到filename, execve 才会返回到调用程序。
所以,与fork 一次调用返回两次不同, execve 调用一次并从不返回。execve加载了hello后他调用启动代码,启动代码设置栈,启动程序运行初始化代码。系统会用execve构建的数据结构覆盖其上下文,替换成hello的上下文,然后将控制传递给新程序的主函数。execve只是简单的更换了自己所处进程的上下文,并没有改变进程的pid。
在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:
int main(int argc , char **argv , char *envp);
结合虚拟内存和内存映射过程,可以更详细地说明exceve函数实际上是如何加载和执行程序Hello,需要以下几个步骤:
1.删除已存在的用户区域。
2.映射私有区域。为Hello的代码、数据、bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时复制的。
3.映射共享区域。比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
先简单阐述下面几个概念:
1.进程上下文信息,就是内核重新启动一个被抢占的程序所需的状态,它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈、和各种内核数据结构。
2.进程时间片,是指一个进程和执行它的控制流的一部分的每一时间段。

3.用户模式和内核模式:处理器为了安全起见,不至于损坏操作系统,必须限制一个应用程序可执行指令能访问的地址空间范围。就发明了两种模式用户模式和内核模式,其中内核模式(上帝模式)有最高的访问权限,甚至可以停止处理器、改变模式位,或者发起一个I/O操作,处理器使用一个寄存器当作模式位,描述当前进程的特权。进程只有当中断、故障或者陷入系统调用时,才会将模式位设置成上帝模式,得到内核访问权限,其他情况下都始终在用户权限中,就能够保证系统的绝对安全。
hello进程在内存中执行的过程中,当内核代表用户执行系统调用时,会发生上下文切换,比如说hello中的sleep语句执行时内核中的调度器就会执行上下文切换,将当前的上下文信息保存到内核中,恢复某个先前被抢占的进程的上下文,然后将控制传递给这个新恢复的进程。
Hello进程初始运行在用户模式中,直到Hello进程中的sleep系统调用,它显式地请求让Hello进程休眠,内核可以决定执行上下文切换,进入到内核模式。当定时器2.5后中断时,内核就能判断当前Hello休眠运行了足够长的时间,切换回用户模式。

时间
进程Hello
其他进程
sleep
中断
从sleep返回
用户模式
内核模式
用户模式
内核模式
用户模式
上下文切换
上下文切换
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
异常种类:
1.中断:信号SIGTSTP,默认行为是 停止直到下一个SIGCONT
2.终止:信号SIGINT,默认行为是 终止
3.陷阱和系统调用:陷阱是有意的异常,是执行一条指令的结果,就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令,在用户程序和内核之间提供一个像过程一样的接口,即系统调用。如读一个文件、创建一个进程、加载一个新的程序等。
4.故障:故障是由错误情况引起的,当故障发生时,是利器将控制转移给故障处理程序,如果错误情况可以修正,则将控制返回到引起故障指令,重新执行,否则处理程序返回到内核abort,终止故障的应用程序。
下面演示程序运行时各命令情况:
1.hello运行时什么都不按。程序执行完后,进程被回收。再按回车键,结束。

  1. SIGINT中断信号
    当用户输入ctrl-c时产生中断信号,导致内核发送一个SIGINT信号到前台工作组中的每个进程,默认终止前台作业,在这里,hello被终止。
    SIGTSTP信号

3.运行时乱按。如图6-6,发现乱按的输入并不会影响进程的执行,当按到回车键时,getchar会读入回车符,并且后面的字符串会当作shell的命令行输入。

4.按下Ctrl+Z后运行ps命令。按下Ctrl+Z后,父进程收到SIGTSTP信号,将hello进程挂起,ps命令列出当前系统中的进程(包括僵死进程)。

  1. 按下Ctrl+Z后运行jobs命令。jobs命令列出 当前shell环境中已启动的任务状态。

6.按下Ctrl+Z后运行pstree命令。pstree命令是以树状图显示进程间的关系。

7.fg命令将进程调到前台。

8.kill发送信号给一个进程或多个进程。

6.7本章小结
本章了解了hello进程的执行过程,主要是hello的创建、加载和终止,通过键盘输入,对hello执行过程中产生信号和信号的处理过程有了更多的认识,从而对异常的掌握加深了。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:包含在机器语言中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段和偏移量组成,偏移量指明了从段开始的地方到实际地址之间的距离。在hello中hello.o里面的相对偏移地址。
线性地址:逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。是hello中的虚拟内存地址。
虚拟地址:CPU从一个有N=2^n个地址空间中生成虚拟地址,这个虚拟地址在被送到内存之前先转换成适当的物理地址。
物理地址:计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。hello在运行时虚拟内存地址对应的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址分成段标识符+段内偏移量,然后先判断TI字段,看看这个段描述符究竟是局部段描述符(LDT)还是全局段描述符(GDT),然后再将其组合成段描述符+地址偏移量的形式,这样就转换成线性地址了。

索引号,是“段描述符(segment descriptor)”,段描述符具体地址描述了一个段。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,由8个字节组成,如图

首先,给定一个完整的逻辑地址[段选择符:段内偏移地址]
1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
3、把Base + offset,就是要转换的线性地址了。 还是挺简单的,对于软件来讲,原则上就需要把硬件转换所需的信息准备好,就可以让硬件来完成这个转换了。

7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址作为到数组的索引。磁盘上数组的内容被缓存在主存中。和存储器层次结构中其他缓存一样,磁盘(较低层)上的数据被分割成块,这些块作为自盘和主存(较高层)之间的传输单元。VM系统通过将虚拟内存分割为成为虚拟页的大小固定的块来处理这个问题,对这些虚拟页的管理与调度就是页式管理。同任何缓存一样,虚拟内存系统必须有某种方法来判定一个虚拟页是否缓存在DRAM中的某个地方。如果是,系统还必须确定这个虚拟页存放在哪个物理页中。如果不命中,系统必须判断这个虚拟页存放在磁盘的那个位置,在物理内存中选择一个牺牲页,并将虚拟页从磁盘复制到DRAM中,替换这个牺牲页。

分页单元中,页目录是唯一的,它的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。
每一个活动的进程,因为都有其独立的对应的虚似内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录地址放到cr3寄存器中,将别个的保存下来。
每一个32位的线性地址被划分为三部份,面目录索引(10位):页表索引(10位):偏移(12位) 依据以下步骤进行转换:
(1)从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
(2)根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
(3)根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
(4)将页的起始地址与线性地址中最后12位相加,得到最终的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
为减少内存读取数据的次数,在MMU中包括了一个关于PTE的小的缓存,即TLB,每一行都保存着一个由单个PTE组成的块。TLB索引由VPN的t个最低位组成,剩余的为为TLB标记。64位计算机采用4级页表,36位VPN被封为4个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表物理地址。VPN1提供到一个L1 PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供一个L2PTE的偏移量,以此类推。

下图给出了Core i7 MMU如何使用四级页表来将虚拟地址翻译成物理地址。36位的虚拟地址被分割成4个9位的片。CR3寄存器包含L1页表的物理地址。VPN1有一个到L1 PTE的偏移量,找到这个PTE以后又会包含到L2页表的基础地址;VPN2包含一个到L2PTE的偏移量,找到这个PTE以后又会包含到L3页表的基础地址;VPN3包含一个到L3PTE的偏移量,找到这个PTE以后又会包含到L4页表的基础地址;VPN4包含一个到L4PTE的偏移量,找到这个PTE以后就是相应的PPN(物理页号)。

7.5 三级Cache支持下的物理内存访问
得到物理地址之后,先将物理地址拆分成CT(标记)+CI(索引)+CO(偏移量),CPU访问物理地址是访问三级cache L1、L2、L3。MMU将物理地址发送给L1缓存,从物理地址中得出CT(缓存标记)、CI(缓存组索引)、CO(缓存偏移)。根据缓存组索引找到L1缓存中对应的组,若缓存标记为1,根据缓存偏移直接从缓存中读取数据并返回。如果缓存标记为0,即缓存不命中,需要从L2、L3中去读取,如果在三级缓存中都不存在,需要到主存中读取。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据,并分配给它一个唯一PID,为了给这个新进程创建虚拟内存,它创建了当前进程mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当着两个进程中的任一个后来进程写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
exceve函数加载和执行程序Hello,需要以下几个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。
3.映射共享区域。如果a.out程序与共享对象或目标链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的进程计数器,使之指向代码区域的入口点。
下一次调度这个程序时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

7.8 缺页故障与缺页中断处理
在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页(page fault) 。下图展示了在缺页之前我们的示例页表的状态。

CPU引用了VP3中的一个字,VP3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE3,从有效位推断出VP3未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,反映出VP4不再缓存在主存中这一事实。
缺页处理程序从磁盘上用VP3的副本取代VP4,在缺页处理程序重新启动导致缺页的指令之后,该指令将从内存中正常地读取字,而不会再产生异常。

7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合,来维护,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
C标准库提供了一个称为malloc程序包的显式分配器。程序通过调用malloc函数来从堆中分配块。
void *malloc( size_t size)
成功:返回已分配块的指针,块大小至少 size 字节 对齐方式依赖编译模式:8 字节 (32 位模式) 16 (字节 64 位模式)。若 size == 0 , 返回 NULL ( 0 )
出错 : 返回 NULL 同时设置 errno

1.显式分配器:要求应用显式地释放任何已分配的块。例如C程序通过调用malloc函数来分配一个块,通过调用free函数来释放一个块。其中malloc采用的总体策略是:先系统调用sbrk一次,会得到一段较大的并且是连续的空间。进程把系统内核分配给自己的这段空间留着慢慢用。之后调用malloc时就从这段空间中分配,free回收时就再还回来(而不是还给系统内核)。只有当这段空间全部被分配掉时还不够用时,才再次系统调用sbrk。当然,这一次调用sbrk后内核分配给进程的空间和刚才的那块空间一般不会是相邻的。

2.隐式分配器:也叫做垃圾收集器,例如,诸如Lisp、ML、以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

隐式空闲链表:

这样的一种结构,主要是由三部分组成:头部、有效载荷、填充(可选);
头部:是由块大小+标志位(a已分配/f空闲);有效载荷:实际的数据
简单的放置策略:
首次适配:从头搜索,遇到第一个合适的块就停止;
下次适配:从头搜索,遇到下一个合适的块停止;
最佳适配:全部搜索,选择合适的块停止。

分割空闲块:
适配到合适的空闲块,分配器将空闲块分割成两个部分,一个是分配块,一个是新的空闲块,如图

增加堆的空间:
通过调用sbrk函数,申请额外的存储器空间,插入到空闲链表中 。
合并空闲块:

  1. 合并空闲块的目的
    虽然释放了两个3字节大小的数据空间,而且空闲的空间相邻,但是就是无法再分配4字节的空间了,这时候就需要进行一般合并:合并的策略是立即合并和推迟合并,立即合并,可能有不好的地方。如果我们马上合并上图的空间后又申请3字节的块,那么就会开始分割,释放以后立即合并的话,又将是一个合并分割的过程,这样的话推迟合并就有好处了。需要的时候再合并,就不会产生抖动了。
  2. 带边界标记的合并

Knuth提出了一种边界标记技术,允许在常数时间内进行对前面快的合并。这种思想是在每个块的结尾处添加一个脚部,其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。

显式空闲链表的基本原理:
根据定义,程序不需要一个空闲块的主体,所以实现空闲链表数据结构的指针可以存放在这些空闲块的主体里面。
显式空闲链表结构将堆组织成一个双向空闲链表,在每个空闲块的主体中,都包含一个pred(前驱)和succ(后继)指针。
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于空闲链表中块的排序策略。
一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。

7.10本章小结
本章介绍了hello的存储器地址空间、Intel逻辑地址到线性地址的变换-段式管理、Hello的线性地址到物理地址的变换-页式管理、学会了TLB和四级页表支持下VA到PA的转换,以及得到了PA后,三级cache下的物理内存的访问过程、hello进程fork时的内存映射、hello进程execve时的内存映射、缺页故障与缺页中断处理以及动态存储分配管理。

(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
一个linux文件就是一个m个字节的序列:
B0 , B1 , … , Bk , … , Bm-1
所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
File Types文件类型
每个 Linux 文件都有一个类型( type )来表明它在系统中的角色:
普通文件 (Regular file): 包含任意数据
目录 (Directory): 包含一组链接的文件,每个链接都将一个文件名映射到一个文件
套接字 (Socket): 用来与另一个进程进行跨网络通信的文件
其他文件类型
命名通道( Named pipes (FIFOs)
符号链接( Symbolic links)
字符和块设备( Character and block devices)
8.2 简述Unix IO接口及其函数
Unix IO接口:
打开文件,内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。
Linux 内核创建的每个进程都以与一个终端相关联的三个打开的文件开始:
0: 标准输入 (stdin)
1: 标准输出 (stdout)
2: 标准错误 (stderr)
头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,他们可用来代替显式的描述符值。
改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。
读写文件:读操作:读文件从当前文件位置复制字节到内存位置,然后更新文件位置,返回值表示的是实际传送的字节数量,返回类型ssize_t是有符号整数,nbytes<0表明发生错误。同读文件一样,不足值是可能的,并非错误。写操作:写文件从内存复制字节到当前文件位置,然后更新文件位置。返回值表示的是从内存向文件fd实际传送的字节数量,nbytes<0表明发生错误。同读文件一样,不足值是可能的,并非错误。当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中。
I/O重定向:Linux shell提供了I/O重定向操作符,允许用户将磁盘文件和标准输入输出联系起来。
函数为int dup2(int oldfd, int newfd);dup2函数复制描述符表象项oldfd到描述符表项newfd,覆盖newfd之前的内容,如果newfd已经打开了,dup2会在复制oldfd之前关闭newfd。
8.3 printf的实现分析
首先观察一下Linux下printf的函数体
static int printf(const char *fmt, …)
{
va_list args;
int i;

 va_start(args, fmt);
 write(1,printbuf,i=vsprintf(printbuf, fmt, args));
 va_end(args);
 return i;
}

调用printf函数的时候,先是最右边的参数入栈。fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素。fmt也是个变量,它的位置,是在栈上分配的,它也有地址。

printf函数主要调用了vsprintf和write函数。
下面首先介绍vsprintf(buf, fmt, arg)是什么函数。
其函数原型为:int printf(const char *format, …);
其函数返回值:打印出的字符格式
其调用格式为:printf("<格式化字符串>", <参量表>);
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);
}
从上面vsprintf函数可以看出,这个函数的作用是将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
一个int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。
sys_call的实现:
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
关于printf缓冲:在printf的实现中,在调用write之前先写入IO缓冲区,这是一个用户空间的缓冲。系统调用是软中断,频繁调用,需要频繁陷入内核态,这样的效率不是很高,而printf实际是向用户空间的IO缓冲写,在满足条件的情况下才会调用write系统调用,减少IO次数,提高效率。
printf在glibc中默认为行缓冲,遇到以下几种情况会刷新缓冲区,输出内容:
(1)缓冲区填满;
(2)写入的字符中有换行符\n或回车符\r;
(3)调用fflush手动刷新缓冲区;
(4)调用scanf要从输入缓冲区中读取数据时,也会将输出缓冲区内的数据刷新。
可使用setbuf(stdout,NULL)关闭行缓冲,或者setbuf(stdout,uBuff)设置新的缓冲区,uBuff为自己指定的缓冲区。也可以使用setvbuf(stdout,NULL,_IOFBF,0);来改变标准输出为全缓冲。全缓冲与行缓冲的区别在于遇到换行符不刷新缓冲区。
于是可以直到printf函数执行过程如下:
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar由宏实现:#define getchar() getc(stdin)。getchar有一个int型的返回值。当程序调用getchar时.程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才等待用户按键。
getchar的代码:
int getchar(void)
{
static char buf[BUFSIZ];
static char
bb=buf;
static int n=0;
if(n==0)
{
n=read(0,buf,BUFSIZ);
bb=buf;
}
return (–n>=0)?(unsigned char)*bb++:EOF;
}
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
每次调用getchar函数,它就会从输入缓冲区中读出第一个字符,并把这个字符从输入缓冲区中清除。然而,这个输入缓冲区的设计,是把所有从键盘上输入的东西都放进去的,包括你每次按的回车符‘\n’,而getchar函数只读走了你在回车前输入的那个字符,而将回车符保留在了输入缓冲区中。于是,第二次调用getchar时,函数就从输入缓冲区中读出了’\n’。
要解决这个问题,多加一个getchar(),过滤掉回车,但是这种方法有不足,就是如果你在调用第一个getchar时输入了多个字符,那么,加入一个getchar并不能把所有未读取的字符过滤。如果你的本意是重新从“键盘”读取的话,最好是加一个fflush(stdin);清除输入缓冲区。
8.5本章小结
这一章介绍了Linux的I/O管理方法、I/O接口及其函数、printf的实现分析、getchar的实现分析。
(第8章1分)
结论
hello所经历的:
1.hello被IO设备编写,以文件的方式储存在主存中。
1.用户从键盘输入,得到hello.c的C源文件。
2.hello.c被预处理hello.i文件
3.hello.i被编译为hello.s汇编文件
4.hello.s被汇编成可重定位目标文件hello.o
5.链接器将hello.o和外部文件链接成可执行文件hello
6.在终端中输入命令,shell-bash调用fork函数创建一个新的子进程,并在新的子进程中调用execve函数,加载并运行hello。
7.在一个时间片中,hello有自己的CPU资源,顺序执行逻辑控制流。
8.Hello会与多个进程并行运行,当发生中断或异常时,发生上下文切换,转到内核模式,内核调度另一个进程运行。
9.hello在运行过程中可能会遇到各种信号和键盘输入,shell为其提供信号处理程序。
10.CPU为hello分配内存空间,hello从磁盘加载到内存。
11.hello访存时,请求一个虚拟地址,通过MMU、TLB、四级页表得到虚拟地址对应的物理地址,在三级cache中进行访存。
12.hello在运行过程中会有异常和信号等。
13.hello输出信息调用printf函数和getchar函数,这两个函数需要调用Unix I/O接口函数实现。
14.shell父进程回收hello子进程,内核删除为hello创建的所有数据结构。

我的感悟:
通过对计算机系统的深入理解,我在编写代码的时候逐渐从计算机的底层考虑问题,思考自己该怎么优化程序的性能,编写对编译器有好的代码。hello可以说囊括了这一个学期学习CSAPP的大部分内容,用一个大作业的形式将这些内容很好的串联在一起。
(结论0分,缺失 -1分,根据内容酌情加分)

附件

hello.c hello源代码
hello.i 预处理后的文本文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标文件
hello .d hello的反汇编文件,查看汇编代码
hello l.d hello的反汇编文件,查看链接
hello 链接后的可执行文件

(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 深入理解计算机系统 Randal E.Bryant David R.O’Hallaron 机械工业出版社
[2] 博客园 printf函数实现的深入剖析
https://www.cnblogs.com/pianist/p/3315801.html
[3] 维基百科 virtua memory
https://en.wikipedia.org/wiki/Virtual_memory
[4] Linux 逻辑地址、线性地址、虚拟地址、物理地址
https://blog.csdn.net/baidu_35679960/article/details/80463445

(参考文献0分,缺失 -1分)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值