程序人生-Hello’s P2P
Posted 鹹鱻鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了程序人生-Hello’s P2P相关的知识,希望对你有一定的参考价值。
程序人生-Hello’s P2P
本文旨在介绍linux环境下hello程序从预处理到编译再到链接,最后执行的整个过程,以及进程管理,存储管理及IO管理的实现。一个hello程序的一生非常简单,而深入了解之下又很不平凡。hello跌宕起伏的一生,波澜壮阔的一生,蕴含了丰富的计算机概念和知识,包罗万象,将一个小小的程序剖析之下便是整个计算机世界。
关键词:预处理;编译;链接;进程管理;存储管理;IO管理
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 5 -
5.3 可执行目标文件hello的格式....................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 10 -
6.3 Hello的fork进程创建过程........................................................................ - 10 -
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 -
8.2 简述Unix IO接口及其函数.......................................................................... - 13 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P是From Program to Process的简写,hello.c从一个.c文件,经过预处理,编译,汇编,链接可以得到可执行文件hello,通过命令行shell输入./hello,shell将fork子进程,再通过execve加载进程,实现了由程序到进程的转化。
020是From Zero-0 to Zero-0的简写,操作系统中命令行shell调用execve加载运行hello程序,而后映射到虚拟内存,进入程序入口后载入物理内存,再进入main函数执行hello目标代码,至代码完成后,父进程回收hello进程。
1.2 环境与工具
硬件环境
Intel i5-1035G1 CPU
16.00 GB RAM
512 GB Disk
软件环境
Windows11家庭中文版x64
Ubuntu 20.03 LTS x86_64 in VMWare Workstation Pro 16
开发工具
Visual Studio 2022
CodeBlocks 64位
vi/vim/gedit+gcc edb gedit
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.i:hello.c经预处理生成的文本文件。
hello.s:hello.i经编译生成的汇编语言文件。
hello.o:hello.s经汇编生成的可重定位目标文件。
hello.out:hello.o经链接生成的可执行目标文件。
1.4 本章小结
本章介绍了程序研究的环境,工具,简述了Hello的P2P,020的整个过程。
第2章 预处理
2.1 预处理的概念与作用
概念
ISO C和ISO C++都规定程序由源代码被翻译分为若干有序的阶段,通常前几个阶段由预处理器实现。预处理中会展开以#起始的行,试图解释为预处理指令(preprocessing directive),其中ISO C/C++要求支持的包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。
作用
1)将源文件中以”include”格式包含的文件复制到编译的源文件中。
2)用实际值替换用“#define”定义的字符串。
3)根据“#if”后面的条件决定需要编译的代码
2.2在Ubuntu下预处理的命令
cpp hello.c > hello.i
2.3 Hello的预处理结果解析
预处理之后hello.c文件转化为hello.i文件,阅读hello.i,可以发现hello由原来的23行(.c)变为3060行(.i)。此外对于源文件保持了main函数部分不变,头文件部分被展开,宏定义也被进行宏替换和宏展开。
2.4 本章小结
本章主要介绍了预处理的概念与作用,给出了ubuntu的预处理命令,并对hello.c预处理之后的hello.i文件进行了解析。
第3章 编译
3.1 编译的概念与作用
概念
将程序员所撰写的编程语言翻译成汇编语言的过程:编译器将文本文件 hello.i 翻译成文本文件 hello.s。输入为高级程序设计语言书写的源程序,输出为汇编语言或机器语言表示的目标程序。
作用
程序员用高级语言编写的源程序,必须由相应的编译程序将其翻译成目标程序后,再由连接程序将必要的其他模块进行连接,从而形成可执行程序,在计算机上直接执行。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1数据
1)常量
程序printf中使用了2个字符串常量。
2)局部变量
对局部变量i的第一次使用是在for循环赋初值0,对应的汇编语句用movl赋值0,即31行。
3)全局变量
main()函数的函数名被定义为强符号,全局变量
3.3.2 赋值
源程序中for循环把i初始化为0,循环体是i++。i是保存在栈中的局部变量,直接用mov语句对i进行赋值(movl)。
3.3.3 算数操作
i++,用add实现。
3.3.4关系操作
Cpu中关系操作主要依靠cmp及test系列指令,cmp指令根据两个操作数之差来设置条件码。一般在此类指令之后都会跟着检查条件码的操作,依此实现操作数的关系操作。
即为cmpl,je 和cmpl,jle
3.3.5 数组/指针/结构操作
数组指针结构的位置是连续的存储空间,通过起始位置和偏移位置就能够定位到目标位置。
hello中使用了argv指针数组,每个指针占用8位。argv[]先是被存在用户栈中,再使用基址加偏移量寻址访问字符串数组中的内容。
3.3.6控制转移
控制转移有两种形式,一种是条件转移。另一种是比较转移指令。C语言中的语句需要有条件的执行,因此我们应该根据数据测试的结果来跳转。一般来说关系操作会和控制转移相关联。
Hello中使用了if和for控制
3.3.7函数操作
函数操作涉及到参数传递(地址/值)、函数调用()、函数返回 return等。在汇编代码中涉及到函数的调用与返回的指令是call与ret,函数的调用一般与运行时栈联系紧密。
hello.c中涉及的函数操作
main(int argc,char*argv[])函数:主函数,第一个参数表示命令行输入了几个字符串,第二个参数的每一个指针各自指向了每一个字符串。
exit()函数:退出函数。与return功能上大致相同。不过要设置%edi值为1。
printf()函数:打印函数,打印相应字符串.两次printf函数在被调用前都各自将LC0与LC1的首地址传入%rdi中。
getchar()函数:输入函数,输入字符(串)。并将输入的字符串转换为ASCII码。若出错则返回为-1.
sleep()函数:将%edi设置为sleepsecs的值.,通过call指令调用sleep函数,期间,若信号中断,则返回剩余时间,否则,返回0.
3.4 本章小结
本章介绍了有关编译的概念以及作用,使用gcc -S hello.c -o hello.s命令生成编译后的文件,并且还分析了各种数据与操作是如何被处理编译的,给出了编译的解析。
第4章 汇编
4.1 汇编的概念与作用
概念
汇编器(as)将汇编语言书写的程序翻译成机器语言程序,并把这些指令打包成可重定位目标程序的格式,将结果保存在.o目标文件中,.o文件是一个二进制文件,它包含程序的指令编码,且是一个可重定位文件。
作用
汇编语言的诞生是由于机器代码难以记忆,所以用助记符代替操作码形成的方便记忆的语言,这种机器不能直接识别。用程序将其翻译为机器语言后,才可以被识别运行。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
先使用readelf命令查看hello.o的ELF格式,并将其重定位为文本文件。
指令:readelf -a hello.o > helloelf.txt
ELF⽂件主要包含以下节
1)ELF头
ELF头位于ELF⽂件开始,包含⽂件结构说明信息。分32位系统对应结构和64位系统对应结构(32位版本、64位版本)。包括16字节标识信息、⽂件类型 (.o,exec, .so)、机器类型(如 IA-32)、节头表的偏移、节头表的表项⼤⼩以及表项个数。
2).text 节
编译后的代码部分
3).rodata 节
只读数据,如 printf 格式串、switch跳转表等
4).data 节
已初始化的全局变量
5).bss 节
未初始化全局变量,仅是占位符,不占据任何实际磁盘空间。区分初始化和⾮初始化是为了空间效率
6).symtab 节
存放函数和全局变量 (符号表)信息 ,它不包括局部变量
7).rel.text 节
.text节的重定位信息,⽤于重新修改代码段的指令中的地址信息
8).rel.data 节
.data节的重定位信息,用于对被模块使⽤或定义的全局变量进⾏重定位的信息
9).debug 节
调试⽤符号表 (gcc -g)
10)strtab 节
包含symtab和debug节中符号及节名Section header table(节头表)每个节的节名、偏移和⼤⼩
11)Section header table(节头表)
每个节的节名、偏移和⼤⼩
4.4 Hello.o的结果解析
objdump -d -r hello.o得到反汇编代码,分析hello.o的反汇编,与第3章的 hello.s进行对照分析。
1)分支转移部分:hello.s中对跳转指令使用.Lx这样的名称,而hello.o中的反汇编中的跳转是直接使用确定的地址。
2)函数调用:hello.s中的函数调用只写了函数名称,hello.o的反汇编中则是使用了当前指令的下一个字节(即下一条指令的地址)。
3)汇编中mov、push、sub等指令都有表示操作数大小的后缀,比如l\\q等, 反汇编得到的代码中则不全有。
4)汇编代码中有很多“.”开头的伪指令用来指导汇编器和链接器工作,而反汇编得到的代码中则没有。
4.5 本章小结
本章通过使用汇编器,将.s文件汇编成二进制的机器指令,得到重定位目标文件。此外我们还通过readelf查看了文件头、节头段、重定位段、符号表等信息并了解了其含义。最后我们利用objdump进行反汇编,比较反汇编代码与编译得到的文件,并分析了他们的差异。
第5章 链接
5.1 链接的概念与作用
概念
链接是将相关的代码函数和数据组合成为一个完整文件的过程,这个文件可直接加载到内存执行。
作用
使分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而实可以分解为更小的、更好管理的模块,可以独立地修改和编译单一模块。
注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在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/9/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello.out
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
相比于可重定位目标文件hello.o,可执行目标文件hello的类型从REL(可重定位文件)转变为了EXEC (可执行文件)。ELF头描述hello文件的总体格式。它还包括程序的入口点(entry point)(非0),也就是当程序运行时要执行的第一条指令的地址。
5.4 hello的虚拟地址空间
使用edb加载hello.out,查看本进程的虚拟地址空间各段信息,并与5.3对照分析。
可以发现两者具有相同点。
5.5 链接的重定位过程分析
objdump -d -r hello.out> hello.txt
分析hello与hello.o的不同。
1)增加了一些外部函数。
2)增加了包括.init, .plt, .fini在内的一些节。
3)相对偏移地址变为了虚拟内存的地址。
链接时,链接器通过符号表和节头了解到.data和.text在每个文件中的偏移和大小,进行合并,然后为新的合并出来的数据和代码节分配内存,并映射虚拟内存地址。最后修改对各种符号的引用,完成重定位。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
ld-2.27.so!_dl_init
LinkAddress!_start
ld-2.27.so!_libc_start_main
ld-2.27.so!_cxa_atexit
LinkAddress!_libc_csu.init
ld-2.27.so!_setjmp
LinkAddress!main
ld-2.27.so!exit
5.7 Hello的动态链接分析
动态链接是为了减少共享库函数的代码频繁出现在各个程序中从而占用宝贵而昂贵的内存空间从而提出的一个有效的方法,共享库是一个.so目标模块(elf文件)在加载时由动态链接器程序加载到内存的任意位置并和一个内存中的程序(如当前的可执行文件)动态完全连接为一个可执行程序。
动态链接的步骤和实现:基本分成三步:先是启动动态链接器本身。然后装载所有需要的共享对象,最后是重定为位和初始化。
1)首先是启动动态链接器本身,这就是动态链接器自举。动态链接器的入口地址就是字句代码的入口,当操作系统将控制权给动态链接器后,自举代码开始执行,首先访问自己的GOT段。然后找到dynamic段, 找到自身的符号表,重定位表等从而得到动态链接器本身的重定为入口,先将他们重定位。从这一步开始时用自己的全局变量和静态变量。实际上在动态链接器在自举的过程中,除了不可以使用全局变量和静态变量之外甚至不能调用函数,时用PIC 模式编译的共享对象,对于模块内部函数调用和外部函数调用的方式是一样的,时用GOT/PLT方式,所以在GOT/PLT没有被重定位之前,自举代码不可以使用任何全局变量。
2)装载共享对象。完成基本的自举以后,动态链接器将可执行文件和链接器本身的符号都合并到一个符号表当中,我们可以称之为全局变量。如果将这些装载的过程看作是一个图的遍历过程,使用的算法一般都是广度优先。当一个符号表需要被加入全局符号表时,如果相同的符号已经存在,则后加入的符号被忽略。
3)重定位和初始化:首先进行GOT/PLT重点定位完成后,如果某个段有init 段,那么动态链接器就会执行init段中的代码,用以实现共享对象的初始化过程,比如C++的全局变量和静态对象,就需要通过init 来初始化,相应的可能还有 finit 段,当进程执行完成后就会执行实现C++全局对象析构之类的操作。
通过edb调试,分析可以发现在dl_init前后这些项目的内容发生变化
5.8 本章小结
本章分析了链接过程中对程序的处理。Linux系统使用可执行可链接格式,即ELF,具有ELF头,.text,.rodata等节,经过链接,ELF可重定位的目标文件变成可执行的目标文件。此外还对于hello的虚拟地址空间、重定位过程、执行过程以及动态链接过程进行了分析。
第6章 hello进程管理
6.1 进程的概念与作用
概念
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
作用
一个程序在系统上运行时,操作系统会提供一种,程序在独占这个系 统,包括处理器,主存,I/O设备的假象。处理器看上去在不间断地一条一条执行程序中的指令…这些假象都是通过进程的概念实现的。
6.2 简述壳Shell-bash的作用与处理流程
作用
接收用户的操作(点击图标、输入命令),并进行简单的处理,然后再传递给内核,内核和用户之间就多了一层“中间代理”,Shell 其实就是一种脚本语言,也是一个可以用来连接内核和用户的软件,我们编写完源码后不用编译,直接运行源码即可。
处理流程
1) 终端进程读取用户由键盘输入的命令行。
2) 分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量。
3) 检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令。
4) 如果不是内部命令,则调用fork()创建新进程/子进程。
5) 在子进程中,用步骤2获取的参数调用execve()执行指定程序。
6) 如果用户没要求后台运行(命令末尾没有&号),则shell使用waitpid(或wait...)等待作业终止后返回。
7) 如果用户要求后台运行(如果命令末尾有&号),那么shell返回并继续等待 用户的下一次输入。
6.3 Hello的fork进程创建过程
创建过程:
(1)给新进程分配一个标识符
(2)在内核中分配一个PCB,将其挂在PCB表上
(3)复制它的父进程的环境(PCB中大部分的内容)
(4)为其分配资源(程序、数据、栈等)
(5)复制父进程地址空间里的内容(代码共享,数据写时拷贝)
(6)将进程置成就绪状态,并将其放入就绪队列,等待CPU调度。
6.4 Hello的execve过程
execve(执行文件)在父进程中fork一个子进程,在子进程中调用exec函数启动新的程序。exec函数一共有六个,其中execve为内核级系统调用,其他(execl,execle,execlp,execv,execvp)都是调用execve的库函数。
当对hello进行execve时,由于在shell中输入命令行参数并构造了argv与envp向量。所以execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。有几个步骤:删除已存在的用户区域、映射私有区域、映射共享区域、设置程序计数器。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
1)当前进程在用户态用汇编指令 int <中断向量号> 发出中断请求,这个中断请求里面有中断向量。在系统内存中存放着“中断向量表”,这个中断向量表里面存放着各种中断描述符,中断描述符里存放着中断服务函数的入口地址,以及他所需要的特权级别,内核态的特权级为0,用户态为3。
2)CPU 在总线上捕捉到中断向量 ,通过寄存器 IDTR (里面保存了中断向量表的首地址和长度)找到中断向量表,然后找到中断向量对应的中断描述符,进行权限的验证。
3)如果权限符合(中断描述符里面一般有四类中断服务函数是用户态可以调用的:断点、溢出、边界检查、系统调用),接下来用户栈切换到内核栈,因为这两个栈里面保存的代码和数据都是不同的,切换的关键在于栈地址的切换,而用户栈和内核栈的栈地址都保存在了”任务状态段“TSS中。从用户栈切换到内核栈之后,就完成了从用户态到核心态的切换(之后可能就要去执行中断服务函数了)。
6.6 hello的异常与信号处理
hello执行过程中会出现的异常有
1)中断:外部IO设备导致的。
2)陷阱:使用了系统调用,比如sleep,read
3)故障:例如缺页故障
4)中止:由于一些硬件错误,比如位损坏时发生的奇偶错误。
在发生异常时会产生信号,典型的有:
-SIGINT,ID为2,默认行为终止,来自键盘的中断;
-SIGKILL,ID为9,默认行为终止,用于杀死程序
-SIGSEGV,ID为11,默认行为终止,由于无效的内存引用,即段故障;
-SIGALRM,ID为14,默认行为终止,来自于alarm函数的定时器信号;
-SIGCHLD,ID为17,默认行为忽略,来自于子进程的停止或终止。
处理方法
- 运行中乱按:不影响程序的执行。
- 按到回车后,getchar读入回车符, 然后其后的字符串当做shell的命令行输入。
- 运行中按ctrl+c:会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业。
- 运行中按下ctrl+z后运行jobs/ps命令:父进程(shell)会收到SIGTSTP 信号,然后挂起hello进程,jobs命令显示系统中的任务列表及其运行状态。ps命令显示当前进程的状态。
- 运行中按下ctrl+z后运行fg命令:将hello进程发送到前台。
- 运行中按下ctrl+z后运行相应kill命令:杀死(终止)hello进程。
- 运行中按下ctrl+z后运行相应pstree命令
6.7本章小结
本章介绍了进程的概念与作用,简述了壳(Shell-bash)的作用与处理流程。此外还讲了通过hello的fork进程创建过程和execve过程,进程调度的过程和用户态与核心态转换等等。最后又解决了hello进程执行期间可能遇到的异常情况以及处理方法。
第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
物理地址,CPU地址总线传来的地址,由硬件电路控制(现在这些硬件是可编程的了)其具体含义。物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上(如显存、Bios等)。在没有使用虚拟存储器的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储器被读写;而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到存储器管理单元MMU,把虚拟地址映射为物理地址。
线性地址也叫虚拟地址,是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。是一个32位无符号整数,可以用来表示高达4GB的地址,也就是,高达4294967296个内存单元。线性地址通常用十六进制数字表示,值得范围从0x00000000到0xfffffff)程序代码会产生逻辑地址,通过逻辑地址变换就可以生成一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址。如果没有启用分页机制,那么线性地址直接就是物理地址。
逻辑地址是在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址,也就是是机器语言指令中,用来指定一个操作数或是一条指令的地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址即物理地址。一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是个索引号,后面3位包含一些硬件细节 。
7.2 Intel逻辑地址到线性地址的变换-段式管理
分段机制将逻辑地址转化为线性地址的步骤:
1)使用段选择符中的偏移值(段索引)在GDT或LDT表中定位相应的段描述符.(仅当一个新的段选择符加载到段寄存器中是才需要这一步)
2)利用段选择符检验段的访问权限和范围,以确保该段可访问。
3)把段描述符中取到的段基地址加到偏移量(也就是上述汇编语言汇中直接出现的操作地址)上,最后形成一个线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
7.5 三级Cache支持下的物理内存访问
三级Cache原理如下:
1)首先获取物理地址VA,使用物理地址的CI进行组索引(8路组相联),对8 路的块分别与缓存标记CT进行标志位匹配。若匹配成功且块的valid标志 位为1则命中。然后根据块偏移CO取出数据并返回。
2)若未到相匹配的块或者标志位为0,则miss。一级cache向下逐级cache, 即二级cache甚至是三级cache中寻找查询数据。然后向上逐级写入cache。
3)在更新cache的时候,需要判断是否有空闲块。如果有空闲块(即有效位 为0)则写入;如果不存在则驱逐一个块(LRU策略,Least Recently Used, 即最近最少使用)。
7.6 hello进程fork时的内存映射
fork函数可以创建一个带有自己独立虚拟地址空间的新进程。
当fork函数被当前进程调用的时候,内核为新进程创建各种数据结构,并且分配它一个唯一的PID。为了给这个新进程创建虚拟内存。它创建了当前进程的mm_struct,区域结构和页表的原样副本。它将两个进程的每个页面都标记成只读,并且将两个进程中的每个区域接哦古都标记成私有的写时复制。
当fork在新进程返回的时候,新进程现在的虚拟内存刚好和调用的fork时存在的虚拟内存相同。当这两个进程中的任意一个后来进行写操作,写时复制机制就会创建新的页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在虚拟内存和内存映射中将程序加载到内存的过程中扮演了关键的角色。
假设当前运行的程序执行了execve调用,就会执行删除已存在的用户区域,映射私有区域,映射共享区域,设置程序计数器。
7.8 缺页故障与缺页中断处理
OS 处理缺页中断:
1)若有空闲页框,则根据辅存地址调入页,更新页表与快表等
2)若无空闲页框,则决定淘汰页,调出已修改页,调入页,更新页表与快表
由于这条指令没有执行完就中断了,因此中断处理结束后需要回退执行该指令。
7.9动态存储分配管理
Printf会调用malloc,下面简述动态内存管理的基本方法与策略。
在内存上有三个区,分别是栈区、堆区和静态区
malloc注意事项:
- size表示申请多少个字节的内存
- malloc申请到的是一块连续的内存空间
- 申请到的内存空间并没有指定类型
- 返回值为void*类型,只关注内存大小,不关注大小(大小隐含在参数size里)
- 如果申请成功,则返回申请到内存的起始位置的地址
- 如果申请失败,就会返回一个NULL,在笔试/面试中使用malloc时一定要检查
- malloc申请到的内存,内存会一直存在,直到手动释放(free)或者程序结束为止
7.10本章小结
本章结合hello程序介绍了逻辑地址、线性地址、虚拟地址、物理地址的概念,对段式管理与页式管理进行分析,此外还分析了进程fork和execve时的内存映射的内容,描述了系统对缺页中断的处理方法,最后描述了动态内存分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
所有的IO设备都被模型化为文件,而所有的输入输出都被当做对相应、文件的读和写来执行。
设备管理:unix io接口
Linux内核有一个简单的接口,称为UnixI/O接口,是所有的输入和输出都能以统一的方式来执行。
8.2 简述Unix IO接口及其函数
Unix IO接口
1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
2)Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
4)读写文件:一个读操作就是从文件复制 n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k。
5)关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix IO函数
1) open和openat 打开文件
2) close 关闭文件
3) lseek 设置偏移量
4) read 读取数据
5) write 写入数据
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和write函数。
vsprintf:接受一个格式化的命令,并把制定的匹配的参数格式化输出。
write:把字符串中n个元素的值写到终端(n为第二个参数)。
系统调用:显示格式化的字符串。
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
1)getchar其实返回的是字符的ASCII码值(整数)。
2)getchar在读取结束或者失败的时候,会返回EOF。
EOF意思是end of file,本质上是-1.
用getchar()函数读取字符串时,字符串会存储在输入缓冲区中,包括输入的回车字符。要使字符串读取正确则要使用while((ch=getchar())!='\\n')来消除回车字符。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,此外还分析了printf函数和getchar函数的实现。
以上是关于程序人生-Hello’s P2P的主要内容,如果未能解决你的问题,请参考以下文章
程序人生——Hello‘s P2P(HIT CSAPP大作业)