2021-06-27

计算机系统

大作业

计算机科学与技术学院

2021年6月

摘  要

本文通过hello程序从头到尾的生命周期,回顾并串联了《深入理解计算机系统》各章节的内容。探究了hello程序源代码通过预处理、编译、汇编、链接等步骤成为可执行程序、并通过进程管理、存储管理、IO管理等系统机制在操作系统中得以运行和结束的整个过程 。体现了hello程序的“P2P”(From Program to Process)和“O2O”(From Zero-0 to Zero-0)。

关键词:Hello程序;预处理;编译;汇编;链接;进程管理;存储管理;IO管理;                           

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分

目  录

第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:From Program to Process

从源文件到目标文件的转化是由编译器驱动程序完成的,用高级语言编写hello.c文件,GCC编译器驱动程序读取源文件hello.c,并把它编译成一个可执行目标文件hello。这个编译可分为四个阶段完成。首先,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序,将hello.c文件转化为hello.i;而后通过编译器(ccl)将文本文件hello.i翻译成为文本文件hello.s汇编程序,它包含一个汇编语言程序;再用汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件hello.o中;最后,由于hello程序调用了printf等函数(属于标准C库的一个函数),因此要将含printf等函数的printf.o等用链接器(ld)合并到我们hello.o程序中,结果得到hello文件(可执行文件)。

Linux系统中通过内置命令行解释器shell加载运行hello程序,为hello程序fork进程,至此,hello.c完成了P2P的过程。

O2O: From Zero-0 to Zero-0

shell通过execve在fork产生的子进程中加载hello,先删除当前虚拟地址的用户部分已存在的数据结构,为hello的代码段、数据、bss以及栈区域创建新的区域结构,然后映射虚拟内存,设置程序计数器,使之指向代码区域的入口点,进入程序入口后程序开始载入物理内存,而后进入main函数,CPU为hello分配时间片执行逻辑控制流。hello通过Unix I/O管理来控制输出。hello执行完成后shell父进程会回收hello进程,并且内核会从系统中删除hello所有痕迹,至此,hello完成O2O的过程。

1.2 环境与工具

硬件环境:Intel i5-9300H @ 2.40GHz X64 CPU , 8G RAM

软件环境:Windows 10 64位 ,Vmware 15 ,Ubuntu 64 位

开发工具:gcc , gedit , Codeblocks , gdb , edb ,

1.3 中间结果

hello.i

预处理之后文本文件

hello.s

编译之后的汇编文件

hello.o

汇编之后的可重定位目标执行

hello

链接之后的可执行目标文件

hello.elf

Hello的ELF格式

1.4 本章小结

本章对Hello程序P2P和O2O的概念进行了简单的描述,对于实验的环境与工具进行了简单的说明,对于实验中的中间产物进行了罗列。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

概念:

在嵌入式系统编程中不管是内核的驱动程序还是应用程序的编写,涉及到大量的预处理与条件编译,这样做的好处主要体现在代码的移植性强以及代码的修改方便等方面。因此引入了预处理与条件编译的概念。

在C语言的程序中可包括各种以符号#开头的编译指令,这些指令称为预处理命令。预处理命令属于C语言编译器,而不是C语言的组成部分。通过预处理命令可扩展C语言程序设计的环境。

作用:

在集成开发环境中,编译,链接是同时完成的。其实, C语言编译器在对源代码

编译之前,还需要进一步的处理: 预编译。预编译的主要作用如下:

●将源文件中以“include”格式包含的文件复制到编译的源文件中。

●用实际值替换用"#define" 定义的字符串。

●根据"#if”后面的条件决定需要编译的代码。

2.2在Ubuntu下预处理的命令

命令:gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i

2.3 Hello的预处理结果解析

经过预处理之后,hello.c转化为hello.i文件,打开该文件可以发现,程序的内容增加,程序的主要部分被保留成最简单的格式,删去了所有注释,位于.i文件的末尾且仍为可以阅读的C语言程序文本文件。且对原文件中的宏进行了宏展开,头文件中的内容被包含进该文件中。例如声明函数、定义结构体、定义变量、定义宏等内容。另外,如果代码中有#define命令还会对相应的符号进行替换。

2.4 本章小结

本章解释了预处理的概念和作用,并在Ubuntu中通过将hello.c实际预处理为了hello.i,展示了预处理后的文件,通过浏览hello.i 文件更好地了解了预处理的作用, 例如实现将定义的宏进行符号替换、引入头文件的内容、根据指令进行选择性编译等。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

概念:

编译,就是把代码转化为汇编指令的过程,把预处理完的文件进行一系列语法分析及优化后生成相应的汇编文件。将.i形成的简单直接的c语言码经过分析并优化,翻译成一份汇编语言代码,得到的结果为.s文件。

作用:

扫描,语法分析,语义分析,源代码优化,目标代码生成,目标代码优化再生成汇编代码,再汇总符号,最后生成.s文件。

3.2 在Ubuntu下编译的命令

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

3.3 Hello的编译结果解析

.file:声明源文件

.text:代码节

.global:声明全局变量 sleepsecs,main

.align:数据或者指令的地址对其方式

.type:声明一个符号是数据类型还是函数类型

.size 声明大小

.section:

.rodata:只读代码段

.string:声明字符串

.long 长整型数据

3.3.1 数据

1.常量

以十进制数字的形式存在。

字符串常量:“Usage: Hello 学号 姓名!\n"和"Hello %s %s\n”

在.LC0、.LC1段声明的字符串常量,且都是在.rodata只读数据节中

2.全局变量

1.变量名:sleepsecs

类型:int

操作:在汇编文件中,使用语句sleepsecs(%rip)进行引用。

赋值:在文件开头,为其赋初值,因为是int型整数,将初值小数位舍去,赋初值为2。

2.变量名:main

类型:函数地址

2.局部变量

1.变量名:i

类型:int

操作;i储存在以%rbp中值减4为地址的内存中,使用语句-4(%rbp)进行引用

赋值:未赋初值,使用movl $0, -4(%rbp)语句,赋值0;

2.变量名:argc

类型:int

操作:刚开时储存在%edi寄存器中,然后将其放置在以%rbp中值减去20为地址的内存中,使用语句-20(%rbp)进行引用。

3.变量名:argv

类型:char **

操作:刚开始储存在%rsi寄存器中,然后将其放置在以%rbp中值减去32 为地址的内存中,使用语句-32(%rbp)进行引用。

3.3.2 赋值

还以局部变量i的赋值为例,直接向栈的寄存器中存入32位整型(对应movl的l后缀),即int i = 0。

3.3.3 类型转换

sleepsecs存在2次类型转换。

一个是赋初值时,将double型的常数舍去小数赋值给sleepsecs

一个实在作为sleep函数的参数时,直接将其值作为unsigned int类型,作为参数传递给sleep函数。

3.3.4  算术操作

++: 使用语句:addl $1, -4(%rbp)对int型变量i加1,并将结果储存在i中

3.3.5  关系操作

1. argc!=3 :

使用cmpl $3, -20(%rbp)向对argc值和3进行比较,并设置条件位,再使用je .L2语句,如果b==a则跳转到.L2处,执行.L2处的语句;如果不想等,则不跳转,执行je L2后面的语句,也就是!=的情况下,要执行的语句。

2. i<10 :

在这里为刚开始赋值为0,然后增大,所以编译器实际上是执行<=的操作,使用语句cmpl $9, -4(%rbp),对i和9进行比较,并设置条件位,再使用语句jle .L4,如果i<=9则跳转到.L4处,继续循环,否则跳出循环。

3.3.6  数组/指针/结构

1.指针:argv是一个指针,argv所指向的数组中的每一个元素也是指针。

字符串也表示为一个地址值。用如:.LC0,进行应用。

对于指针的所指向值得引用是语句(指针),表示指针所指向地址处内存中的值。

2.数组:argv[]是一个数组,对于数组元素的引用,是按照数组的首地址argv加 上一个所要引用的元素在数组中地址的偏移量(一般是索引值*元素类型的字节数),得到元素的首地址,再使用(首地址)进行引用获得其值。

3.3.7  控制转移

1.if (if(argc!=3)):

使用je .L2语句,如果不条件成立则跳转到.L2处,执行.L2处的语句;如果成立,则不跳转,执行je L2后面的语句,也就是if后面的要执行的语句。

2.for (for(i=0;i<10;i++)):

在循环刚开始,给i赋值为0(movl $0, -4(%rbp)),然后跳转到.L3处进行比较,如果符合i<10的条件,则跳转到.L4(jle.L4),执行循环中的语句,在循环的最后,会给i加上1,.L3紧跟在.L4后,所以执行完循环体.L4中的语句后,会顺序执行.L3中的内容,进行条件判断。

3.3.8  函数操作

1.main函数:

参数传递:%edi存储着argc的值,是参数变量的个数;作为第一个参数

%rsi储存值argv的值,作为一个地址,指向参数字符串数组的首地址.作为第二个参数

函数调用:主函数,第一个执行的函数

函数返回:返回值0,使用movl $0, %eax语句,将%eax中值作为返回值,使用指令ret进行返回

2.printf函数:

参数传递:第一次 printf 将%rdi 设置为“Usage: Hello 学号 姓名! \n”字符串的首地址。第二次 printf 设置%rdi 为“Hello %s %s\n” 的首地址,设置%rsi 为 argv[1],%rdx 为 argv[2]。

函数调用:第一次 printf 因为只有一个字符串参数,所以 call puts;第二次 printf 使用 call printf。

3.exit函数:

参数传递:将1放置在寄存器%edi中(movl $1, %edi),做为参数传递

函数调用:使用指令call进行函数调用

函数返回:无返回值,不返回

4.sleep函数:

参数传递:将sleepsecs的值作为参数,通过%eax赋值给%edi,%edi中值作为第一个参数

函数调用:使用指令call进行函数调用

函数返回:返回值存储在%eax,使用ret指令返回

5.getchar函数:

参数传递:无参数

函数调用:使用指令call进行函数调用

函数返回:返回值存储在%eax中,使用ret指令返回.

3.4 本章小结

本章内容,对汇编的概念和作用进行了阐述,并通过对文本文件hello.c的编译以及编译结果演示再现了编译的过程。

在以上的分析过程中,解释了hello.c文件与hello.s文件间的映射关系,详细的分析了编译器是怎么处理C语言的各个数据类型以及各类操作的,按照不同的数据类型和操作格式c代码到汇编代码的翻译方式。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

概念:

汇编器(as)将汇编文件hello.s翻译成机器语言指令,并把这些指令打包成可重定位目标程序的格式,并将结果保存在二进制文件中hello.o。

作用:

将汇编代码转为机器指令,使其在链接后能被机器识别并执行。。

4.2 在Ubuntu下汇编的命令

gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

4.3 可重定位目标elf格式

使用readelf -a hello.o > hello.elf 指令获得hello.o文件的ELF格式。

4.3.1  elf头

以一个16字节序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,剩下的信息帮助链接器语法分析和解释目标文件的信息。包括ELF头大小,目标文件类型,机器类型,节头部表的文件偏移以及节头部表中条目的大小和数量等。

4.3.2  节头

记录了各节名称、类型、地址、偏移量、大小、全体大小、旗标、连接、信息、对齐信息。

4.3.3  重定位节

文件中有一些内存地址或引用,这些地方在链接前是待定的,需要视链接的情况指定确切的地址。因此,需要对这些地址进行重定位。每个代码段或数据段都对应一个重定位表,记录了段中的这些位置,方便对它们进行查找和操作。

4.3.4  符号表

.symtab存放着程序中定义和引用函数和全局变量的信息。且不包含局部变量的条目。重定位中的符号类型全在该表中有声明。

4.4 Hello.o的结果解析

将反汇编的代码与hello.s比较,指令并没有太大差异,只是反汇编代码所显示的不仅是汇编代码,还有机器代码。机器指令由操作码和操作数构成,每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言(操作码和操作数)和二进制机器语言建立一一映射的关系,因此可以将汇编语言转化为机器语言,通过对机器代码的分析可以看出一下不同的地方。

  

(1) 机器语言中的操作数使用的是十六进制格式;而汇编语言则是十进制

(2)分支转移:反汇编的跳转指令用的不是段名称比如.L3,二是用的确定的地址,因为,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。

(3)函数调用:在.s 文件中,函数调用之后直接跟着函数名称,而在反汇编程 序中,call的目标地址是当前下一条指令。这是因为 hello.c 中调用的函数 都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。

4.5 本章小结

本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,比较了hello.s和hello.o反汇编代码的不同之处,分析了从汇编语言到机器语言的一一映射关系,并分析了反汇编代码与.s文件不同之处。

(第41分)

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的格式

首先是ELF 头,如图发现可执行目标文件格式类似于可重定位目标文件格式。Type类型为EXEC表明hello是一个可执行目标文件,有27个节。ELF描述文件总体格式,发现它还包括了程序的入口点,即程序运行时要执行的第一条指令的地址。

再看节头,在 ELF 格式文件中,节头对 hello 中所有的节信息进行了声明,其中包括大小以及在程序中的偏移量,因此根据节头中的信息我们就可以用定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。

除此之外,.text、.rodata、.data节与可重定位目标文件的节是相似的,这些节已经被重定位到他们最终的运行时内存地址。

5.4 hello的虚拟地址空间

通过查看edb,看出hello的虚拟地址空间开始于0x401000,结束与0x402000,如图  

根据节头部表图,可以通过edb找到代码段各个节的信息

例如.txt节,虚拟地址开始于0x4010d0,大小为0x135.

也可以查看整个内存映像各段的信息

由上到下依次为只读代码段,读写段,运行时堆,用户栈等

5.5 链接的重定位过程分析

使用命令:objdump -d -r hello

分别获得hello  hello.o的反汇编代码。

hello.o的objdump

hello的objdump

分析hello与hello.o的差异。可以发现以下不同的地方:

1.在hello.o中,main函数地址从0开始,即hello.o中保存的都是相对偏移地址;而在hello中main函数0x401105开始,即hello中保存的是虚拟内存地址,对hello.o中的地址进行了重定位。

2.ELF描述文件总体格式,发现它还包括了程序的入口点,即程序运行时要执行的第一条指令的地址。除此之外,由于可执行文件是完全链接的,故也没有了rel节。

3.hello可执行目标文件中多出了.init节和.plt段。.init节定义了一个小函数叫做_init,程序的初始化代码会用到,用于初始化程序执行环境;.plt段是程序执行时的动态链接。所有的重定位条目都被修改为了确定的运行时内存地址。

4.在hello中链接加入了在hello.c中用到的函数,如exit、printf、sleep、getchar等函数

链接过程:

链接就是链接器ld将各个目标文件组装在一起,就是把.o文件中的各个函数段按照一定规则累积在一起,指定了动态链接器为64的/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o中主要定义了程序入口_start、初始化函数_init,_start程序调用hello.c中的main函数,libc.so是动态链接共享库,其中定义了hello.c中用到的printf、sleep、getchar、exit函数和_start中调用的__libc_csu_init,__libc_csu_fini,__libc_start_main。链接器将上述函数加入。

重定位:

在这个步骤中,将合并输入模块。并为每个符号分配运行时的地址。重定位由两步组成:重定位节与符号定义、重定位节中的符号引用。

在重定位节与符号定义这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节,而后,链接器将运行时内存地址赋值给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。

在重定位节中的符号引用中,链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行地址,这一步依赖hello.o中的重定位条目。

除此之外重定位类型分为两种,分别为R_X86_64_PC32与R_X886_64_32,这两种分别为PC相对寻址与绝对寻址。对于hello.o中使用PC相对寻址的指令使用R_X86_64_PC32类型进行重定位,而对hello.o直接引用地址的指令,采用R_X886_64_32类型进行重定位。

5.6 hello的执行流程

_dl_start_user

_dl_init

_start

_libc_start_main

_cxa_atexit

_libc_csu_init

_init

_setjmp

_sigsetjmp

_sigjmp_save

main

(main后)

puts@plt

exit@plt

printf@plt

atoi@plt

sleep@plt

getchar@plt_dl_runtime_resolve_xsave

_dl_fixup

_dl_lookup_symbol_x

Exit

5.7 Hello的动态链接分析

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。

在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。

延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:

PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。

GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。

根据hello的ELF文件可知GOT起始表位置为0x404000

GOT表位置在调用dl_init之前0x404008后的16个字节均为0:

调用_start之后发生改变,0x404008后的两个8个字节分别变为:0x7ff78e2b4190、0x7ff78e29dbb0,其中GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,改变后的GOT表如下: ​

GOT[2]对应部分是共享库模块的入口点,如下:

5.8 本章小结

本章详细地讨论了链接,对hello.o文件的链接和执行流程进行了解释。阐述了hello.o是怎么链接成为一个可执行目标文件的,详细介绍了hello.o的ELF格式和各个节的含义并分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程

链接实现多个文件的合并,最终创建一个可执行的完整的程序,到此,一个可执行程序的建立就已完成.

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

概念:进程是计算机科学中对深刻最成功的概念之一,进程是操作系统对一个正在运行的程序的一种抽象,进程的经典定义就是一个执行中程序的实例。系统中每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的沉痼的代码和数据,它的栈、通用目的寄存器的内容,程序计数器、环境变量以及打开文件描述符的集合。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。

作用:它提供一个假象,好像我们的程序是系统当前运行的唯一的程序一样。我们的程序好像是独占的使用处理器和内存。处理器好像无间断的一条接一条的执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。Shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,代表用户运行程序。

其基本功能是解释并运行用户的指令,重复如下处理过程:

(1)终端进程读取用户由键盘输入的命令行。

(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量

(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令

(4)如果不是内部命令,调用fork( )创建新进程/子进程

(5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。

(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait…等待作业终止后返回。

(7)如果用户要求后台运行(如果命令末尾有&号),则shell返回;

6.3 Hello的fork进程创建过程

根据shell的处理流程,输入命令执行当前目录下的可执行文件hello,由于命令行参数不是一个内置的shell命令,所以父进程通过调用fork函数创建一个新的运行的子进程。

新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得父进程打开任何文件描述符相同的副本,这意味着当父进程调用fork函数时,子进程可以读写父进程中任何打开的文件。父进程与子进程之间最大的区别在于它们拥有不同的 PID。

Fork函数制备调用一次,却返回两次;一次是在调用父进程中,一次是在创建新的子进程中。在父进程中,fork返回子进程的PID。在子进程中,fork返回0.因为子进程的pid总是非0,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中进行。

在子进程执行期间,父进程默认选项是显示等待子进程的完成。

接下来 hello 将在 fork 创建的子进程中执行。

6.4 Hello的execve过程

当创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,加载并运行需要以下几个步骤:

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

(2)映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。如图6.4

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

(4)设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

6.5 Hello的进程执行

为了使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。处理器通常使用某个控制寄存器的一个模式位提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

操作系统内核使用一中称为上下文切换的较高层形式的异常控制流来实现多任务:内核为每个进程维持一个上下文,上下文就是内核重新启动一个被抢占的进程所需的状态,它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程一打开文件的信息的文件表。

内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种调度决策,是由内核中调度器的代码处理的。当内核选择一个新的进程时,内核调度了这个过程。在内核调度了一个新的进程运行后,他就抢占当前进程,使用上下文切换机制来控制转移到新的进程,上下文切换的流程是:1.保存当前进程的上下文。2.恢复某个先前被抢占的进程被保存的上下文。3.将控制传递给这个新恢复的进程。

具体抽象如下:

(1) 一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器

(2) 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用CPU内存。

hello进程的执行是依赖于进程所提供的抽象的基础上,下面阐述操作系统所提供的的进程抽象:

①逻辑控制流::一系列程序计数器 PC 的值的序列叫做逻辑控制流,进程是轮流 使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占 (暂时挂起),然后轮到其他进程。

②并发流:一个逻辑流的执行时间与另一个流重叠,成为并发流,这两个流成为并发的运行。多个流并发的执行的一般现象成为并发。

③时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。

④私有地址空间:进程为每个流都提供一种假象,好像它是独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个内存字节是不能被其他进程读或者写的,在这个意义上,这个地址空间是私有的。

⑤用户模式和内核模式::处理器通常使用一个寄存器提供两种模式的区分,该寄 存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的 代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任 何命令,并且可以访问系统中的任何内存位置。

⑥上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。

⑦上下文切换:当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程:

1) 保存以前进程的上下文

2)恢复新恢复进程被保存的上下文,

3)将控制传递给这 个新恢复的进程 ,来完成上下文切换。

hello的进程调度过程:先执行hello程序,进程在用户态,控制暂时在hello的进程中。当输入的参数不是3个时,会调用exit函数,终止进程,shell回收hello进程; 当输入的参数时3个时,会进入循环,调用sleep函数,进程会休眠一段时间,控制可能会传递给内核,内核保存此时hello的上下文,包括i,sleepsecs的值,寄存器,栈,pc,条件码,state等的值,然后恢复要进入进程的上下文,最后将控制传递给该进程,并开始计时,在休眠了固定时间后,sleep会传送一个信号,可能会调用中断信号处理函数,将控制再传递给内核,内核保存当前进程的上下文,恢复hello进程的上下文,将控制传递给hello进程,执行hello程序。

6.6 hello的异常与信号处理

1.异常:控制流的突变,用来响应某种变化。

异常分为4类:

1)中断:异步异常,来自处理器外部的I/O设备。异常处理后会执行下一条指令

2)陷阱:同步异常,是执行系统调用函数的结果。函数调用结束后会执行下一条指令

3)故障:同步异常,由错误情况引起,如缺页,浮点异常等等。异常处理成功则重新执行该指令,否则程序终止

4)终止:同步异常,由致命错误造成。该异常将终止程序

2.信号处理:在发生异常时会产生信号,用来通知系统。

常用信号为:

3.在输入./hello 学号 姓名 循环间隔后,进程的几种处理方式:

1)程序正常退出:字符串输出8次后正常退出,进程被回收

2)Ctrl+c:使内核发送一个SIGINT信号,使程序终止,信号处理程序会回收子进程,用ps查看前台进程组发现没有hello进程。

3)随便乱按:输入字符后键入回车键会将字符串内容存入缓冲区,作为终端的下一条命令

4)Ctrl+z:程序将被暂时挂起,并没有被回收,它会等待其他信号令其继续运行

      

6.7本章小结

本章介绍了进程的相关概念和作用,了解了Shell-bash的作用与处理流程,描述了hello子进程fork和execve的过程,解释了hello的进程执行过程以及hello 的异常与信号处理。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址: 是指由程序产生的与段相关的偏移地址部分。例如,在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址,不和绝对物理地址相干。只有在Intel实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,CPU不进行自动地址转换);逻辑也就是在Intel 保护模式下程序执行代码段限长内的偏移地址(假定代码段、数据段如果完全一样)。应用程序员仅需与逻辑地址打交道,而分段和分页机制对我们来说是完全透明的,仅由系统编程人员涉及。应用程序员虽然自己可以直接操作内存,那也只能在操作系统给自己分配的内存段操作。简单说逻辑地址是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。

线性地址: 是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel 80386的线性地址空间容量为4G(2的32次方即32根地址总线寻址)。

虚拟地址:CPU启动保护模式后,程序运行在虚拟地址空间中。(但并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,Bootloader以及内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的)。如果CPU寄存器中的分页标志位被设置,那么执行内存操作的机器指令时, MMU会自动根据页目录和页表中的信息,把虚拟地址转换成物理地址,完成该指令。虚拟地址是一个抽象的概念空间,每一个虚拟地址对应于一个虚拟页,每一个虚拟页会映射一个磁盘空间的一页,如果要使用该数据,则会将该页载入内存,这样每个虚拟地址就对应于唯一的一个物理地址。

物理地址: 地址从0开始编号,顺序地每次加1,因此存储器的物理地址空间是呈线性增长的。它是用二进制数来表示的,是无符号整数,书写格式为十六进制数。它是出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果。用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。可以将内存看成一个从0字节开始的大数组,数组中每个字节拥有独有的物理地址。

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

存储地址要经过以下阶段:

逻辑地址 -------> 线性地址 -------> 物理地址

分段过程

分段过程的实质就是逻辑地址 -------> 线性地址 的过程

整个过程如下图所示:

逻辑地址实际是由 48 位组成的,前 16 位包括「段选择符」后 32 位「段内偏移量」

如何通过「段选择符」找到段基址

逻辑地址一共有 48 位。前 16 位是段选择符。

这 16 位的格式如上图。

索引:「描述符表」的索引(Index)

TI:如果 TI 是 0。「描述符表」是「全局描述符表(GDT)」,如果 TI 是 1。「描述符表」是「局部描述表(LDT)」

RPL:段的级别。为 0,位于最高级别的内核态。为 11,位于最低级别的用户态。在 linux 中也仅有这两种级别。

整体过程就是:

通过索引在描述符表中找到段基址

(其中 GDT LDT 的首地址,存在用户不可见的寄存器中)

线性地址=段基址+段内偏移量

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

概念上而言,虚拟内存被组织为一个由存放在磁盘上的 N 个连续的字节大小 的单元组成的数组。每字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上数组的内容被缓存在主存中。和存储器层次结构中其他缓存一样,磁盘(较低层) 上的数据被分割成块,这些块作为磁盘和主存(较高层)之间的传输单元。

VM 系统通过将虚拟内存分割为虚拟页的大小固定的块来处理这个问题。每个虚拟页的大小为 P = 2p 字节。类似地,物理内存被分割为物理页,大小也为 P 字节。

在任意时刻,虚拟页面的集合都被分为三个不相交的子集:已缓存、未缓存和未分配。

非分配:VM系统还未分配的页。未分配的块没有任何数据和它们像关联,因此也就不占用任何磁盘空间。

已缓存:当前已经缓存在物理内存中的已分配页。

未缓存的:未缓存在物理内存中的已分配页。

每次将虚拟地址转换为物理地址,都会查询页表来判断一个虚拟页是否缓存在DRAM的某个地方,如果不在DRAM的某个地方,通过查询页表条目可以知道虚拟页在磁盘的位置。页表将虚拟页映射到物理页。如图,页表就是一个页表条目的数组,每一个页表条目是由一个有效位和一个n为地址字段组成。有效位表明虚拟页是否缓存在DRAM中,n位地址字段是物理页的起始地址或者虚拟页在次胖的起始地址。

如图实现虚拟地址到物理地址的转换:

n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,例如VPN 0选择PTE 0。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。

这里的VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。

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

(1)TLB

正如我们看到的,每次CPU产生一个虛拟地址,MMU就必须查阅一个PTE,以便将虛拟地址翻译为物理地址。在最糟糕的情况下,这会要求从内存多取一-次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就下降到1个或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓冲器(Translation Lookaside Buffer, TLB)。

TLB是一个小的、虚拟寻址的缓存,其中每一-行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度。所示,用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。如果TLB有T=2'个组,那么TLB索引(TLBI)是由VPN的t个最低位组成的,而TLB标记(TLBT)是由VPN中剩余的位组成的。

图9展示了当TLB命中时(通常情况)所包括的步骤。这里的关键点是,所有的地

址翻译步骤都是在芯片上的MMU中执行的,因此非常快。

●第1步: CPU产生一个虚拟地址。

●第2步和第3步: MMU从TLB中取出相应的PTE。

●第4步:MMU将这个虚拟地址翻译成-一个物理地址,并且将它发送到高速缓存/主存。

●第5步:高速缓存/主存将所请求的数据字返回给CPU。

当TLB不命中时,MMU必须从L1缓存中取出相应的PTE,如图所示。新取出的PTE存放在TLB中,可能会覆盖一个已经存在的条目。

(2)四级页表支持下的VA到PA的变换

Intel Core i7 环境下研究 VA 到 PA 的地址翻译问题。前提如下:虚拟地址空间 48 位,物理地址空间 52 位,页表大小 4KB,4级页表。TLB 4 路 16 组相联。CR3 指向第一级页表的起始位置。

CPU 产生虚拟地址 VA,VA 传送给 MMU,MMU 使用前 36 位 VPN 作为 TLBT(前 32 位)+TLBI(后 4 位)向 TLB 中匹配,如果命中,则得到 PPN (40bit)与 VPO(12bit)组合成 PA(52bit)。 如果 TLB 中没有命中,MMU 向页表中查询,CR3 确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查 询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目。如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。

如图

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

Core i7就是通过四级页表翻译地址,三级cache访问物理内存,如下图所示:

如何访问物理内存:

1、CPU给出VA

2、MMU用VPN到TLB中找寻PTE,若命中,得到PA;若不命中,利用VPN(多级页表机制)到内存中找到对应的物理页面,得到PA。

3、PA分成PPN和PPO两部分。利用其中的PPO,将其分成CI和CO,CI作为cache组索引,CO作为块偏移,PPN作为tag。

先访问一级缓存,不命中时访问二级缓存,再不命中访问三级缓存,再不命中访问主存,如果主存缺页则访问硬盘

如图

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的pid。为了给这个新进程创建虚拟内存。它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。在这两个进程中的任一个后来进行写操作时,写时赋值机制就会创建新页面。

7.7 hello进程execve时的内存映射

execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。加载并运行 hello 需要以下几个步骤:

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

●映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为a.out文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在a.out中。栈和堆区域也是请求二进制零的,初始长度为零。图9-31概括了私有区域的不同映射。

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

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

如图

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

假设MMU在试图翻译某个虛拟地址A时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序随后就执行下面的步骤:

1)虚拟地址A是合法的吗?换句话说,A在某个区域结构定义的区域内吗?为了回答这个问题,缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_ end做比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。这个情况在图9-28中标识为“1”。因为一个进程可以创建任意数量的新虚拟内存区域(使用在下一节中描述的mmap函数),所以顺序搜索区域结构的链表花销可能会很大。因此在实际中,Linux 使用某些我们没有显示出来的字段,Linux在链表中构建了一棵树,并在这棵树上进行查找。

2)试图进行的内存访问是否合法?换句话说,进程是否有读、写或者执行这个区域内页面的权限?例如,这个缺页是不是由一条试图对这个代码段里的只读页面进行写操作的存储指令造成的?这个缺页是不是因为一个运行在用户模式中的进程试图从内核虚拟内存中读取字造成的?如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。这种情况在图9-28中标识为“2” 。

3)此刻,内核知道了这个缺页是由于对合法的虚拟地址进行合法的操作造成的。它是这样来处理这个缺页的:选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换人新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU。这次,MMU就能正常地翻译A,而不会再产生缺页中断了。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虛拟内存区域,称为堆(heap)(见图9-33)。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk(读做“break"), 它指向堆的顶部。

分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。

●显式分配器(explicit allocator), 要求应用显式地释放任何已分配的块。

●隐式分配器(implicit allocator), 另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbagecollec-tor),而自动释放未使用的已分配的块的过程叫做垃圾收集(garbage collection)。

以显示分配器举例:

基本方法:

隐式空闲链表: 每个块包含一个4字的头部和4字的相同信息的尾部,分别在块的开始和结尾,其中存储了块的大小和块的分配位.(如果是双字对齐,后3位储存分配位),分配位为1则表示该块已分配;位0则表示该块式空闲的。每次寻找会根据的堆的头部分配位判断是否是空闲的,大小值判断是否适合存储,并根据大小信息找到一个块的块头。尾部则是在空闲块合并时,提供前一个块的大小和是否位空闲块的信息。

显示空闲链表:在空闲块中,除了头部和尾部,还存在指向前一个空闲块和后一个空闲块的各一个指针,通过指针可以找到所有的空闲块。

策略

首次分配:每一次分配,都从堆的起点开始寻找空闲块,直到找到一个可以存储的下的空闲块,就分配该块.

下次分配:每次分配都从上次的地方开始寻找空闲块,一旦找到可以存储的下的空间块,就分配该块.

最佳分配:遍历所有的空闲块,找到可以存储的下,且最小的块进行分配

所有的分配中,如果找不到合适的块,就会调用extend函数,扩大堆,增大brk

7.10本章小结

本章主要介绍了hello 的存储器地址空间、段页式管理,TLB以及VA 到PA 的地址翻译、物理内存访问,还介绍了hello进程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.Unix IO接口:

●打开文件:一个应用程序通过要求内核打开相应的文件,宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符。它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。

●shell创建的每个进程开始时都有三个打开的文件:标准输入、标准输出和标准错误。

●改变当前的文件位置:对于每个打开的文件,内核保持着一个文件的位置k,初始为0,这个文件的位置是从文件开头起始的字符偏移量。应用程序能够通过执行seek操作,将文件的当前位置设置为k。

●读写文件:读操作就是从文件中复制n>0个字节到内存中,从当前文件位置k开始,然后将k增加到k+n。而写操作就是从内存复制字节到文件中。

●关闭文件:当应用完成了对文件的访问后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

2.Unix IO函数:

open 函数:

功能描述:用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。

函数原型:int open(const char *pathname,int flags,int perms)

参数:pathname:被打开的文件名(可包括路径名如"dev/ttyS0")flags:文件打开方式。

close函数:

功能描述:用于关闭一个被打开的的文件。

所需头文件:#include

函数原型:int close(int fd)

参数:fd文件描述符。

返回值:0成功,-1出错。

read函数:

功能描述:从文件读取数据。

所需头文件:#include

函数原型:ssize_t read(int fd, void *buf, size_t count);

参数:fd:将要读取数据的文件描述词。buf:指缓冲区,即读取的数据会被放到这个缓冲区中去。count: 表示调用一次read操作,应该读多少数量的字符。

返回值:返回所读取的字节数;0(读到EOF);-1(出错)。

write函数:

功能描述:向文件写入数据。
所需头文件:#include
函数原型:ssize_t write(int fd, void *buf, size_t count);
返回值:写入文件的字节数(成功);-1(出错)。

stat函数:

功能描述:检索到关于文件的信息(有时也称为文件的元数据(metadata))。

所需头文件:#include ,#include

函数原型:int stat (const char*f ilename ,struct stat *buf) ;

参数:filename;文件名。buf:结构

返回值:成功:返回0;失败:返回-1。

Iseek函数:

功能描述:用于在指定的文件描述符中将将文件指针定位到相应位置。

所需头文件:#include ,#include

函数原型:off_t lseek(int fd, off_t offset,int whence);

参数:fd;文件描述符。offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移)。

返回值:成功:返回当前位移;失败:返回-1。

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;
    }

由vsprintf生成显示信息.对字符串进行格式化,他接受输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出,返回字符串的长度,将长度储存在变量i中.

从vsprintf代码中可以看到当*fmt是’%’,会进行相应格式的替换,这里只进行十六进制数输出的替换

接着调用write函数将修改好的格式化字符串和字符串长度传递给他.

write的部分运行如下: write:

mov eax, _NR_write

mov ebx, [esp + 4]

mov ecx, [esp + 8]

int INT_VECTOR_SYS_CALL

在 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就是完成不断地打印出字符,直到遇到:’\0’

在hello程序中syscall 将字符串中的字节“Hello 1190200927 朱永燊”从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的 ASCII 码。

字符显示驱动子程序将通过 ASCII 码在字模库中找到点阵信息将点阵信息存储到 vram 中。

显示芯片会按照一定的刷新频率逐行读取 vram,并通过信号线向液晶显示器传输每一个点(RGB 分量)。于是 “Hello 1190200927 朱永燊”就显示在了屏幕上。

8.4 getchar的实现分析

getchar代码部分:

int getchar(void)

{

static char buf[BUFSIZ];

static char* bb=buf;

static int n=0;

if(n==0)

{

n=read(0,buf,BUFSIZ);

bb=buf;

}

return (–n>=0)?(unsigned char)*bb++:EOF;

}

调用getchar函数,程序发生陷阱异常,会发生上下问切换,控制转移到别的进程中。

当用户按下键盘,会产生中断,触发异步异常-键盘中断的处理:键盘中断处理子程序,接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar调用read系统函数,通过系统调用从stdin流中读取一个按键ascii码,。直到用户按下回车建,才会返回,如果读取的使EOF,则返回-1,否则返回这个ascii码返回。

8.5本章小结

本章介绍了 Linux 中 I/O 设备的管理方法,Unix I/O 接口和函数,并且做了有关printf 和 getchar 函数的Unix I/O 实现分析。

(第81分)

结论

Hello程序的一生:

1.被程序员编写成程序源代码,该文件称为hello.c

2.Hello.c文件中的编译预处理指令所指内容经过预处理器被穿插在程序源代码中,成为修改过的源程序,称为hello.i

3.Hello.i经过编译器生成代表程序执行顺序的指令语言——汇编程序,其文件称为hello.s

4.hello.s经过汇编器翻译成可重定位目标文件,用于之后的模块整合

5.Hello.o经过与其他目标模块整合,形成可执行目标文件hello

6.创建进程:在shell利用./hello运行hello程序,父进程通过fork函数为hello创建进程

7.加载程序:通过加载器,调用execve函数,删除原来的进程内容,加载我们现在进程的代码,数据得到进程自己的虚拟内存空间。

8.执行指令:CPU取指令,顺序执行进程的逻辑控制流。这里CPU会给出一个虚拟地址,通过MMU从页表里得到物理地址, 在通过这个物理地址去cache或者内存里得到我们想要的信息

9.异常(信号):程序执行过程中,如果从键盘输入Ctrl-C等命令,会给进程发送一个信号,然后通过信号处理函数对信号进行处理。

10.结束:程序执行结束后,父进程回收子进程,内核删除为这个进程创建的所有数据结构

计算机程序的执行是一个复杂的过程,我们学习的只能说是看到了计算机复杂机制的冰山一角。我们只是从比较宽泛,普遍的角度来遵循一个程序的开始到结束。计算机本身的运行机制也是十分复杂,从基础的门电路,到一些基本的逻辑部件,再到由许多部件组成的集成电路,通过这些硬件大厦来构建操作系统与应用软件。未知的知识还有许多。总而言之,计算机世界的大门才刚刚打开,我们需要学习的事物还有很多。

(结论0分,缺失 -1分,根据内容酌情加分)

附件

1.hello.c   程序的原始代码

2.hello.i   将编译预处理指令内容插入源代码后的文件

3.hello.s   记录汇编代码的汇编文件

4.hello.o   可重定位目标文件

5.hello   可执行目标文件

6.hello.elf   hello的ELF格式文件

(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等

[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.

(参考文献0分,缺失 -1分)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值