目 录
第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简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:如一个c程序在linux下需要经过cpp的预处理,ccl的编译,as的汇编,以及ld的链接才会变成一个可执行程序。然后在shell中输入命令,shell会为这个程序fork一个子进程,于是,这个程序转变成了进程。
020:一旦为这个程序execve,那么就会将其映射到虚拟内存中。然后找到程序的入口点,将其加载进物理内存,再之后,找到main函数执行代码,在此期间,cpu会为其分配时间片执行逻辑控制流。当程序结束之后,shell会回收该进程,内核会删掉相关的数据结构。
1.2 环境与工具
硬件环境:处理器:Intel® Core™ i7-8550U CPU @ 1.80GHz 1.99GHz
RAM:8.00GB 系统类型:64位操作系统,基于x64的处理器
软件环境:Windows10 64位;Ubuntu 19.04
开发与调试工具:gcc,as,ld,vim,edb,readelf,VScode
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名称 文件名
hello.i hello.c预处理后的文本文件
hello.s hello.i编译之后的汇编文件
hello.o hello.s汇编之后的可重定位文件
hello 链接之后的可执行文件
hello.txt hello程序的反汇编代码
1.4 本章小结
本章是一个概括,列出了hello的p2p、020的大致步骤。还有本次实验的实验环境,以及中间结果。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:程序设计领域,预处理是在程序源代码被编译之前,由预处理器(Preprocessor)对程序源代码进行的处理。这个过程并不对程序的源代码进行解析,但它把源代分割或处理成为特定的符号用来支持宏调调用。
作用:预处理最常见的功能有四种
(1).文件包含:#include是一种常见的预处理,其主要作用为文件的引用组
合源程序正文。其形式为#include “文件名 ”或#include <文件名 >
(2).条件编译:#if,#ifndef,#ifdef,#endif,#undef等也是比较常见的预处
理,主要是在编译的时候进行有选择的挑选,注释掉一些指定的代码,以达到版本控制、防止对文件重复包含的功能。
(3).布局控制:#progma,这也是预处理一个重要的功能,其主要的作用是
为编译程序提供非常规的控制流信息。
(4).#define,这是预处理最常见的功能,它实现了定义符号变量、函数功能、
重新命名、字符串的拼接等功能。
2.2在Ubuntu下预处理的命令
命令:gcc hello.c -E -o hello.i
图 2-1 预处理命令
2.3 Hello的预处理结果解析
图2-2 hello预处理后的代码
可以看出,在经过预处理之后文本文件的内容相比源代码更多了。但是仍然可以发现还有源代码的踪迹。头文件的内容被复制到了当前文件下。同时如果该程序使用了#define,还会对变量进行替换。
2.4 本章小结
本章介绍了预处理的相关概念及作用,包括文件包含、条件编译、符号替换等。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:编译过程是整个程序构建的核心部分,编译成功,会将源代码由文本形式转换成机器语言,编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
图3-1 编译命令
3.3 Hello的编译结果解析
3.3.0汇编指令的介绍
.file:声明源文件
.text:代码节
.section:
.rodata:只读代码段
.align:数据或者指令的地址对其方式
.string:声明一个字符串(.LC0,.LC1)
.global:声明全局变量(main)
.type:声明一个符号是数据类型还是函数类型
图3-2 hello.i头部分
3.3.1数据
1.字符串
图3-3 字符串
在hello程序中,一共有两个字符串,分别存储在如图lc0和lc1中,他们均位于只读段中。这两个字符串作为printf的参数
2.局部变量i
图3-4 局部变量i
如图,还指令实现了将i赋值为0,由此看见,i其实存储在了堆栈当中。
3.main函数:
参数argc作为参数被存放在了堆栈中。
4.立即数:
立即数直接体现在汇编代码中,如下图:
图3-5 立即数
此处对应if(argc!=4),可见直接体现在汇编代码中。
5.数组:char *argv[]
argv是本程序当中唯一的数组,其中存储的是一些指向字符串的指针。如下
图:
图3-6 argv参数
此处的代码实现了将传入main函数的两个字符串首地址分别传到%rdx和%rsi寄存器中,以准备令printf调用。其中-32(%rbp)为argv数组首地址,-24(%rbp)存储的是argv[1],-16(%rbp)存储的是argv[2]。
3.2.2全局函数
有源代码可知,在hello.c中生命了一个int main(int argv,char *argv[]),main函数中的字符常量也被存储在数据区。
这说明main是全局函数。
3.2.3赋值操作
程序中赋值主要用过mov来实现。根据传送数据类型的不同,又可以在mov后面加上不同的后者,如一个字节movb,两个字节movw,四个字节movl,八个字节mobq。
3.3.4算数操作
在本程序中,主要是i++用到了算数操作,只需要add即可。格式为add S,D 效果为DS+D,除此之外,还有一系列的算数操作,如图3-7:
图3-7 常见算数操作
3.3.5关系操作:
(1).在本程序中,主要用到了cmpl判断关系,在if(argc!=4)用到,被翻译成了如下图3-8的汇编指令,同时在比较后设置条件码,以配合条件码实行跳转。
图3-8
(2).另外,在for(i=0;i<8;i++)中,也用到了cmpl比较指令。被翻译成了如下图3-9的汇编指令,同时,在比较后设置条件码,以配合指令跳转。
图3-9
3.3.6控制转移指令:
汇编语言会在比较操作后设置条件码,然后控制转移指令会根据条件码进行控制跳转。在本程序中两处使用到了控制转移指令。
(1).首先在for循环中,for(i=0;i<8;i++),如下图3-10,在31-32行中,实现了使i赋值为0,然后在53行i与7比较后,如果i<=7,那么跳转到L4继续循环。
.
图3-10
(2).在if(argc!=4)中,使用了je控制跳转指令。如果等于4则跳转到L2,否则继续执行,实现了if分支。
图3-11
(3).除了在本程序中出现的jle和le,还有很多控制跳转指令,如下图:
图3-12 常见跳转指令
3.3.7函数操作:
调用函数时函数做了一下操作:
(1).转移控制:在进行函数P转移到函数Q的时候,调用call指令,该指令实现了将P的返回地址A压入栈中,并将PC设置为Q的起始地址。对应的ret指令会将栈中的返回地址A弹出,并将PC设置为A。
(2).数据传送:当调用一个过程时,除了要把控制传递给他并在过程返回时再传递回来之外,过程调用还可能将数据作为参数传递,而从过程返回还有可能包括返回一个值。x86-64大多数的数据传送是通过寄存器实现的。
(3).分配和释放内存空间:在跳转到新的函数时,操作系统会为函数分配新的栈阵已存储数据。当函数返回时必须将栈帧和栈指针还原。
在本程序中设计到的函数调用有:在main函数中,对printf、exit、getchar、sleep函数的调用。
3.4 本章小结
概括了汇编的概念及作用,以hello.s为例分析解释了在汇编语言下是如何实现一些c语句的功能的,即c是如何转变为汇编语言的。
第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
gcc hello.s -c hello.c
图4-1 汇编指令
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
(1).使用命令readelf -h hello.o得到如图4-2信息:
Magic是ELF头的开始,他是一个16字节的序列,描述了生成该文件
的字的大小和字节顺序。ELF头剩下的部分包含链接器语法分析和解释目标文件的信息。其中包括ELF头的大小。目标文件的类型,在这里为REL;机器类型,在这里为x86-64;节头部表的文件偏移,以及节头部表中条目的大小和数量。 在这里我们可以看到共有13个节。
图4-2
(2)节头命令:readelf -S hello.o
节头描述了文件出现的各个节的语义,包括节的类型、大小、位置、对齐等信息。如图4-3,可以看到在重定位目标文件中,各个节都是从0开始的。根据节头表提供我们可以找到每一个节对应的位置,并知道它的大小。我们也可以看到,.text段是可执行的不可写的,.data是可写的,.rodata也是不可写的。
.
图4-3
(3).查看符号表,符号表是用来存放程序中定义和引用的函数和全局变量的
信息命令:readelf -s hello.o
如下图4-4,我们可以看出该文件共包含有18个符号。并列出了每个符号的符号名称(name),相对于所在节的偏移地址(value),类型Type是函数还是数据。bind表示的是符号是本地的还是全局的。size表示目标的大小
图4-4
(4)重定位节:.rela.text
重定位节:在重定位节中存放的是重定位条目。重定位条目会告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。
在ELF重定位条目中,offset是需要被修改的引用的节偏移。symbol是标识被修改引用应该指向的符号。type告知链接器如何修改新的引用,addend是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。
.
图4-5
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
图4-6 hello.s
图4-7 hello的反汇编代码
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
通过对比hello.s和hello.o的反汇编代码,我们发现二者在大体的结构上是一致的,只有一些细节上的不同。除此之外,在反汇编代码中,不仅仅只有汇编代码,还有机器代码。机器代码是机器可以直接识别的语言。之所以在反汇编后二者的区别不大,原因在于对于汇编指令来说,每一条汇编都可以转化为机器语言来表示,同时,机器语言又可以找到对应的汇编语句来表示,故而二者大致上是相通的。
而二者有有一些不同的地方。比如在分支跳转中,汇编语言的跳转指令是段名称,而在反汇编当中确实一个确定的地址,这是由于在汇编代码中使用段名称可以利于人们阅读和编写,而在反汇编中,机器只能根据机器语言还原汇编代码,自然不会将段名称还原出来。又比如说在函数调用时,汇编语言显示的是欲调用的函数,而在反汇编代码中call的目标相对地址是0,这是因为函数调用了共享库中的函数,因此必须要动态链接才可以有确定的地址,而对于不确定的地址,就需要将其目标地址设置为0,直到链接时重新定位修改目标地址。
4.5 本章小结
本章对hello.s进行了汇编,生成了可重定位文件,并分析了可重定位文件的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 :
在hello的ELF头中有两处明显与hello.o中的ELF不同,分别是类型,前者为EXEC,表示是可执行文件;后者为REL,表述是可重定位文件。并且节的数量也增加了,变成了25个。
图5-1
(2).Section Headers:
在节头部表中,对所有的节的信息进行了声明,其中包括名称,大小,起始地址,偏移量。与hello.o不同,变成了可执行文件后,起始地址不再为 0,而是加载进内存时的虚拟地址。
图5-2
(3).重定位节.rela.text
对比hello.o,可以看出在链接的时候链接器也会将可重定位节修改,以满足后续对链接器修改代码节和数据节中对符号的引用。
图5-3
(4).符号表:可以看出链接后符号表也发生了变化,说明链接器也会修改符号表
图5-4
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
图5-5
图5-6
上图分别为hello和hello.o的反汇编代码。仔细观察二者,可以发现二者之间的不同:
(1).在hello.o中有的符号引用或者是函数引用并没有提供确定的虚拟地址,而在hello的反汇编代码中由于进行了重定位,所以可以看到虚拟地址在这里都被填充上了。
(2).同时在链接之后hello当中多出了很多节,这些节都具有一定的功能和含义。……………………….
如.init节是用来初始化程序需要执行的代码;.plt是动态链接-过程链接表;.dynamic是用来存放被ld.so使用的动态链接的信息;.data是初始化的数据;.fini程度正常终止所需代码。上述说的这些节都是我们有必要了解的。
重定位的过程:
在重定位的时候,链接器将合并输入模块,并未每个符号分配运行时地址。重定位由两步组成:
-
重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节,这个节成为输出的可执行文件的节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量就有唯一的运行时内存地址了。
-
重定位节的符号引用:在这一步中,链接器修改代码节和数据节对每个符号的引用,使得他们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位条目这一数据结构。
-
当汇编器生成一个目标模块时,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目。如下图为hello的可重定位条目,以及ELF重定位条目的格式
图5-7
图5-8
(4).重定位过程:重定位算法如图所示
图5-9
1.exit()相对地址引用定义exit的重定位条目为r,则ADDR(s) =0x400530refaddr = ADDR(s)r.offset=0x400582+0x27=0x4005a9*refptr=(unsigned)(ADDR(r.symbol)+r.addend-refaddr)=(0x400530+(-4)-0x4005a9)=-125=(unsigned)(0xffffff83),
hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
401000 <_init>
401030 puts@qlt
401040 printf@plt
401060 getchar@plt
401050 sleep@plt
401080
4010b0 <_dl_relocate_static_pie>
4010b1 :
401140 <__libc_csu_init>:
4011a0 <__libc_csu_fini>:
4011a4 <_fini>
403ff0 <__libc_start_main@GLIBC_2.2.5>
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
在调用共享库的时候,编译器没有办法预测这个函数的运行地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。
延迟绑定通过GOT和PLT实现。GOT是数据段的一部分,而PLT是代码段的一部分。PLT是一个数组,其中的每个条目都是16字节的代码。PLT[0]是一个特殊条目,它跳转到动态链接表。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
可以看到GOT表位置在调用dll_init之前0x404008后面的16个字节均为0,但是在调用dll_init后他们的字节变为了0x7fa867907190和0x7fa8678f0bb0,其中GOT[0],GOT[1]是动态链接器在1d-linux.so模块的入口点。其中的每个条目对应于一个被调用的函数,改变的GOT表如下图:
当程序调用一个动态链接库内定义的函数时,call指令并没有直接跳转到对应的函数中,由于绑定机制,此时的函数还不知道在哪,取而代之的是控制流会跳转到该函数对应的PLT表,然后通过PLT表将当前将要调用的函数序号压入栈中,下一步调用动态链接器,接下来动态链接器会根据栈中信息执行重定位,将真实的printf的运行地址写入GOT表,取代GOT原来用来跳转到PLT的地址,变成了真正的函数地址。
5.8 本章小结
本章主要介绍了链接的知识,链接可分为符号定义和重定位,了解了可执行文件的elf格式,分析了Hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程的经典定义是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
作用:在现代系统上运行一个程序时,我们会得到一个假象,就好像我们的程序时系统中当前运行的唯一的程序一样。我们的程序好像独占着处理器和内存的使用。处理器就好像是无间断的一条接一条的执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通过进程的概念提供给我们的。
6.2 简述壳Shell-bash的作用与处理流程
Shell是一个命令行解释器,它为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用Shell来启动、挂起、停止甚至时编写一些程序。Shell还是一个功能相当强大的编程语言,易编写,易调试,灵活性较强。Shell是解释执行的脚本语言,在Shell中可以直接调用Linux系统命令。
下面简述一个最基本的shell的处理流程:
(1).终端进程读取用户输入的命令行
(2).对用户输入的命令行字符串进行解析,获取命令行参数,并构造最终会传递给execve的aegv向量。。
(3).检查第一个字符,查看该命令行是否为内置shell命令
(4).如果是内置命令,则执行内置命令。
(5).如果不是内置命令,则fork一个子进程,在子进程中获取(2)中得到的命令行参数并调用execve执行程序
(6).查看命令行参数的最后一个字符,如果是(通常以&结尾)前台命令,则shell使用waitpid等待前台命令结束。否则返回shell。
6.3 Hello的fork进程创建过程
在shell中输入命令行运行hello后,终端程序会调用fork()函数来创建一个子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,也就是说子进程可以读写父进程中打开的任何文件。但是父进程与子进程的PID不同。子进程运行期间,内核以任何方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是等待子进程的完成。
(以下格式自行编排,编辑时删除)
6.4 Hello的execve过程
当创建了一个子进程之后,子进程调用execve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,在这里,虚拟内存和内存映射起到了非常关键的作用。
(1).删除已存在的用户区域:删除当前进程虚拟地址的用户部分的已存在的区域结构。
(2)映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。如图6-1
图6-1
(3)映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
(4)设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
(以下格式自行编排,编辑时删除)
6.5 Hello的进程执行
进程提供给应用程序的抽象:
(1) 一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器
(2) 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用CPU内存。
下面深入的阐述抽象:
○1逻辑控制流:进程为每个程序提供了一种假象,好像程序在独占地使用处
理器。其关键在于进程是轮流使用处理器的。每个进程执行它的流的一部分,然后被抢占(暂时挂起),然后轮到其他进程。
○2并发流:一个逻辑流的执行在时间上与另一个流重叠,成为并发流。多个
流并发的执行称为并发,一个进程和其他进程轮流运行的概念成为多任务。
○3时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
○4私有地址空间:私有地址空间:进程为每个流都提供一种假象,好像它是
独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个内存字节是不能被其他进程读或者写的,在这个意义上,这个地址空间是私有的。
○5用户模式和内核模式:处理器同样用某个控制寄存器中的一个模式位来区
别用户模式和内核模式,在内核模式下进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。而在用户模式下,则不允许执行特权指令,比如停止处理器、改变模式位、或者发起一个I/O操作
○6上下文:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由
通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。
○7当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了
一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程:
1) 保存以前进程的上下文
2)恢复新恢复进程被保存的上下文,
3)将控制传递给这 个新恢复的进程 ,来完成上下文切换。
在详细了解了进程的抽象之后,我们看看hello是如何执行的。
在进程调用了execve之后,会删除当前进程的虚拟地址的用户部分的已存在的区域结构。然后hello为进程分配新的虚拟地址空间,并将虚拟空间的代码区域和数据区域映射到hello的.text和.data区域。然后hello在用户模式下运行,知道遇到sleep()函数,在hello、调用sleep函数之后进程陷入内核模式,这是内核决定将hello挂起,将其加入等待队列中,定时器开始计时,然后内核进行上下文切换将控制权交给其他程序,直到计时结束并给内核发出一个信号,内核收到信号后将当前的进程加入等待队列,然后上下文切换,将控制权交换给hello。直到下一次遇到sleep()函数,如此循环,直到最后hello终止。
除此之外,当hello在调用getchar时,也会切换进程,这是由于系统会调用read函数,而这时内核中的陷阱处理程序请求来自磁盘控制器的DMA传输,并且安排在磁盘控制器完成从磁盘到内存的数据传输后,磁盘中断处理器,由于磁盘去数据需要较长一段时间,所以内核不会在这个时间段内等待,而是切换到其他进程工作,知道接受来自磁盘的中断信号,重新把控制权归还给hello。
(以下格式自行编排,编辑时删除)
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信号处理
(1).异常和信号异常可以分为四类:中断、陷阱、故障、终止,如下表为其的详细属性
类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令
陷阱 有意的异常 同步 总是返回到下一条指令
终止 不可恢复的错误 同步 不会返回
表6-1
在hello程序中可能出现的异常:
中断:在hello执行过程中可能出现外部I/O设备引起的异常。如在调用getchar
时。
故障:比如在执行hello程序时发生缺页故障。
陷阱:陷阱是有意的异常,比如hello在执行sleep函数的时候。
终止:终止是不可恢复的错误,比如外部因素 导致hello的数据受损导致程序终
止。
在程序发生异常的时候程序会发出信号,然后收到信号的程序会做出响应。
ID 名称 默认行为 相应事件
2 SIGINT 终止 来自键盘的中断
9 SIGKILL 终止 杀死程序(该信号不能被捕获或忽略)
11 SIGSEGV 终止 无效的内存引用(段故障)
14 SIGALRM 终止 来自alarm函数的定时器信号
17 SIGCHLD 忽略 一个子进程停止或终止
表6-2
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,
Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运
行结截屏,说明异常与信号的处理。
正常处理结果:
图6-2
在程序运行后按下ctrl+z时:
可以看到程序被挂起,hello进程并没有被回收,而是在后台挂起,如图用ps命令可以看到程序确实仍然存在后台中。此时它的工作组号为1,PID为110650,输入fg1将hello调到前台,hello会继续运行之后的程序,运行结束后被进程回收。
图6-3
图6-4
在程序运行后按下ctrl+c时:
在按下ctrl+c后,前台程序会收到程序终止的信号,如图,我们使用ps查看进程的时候确实hello已经终止了:
图6-5
如图,当我们随意输入字符串的时候,我们会发现,输入的字符串会被存入到缓冲区,然后getchar运行的时候,如果读到\n,那么会结束读取字符串,如果缓冲区还有其他字符,则会被当做新的命令行参数。
图6-6
6.7本章小结
在本章中,主要阐述了进程的定义和作用,同时阐述了命令行的作用,以及在linux下通过shell是如何运行程序的,同时详细介绍了fork函数和execve函数,以及程序如何实现并行。还有一些信号异常的处理。
(以下格式自行编排,编辑时删除)
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址: 编译器编译程序时,会为程序生成代码段和数据段,然后将所有代码放
到代码段中,将所有数据放到数据段中。最后程序中的每句代码和每条数据都会有自己的逻辑地址。一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]
线性地址:跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的
硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。线性地址=段基址+逻辑地址偏移量。
虚拟地址:在分页机制下,虚拟地址与线性地址相同。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些。
比如在hello中,对于程序当中的一个逻辑地址,会先将其转换为一个线性地址。需要说明的是,在linux下段机制形同虚设。或者说,linux下的分段机制只是为了兼容A32而设计,在linux中,所有的段基地址都为0。因此,在linux下逻辑地址和线性地址一致的。由于hello运行时采用的是分页模式,所有线性地址等价于虚拟地址。然后虚拟地址经过变换转变为物理地址。下面详细的介绍各个段之间是如何转换的。
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
由路基地址转换为线性地址的整个过程如下:
逻辑地址实际上是由48位组成的,前16位为段选择符,后32位为段内偏移量。
段选择符怎样找到段基址?
我们知道,对于一个逻辑地址,它的前16位是段选择符,其格式如下图所示:
• 索引:「描述符表」的索引(Index)
• TI:如果 TI 是 0。「描述符表」是「全局描述符表(GDT)」,如果 TI 是 1。「描述符表」是「局部描述表(LDT)」
• RPL:段的级别。为 0,位于最高级别的内核态。为 11,位于最低级别的用户态。在 linux 中也仅有这两种级别。
图7-1
整体过程就是:
通过索引在描述符表中找到段基址,如图:
图7-2
其中GDT和LDT的首地址存在用户不可见的寄存器中。
什么是描述表?
实际上就是「段表」,由「段描述符(段表项)」组成。有三种类型:
• 全局描述符 GDT:只有一个,用来存放系统内用来存放系统内每个任务共用的描述符,例如,内核代码段、内核数据段、用户代码段、用户数据段以及 TSS(任务状态段)等都属于 GDT 中描述的段。
• 局部描述符表 LDT:存放某任务(即用户进程)专用的描述符
• 中断描述符表 IDT:包含 256 个中断门、陷阱门和任务门描述符
什么是段描述符?
段描述符就是表项,一种记录每个段信息的数据结构。我们之前说到的「段选择符」就是描述符表(段表)中的索引。如下图所示:
图7-3
一个段描述符的大小是 8B。现在把段描述符的每个部分讲清楚:
• B31~B0:32 位基地址(段的基地址)
• L19~L0:20 位界限,表示段中的最大页号
• G:与界限的单位有关。设置 G = 1,以页(4 KB)为单位,所以最大段为 ;G = 0 时,以字节位单位,所以最大段为
• D:D = 1 表示段内偏移量为 32 位宽,D = 0 表示段内偏移量为 16 位宽
• P:P = 1 表示存在,P = 0 表示不存在。Linux 总把 P 置 1,不会以段为单位淘汰。
• DPL:访问段时对当前特权级的最低等级要求。因此,只有 CPL 为 0(内核态)时才可访问 DPL 为 0 的段,任何进程都可访问 DPL 为 3 的段(0 最高、3 最低)
• S:S = 0 系统控制描述符,S = 1 普通的代码段或数据段描述符
• TYPE:段的访问权限或系统控制描述符类型
• A:A = 1 已被访问过,A = 0 未被访问过。(通常 A 包含在 TYPE 字段中)
图7-4
综上:逻辑地址转换为线性地址的一般步骤为:
-
看段选择符以判断要转换的是GDT中的段还是LDT的段
-
根据段选择符中的段索引,找到对应的段描述符,这样我们就得到了它的基地址。
-
基地址加偏移就是需要转换的线性地址了。
在linux下,分段机制得到了简化:
linux在初始化时将代码段和数据段的段基址都设置为如下表的信息:
图7-5
也就是说,这些段的基地址都为0,因此,在linux下逻辑地址和线性地址是一致的。
(以下格,式自行编排,编辑时删除)
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址到物理地址之间的转换时通过分页机制来完成的。
使用虚拟寻址,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存之前首先转换为适当的物理地址。将一个虚拟地址转换为物理地址叫做地址翻译,需要CPU硬件和操作系统之间的紧密合作。CPU芯片上叫做内存管理单元的专用硬件,利用存放在内存中的查询表来动态翻译虚拟地址,该表的内容有操作系统管理。
再来说明一下虚拟内存。虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上的数据被分割为块,这些块作为磁盘和主存之间的传输单元,称之为物理页。同时,虚拟内存也被分割成页,大小与物理页相同。
在任意时刻,虚拟页面的集合被分成三个不相交的子集。
未分配的:VM系统还未分配的页。
缓存的:当前已缓存在物理内存中的已分配页。
未缓存的:未缓存在物理内存中的已分配页。
每次将虚拟地址转换为物理地址时,都需要用到页表。页表就是一个页表条目(PTE)的数组。虚拟地址中的每个页在页表中一个固定偏移量处都有一个PTE。每个PTE中都包括了有效位和n位地址字段。有效位表明了该虚拟页也是否被缓存到DRAM中。
n为的虚拟地址包含两个部分:一个p位的虚拟页面偏移和一个n-p位的虚拟页号,MMU利用VPN来选择适当的PTE。将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。注意:物理页面偏移(PPO)和VPO是相同的。
当页面命中时:
(1).CPU生成一个虚拟地址,并将它传送给MMU。
(2).MMU生成PTE地址,并从高速缓存/主存请求得到它。
(3).高速缓存/主存向MMU返回PTE。
(4).MMU构造物理地址,并将它传送给高速缓存/主存
(5).高速缓存/主存返回所请求的数据字给处理器。
当缺页时:
(1).CPU生成一个虚拟地址,并将它传送给MMU。
(2).MMU生成PTE地址,并从高速缓存/主存请求得到它。
(3).高速缓存/主存向MMU返回PTE。
(4).PTE中的有效位是0,所以MMU触发了一次异常,传递CPU的控制到操作系统中内核中的缺页一次处理程序。
(5).缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。
(6).缺页处理程序页面调入新的页面,并更新内存中的PTE。
(7).缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,主存就会将所请求字返回给处理器。
7.4 TLB与四级页表支持下的VA到PA的变换
core i7地址翻译采用了四级页表层次结构,其支持48位虚拟地址空间以及52位物理地址空间。
如图,CPU产生虚拟地址VA并送到MMU中,之后与TLB匹配,如果命中,则得到PPN,然后与PPO组合起来得到物理地址。如果TLB没有命中,那么会到页表中查询。在corei7中,MMU将36位VPN分成了4个9位的片,每个片被用作到一个页表 的偏移量。CR3寄存器包含L1页表的物理地址。VPN1提供到一个L
1PET的偏移量,这个PTE包含L2页表的基地址,VPN2提供到一个L2PTE的偏移量,以此类推。最终在第四级页表中查 询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目,如果查询PTE时发现不在物理内存中,则发生缺页故障。
图7-6
7.5 三级Cache支持下的物理内存访问
以cashe1为例,cashe2,cache3原理与cashe1相同
由于L1cache有64组,因此组索引s为6,每组有八个高速缓存行,由于每块大小为64B,所以块偏移为6,因此标记为为64-6-6=40位。
使用cache1访问物理内存的过程大致如下:
(1).对于CPU提供的地址w,从其中间抽取出6位组索引为。这些位被解释个一个对应于一个组号的无符号整数。也就是说,将高速缓存看作是一个关于组的一维数组,那么这些组索引位就是一个到这个数组的索引。
(2).在选定了某个组i之后,接下来需要确定是否有字w的一个副本存储在组i的一个高速缓存行中,只有标记位相匹配且有效位为1的时候才缓存命中,否则缓存不命中。
(3).如果缓存命中,根据提供的块偏移位(所需要的字的第一个字的偏移),我们就可以将其取出 并返还给CPU。
(4).如果缓存不命中,那么它需要从存储器层次结构中的下一层取出被请求的块,然后将新的块存储到组索引位指示的组中的一个高速缓存行中。如果组中都是有效高速缓存,那么必须驱逐出一个现有的行,可以采用最近最少使用策略LFU进行替换。
(以下格式自行编排,编辑时删除)
7.6 hello进程fork时的内存映射
当shell进程调用了fork函数时,内核会为新进程创建各种数据结构,并分配给它唯一的PID。为了给这个进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。
但fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建一个新页面,因此,也就为每个进程保持了私有地址的空间。
(以下格式自行编排,编辑时删除)
7.7 hello进程execve时的内存映射
execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。 加载并运行 hello 需要以下几个步骤:
1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存 在的区域结构。
2)映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结 构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名 文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长 度为零。
3)映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链 接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4)设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程 上下文的程序计数器,使之指向代码区域的入口点。
图7-7
7.8 缺页故障与缺页中断处理
缺页故障:当指令给出的虚拟内存对应的物理地址并不在主存中时,会发生缺页故障,这时会触发缺页中断处理程序。缺页处理程序会根据页表中提供的信息到相应的磁盘位置上取出页面并重新加载到内存中,然后更新PTE,然后将控制返回给缺页故障的指令。当指令再次执行时,相应的页面已经驻存在内存中,因此指令可以没有故障的运行。
(以下格式自行编排,编辑时删除)
7.9动态存储分配管理
动态内存分配器维护者一个进程的虚拟内存区域,成为堆。(如图7.9.1所示),分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格。两种风格都是要求显示的分配块。
(1) 显式分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。
(2) 隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。
分配器必须在一些严格的约束条件下工作:
(1).处理任意请求序列
(2).立即响应要求
(3).只使用堆
(4).对切块
(5).不修改已分配的块。
我们如何组织空闲块才能尽可能的提高堆的利用率,减少碎片?
我们在隐式空闲链表这一简单空闲块组织结构中简单说明:
隐式空闲链表:
如图,采用这样的数据结构,在这种情况下,一个块是由一个字的头部、有效载荷,以及可能的填充组成。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。块的头最后一位指明这个块是已分配的还是空闲的。
图7-8
头部后面是应用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的,需要填充有很多原因,比如,填充可能是分配器策略的一部分用来对抗外部碎片,或者需要用它满足对齐要求。块的格式如图所示,空闲块通过头部块的大小字段隐含的连接着,所以我们称这种结构就隐式空闲链表。
图7-9
放置已分配的块:
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大可以放置所请求快的空闲块。分配器执行这种搜索方式是由放置策略确定的。一些常见策略是首次适配、下一次适配、最佳适配。
分割空闲块
一旦分配器找到一个匹配的空闲块,它就必须做另一个策略决定,就是分配这个空闲块多少空间。为了减少内部碎片,可以将这个空闲块分割为两部分。一部分变成分配块,另一部分变成新的空闲块。如图所示
图7-10
获取额外的堆内存
如果即使合并了物理上相邻的空闲块仍不能使请求的块的大小,那么分配器就会调用sbrk函数,向内核请求额外的堆内存,分配器将额外的内存转化为一个大的空闲块,将这个块插入到空闲链表中。然后将请求的块放置在新的空闲块中。
合并内存块
可以采用边界标记方法优化空闲块的合并,如图所示,在每个块的结尾处添加一个脚部,这个脚部是头部的副本。分配器可以通过检查它的脚部判断前面一个块的起始位置和状态。需要注意,合并时会出现四种情况,分别是前空后不空,前不空后空,前后都空,前后都不空,每种情况下的合并是不同的。
显示空闲链表:
显示空闲链表是将空闲块组织为某种形式的显示数据结构。如图7.9.6所示。堆被组织为一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继的指针。
图7-11
使用双向链表而不是隐式空闲链表,可以将首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。
一种方法使用后进先出的顺序维护链表,将新释放的块在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过 的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界 标记,那么合并也可以在常数时间内完成。
另一种方法是按照地址顺序来维护链表,其中链 表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要 线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比 LIFO 排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。
一种流行的减少分配时间的方法,通常称为分离存储,就是维护多个空闲链表,其中每个链表中的块有大致相等的大小。一般的思路是将所有可能的块大小分成一个等价类,也叫做大小类。分配器维护者一个空闲链表数组,按照大小的升序排列。当分配器需要一个大小为n的块时它就搜索相应的空闲链表。如果不能找到合适的空闲链表就搜索下一个链表。下面我们详细介绍一种基本的方法----分离适配:
使用这种方法,分配器维护一个空闲链表的数组。每个空闲链表和一个空闲类相关联,并且被组织成某种类型的显式或隐氏链表。每个链表包含潜在的大小不同的块,这些块的大小是大小类的成员。为了分配一个块,必须确定请求的大小类,并对适当的空闲链表做首次适配,查找到一个合适的块。如果找到了一个,那么就分割它,并将剩余部分插入到合适的空闲链表中,如果找不到合适的块,就搜索下一个更大的大小类的空闲列表。如此重复,直到找到一个合适的块,那么就向操作系统请求额外的块,从新的堆内存中分配出一个块,将剩余部分放置在合适的大小块中。
(以下格式自行编排,编辑时删除)
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
7.10本章小结
本章主要介绍了hello的存储器的地址空间,介绍了四种地址空间的差别和地址的相互转换。同时介绍了hello的四级页表的虚拟地址空间到物理地址的转换。阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
(以下格式自行编排,编辑时删除)
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个linux文件就是一个m个字节的序列:
B0,B1,····Bk···Bm-1
所有的I/O设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行:
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(1).打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
(2).Shell 创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2).
(3). 改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
(4)读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文 件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文 件,当 k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。
(5)关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
UNIxI/O函数:
进程通过调用 open 函 数来打开一个存在的文件或是创建一个新文件的:
int open(char* filename,int flags,mode_t mode)
open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访 问这个文件,mode 参数指定了新文件的访问权限位。
进程通过调用close函数关闭一个文件的打开:
int close(int fd)
应用程序通过read来执行输入操作:
ssize_t read(int fd,void *buf,size_t n)
read函数从描述符为fd的当前位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。
应用程序通过write来执行输出:
ssize_t write(int fd,void *buf,size_t n)
write 函数从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置。
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数的实现。
(以下格式自行编排,编辑时删除)
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
hello.c:这里存放的是hello程序的源码,是一个二进制文本文件,其中的每一
个字符都是ascaaII码。
hello.i:hello.c经过预处理得到hello.i
hello.s:hello.i经过编译器转化为hello.s,这是hello.s中存放的是汇编语言。
hello.o:hello.s经过汇编器变为机器语言。
hello:hello.o与可重定位目标文件和动态链接可链接成为可执行文件。这时hello
已经可以被加载到内存中运行了。
运行:在shell终端输入./hello 高杰 1190501419 1
创建子进程:shell会检测到这不是一个内置命令,于是会调用fork函数创建一个
子进程。
加载:在进创建的子进程中,会调用execve,execve会启动加载器,将程序映射
到虚拟内存,之后程序会跳转到第一条指令或者是入口点来运行这个程
序。
上下文切换:hello在运行时,会调用sleep函数,这是会陷入内核模式,内核会将
当前进程挂起并将控制权交给其他进程,当sleep完成后,内核执行上下文切换将控制权交还给当前进程。
信号管理:当程序运行时,我们按下Ctrl+c,会发送SIGINT信号给进程并终止前
台作业,当按下Ctrl+z,内核会发送SIGSTP信号给进程,将前台作业停止挂起。
终止:当子程序执行完成时,内核安排父进程回收子程序,将子程序的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
计算机系统设计地优美、精巧:从一个程序的产生,到最后的运行,中间一环扣
着一环,每一步都非常的巧妙,包括一条一条指令之间是如何配合最终使得程序运行起来,还有内核通过上下文的切换使得使用者感觉很多程序都在并行等等等等,这些巧妙的设计使得计算机的功能十分强大。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.i hello.c预处理后的文本文件
hello.s hello.i编译之后的汇编文件
hello.o hello.s汇编之后的可重定位文件
hello 链接之后的可执行文件
hello.txt hello程序的反汇编代码
/