csapp大作业

计算机系统大作业

摘 要

helloworld程序是在每个初学者学习一门编程语言时,都会学习到的第一个程序,它实现起来简单,却包含了一个完整的程序所拥有的所有内容。本文就hello程序,从代码编辑完毕到运行结束回收进程的过程中,所需要的各个环节进行剖析,以此分析一个程序的完整的生命周期。

关键词:预处理;编译;汇编;链接;进程;linux IO;

目 录

第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:

linux下hello.c经预处理、编译、汇编、链接最终成为可执行文件hello,之后又在shell中输入命令后,进而fork生成子进程、hello获得了相关资源的整个过程为P2P

020:

From zero-0 to zero-0,shell为hello程序的fork,使其存在于进程中;在运行完后,shell同时承担着回收该进程的作用,从内存中抹去,防止出现僵死进程。

1.2 环境与工具

Intel i5

VMware
Workstation Pro Ubuntu64位

Window10

gcc
ld readelf gedit objdump edb hexedit

1.3 中间结果

hello.i:预处理生成的文本文件

hello.s:.i编译后得到的汇编语言文件

hello.o:.s汇编后得到的可重定位目标文件

hello.out:.o经过链接生成的可执行目标文件

1.4 本章小结

本章对hello的P2P,020的整个过程作了简要介绍,列出了运行的环境及使用的工具以及中间结果

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:

预处理指的是在编译前对源代码进行的处理,预处理命令以#开头,不对源码进行分析,只进行一些预备处理。

预处理的作用:

宏定义:宏定义又称为宏代换、宏替换,简称“宏”。预处理(预编译)工作也叫做宏展开:将宏名替换为字符串, 即在对相关命令或语句的含义和功能作具体分析之前就要换。

文件包含:文件包含处理是指在一个源文件中,通过文件包含命令将另一个源文件的内容全部包含在此文件中。在源文件编译时,连同被包含进来的文件一同编译,生成目标目标文件。

条件编译:程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。条件编译指令将决定哪些代码被编译,而哪些不被编译的。可以根据表达式的值或者某个特定的宏是否被定义来确定编译条件。

2.2在Ubuntu下预处理的命令

gcc -E hello.c -o hello.i

在这里插入图片描述

图2.1 gcc下运行预处理命令
在这里插入图片描述

图2.2 生成的预处理文件hello.i

2.3 Hello的预处理结果解析

预处理将hello.c前面的注释删除掉,所有的#define删除,展开所有的宏定义,将所有的头文件内容插入文件中。

2.4 本章小结

本章节学习了预处理的过程。作为第一步,预处理对代码进行了简单的替换与处理。

第3章 编译

3.1 编译的概念与作用

在预处理后,编译器就需要对代码进行编译过程,将预处理后的代码转变为汇编语言代码。将hello.i文件翻译为汇编语言的hello.s文件。

3.2 在Ubuntu下编译的命令

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

在这里插入图片描述

图3.1 linux下运行编译命令

在这里插入图片描述
在这里插入图片描述

图3.2 编译生成的.s文件的截图

3.3 Hello的编译结果解析

3.3.0文件声明

在这里插入图片描述

声明 含义

.file hello.c源文件

.text 代码段

.align 对齐格式

.type 符号类型

.size 数据空间大小

.section.rodata 只读代码段

.global 全局变量

.string 字符串类型数据

3.31数据

①:main函数有两个参数,argc和argv[],argc是传入参数的个数,argv[]数组存放传入的参数,其中第一个参数也就是argv[0]是函数的名字,在main函数中,argc出现在if判断语句中,

在这里插入图片描述

argc存储在-20(%rbp)中,argv的参数存取在栈中。

使用寄存器传参,%rdi传第一个参数,%rsi传第二个参数。

②:main函数中有两个int型局部变量,argc和i,i存在于-4(%rbp)中。

③:字符串常量:

在这里插入图片描述

字符串常量存在于.rodata中,\347\224等是汉字,采用UTF-8编码。

3.32 赋值操作

Hello.c中有一个局部变量i,存在-4(%rbp)中,在编译环节赋值为0,

在这里插入图片描述

3.33类型转换:

在这里插入图片描述

atoi函数将字符串转换为整型,在hello.c中将argv[3]转换为整型数字。

3.34:算术操作:

Hello.c中在for循环中对参数i有一个操作i++,在此句实现,

在这里插入图片描述

编译过程中会进行此类类似操作,如sub,mul,div等等

3.35:关系操作

在hello.c中有argc!=4,i<8两个关系操作,在汇编过程中编译为,

在这里插入图片描述在这里插入图片描述

JE表示相等跳转。JLE表示小于或等于跳转,类似还有JN,JZ,JNE等等,他们是根据ZF,CF,SF,OF符号位来进行跳转的,同时还有JMP,无条件跳转。

3.36:数组

argv[]数组

在这里插入图片描述

由此知此数组存在于-32(%rbp)中,此为数组的初地址,然后根据在数组中的偏移来对数组元素进行索引。

3.37控制转移:

①:hello.c中包含if(argc!=4),汇编过程中,

在这里插入图片描述

此句便是进行if控制,若argc等于4,则跳转到.L2,否则打印提示语。

②:hell.c中的for循环:

在这里插入图片描述

-4(%rbp)存放的是局部变量i,i每次++,每次小于或等于7就跳转回.L4继续,当i=8时,程序运行了8次,结束运行。

3.38函数:

main 函数

main 函数开始时被存在.text 节,标记类型为函数,程序运行时,将由系统 启动函数调用,因此 main 函数是 hello.c 的起点。main 函数的两个参数分 别为 argc 和 argv[],由命令行输入,存储在%rdi 和%rsi 中。main函数的返回值放在eax传递

printf 函数

   调用printf函数打印,

使用call来调用printf,而printf的返回值则会被存入eax返回。

exit 函数

exit函数的调用,终止程序!参数从edi传递。

sleep 函数

sleep函数可以使计算机程序(进程,任务或线程)进入休眠,使其在一段时间内处于非活动状态。当函数设定的计时器到期,或者接收到信号、程序发生中断都会导致程序继续执行。从edi传参。

3.4 本章小结

在本章中,编译器将代码编译成汇编语言,在此过程中,编译器处理了C语言的各种不同的类型的数据以及逻辑结构等等,使得代码更加靠近机器级的一层。

第4章 汇编

4.1 汇编的概念与作用

概念:通过汇编器,把汇编语言翻译成机器语言

作用:通过汇编这个过程,把汇编代码转化成了计算机完全能够理解的机器代码(机器语言),变成了一条条对应的机器指令。

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

gcc -c hello.s -o hello.o

在这里插入图片描述

图4.1 linux下运行汇编命令

4.3 可重定位目标elf格式

ELF头:

在这里插入图片描述

图4.2 ELF文件ELF头截图

ELF头提供Magic,类别,数据,版本,OS/ABI,ABI,ELF头的大小、目标文件的类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息

节头:

在这里插入图片描述

图4.3 ELF文件节头的截图

.text : 已编译程序的机器代码

.rel.text : 一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

.data : 已初始化的全局变量和静态C变量。

.bss : 未初始化的全局和静态C变量,以及被初始化为0的全局或静态变量。

.rodata : 只读数据,比如printf语句中的格式串和开关语句的跳转表。

.symbal : 一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。

.strtab : 一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以null结尾的字符串的序列。

重定位节:

在这里插入图片描述

重定位条目包括引用的外部函数和全局变量等;

符号表
在这里插入图片描述

4.4 Hello.o的结果解析

在这里插入图片描述
在这里插入图片描述

图4.4 hello.o反汇编结果

分支转移:反汇编代码和.s文件对比可以看出跳转指令的操作数从原来的符号(L1,L2,L3),变成了相对于PC的偏移量的方式跳转地址。

函数调用:在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,函数调用之后跟着的是应该是具体的地址,而有些库函数的调用需要在链接之后才能确定真正运行的地址.

4.5 本章小结

在本章中。hello通过汇编生成了可重定位的目标文件,汇编代码转换成了机器能够识别的机器语言。

第5章 链接

5.1 链接的概念与作用

通过汇编过程后,得到了一个可执行目标文件hello.o,其中内容为机器语言,机器能够识别。但是我们无法直接将它运行。因为其中包含外部的函数,例如printf,exit函数等。这些函数没有写在hello.c的中,而存在于标准库。我们需将标准库中的这些函数链接到我们的hello.o中,才能获得可执行目标文件。

链接的作用就是将多个可重定位目标文件链接成为一个可执行目标文件。

5.2 在Ubuntu下链接的命令

在ubuntu下命令:

ld
-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 /usr/lib/gcc/x86_64-linux-gnu/7/crtbegin.o
hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/7/crtend.o
/usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello.out

在这里插入图片描述

图 5.1 linux下运行链接命令

5.3 可执行目标文件hello的格式

linux下执行readelf -a hello.out

在这里插入图片描述
在这里插入图片描述

图5.2 节表部分截图/

5.4 hello的虚拟地址空间

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

在这里插入图片描述

图 5.3  使用edb加载hello.out

使用edb加载hello,可以从DataDump看到虚拟地址空间为0x400000-0x401000。

Data Dump显示的则是数据段的详细信息。而左上窗口显示的是实际的程序里对应地址里的信息。

5.5 链接的重定位过程分析
在这里插入图片描述

图5.4 objdump hello.out 部分截图。

对比可知,链接后对hello.o增添了许多库函数,并且该文件对每条指令前面待修改的地址进行了修改,改成了虚拟内存中的地址,并且对函数的引用和跳转也指向了绝对地址。

重定位过程:

首先合并相同节,然后对定义符号进行重定位(确定地址),再对引用符号进行重定位。

5.6 hello的执行流程

使用edb加载运行hello.out,执行流程为:

在这里插入图片描述

5.7 Hello的动态链接分析

从readelf的结果可以看到got起始表位于0x600ff0处。

在这里插入图片描述

执行_start前的got内容

在这里插入图片描述

执行_start后的got内容

根据执行_start后的got内容,可以找到共享库模块入口如图所示。

在这里插入图片描述

可知,在_start函数运行后,程序动态链接完成。

5.8 本章小结

本章介绍了链接的概念与作用、hello的ELF格式,hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。经过链接后,程序可以正常运行了,链接过程中,链接器将hello.o和其他的文件链接起来,确定了其重定位的地址,生成了可执行目标文件,并进行了动态链接。

第6章 hello进程管理

6.1 进程的概念与作用

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。[2]

当一个程序需要运行时,OS就会为它fork出一个新的进程。然后使用execve来执行这个程序,这是程序边以进程的形式被加载并运行。

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

Bash (GNU Bourne-Again Shell) 是许多Linux发行版的默认Shell[3]。Shell,是用户与操作系统间的一种交互的工具,提供了从用户层到内核层的接口。它可以将用户输入的命令解释到内核来执行。

   Shell本身是一个进程,当它接受到一个命令是,如果是一个内部命令,那么他可以直接在自身执行;而如果是一个合法的外部程序的话,Shell便需要先使用fork函数创然后使用建出一个新的(子)进程,然后在新的进程中使用execve运行进程。

6.3 Hello的fork进程创建过程

当一个进程使用调用fork函数时,会产生一个新的(子)进程,这个进程拥有与父进程(创建该进程的进程)相同的上下文、相同的全局变量等(不共享)。

   由于会产生相同上下文的子进程,这也使得fork函数与其他的函数有一个特殊之处:调用一次,会有两个返回值:父进程中的返回值与子进程中的返回值。

   在父进程中,当fork调用成功时,会返回创建的子进程的pid,而在子进程中返回值为0。这是区分父进程与子进程的方法。

6.4 Hello的execve过程

如果想要运行别的程序,就需要调用execve函数。

execve函数的原型为:

#include <unistd.h>

int execve(const char *filename, const char
*argv[], const char *envp[]);

如果成功,则不返回,如果错误,返回-1。

调用后,控制便会移交给新程序的主函数,于是程序便开始执行。

6.5 Hello的进程执行

时间片(timeslice)又称为“量子(quantum)”或“处理器片(processor slice)”是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间(在抢占内核中是:从进程开始运行直到被抢占的时间)。现代操作系统(如:Windows、Linux、Mac OS X等)允许同时运行多个进程 —— 例如,你可以在打开音乐播放器听音乐的同时用浏览器浏览网页并下载文件。事实上,由于一台计算机通常只有一个CPU,所以永远不可能真正地同时运行多个任务。这些进程“看起来像”同时运行的,实则是轮番穿插地运行,由于时间片通常很短(在Linux上为5ms-800ms),用户不会感觉到。

由于新进程的创建、操作系统的调度、中断等多种原因,会出现进程切换。这样的过程又被称为上下文切换。原进程A运行在用户模式下,当需要运行B进程,此时会进入内核模式,内核开始调度任务。切换上下文等信息之后,又重新进入用户模式,开始执行进程B。

6.6 hello的异常与信号处理

1.在hello运行时,乱按键盘字母键,会产生键盘中断,存入键盘缓冲区,但不会对进程的执行产生影响。如果同时按了回车,回将乱按的字符串作为shell命令,

在这里插入图片描述

2.运行的时候按ctrl+c。
在这里插入图片描述

按下CTRL+C后,向进程发送一个SIGINT信号,该进程会被终止。

3.运行的时候按下CTRL+Z

在这里插入图片描述

按下CTRL+Z后,向进程发送了一个SIGTSTP信号,进程会停止,但进程并未终止,仍然可以继续运行。

使用ps命令,可以看到hello.out进程仍在当前的进程中。

使用jobs命令,查看该工作信息。

使用fg 1命令可以将进程调到前台继续执行,如下图

在这里插入图片描述

6.7本章小结

在本章,我们知道了这个可执行程序,在执行的过程中的具体行为,我们还知道了如何控制这个运行中的程序。通过ps、pstree、jobs等命令查看其状态与信息,通过kill发送信号,发生了异常的时候如何利用信号机制对其进行控制。

第7章 hello的存储管理

7.1 hello的存储器地址空间

计算机系统的主存被组织为一个有M个连续的字节大小的单元组成的数组。每个字节都有一个唯一的物理地址。从0、1、2依次往下数。这种简单的结构,CPU访问内存最自然的方式就是使用物理地址。

   现代处理器使用的是虚拟地址的访存方式。CPU访问一个地址时,会给出一个地址(逻辑地址),经过地址翻译(MMU)后得到物理地址,然后进行访问。

   在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。

   在进行地址翻译时,发出的逻辑地址,经过地址变换,即可产生线性地址。线性地址是逻辑地址与物理地址的中间层。

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

在从逻辑地址至线性地址进行变换时,用的是基地址加上偏移的方法。即:

   逻辑地址(offset) + 基地址 = 线性地址

   比如:一条mov 0xXXXX,

%rax的指令。此处的0xXXXX便是逻辑地址,还需要加上隐含的数据段的偏移地址,才能获得线性地址。

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

从线性地址到物理地址的变换,采用的是一种叫做分页的机制。

   分页的基本方法是,将内存分为若干等大的内存块,按块来进行管理。为了保证访问的效率,页式管理使用了和高速缓存类似的机制。通过页表来记录一些页条目,以实现高速访问的需求。

在这里插入图片描述

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

TLB(Translation Lookaside Buffer,翻译后备缓冲器)是一个用于改进虚拟地址到物理地址转换速度的缓存。它的每一行是一个PTE(页表条目)块,它是从VPN(Virtual Page Number, 虚拟页号)中提取出来的。如果TLB中有T = 2t组,则VPN中的低t位作为TLB的索引,而剩下的位则作为标记。

这样当TLB命中时,便可直接获得PA(物理地址),而无需再一次访问低一级的缓存来获取地址。可以节约几十到几百周期的CPU时间。当TLB不命中时,便需要从高速缓存/内存中找到PTE,才能获得PA。(图7.1)

当只使用一级页表时,即使是对于32位的,4KB的页表,4字节的PTE,也需要4MB的内存常驻。于是,我们引入了多级页表的概念。每一级PTE负责映射1024个页表首地址。只在最后一级保存PPN,在不需要时不用加载,这样可以有效地减小内存的压力

TLB命中与不命中的处理流程
在这里插入图片描述
在这里插入图片描述

多级页表的地址翻译

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

访问低级存储结构时,由于其访问开销过大,于是引入了高速缓存Cache的概念。

   高速缓存的的原理是将第一级的存储访问结果暂时保存在高一级的存储中。这样,当再次访问该处的内容时,可不需要再向低级设备请求,而是直接从缓存中直接载入数据。三级cache逐级作为下一级的缓存,利用了高级存储结构的访问速度的同时也利用了低级存储结构空间大的优点。

7.6 hello进程fork时的内存映射

Hello进程fork时,内核为其创建各种数据结构,并分配给它一个pid,它创建了进程的mm_struct、区域结构和页表的原样副本。并且,两个进程中的每个页面都被标记为私有的写时复制。

在这里插入图片描述

私有的写时复制对象

7.7 hello进程execve时的内存映射

执行execve时,会用hello程序替换当前进程。其运行过程有以下步骤[1]:

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

l 映射私有区域。为新程序的代码、数据、bss和栈区域被映射为a.out文件中的.text和.data区。bss区域时请求二进制零的,映射到匿名文件,其大小包含在a.out中。栈和堆区域也是请求二进制零的,初始长度为零。

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

l 设置程序计数器(PC)。execve做的最后一件事就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

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

DRAM缓存不命中的情况,通常被称为缺页(page fault)。

   当出现缺页时,会发出一个缺页异常,引发缺页中断。此时,会进入缺页中断处理程序。类似于高速缓存,这个程序选择一个牺牲页,如果该牺牲页已经被修改,则此时将修改写回磁盘。之后,将该页废弃,将所缺的页复制到该处,完成一次替换。之后中断处理进程结束,回到导致缺页异常的语句处,重新执行该语句,此时该语句不会再导致缺页异常。

7.9动态存储分配管理

动态内存分配器维护着一个虚拟内存区域,名为“堆”。分配器将堆视为一些大小不同的块的集合,包括已分配块的和空闲的块。当进程请求使用malloc请求内存时,内存分配器根据策略寻找合适的空闲块分配给该请求。

   此处介绍显示分配器的一些简单策略。

   简单的数据组织方式有以下两种:

l 隐式链表

l 显示空闲链表

隐式链表利用头部存储该块的大小。在保证数据双字对齐的情况下,size的低3位一定为0,故可用低位来存储一些信息,如该块的分配/空闲情况。
在这里插入图片描述


7.4 隐式链表的数据组织方式

在这里插入图片描述


7.5 隐式链表的链表情况

另一方面,为了提高内存的利用率,需要将相邻的空闲块合并为一个整体的空闲块。为了这一目的,引入了带边界标记的合并方式。除了块的头部外,再添加一个尾部,这使得可以很方便地找到某一块的前一相邻块,可以很方便地进行合并。

在这里插入图片描述


7.6 带边界标记的块格式

显示空闲链表除了存储该块的size外,还存储了指向祖先、后继空闲块的指针,这使得寻找匹配的空闲块的时间从块总数的线性时间下降为空闲块的线性时间。

在这里插入图片描述


7.7 使用显示双向链表的块格式

  显示双向链表有两种维护方式:

l LIFO:将新释放的块放置再链表的开始处

l 按地址维护:按地址顺序维护链表

LIFO能在常数时间内完成块的释放,而按地址维护方式需要花费线性时间找到其前驱位置。然而,LIFO的内存利用率比按地址维护的方式低。

搜索合适的放置位置的方法,主要有:

l 首次适配:从前往后搜索到第一个能容纳请求的大小的块作为适配

l 下一次适配:从上一次的块处寻找能够容纳的块

l 最佳适配:寻找能容纳的最小的块作为匹配

三者比较:

最佳适配的内存利用率最高

下一次适配的匹配速度最快

7.10本章小结

本章主要介绍了程序运行时的内存情况,包括虚拟内存的概念,虚拟内存的管理页表管理,通过高速缓存提高数据的访问速度,共享块的概念。以及程序运行过程中非常重要的显示内存分配器的工作流程。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

在Linux系统中,一个 Linux 文件就是一个 m 字节的序列:B0,B1…Bk… Bm-1,除此之外,所有的I/O设备都被模型化为文件,例如:/dev/sda2(用户磁盘分区),/dev/tty2(终端),甚至内核也被映射为文件,例如,/boot/vmlinuz-3.13.0

-55-generic(内核映像),/proc(内核数据结构),这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O

8.2 简述Unix IO接口及其函数

open打开文件

函数原型:

int open(char *filename, int flags,mode_t mode);

返回:若成功则为新文件的文件描述符,若出错为-1

描述:

打开文件filename并返回一个整型的文件描述符,返回的文件描述符为当前可用的文件描述符的最小值。.

  1. read读操作

函数原型:

ssize_t read(int fd, void *buf, size_t n);

返回:若成功则为读的字节数,若出错为-1.

描述:

从文件fd的当前位置最多读取n个字节,移动文件位置到已读的位置,并返回实际读出的字节数。

  1. write写操作

函数原型:

ssize_t write (int fd, const void *buf, size_t n);

若成功则为写的字节数,若出错为-1

描述:

从文件fd的当前位置最多写入n个字节,移动文件位置到已读的位置,并返回实际写入的字节数。

4 close关闭文件

函数原型:

int close(int fd);

返回:若成功则为0,若出错为-1.

描述:

将文件描述符为fd的文件关闭。

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;

}

va_list的定义是:typedef char*va_list,arg为第一个参数的地址。

vsprintf()将字符串格式化,比如:printf(“hello %s %s”, “123”,
“abc”);,将被格式化为字符串“hello
123 abc”

write函数如8.2节所说,为写文件的函数,它进行sys_call,从ascii字模库到显示vram。显示芯片逐行读取点阵信息,向显示器输出每一点的RGB分量,实现printf格式化输出。[5]

8.4 getchar的实现分析

在程序运行结束后,为方便观察结果,插入一个getchar函数是程序可以暂停。

getchar函数等待从键盘缓冲区读取一个字符。

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

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。这样,便实现了读取一个字符的功能。

8.5本章小结

本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf函数和getchar函数。

在本章我们会了解到hello是如何实现与外接设备的交互,从外接设备读入数据,最后将信息输出到显示屏幕上,这些过程的实现都在本章得到了明晰。

(第8章1分)

结论

我们编写了一个hello.c文件,直至她成为了我们可以看到的可以完美运行的hello程序中,他经历了如下的旅程。

  1. 预处理:包括文件包含、宏定义、条件编译

  2. 编译:将预处理后的文件编译为汇编代码

  3. 汇编:对汇编代码进行汇编操作,将其转化为机器语言,生成可重定位目标文件。

  4. 链接:将hello.o文件与所需的外部函数(如printf,exit等)链接起来,生成可执行目标文件。

  5. shell使用folk函数产生新的子进程,并在子进程中使用execve函数运行hello程序。

  6. 操作系统为hello程序分配内存空间,使其正常运行。

  7. 在运行过程中,接收到Ctrl-C、Ctrl-Z等按键时,会传递信号到hello程序,hello程序对信号做出响应。

  8. hello程序接收命令行参数后,调用printf函数将内容输出到屏幕上

  9. 接收键盘中断存入键盘缓冲区,结束getchar函数,程序运行结束

  10. shell回收hello进程

hello至此,过完了他的一生。

在我们的日常中,我们日常的写好代码。编译运行,一个结果便展现在我们的眼前,他显得如此轻松平常,但谁知他们的背后,竟有着如此之多的过程,有如此多的背后工作者在默默的为我们付出着,这个简单的hello,正如他的自述,在完成后被我们嫌弃的扔掉,她是如此的简单,可在那些巨大的程序背后,正是这样一个小小的影子,一个一个的完成着,大作业完成了,hello.c结束了,我们的hello才刚刚开始启程。

附件

hello:gcc –m64 –no-pie –fno-PIC生成的hello可执行文件

hello.i:预处理生成的文本文件

hello.s:.i编译后得到的汇编语言文件

hello.o:.s汇编后得到的可重定位目标文件

hello.out:.o经过链接生成的可执行目标文件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值