计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机
学 号 120L020219
班 级 2003003
学 生 张鲁毅
指 导 教 师 史先俊
计算机科学与技术学院
2022年5月
本文介绍一个hello程序从预处理开始,经过编译、汇编、链接的一系列过程,还介绍了在Bash中运行,接受OS管理、接受I/O管理等一整个hello程序的生命周期。通过上述内容,对ICS课程所学的内容进行复习和整合。
关键词:计算机系统;P2P;编译;汇编;链接;OS;存储管理;I/O;
目 录
第1章 概述
1.1 Hello简介
P2P(From Program to Process):
Program:在IDE中输入代码,保存后的一个程序;Process: 在壳中运行时的一个进程。
过程:hello.c程序经过预处理器(cpp)预处理、编译器(ccl)编译、汇编器(as)汇编、链接器(ld)链接最终成为可执行目标程序hello。之后,在壳中输入启动命令,shell会为hello,fork一个子进程来运行,于是就完成了从程序到进程的转变。
020(From Zero-0 to Zero-0):
在壳中,OS(进程管理)调用fork函数产生子进程,再调用execve函数执行文件,之后映射虚拟内存(mmp),为运行的hello分配时间片使其在硬件上执行取指译码 / 流水线等操作。程序结束后,shell会回收hello进程,同时内核会将执行hello产生的痕迹清除。于是hello完成了从无到有,最后到无的过程。
1.2 环境与工具
X64 CPU;Intel Core i7 10875H;2.30GHz;16G RAM
Windows11 64位;Vmware Workstation Pro;Ubuntu 20.04 LTS 64位;
Visual Studio 2010 64位;CodeBlocks 64位;vi/vim/gedit+gcc
1.3 中间结果
hello.i:经过预处理后的文件;hello.s:编译后生成的汇编文件;hello.o:汇编后生成的可重定位文件;hello:链接后生成的可执行目标文件;hello.elf;hello2.elf;hello_asm.txt;hello_asm2.txt。
1.4 本章小结
阅读《Hello的自白》,对其相关内容进行解释说明,简述其所描述的P2P、020,同时介绍本机进行调试的软硬件信息。
第2章 预处理
2.1 预处理的概念与作用
概念:
预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如 hello.c中第1行的#include <stdio.h>命令告诉预处理器读取系统头文件 stdio.h的内容,并把它直接插人程序文本中。结果就得到了另一个C程序,通常 是以.i作为文件扩展名。
作用:
扩展源代码;宏展开(将宏名替换为文本);引入头文件内容;条件编译。
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
打开hello.i文件,一共有3060行,为可阅读的C语言文本文件。可以看出cpp对源文件中注释进行了清除,将系统头文件中的内容直接插入到了程序文本中,同时对头文件中的宏定义以及条件编译语句进行相应的宏替换和条件编译处理。
2.4 本章小结
本章的主要内容是有关预处理的概念以及作用、在Ubuntu下预处理的命令以及对hello.c文件的预处理结果hello.i文本的文件解析。用实例清晰地解释了了的预处理的作用。
第3章 编译
3.1 编译的概念与作用
概念:
编译器(ccl)将预处理文件进行词法分析、语法分析、语义分析、优化后翻译成文本文件hello.s,它包含一个汇编语言程序。
作用:
对代码的正确性进行检查;将高级语言程序转化为汇编语言,为汇编工作做好准备;对代码进行优化。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1汇编指令
.file:声明源文件
.text:代码段
.section:内存段
.rodata:只读代码段
.align:数据或者指令的地址对齐方式
.string:声明一个字符串(.LC0,.LC1)
.global:声明全局变量(main)
.type:声明一个符号是数据类型还是函数类型
3.3.2数据
- 常量
- 变量
i、argc、argv[]为变量。
argc一开始储存在%edi中,后来转移到了-20(%rbp)中。
i储存在-4(%rbp)中
3.3.3赋值
对i赋初值为0,
3.3.4类型转换
3.3.5算术操作
将i加1;
3.3.6关系操作
将argc与4进行比较
将i与7进行比较
3.3.7数组、指针
图中34行是对指针的使用,35、38是对argv数组的使用。
3.3.8控制转移
- if:
将argc与4进行了比较,不相等的话就输出,并退出;相等的话就跳入分支L2。
- for:
L2将i赋初值为0,L3判断i与7的大小,L4是循环体的内部(包括将i加1的部分)。
3.3.9函数操作
调用printf
调用exit
调用atoi
调用sleep
调用getchar
3.4 本章小结
本章主要介绍了编译的概念与作用,在Ubuntu下进行编译的指令,并对hello.s文件按照作业要求,对于其内容的以下方面:汇编指令、数据(常量、变量)、赋值、类型转换、算术操作、关系操作、数组(指针)、控制转移、函数操作,进行了解释与说明。用实例清晰地解释了编译这一过程。
第4章 汇编
4.1 汇编的概念与作用
概念:
汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它包含的字节是函数main 的指令编码。
作用:将汇编指令转变为机器可以直接识别的机器指令(二进制)。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
使用readelf生成elf格式的文件:(命令:readelf -a hello.o > hello.elf)
4.3.1ELF头
查看ELF头,其中包含系统的字的大小和字节顺序、目标文件的类型、机器类型等信息。
图中信息为:大小为64字节、小端存储、文件是重定位的类型等。
4.3.2节头
节头描述了所有节的基本信息,包括以下节:
.text节:程序的机器代码。
.rela.text节:一个.text节中的列表。
.data节:已初始化的静态和全局C变量。旗标为WA(可写可分配)。
.bss节:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。旗标为WA(可写可分配)。
.rodata节:存放只读数据。旗标为A(可分配)。
.comment节:包含版本控制信息。
.note.GNU_stack节:用来标记executable stack(可执行堆栈)。
.eh_frame节:处理异常。
.rela.eh_frame节:.eh_frame的重定位信息。
.symtab节:一个符号表,存放在程序中定义和引用的函数和全局变量的信息。
.strtab节:一个字符串表,内容包括.symtab和.debug节中的符号表,以及节头部的节名字。
.shstrtab节:该区域包含节区名称。
4.3.3重定位节
在链接时,需要通过重定位节对这些位置的地址进行修改。ld会通过重定位条目的类型判断应该使用什么样的方法计算正确的地址值,通过偏移量等信息计算出正确的地址
本程序需要重定位的信息有:.rodata中的串、puts、exit、printf、atoi、sleep、getchar等外部符号。
4.3.4符号表
符号表中包含在程序中的符号定义和引用的信息。
4.4 Hello.o的结果解析
使用命令:objdump -d -r hello.o > hello_asm.txt 获得反汇编文件。
部分内容如下:
对比:
- 数的进制
反汇编文件中是16进制:
hello.s中是10进制:
(2)分支转移
反汇编文件中使用地址进行跳转:
而hello.s文件中使用名称跳转:
(3)函数调用
反汇编文件中也是使用地址调用:
hello.s文件则是声明调用函数的名称:
4.5 本章小结
本章主要对汇编进行了介绍,包括汇编的概念与作用,Ubuntu下使用汇编的命令。之后介绍了可重定位目标elf格式,并对其进行了分析。最后对hello.o文件进行反汇编,并将反汇编代码与hello.s文件进行了对比。在实例中介绍了汇编、elf、反汇编。
第5章 链接
5.1 链接的概念与作用
概念:
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行,链接可以执行于编译时、加载时、运行时。链接执行符号解析、重定位过程。
作用:
当程序调用函数库(标准库)中的一个函数时,该函数存在于一个名为xxx.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生成elf格式的文件:(命令:readelf -a hello > hello2.elf)
5.3.1ELF头
5.3.2节头
5.3.3程序头
5.3.4符号表
5.3.5重定位
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
.text段:
由上图的elf文件可知,.test段从0x4010f0开始,偏移量为0x10f0,大小为0x145,对齐要求为16。可得在edb中的对应部分:
5.5 链接的重定位过程分析
使用命令:objdump -d -r hello.o > hello_asm2.txt获得反汇编文件。
部分内容如下:
将其与前一个反汇编文件hello_asm.txt对比可以发现:
- hello的反汇编中比hello.o多了许多文件节。如.init节和.plt节等
- hello的反汇编中比hello.o多了许多外部链接的库函数,如:puts@plt、printf@plt、getchar@plt等。
结合hello.o的重定位项目,分析得hello中的重定位过程:
重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。之后链接器把运行时的内存地址赋给新的合并节、输入模块定义的每个节以及输入模块定义的每个符号。完成后,程序中每条指令和全局变量都有唯一运行时的地址。重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。
5.6 hello的执行流程
子程序名称 子程序地址
ld-2.33.so!_dl_start 0x7ffff7fcdd70
ld-2.33.so!_dl_init 0x7ffff7fdc060
hello!puts@plt 0x401030
hello!printf@plt 0x401040
hello!strtol@plt 0x401050
hello!sleep@plt 0x401070
hello!getc@plt 0x401080
hello!_start 0x401090
hello!main 0x401172
5.7 Hello的动态链接分析
在调用一个共享库(如标准库)函数时,编译器无法得知函数运行时的地址,因此需要为其添加重定位标记,并等待动态链接器处理。链接器会采用延迟绑定的策略避免运行时修改调用模块的代码段。动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT 中存放函数目标地址,PLT使用 GOT中地址跳转到目标函数。
通过之前的elf文件找到.got.plt信息如图:
用edb运行hello,调用dl_init之前:
之后:
5.8 本章小结
本章主要对链接进行了介绍,内容包括链接的概念及作用,以及Ubuntu下的命令,之后介绍了hello的elf格式。再之后介绍了hello的虚拟地址空间。之后将hello的asm文件与hello.o的asm文件进行了对比。然后分析了重定位过程、列举了hello的执行过程中的子程序。在最后进行了hello的动态链接分析。通过一个个例子加深了对链接这一过程的理解。
第6章 hello进程管理
6.1 进程的概念与作用
概念:
狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。
作用:
进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
进程提供给应用程序两个关键抽象:一个独立的逻辑控制流,一个私有的地址空间。
6.2 简述壳Shell-bash的作用与处理流程
作用:
shell 是一种命令行解释器, 其读取用户输入的字符串命令, 解释并且执行命令. 它是一种特殊的应用程序, 介于系统调用/库与应用程序之间, 其提供了运行其他程序的的接口.它可以是交互式的, 即读取用户输入的字符串;也可以是非交互式的, 即读取脚本文件并解释执行, 直至文件结束.
处理流程:
6.3 Hello的fork进程创建过程
hello在shell运行后,父进程通过调用fork函数(原型为pid_t fork(void);)创建一个新的运行的子进程。其调用一次返回两次,子进程返回0,父进程返回子进程的ID。
子进程会获得与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时。子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程最大的区别在于他们有不同的ID。父进程与子进程是并发运行的独立进程。内核能够以任何方式交替执行他们逻辑控制流中的指令。
6.4 Hello的execve过程
首先hello子进程通过execve系统调用启动加载器。之后,加载器删除子进程所有的虚拟地址段,并创建一组新的代码、数据、堆段。新的栈和堆段被初始化为0。然后通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk),新的代码和数据段被初始化为可执行文件中的内容。最后加载器跳到_start地址,它最终调用hello的main 函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制,直到CPU引用一个被映射的虚拟页时才会进行复制。此时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
上下文信息:
上下文是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
进程时间片:
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
多任务:
也被称为多时间片。
调度:
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被强占的进程。这种决策就叫调度(是由内核中的调度器的代码处理的)。当内核调度一个新的进程的运行的时,内核就会抢占当前进程,通过使用一种上下文切换的较为高层的形式异常控制流将控制转移到新的进程。具体如下:内核首先保存当前进程的上下文,之后恢复之前被抢占的进程保存的上下文,将控制传递给这个恢复的进程。
用户态核心态转换:
进程hello初始运行在用户模式中,直到它通过执行系统调用函数sleep或者exit时便陷入到内核。内核中的处理程序完成对系统函数的调用。之后,执行上下文切换,将控制返回给进程hello系统调用之后的那条语句。
6.6 hello的异常与信号处理
可能的异常:
中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。
陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
故障:在执行hello程序的时候,可能会发生缺页故障。
终止:终止时不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。
可能产生的信号:
SIGINT、SIGKILL、SIGSEGV、SIALARM、SIGCHLD
按Ctrl-C: 在键盘上输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业。
按Ctrl-Z,之后运行ps: 输入ctrl-z,内核会发送SIGSTP。SIGSTP默认挂起前台hello作业,但 hello进程并没有回收,而是运行在后台下,通过ps指令可以对其进行查看。
Ctrl-z后运行jobs:
Ctrl-z后运行pstree:打印所有进程的关系
Ctrl-z后运行fg:fg命令使第一个后台作业变为前台,而第一个后台作业是hello,所以输入fg 后hello程序又在前台开始运行,并且是继续刚才的进程,输出剩下的信息。
Ctrl-z后运行kill:
乱按键盘(包括回车):输入的字符与程序本来的输出会连接在一起。等读取到输入的回车时,会把回车前面的字符串当做shell指令去解析,而shell是没有这些命令的,因此会出现command not found。
6.7本章小结
本章主要介绍了进程的相关知识,首先是进程的概念与作用,之后是shell的作用和处理流程,然后是hello程序的进程创建(fork)、加载(execve)、执行。最后是各种异常与信号处理的相关内容。通过具体的例子,详细的介绍了进程相关的知识。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:
在有地址变换功能的计算机中,访问指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的物理地址。
线性地址:
是逻辑地址到物理地址变换之间的中间层。
虚拟地址:
CPU启动保护模式后,程序运行在虚拟地址空间中。注意,并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,Bootloader以及内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。(百度百科)
物理地址:
在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由两部分组成:段标识符 + 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号,剩下3位包含硬件信息。其中索引号可以直接理解成数组下标,它对应的数组就是段描述符表,段描述符具体描述了一个段地址,这样,很多段描述符就组成段描述符表。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
给定一个完整的逻辑地址[段选择符:段内偏移地址],看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小,由此得到一个数组。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,由此可以得知基地址,把基地址 + 偏移量,就可以得到要转换的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址被分以固定长度为单位的组,称为页(page)。例如一个32位的机器,线性地址最大可以为4G,用4KB来划分的话整个地址就被划分为2^20个页,这个数组称为页目录,目录中的每个目录项,就是对应页的地址;另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。
依据以下步骤进行转换:
1、从CR3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
2、根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
3、根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
4、将页的起始地址与线性地址中最后12位相加,得到最终我们想要的地址;
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是MMU中的一个关于PTE的小的缓存,有了TLB后,VPN又分为了TLB标记(TLBT)和TLB索引(TLBI),TLB的机制与全相联的cache的机制相同,如果TLB有T = 2s个组,那么TLB索引(TLBI)是由VPN的s个最低位组成的,TLB标记(TLBT)是由VPN中剩余的位组成。
引入多级页表后,VPN被划分成了多个区域,例如使用k级页表,VPN被划分成了k个VPN,每个VPNi都是一个到第i级页表的索引,第k个VPN中存储着VPN对应的PPN。
CPU产生一个VA,MMU在根据VPN在TLB中搜索PTE,若命中,MMU取出相应的PTE,根据PTE将VA翻译成PA;若没命中,则通过多级页表查询PTE是否在页中,若在页中,找到对应的PIE,MMU将VA翻译成PA,若没有在页中,则进行缺页处理
7.5 三级Cache支持下的物理内存访问
接收到虚拟地址之后,先在TLB中寻找。若TLB中没有,则可以结合多级页表得到它的物理地址,之后到Cache里面寻找。若在TLB中,则直接被MMU得到。在Cache中,命中就返回,不命中就依次在L1、L2、L3中不断寻找。
7.6 hello进程fork时的内存映射
shell在调用fork函数时,会创建一个基本与父进程相同的子进程,并为子进程分配一个独立的PID。在这个过程中,内核创建了hello进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。同时,写时复制机制也会在其中一个进程写操作时创建新的页面,由此也就有了私有地址的概念。
7.7 hello进程execve时的内存映射
execve 函数在shell中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。
①首先删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。②然后映射私有区域,为新程序的代码、数据、bss、和栈区域创建新的区域,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data、.bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。③之后映射共享区域,hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。④最后设置程序计数器(PC),execve做的最后一件事就是设置当前进程上下文的PC,使其指向代码区的入口。
7.8 缺页故障与缺页中断处理
缺页:虚拟内存中的字不在物理内存中(BRAM缓存不命中),通俗地讲就是内存中没有这个页,所以导致MMU找不到对应的物理地址。缺页故障是一种故障,当指令引用一个虚拟地址,MMU会查找页表,当找不到对应的物理地址时,就会触发缺页中断。
当MMU触发缺页中断,缺页异常处理程序在内存中选择一个牺牲页,从其他存储器(磁盘)中读入这个页替换牺牲页,接着导致缺页的指令重新启动,页面命中。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
管理:
(1)隐式空闲链表管理
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。
在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。
(2)显式空间链表管理
显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。
7.10本章小结
本章的主要内容是关于计算机的存储,其中包括地址空间的分类、两种地址的变换规则、VA到PA的转换、cache的访问、内存映射相关知识、缺页故障及处理以及动态内存的分配。通过图文结合,详细的解释了与存储相关的内容。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件。
设备管理:unix io接口。
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而文件就是一个字节序列。所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。
8.2 简述Unix IO接口及其函数
Unix I/O接口:
打开文件:
一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息,应用程序只需要记住这个描述符。
进程开始时都有三个打开的文件:
标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
改变当前的文件位置:
对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
读写文件:
一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号” 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。
关闭文件:
当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源.
函数:
open()函数,功能描述:用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。
函数原型:int open(const char *pathname,int flags,int perms)
参数:pathname:被打开的文件名(可包括路径名如"dev/ttyS0")flags:文件打开方式,
返回值:成功:返回文件描述符;失败:返回-1。
close()函数,功能描述:用于关闭一个被打开的的文件。
函数原型:int close(int fd)
参数:fd文件描述符
函数返回值:0成功,-1出错。
read()函数,功能描述: 从文件读取数据。
函数原型:ssize_t read(int fd, void *buf, size_t count);
参数:fd:将要读取数据的文件描述词。buf:指缓冲区,即读取的数据会被放到这个缓冲区中去。count: 表示调用一次read操作,应该读多少数量的字符。
返回值:返回所读取的字节数;0(读到EOF);-1(出错)。
write()函数,功能描述: 向文件写入数据。
函数原型:ssize_t write(int fd, void *buf, size_t count);
返回值:写入文件的字节数(成功);-1(出错)。
lseek()函数,功能描述: 用于在指定的文件描述符中将将文件指针定位到相应位置。
函数原型:off_t lseek(int fd, off_t offset,int whence);
参数:fd;文件描述符。offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移)
返回值:成功:返回当前位移;失败:返回-1。
8.3 printf的实现分析
printf函数:
printf接收了一个fmt,然后将其送入vsprintf函数输出。(vsprintf如下)
vsprintf接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar函数:
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
用户输入命令后,getchar从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。
异步异常-键盘中断的处理:
键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
8.5本章小结
本章主内容是文件I/O的操作,首先说明了I/O设备管理方法以及一些I/O函数,之后分析了printf函数和getchar函数的源码。通过对源码的分析,更深入理解I/O操作。
结论
hello所经历的过程:
预处理器cpp将c文件处理为i文件。
编辑器将i文件编译成汇编指令的s文件。
汇编器将汇编语言翻译成二进制机器语言,生成二进制可重定位目标程序,o文件。
链接器将所引用的目标文件符号解析,重定位后完全链接成可执行的目标文件hello。
在shell中输入命令行指令,运行hello程序。
对子进程用命令行分析的参数execve加载,mmap映射虚拟内存。改变PC到_start,最后开始执行main函数
hello运行时printf函数及getchar函数等,这些函数与linux I/O的设备模拟化密切相关
return或exit后,hello子进程终止被shell 父进程回收,内核 回收为其创建的所有信息。
感悟:
一个简单的hello.c程序背后却有着非常复杂又精巧的实现过程,这令我深深被计算机系统所吸引,希望能在以后的学习中,更加详细的了解计算机系统的相关内容。
附件
hello.c:源程序文件
hello.i:预处理后的文本文件
hello.s:编译后汇编程序文本文件
hello.o:汇编后的可重定位目标程序(二进制文件)
hello:链接后的可执行目标文件
hello.elf:ELF格式下的hello.o
hello_asm.txt:hello.o的反汇编文件
hello2.elf.:ELF格式下的hello
hello_asm2.txt:hello的反汇编文件
参考文献
- Randal E. Bryant, David R. O'Hallaon. 深入理解计算机系统. 第三版. 北京市:机械工业出版社[M]. 2018: 1-737
- 重定位节 - 链接程序和库指南
- 博客园:printf函数实现的深入剖析
- 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.