哈工大计算机系统大作业
摘要
本文主要阐述在Linux系统下hello程序的生命周期,了解hello程序从hello.c经过预处理、编译、汇编、链接生成可执行文件的全过程。结合课本的知识详细阐述计算机系统是如何对hello进行进程管理、存储管理和I/O管理,通过对hello生命周期的探索,让我们对计算机系统有更深入的了解。
关键词:hello程序;预处理;编译;汇编;链接;进程管理;存储管理;I/O管理;计算机系统。
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:即为program to process,从程序到进程。在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 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位
开发与调试工具:gcc,vim,edb,readelf,HexEdit等
1.3 中间结果
名称 | 作用 |
---|---|
hello.i | hello.c预处理得到的文件 |
hello.s | hello.i编译得到的汇编文件 |
hello.o | hello.s汇编后得到的可重定位文件 |
hello | 链接后的可执行文件 |
hello.out | hello反汇编后的可重定位文件 |
1.4 本章小结
本章介绍了hello程序的P2P和020,列出了实验环境和工具以及过程文件等。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器cpp根据#开头的宏定义、条件编译等命令,修改原始c程序,将引用的库展开合并成一个完整的文本文件。
作用:
1.处理条件编译指令,将#ifdef、#else等指令进行处理,把某部分代码包含进来或排除在外,将不必要的代码过滤掉。
2.处理#define、#include等指令,把相应的定义和对应的头文件加入进来,使得编译程序对其处理。
3.处理一些特殊符号例如LINE、FILE等,用合适的值对其进行替换。
2.2在Ubuntu下预处理的命令
指令:gcc hello.c -E -o hello.i
2.3 Hello的预处理结果解析
经过预处理把hello.c文件转换为hello.i文件,可以看到,文件内容相比之前增加许多,但仍然可以找到原来的hello.c文件的内容,且在此基础之上还增加了许多其他文本内容,对源文件中的宏进行展开,加入头文件的内容,如声明函数、定义变量、定义结构体等。还会对相应#define变量进行替换。
2.4 本章小结
本章介绍了预处理的概念和作用,以及经过预处理后程序hello.c发生的变化。
第3章 编译
3.1 编译的概念与作用
编译器把hello.i文件翻译为汇编语言的hello.s文件。以高级程序设计语言书写的源程序作为输入,经过编译后,把以汇编语言表示的目标程序作为输出。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
.file:声明源文件
.text:代码节
.section:
.rodata:只读代码段
.align:数据或者指令的地址对其方式
.string:声明一个字符串(.LC0,.LC1)
.global:声明全局变量(main)
.type:声明一个符号是数据类型还是函数类型
3.3.1数据
1.字符串
上图中可看到程序内的两个字符串在只读数据段中。
leaq .LC0(%rip), %rdi
leaq .LC1(%rip), %rdi
这两个字符串作为printf函数的参数。
2.局部变量
main函数声明了局部变量i,存放在-4(%rbp)的位置。
3.main函数
参数argc作为传给main的参数,也被放到堆栈中。
4.立即数
立即数直接体现在代码中。
5.数组
文件中唯一的数组是main函数的第二个参数char *argv[]。
数组的初始地址存放在-32(%rbp)。
3.3.2函数
1.函数调用与参数传递
调用该函数前将rdi设置为rax的值进行参数传递,然后call atoi@PLT进行函数调用。
2.函数返回
通过指令ret进行返回。
3.3.3赋值操作
赋值操作有i=0,主要有mov指令来实现。
movb:一个字节
movw:两个字节
movl:四个字节
movq:八个字节
3.3.4算数操作
程序中算数操作有i++,由addl实现。还有其他操作。
3.3.5关系操作与控制转移
(1)argc!=4
对应编译指令为cmpl $4,-20(%rbp),同时设置条件码决定分支,进行控制转移。
(2)i<8
对应编译指令为cmpl $7,-4(%rbp),同时设置条件码决定分支,进行控制转移。
3.3.6类型转换
hello.c中涉及的类型转换是:atoi(argv[3]),将字符串类型转换为整数类型。其他的类型转换还有int、float、double、short、char之间的转换
3.4 本章小结
本章主要讲述了编译器的作用以及如何处理各种数据和操作,还有c语言各种操作对应的汇编代码。
第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
gcc hello.s -c -o hello.o
4.3 可重定位目标elf格式
(1) ELF Header:
命令:readelf -h hello.o
ELF Header:以序列 Magic 开始,描述生成该文件的系统的字的大小和字节顺序,剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括了 ELF 头的大小、目标文件的类型、机器类型、 字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。
根据头文件的信息,可以看出该文件是可重定位目标文件,有14个节。
(2)Section Headers
命令:readelf -S hello.o
Section Headers:包含文件中各个节的语义,包括节的类型、位置、大小等,每个节都从零开始因为是可重定位目标文件,根据节头表中的字节偏移信息可知各节的起始位置以及所占空间的大小。还可以看到代码段可执行不能写,数据段和只读数据段不可执行。
(3).symtab
命令:readelf -s hello.o
.symtab:存放定义和引用的函数和全局变量的信息,name为名称,对应可重定位目标模块;value为起始位置偏移;size为目标大小;Bind表示是本地的还是全局的;type表示类型,要么是函数要么是数据。
(4)重定位节.real.text
命令:readelf -r hello.o
.real.text:.text 节中位置的列表,包含需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要对这些位置进行修改。
Offset:需要被修改的引用节的偏移
Info:包括symbol和type两部分,symbol在前面三个字节,type在后面三个字节,
symbol:标识被修改的引用应该指向的目标
type:重定位的类型
Type:告知链接器应该如何修改新的应用
Attend:一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整
Name:重定向到的目标的名称。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
将反汇编代码和hello.s比较可看出,汇编语言的指令并没有不同,反汇编代码不仅显示汇编代码,还有机器代码。机器代码是纯粹的二进制语言,由操作码和操作数构成,是电脑可以真正识别的语言;汇编语言是人们熟悉的语句表述cpu动作形成的语言。每条汇编语言都可以由机器语言表示,建立一一映射关系。可以把汇编语言转换成机器语言看出不同的地方。
(1)条件转移
反汇编指令用确定的地址,hello.s用段符号如L1。
(2)函数调用
.s文件中函数调用直接接函数名称,反汇编文件中call后是下一条指令。
4.5 本章小结
本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,比较了hello.s和hello.o反汇编代码的不同之处,分析了从汇编语言到机器语言的一一映射关系。
第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的格式
(1)ELF Header:类型变为EXEC可执行文件,有27个节。
(2)节头部表Section Headers:对Hello中所有节的信息进行了声明,包括大小size和偏移量offset,根据里面的信息可以定位各个节所占的区间,地址为被加载到虚拟地址的初始地址。
(3)重定位节:
(4)符号表.symtab:
5.4 hello的虚拟地址空间
查看edb得出,hello虚拟地址起始于0x400000, 结束于0x400ff0。
根据前面的节头部表,可以通过edb查看各个节的信息。比如.text节,虚拟地址开始于0x4010f0,大小为0x145.
5.5 链接的重定位过程分析
经过对hello和hello.o的比较分析,可以看出hello反汇编的代码有明确的虚拟地址,即完成了重定位,而hello.o中虚拟地址为0,未完成重定位,如下图所示。
除此之外,hello反汇编代码中多出了其他节和额外的汇编代码。如下图。
5.6 hello的执行流程
ld-2.27.so!_dl_start—
ld-2.27.so!_dl_init—
hello!_start—
hello!_init—
hello!main—
hello!puts@plt–
hello!exit@plt–
hello!printf@plt–
hello!sleep@plt–
hello!getchar@plt–
sleep@plt—
5.7 Hello的动态链接分析
共享链接库代码是动态的目标模块,在程序开始运行或者调用程序加载时,可以自动加载该代码到任意的一个内存地址,并和一个在目标模块内存中的应用程序链接起来,这个过程就是对动态链接的重定位过程。
动态的链接器在正常工作时采取了延迟绑定的链接器策略,由于静态的编译器本身无法准确预测变量和函数的绝对运行时地址,动态的链接器需要等待编译器在程序开始加载时再对编译器进行延迟解析,这样的延迟绑定策略称之为动态延迟绑定。在plt和got中分别存放着链接器的目标变量和函数的运行时地址。一个动态的链接器通过静态的过程偏移链接表plt+got链接器实现函数的动态过程链接,这样一来,它就已经包含了正确的绝对运行时地址。
5.8 本章小结
本章介绍了链接的概念和作用,阐述hello.o如何链接成为可执行文件,介绍了ELF格式和各个节的含义。
第6章 hello进程管理
6.1 进程的概念与作用
进程是执行中程序的实例,有其自己的地址空间,进程为客户提供了一种假象:
(1)我们的进程是系统中当前唯一运行的程序,独占处理器和内存。
(2)处理器无间断地执行进程。
6.2 简述壳Shell-bash的作用与处理流程
Linux系统中shell是交互型应用级程序,代表用户运行其他程序。
其基本功能是解释并运行用户的指令,重复如下处理过程:
(1)终端进程读取用户通过键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个命令行参数是否是一个内置的shell命令
(4)如果不是内部命令,调用fork( )创建子进程
(5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(6)如果用户没要求后台运行(命令末尾没有&号)则shell使用waitpid(或wait…)等待作业终止后返回。
(7)如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
终端程序通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是相互独立的副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程的PID是不同的,是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
以我们的hello为例,当我们输入 ./hello 1190201403 张偲博 1 的时候,首先shell对我们输入的命令进行解析,由于我们输入的命令不是一个内置的shell命令,因此shell会调用fork()创建一个子进程。
6.4 Hello的execve过程
在创建了一个子进程后,子程序调用execve函数在上下文加载hello程序。
(1)删除当前虚拟地址中已存在的用户区域。
(2)为新程序建立新的区域结构,这些区域结构是私有的,虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区,bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
(3)如果hello与共享对象链接,那么这些对象都被动态链接到这个程序,然后映射到用户虚拟地址空间中的共享区域。
(4)设置程序计数器,使之指向代码区域的入口点,下次调用这个进程时,从这个入口点开始执行。
6.5 Hello的进程执行
在调用进程发送sleep之前,hello在当前的用户内核模式下运行,在内核中进程再次调用当前的sleep之后进程转入用户内核等待休眠模式,内核中所有正在处理等待休眠请求的应用程序主动请求释放当前正在发送处理sleep休眠请求的进程,自动将当前调用hello的进程加入正在执行等待的队列,移除或退出正在内核中执行的进程等待队列。
设置定时器,休眠的时间为自己设置的时间,当计时器时间到,发送一个中断信号。内核收到中断信号进行中断处理,hello被重新加入运行队列,等待执行,这时候hello就可以运行在自己的逻辑控制流里面了。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
异常分为四类:中断、陷阱、故障、终止
常见信号:
正常执行:
按下ctrl+z将进程挂起,hello进程没有回收,而是运行在后台,使用ps命令可以看到。调用fg 1将其调到前台,首先打印命令行命令,然后把剩余info输出。
按下ctrl+c会导致内核发送SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业,用ps查看前台进程组没有hello进程。
程序运行时乱按键盘,会发现屏幕的输入缓存到stdin。getchar的时候读出一个’\n’结尾的字符串,其他当作命令行输入。
6.7本章小结
本章阐述了进程的概念和作用,介绍了shell的处理流程,分析了fork创建进程和调用execve函数的流程,hello的进程执行和异常处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:程序编译后出现在汇编代码中的地址,用来指定一个操作数或一条指令的地址,由段标识符加上偏移量表示。
线性地址:一个逻辑地址经过段地址机制转化后变成一个线性分页地址,线性地址可以再经过物理地址变换以产生一个新的物理分页地址。线性地址是逻辑地址和物理地址变换的中间层。
虚拟地址:即线性地址
物理地址:用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。可以直接把物理地址理解成插在机器上的内存本身,把内存看成一个从0字节一直到最大空量逐字节编号的大数组,把这个数组叫做物理地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],
1.看段选择符的T1=0还是1,判断当前要转换是GDT中的段还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组。
2.拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样基地址就知道了。
3.把Base + offset,就是要转换的线性地址了。
7.3 Hello的线性地址到物理地址的变换-页式管理
在分页的机制下地址转化管理机制主要实现了虚拟地址向虚拟地址物理页或内存地址的非线性分页转化。vm内存系统将虚拟内存的块大小分割成作为一个被我们称为基于虚拟页的内存大小固定的块用来进行处理这个固定大小的内存问题,每个称为虚拟页的内存大小可以固定为2^p个单位字节。类似的,物理块和虚拟内存被再一次细分为一个物理页,大小与每一个虚拟页的地址大小与其对应的值相等。例如:一个32位的虚拟机器,线性的地址可以最大达到4gb,用4kb为一个页来进行划分,也可以分为1m个页,通过页表处理和查找这些虚拟页的数据,方便对线性地址的大小进行管理。
之后计算机会进行一个翻译操作,把一个n元素的虚拟地址空间中的虚拟元素与一个m元素的另一个物理虚拟地址空间相互进行映射,这个翻译操作被我们称为地址翻译。
地址翻译示意图如下
虚拟地址由对应的虚拟物理页号和虚拟页偏移量共同组成,类似的,物理地址由虚拟物理页偏移号和对应的物理页偏移量共同分配组成。页表中物理地址存在三种常见的情况:未分配:没有在虚拟内存的空间中分配该条目的内存;未分配缓存:在虚拟内存的空间中已经分配了但是没有被直接缓存到对应物理地址的内存中;已分配已缓存:内存已经缓存在了对应物理地址的内存中。页表的基址寄存器在页表中可以获得条目,通过对比条目对应的有效位判断物理地址是上述哪一种的情况,如果有效则通过提取得出对应物理地址的页号寄存器,与对应的虚拟页偏移量共同分配构成了物理地址寄存器pa。
当页面命中时CPU硬件执行的步骤:
第1步:处理器产生一个虚拟地址,将它传送给地址管理单元MMU。
第2步: MMU生成PTE地址,并从高速缓存/主存请求得到它。
第3步:高速缓存或者主存向MMU返回PTE。
第4步:MMU构造物理地址,并把它传送给高速缓存/主存。
第5步:高速缓存或者主存会返回所请求的数据字给处理器。
7.4 TLB与四级页表支持下的VA到PA的变换
按照上述模式,每次CPU产生一个虚拟地址并且发送给地址管理单元,MMU必须找到一个PTE用来将虚拟地址翻译为物理地址。为了消除这种操作带来的大量时间开销,MMU中被设计了一个关于PTE的小的缓存,称为翻译后备缓冲器。例如当每次cpu发现需要重新翻译一个虚拟地址时,它就必须发送一个vpn得到虚拟地址mmu,发送一个vpo位得到一个l1高速缓存.
corei7采用四级页表层次结构,每个四级页表进程都有自己私有的页表层次结构,这种设计方法从两个基本方面减少了对内存的需求,如果一级页表的pte全部为空,那么二级页表就不会继续存在,从而为进程节省了大量的内存,而且也只有一级页表才会有需要总是在一个内存中。四级页表的层次结构操作流程如下:36位虚拟地址被寄存器划分出来组成四个9位的片,每个片被寄存器用作到一个页表的偏移量。cr3寄存器内储存了一个l1页表的一个物理起始基地址,指向第一级页表的一个起始和最终位置,这个地址是页表上下文的一部分信息。vpn1提供了到一个l1pet的偏移量,这个pte寄存器包含一个l2页表的起始基地址.vpn2提供了到一个l2pte的偏移量,一共四级,逐级以此层次类推。
7.5 三级Cache支持下的物理内存访问
L1Cashe的物理访存大致过程如下:
(1) 组选择:取出虚拟地址的组索引位,把二进制组索引转化为一个无符号整数,找到相应的组
(2) 行匹配:把虚拟地址的标记为拿去和相应的组中所有行的标记位进行比较,当虚拟地址的标记位和高速缓存行的标记位匹配时,而且高速缓存行的有效位是1,则高速缓存命中。
(3) 字选择:一旦高速缓存命中,我们就知道我们要找的字节在这个块的某个地方。因此块偏移位提供了第一个字节的偏移。把这个字节的内容取出返回给CPU。
(4)不命中:如果高速缓存不命中,那么需要从存储层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,产生冲突,则采用最近最少使用策略 LFU进行替换。
Cashe2,Cashe3原理相同。
7.6 hello进程fork时的内存映射
shell调用fork时,内核为新进程创建各种数据结构并分配唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将两个进程的两个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
在创建了一个子进程后,子程序调用execve函数在上下文加载hello程序。
(1)删除当前虚拟地址中已存在的用户区域。
(2)为新程序建立新的区域结构,这些区域结构是私有的,虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区,bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
(3)如果hello与共享对象链接,那么这些对象都被动态链接到这个程序,然后映射到用户虚拟地址空间中的共享区域。
(4)设置程序计数器,使之指向代码区域的入口点,下次调用这个进程时,从这个入口点开始执行。
7.8 缺页故障与缺页中断处理
缺页故障:当指令引用一个相应的虚拟地址,而与该地址相应的物理页面不再内存中,会触发缺页故障。通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成。
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
动态内存分配器维护者一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格。两种风格都是要求显示的释放分配块。
(1) 显式分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。
(2) 隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。
1.隐式空闲链表
这种情况下,一个块是由一个字的头部、有效载荷,以及可能的填充组成。头部编码了这个块的大小以及这个块是已分配的还是空闲的。块的头最后一位指明这个块是已分配的还是空闲的。
头部后面是应用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小任意。空闲块通过头部块的大小字段隐含的连接着,所以我们称这种结构为隐式空闲链表。
2.显示空闲链表
显示空闲链表是将空闲块组织为某种形式的显示数据结构。堆被组织为双向空闲链表,在每个空闲块中,都包含一个前驱和后继的指针。
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。
一种方法使用后进先出的顺序维护链表,将新释放的块在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并可以在常数时间内完成。
按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配有着更高的内存利用率,接近最佳适配的利用率。
7.10本章小结
本章对hello程序运行时虚拟地址的变化进行分析,解析了hello应用程序的虚拟存储地址空间,分析了虚拟地址,线性地址和虚拟物理线性地址之间的互相转换,页表的命中与不页表的命中,使用动态快表缓存作为页表的高速缓存以及如何加速页表,动态内存管理的操作,fork时的动态内存中断与映射、execve时的动态内存中断与映射、缺页的中断与缺页映射和中断的处理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
一个Linux文件就是一个m字节的序列。
所有的I/O设备都可以被模型化为文件,相应的输入输出就是对文件的读和写,这种方式允许Linux内核引出简单低级的应用接口即unix io接口
8.2 简述Unix IO接口及其函数
接口:
(1)打开文件。一个应用程序通过要求内核打开相应的文件,来访问一个 I/O 设备,内核返回一个小的非负整数作为描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
(2)Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。 (3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量。
(4)读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文件,当 k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置k开始,然后更新k。
(5)关闭文件,内核释放打开文件时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
函数:
(1)int open(char* filename,int flags,mode_t mode) ,进程通过调用 open 函数来打开一个存在的文件或是创建一个新文件。
(2)int close(fd),fd 是需要关闭的文件的描述符,close 返回操作结果。
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
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;
}
该程序按照fmt格式结合参数args生成字符串,并返回长度。
write函数:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
在printf中调用系统函数write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,
int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。
syscall函数:
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
syscall将字符串中的字节从寄存器中复制到显卡的显存中,显存中存储的是字符的ASCII码。字符显示驱动子程序通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点。于是我们的打印字符串就显示在了屏幕上。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。大概思想是读取字符串的第一个字符后返回。
8.5本章小结
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数的实现。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
首先,从文本编辑器开始我们写出hello.c文件,也是我们之前最为熟悉的一种。之后我们往往直接在软件上将其编译链接,得出程序运行的结果,但其实这个过程中还包括了许多步骤。
先要对其进行预处理,把各种库加入进来,把一些宏定义进行处理,产生hello.i,到此处才只是一个开始,接下来通过编译,hello.i又变为了hello.s文件,里面是用汇编语言书写的,经过学习我们也可以大致看懂里面的内容,和我们在c文件里的大致相近,实现的目的相同,但往往也可以看到一些不同,我们可以据此对程序进行优化。
随后要把hello.s变为我们的计算机真正看得懂的可重定位文件hello.o,通过动态链接最终得到可执行文件hello,到此为止完成了P2P。
随后我们在shell上输入命令行,shell读取我们的命令,调用fork和execve为我们的hello加载虚拟内存,分配时间片,在此期间程序通过我们的缓存结构,从内存或cache、磁盘等位置取得数据。
ctrl+z可以将其挂起,ctrl+c可以将其终止。最后,shell将其回收,删除创立的数据结构,至此hello结束了它的一生。
计算机系统这门课让我对于计算机有了系统的了解,也让我感受到了计算机的魅力,环环相扣严丝合缝,追求效率的同时也要保持正确性,希望以后可以对于计算机有更深刻的认知。
附件
列出所有的中间产物的文件名,并予以说明起作用。
名称 | 作用 |
---|---|
hello.i | hello.c预处理得到的文件 |
hello.s | hello.i编译得到的汇编文件 |
hello.o | hello.s汇编后得到的可重定位文件 |
hello | 链接后的可执行文件 |
hello.out | hello反汇编后的可重定位文件 |
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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.