HITICS-2018大作业报告

 

摘  要

关键词:p2p;020;hello

本文主要是讲述hello从最开始的代码文件,到最后在屏幕上输出了要输出的东西,最终结束的过程。From Program to Process,From Zero-0 to Zero-0,细致讲述这其中看似简单但是又紧紧有条的内部过程。Hello程序虽然是最简单的,新手一定会使用的一组代码,但是其中却蕴含了完整有序的程序生成过程。这其中调用了很多系统本身的函数,有许多涉及到系统逻辑的本质,虽然我还是第一次学习,但是还是尽力的讲解了这其中的道理。

 

 

目  录

 

第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:首先先把hello.c的代码打好,存入主存里面。然后再经过cpp的预处理、ccl的编译、as的汇编、ld的链接之后,生成一个可执行文件。然后再bash里面输入./hello,fork会先生成它的子进程。这样就从program变成了process

 

020:通过execve映射虚拟内存,通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk), 新的代码和数据段被初始化为可执行文件的内容,当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。

1.2 环境与工具

1.2.1 硬件环境

X64 CPU;2GHz;2G RAM;256GHD Disk

1.2.2 软件环境

Windows7 64位;VirtualBox/Vmware 11;Ubuntu 16.04 LTS 64位/优麒麟 64位;

1.2.3 开发工具

Visual Studio 2010 64位以上;CodeBlocks;vi/vim/gpedit+gcc

 1.3 中间结果

Hello            hello.o链接之后生成的可执行文件

       Hello.c          hello程序的源C代码

       Hello.elf       临时文件,用来观察hello的readelf和hello.o的readelf形式

       Hello.i          预处理之后的文件

       Hello.o         汇编之后的文件

       Hello.s          编译之后的文件

       Hello.txt       临时文件hello的反汇编代码

       Helloo.txt     临时文件hello.o的反汇编代码

1.4 本章小结

准备工作,为探索hello的一生做出充分的准备

 

第2章 预处理

2.1 预处理的概念与作用

概念:预处理主要是指cpp把头文件,定义的宏之类的,通过修改c文件,把他们变成一个完整的c文件。(就是所有带#开头的)

作用:

按照ANSI标准的定义,预处理程序应该处理以下指令:

#if #ifdef #ifndef #else #elif

#endif

#define

#undef

#line

#error

#pragma

#include

2.2在Ubuntu下预处理的命令

       Cpp hello.c > hello.i(居然还处理了半天…)

2.3 Hello的预处理结果解析

只能截一部分图。但是可以看出来,hello.i的行数明显比hello.c多了不少,因为把头文件已经合并进来了,箭头指的是hello.c的开头,可以注意到,注释已经被删除了,源代码没有进行修改。箭头上方看出来是在合并stdlib.h,就举个例子,其他头文件的合并就不截图展示了。

2.4 本章小结

预处理是一个很重要的过程,因为一般打的.c代码,会调用许多并不在文件本身的函数代码,合并这样的文本文件可以使之后的调用不受阻碍,完善文本。

(第2章0.5分)

 

第3章 编译

3.1 编译的概念与作用

概念:编译器把.i文件编译成.s,这个过程叫做编译

作用:就是把与处理好的文件,通过编译器内置的语法要求,翻译成汇编代码的文件,方便进行下一步机器码的操作。

3.2 在Ubuntu下编译的命令

       gcc -S hello.i -o hello.s

      

3.3 Hello的编译结果解析

1.数据:

在hello.c里面定义了一个全局变量sleepsecs,类型是int 大小是2.5

我们可以看出来,这个人c语言没学好,你这个int类型赋值成2.5有什么用呢

2.赋值:

=2.5,当然int赋值成2.5没有什么用

你看把编译器吓得,只能赋值一个2还是通过long类型赋值的

还有一个赋值是在for循环里面有个i=0

这个赋值就比较正常,编译成了这样

3. 类型转换(隐式或显式)

就是上面赋值说的,将2.5这个本应该是long类型的数,赋值到int类型的参数里面的时候,隐式转换成了2赋值到里面。遵循保留整数的原则,把小数位直接省略了。

4.算术操作:

在for循环里面有一个i++是算术操作

编译成了addl $1, -4(%rbp)

5.关系操作:

在for循环里面,i<10是关系操作,就不单独截图了。

Argc!=3也是关系操作,单独截一下图。

编译之后:i<10

它运用了计算方法更快的方法,把<10变成了<=9可以减少一次判断,这是程序自我优化的体现。

编译之后:argc!=3

普通的编译。

6.数组/指针/结构操作:

For循环里的printf里面涉及到了数组,虽然不知道这数组哪来的,也没有定义,但是它不报错就行吧。

编译成了:

因为这个数组其实在int main的传参里面,虽然这整个代码里也没其他函数了…所以用到了这些个寄存器。

7.控制转移:

这是一种对if,for这些语言的一种编译方式,一般都是je jle之类的,在原本的代码里面其实没有明确的体现。

这里面je jle jmp 是控制转移指令。值得注意的是其中jmp是无条件转移,剩下的两个都是有条件的。

8.函数操作:

涉及到的函数有,printf两个,sleep一个,没有一个内部函数…

printf被编译成了:

这三个call就是函数的调用指令,中间有个叛徒call exit 这是源代码中exit(0)的编译

Sleep函数被编译成了:

还有几个函数,getchar(),主函数main其实也是一种函数调用

是这么编译的。

3.4 本章小结

这就是编译的主要类型操作,这都是编译器内部操作的,根据.c的代码映射成.s文件,将高级语言转换成汇编语言之后,这种代码当然不如高级语言好看,但是对于机器来说,可以获得更快的处理速度。

通过这些代码,能看出来汇编代码会直接调用寄存器和存储器,而不是对数据进行处理,所以可以解释为什么汇编代码会更快一些。

(第32分)

 

第4章 汇编

4.1 汇编的概念与作用

汇编器(as)将.s汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中,.o文件是一个二进制文件,它包含程序的指令编码。这个过程称为汇编,亦即汇编的作用。

4.2 在Ubuntu下汇编的命令

gcc –c hello.s –o hello.o

4.3 可重定位目标elf格式

利用readelf来看hello.o文件,因为正常打不开。下面是详细信息

ELF Header

节点信息:

重定位节

4.4 Hello.o的结果解析

      

大概对比一下,最直观的就是,短了…明显hello.s更长,因为一篇都放不下。

反汇编代码跳转指令的操作数使用的不是段名称如.L3,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。

为每条语句加上了具体的地址,全局变量和常量都被安排到了具体的地址里面。

操作数在hello.s里面都是十进制,在到hello.o里面的机器级程序时都是十六进制。在.s文件中,访问rodata(printf中的字符串),使用段名称+%rip,在反汇编代码中0+%rip,因为rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。

4.5 本章小结

汇编的作用是把代码变成机器语言,机器语言(machine language)是一种指令集的体系。这种指令集,称机器码(machine code),是电脑的CPU可直接解读的数据。

机器码有时也被称为原生码(Native Code),这个名词比较强调某种编程语言或库,它与运行平台相关的部份。

优    点 :直接执行,速度快,资源占用少

缺    点 :可读性、可移植性差,编程繁杂

(第41分)

 

5章 链接

5.1 链接的概念与作用

Linux 链接分两种,一种被称为硬链接(Hard Link),另一种被称为符号链接(Symbolic Link)。默认情况下,ln 命令产生硬链接。

硬连接

    硬连接指通过索引节点来进行连接。在 Linux 的文件系统中,保存在磁盘分区中的文件不管是什么类型都给它分配一个编号,称为索引节点号(Inode Index)。在 Linux 中,多个文件名指向同一索引节点是存在的。比如:A 是 B 的硬链接(A 和 B 都是文件名),则 A 的目录项中的 inode 节点号与 B 的目录项中的 inode 节点号相同,即一个 inode 节点对应两个不同的文件名,两个文件名指向同一个文件,A 和 B 对文件系统来说是完全平等的。删除其中任何一个都不会影响另外一个的访问。

    硬连接的作用是允许一个文件拥有多个有效路径名,这样用户就可以建立硬连接到重要文件,以防止“误删”的功能。其原因如上所述,因为对应该目录的索引节点有一个以上的连接。只删除一个连接并不影响索引节点本身和其它的连接,只有当最后一个连接被删除后,文件的数据块及目录的连接才会被释放。也就是说,文件真正删除的条件是与之相关的所有硬连接文件均被删除。

软连接

    另外一种连接称之为符号连接(Symbolic Link),也叫软连接。软链接文件有类似于 Windows 的快捷方式。它实际上是一个特殊的文件。在符号连接中,文件实际上是一个文本文件,其中包含的有另一文件的位置信息。比如:A 是 B 的软链接(A 和 B 都是文件名),A 的目录项中的 inode 节点号与 B 的目录项中的 inode 节点号不相同,A 和 B 指向的是两个不同的 inode,继而指向两块不同的数据块。但是 A 的数据块中存放的只是 B 的路径名(可以根据这个找到 B 的目录项)。A 和 B 之间是“主从”关系,如果 B 被删除了,A 仍然存在(因为两个是不同的文件),但指向的是一个无效的链接。

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

   

这是Section Headers的详细信息,里面有所有的节信息,名字,大小,程序中的偏移量,虚拟地址的起始地址。

5.4 hello的虚拟地址空间

虚拟地址是在elf中address部分的

我的edb出了点问题

不过能确定的是在edb中对虚拟地址空间还是有显示的,而且更具体一点。

在下面可以看出,程序包含8个段:

PHDR保存程序头表。

INTERP指定在程序已经从可执行文件映射到内存之后,必须调用的解释器(如动态链接器)。

LOAD表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串)、程序的目标代码等。

DYNAMIC保存了由动态链接器使用的信息。

NOTE保存辅助信息。

GNU_STACK:权限标志,标志栈是否是可执行的。

GNU_RELRO:指定在重定位结束之后那些内存区域是需要设置只读。

5.5 链接的重定位过程分析

首先,最大的区别,hello.o的反汇编只出现了main,hello的反汇编出现了很多其他的函数。这其中包括节比如.init,还有基本的puts@plt之类的

其次就是表达的方式,从先对偏移地址变成了虚拟内存地址

Hello中是从hello.elf开始,而hello.o是从.text节开始

过程分析:

当一个源代码需要引用另一个源代码中的变量或者函数时,这个源代码并不知道该函数的地址,所以编译器先暂时随便看一个地址,一般是0,然后其他的函数在使用偏移量。而链接器在完成地址和空间的分配之后就可以确定所有符号的虚拟地址,然后就可以根据符号的地址对每个需要重定位的指令进行地址修正。链接器在这块之所以能知道哪个指令需要修正,因为在ELF文件中,包含有一个重定位表,他的作用是描述如何修改相应的段中的内容。例如代码段.text有需要被重定位的地方,那么会有一个相应的叫.rel.text的段保存代码段的重定位表。重定位表的结构是一个结构体类型的数组,每个数组元素对应一个重定位入口。

5.6 hello的执行流程

因为edb不太好使,所以我用gdb手动运行,只能直接写结果了

_dl_start 0x00007fff6f0674a0)

0x00007f0625d5e630 <ld-2.27.so!_dl_init+0>

hello!_start 0x400500

libc-2.27.so!__libc_start_main 0x7fce 8c867ab0

-libc-2.27.so!__cxa_atexit 0x7fce 8c889430

-libc-2.27.so!__libc_csu_init 0x4005c0

hello!_init 0x400488

libc-2.27.so!_setjmp 0x7fce 8c884c10

libc-2.27.so!_sigsetjmp 0x7fce 8c884b70

libc-2.27.so!__sigjmp_save 0x7fce 8c884bd0

hello!main 0x400532

hello!puts@plt 0x4004b0

hello!exit@plt 0x4004e0

*hello!printf@plt

*hello!sleep@plt

*hello!getchar@plt

ld-2.27.so!_dl_runtime_resolve_xsave 0x7fce 8cc4e680

ld-2.27.so!_dl_fixup 0x7fce 8cc46df0

ld-2.27.so!_dl_lookup_symbol_x 0x7fce 8cc420b0

libc-2.27.so!exit 0x7fce 8c889128  

5.7 Hello的动态链接分析

这是dl_init之前的截图

  

这是dl_init之后的截图

在dl_init调用之后0x601008和0x601010处的两个8B数据分别发生改变为0x7fd9 d3925170和0x7fd9 d3713680。

5.8 本章小结

       在本章中主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。

       链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译(separate compilation)成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

 

6章 hello进程管理

6.1 进程的概念与作用

概念:

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体

作用:

每次用户通过向shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。

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

Shell是一个命令行解释器,为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用shell来启动、挂起、停止甚至是编写一些程序。

Shell还是一个功能相当强大的编程语言,易编写,易调试,灵活性较强。Shell是解释执行的脚本语言,在shell中可以直接调用Linux命令。

shell 执行一系列的读/求值(read /evaluate ) 步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。

6.3 Hello的fork进程创建过程

fork函数用于从已存在进程中创建一个新进程,新进程成为子进程,原进程成为父进程。这两个进程分别返回他们各自的返回值,其中父进程的返回值是子进程的进程号,子进程则返回0,因此返回值大于0标识父进程,等于0标识子进程。所以我们可以通过返回值来判定该进程是父进程还是子进程。

在输入./hello之后,因为hello不是一个内置命令,所以就会去找hello这个可执行文件,然后就会调用fork,创造一个子程序,这个子程序和父程序差不多,并且使用相同的虚拟地址。但他们的pid不同,在子程序运行时,父程序会等待子程序运行直到结束。

6.4 Hello的execve过程

       execve()函数创建一个新进程。这个系统调用会销毁所有的内存段去重新创建一个新的内存段。然后,execve()需要一个可执行文件或者脚本作为参数,这和fork()有所不同。注意,execve()和fork()创建的进程都是运行进程的子进程。

       Execve调用之后,会运行一个hello程序,通过调用本身在内存中的启动加载器,执行hello程序。加载器会删除现有的虚拟内存段,新建一组并初始化新的堆栈为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。

与fork 一次调用返回两次不同, execve 调用一次并从不返回。

6.5 Hello的进程执行

时间片:简单来说就是CPU分配给各个程序的时间,使各个程序从表面上看是同时进行的,而不会造成CPU资源浪费。

上下文:当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够必得到切换时的状态执行下去。在LINUX中,当前进程上下文均保存在进程的任务数据结构中。在发生中断时,内核就在被中断进程的上下文中,在内核态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中断服务结束时能恢复被中断进程的执行。

用户模式和内核模式:内核模式下运行的程序能够访问所有的内存,能够处理中断;用户模式的程序只能访问有限的内存,不能直接处理中断。

处理器总处于以下状态中的一种:

1、内核态,运行于进程上下文,内核代表进程运行于内核空间;

2、内核态,运行于中断上下文,内核代表硬件运行于内核空间;

3、用户态,运行于用户空间。

      

Linux 系统中的每个程序都运行在一个进程上下文中,有自己的虚拟地址空间。当shell 运行一个程序时,父shell 进程生成一个子进程,它是父进程的一个复制。子进程通过execve 系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk), 新的代码和数据段袚初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用应用程序的main 函数。

hello初始运行在用户模式,在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时时(2.5secs)发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。

6.6 hello的异常与信号处理

正常运行:程序好像不会自己结束,因为最后有一个getchar

      

乱按:

可以发现对进程没有任何影响,而且输入的字符被扔到了缓存里,对getchar也没有任何影响。但是当乱按回车时,虽然在运行时没有影响,但是会影响getchar,在getchar读取完第一个回车后,之后输入的指令会被当做shell命令,直到下一个回车,就是下一个指令。

       Ctrl+C:很普通的终止了。

      

       Ctrl+Z:有各种各样的指令,Ctrl Z本身是挂起程序的意思,fg是继续运行,kill是杀掉程序都一一截图

       Pstree

       剩下的:

6.7本章小结

这个程序终于完事了…讲述了进程的定义和作用,fork,execve的过程和作用,还有异常信号的处理。让我了解到了更多关于进程从运行到结束的过程,也了解了信号的机制,尤其是这个进程在遇到中断信号的复杂过程。

(第61分)

 

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:逻辑地址(LogicalAddress)是指由程序产生的与段相关的偏移地址部分。也就是hello.o中的偏移地址。

线性地址:线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。

虚拟地址:虚拟地址是Windows程序时运行在386保护模式下,这样程序访问存储器所使用的逻辑地址称为虚拟地址,与实地址模式下的分段地址类似,虚拟地址也可以写为“段:偏移量”的形式,这里的段是指段选择器。也就是hello中的虚拟内存地址。

物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。也就是hello中虚拟内存地址对应的物理地址。

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

1)基本原理。

在段式存储管理中,将程序的地址空间划分为若干个段(segment),这样每个进程有一个二维的地址空间。在前面所介绍的动态分区分配方式中,系统为整个进程分配一个连续的内存空间。而在段式存储管理系统中,则为每个段分配一个连续的分区,而进程中的各个段可以不连续地存放在内存的不同分区中。程序加载时,操作系统为所有段分配其所需内存,这些段不必连续,物理内存的管理采用动态分区的管理方法。在为某个段分配物理内存时,可以采用首先适配法、下次适配法、最佳适配法等方法。在回收某个段所占用的空间时,要注意将收回的空间与其相邻的空间合并。段式存储管理也需要硬件支持,实现逻辑地址到物理地址的映射。程序通过分段划分为多个模块,如代码段、数据段、共享段。这样做的优点是:可以分别编写和编译源程序的一个文件,并且可以针对不同类型的段采取不同的保护,也可以按段为单位来进行共享。总的来说,段式存储管理的优点是:没有内碎片,外碎片可以通过内存紧缩来消除;便于实现内存共享。缺点与页式存储管理的缺点相同,进程必须全部装入内存。

2)段式管理的数据结构。

为了实现段式管理,操作系统需要如下的数据结构来实现进程的地址空间到物理内存空间的映射,并跟踪物理内存的使用情况,以便在装入新的段的时候,合理地分配内存空间。

·进程段表:描述组成进程地址空间的各段,可以是指向系统段表中表项的索引。每段有段基址(baseaddress)。

·系统段表:系统所有占用段。

·空闲段表:内存中所有空闲段,可以结合到系统段表中。

3)段式管理的地址变换。

在段式管理系统中,整个进程的地址空间是二维的,即其逻辑地址由段号和段内地址两部分组成。为了完成进程逻辑地址到物理地址的映射,处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到实际的物理地址。这个过程也是由处理器的硬件直接完成的,操作系统只需在进程切换时,将进程段表的首地址装入处理器的特定寄存器当中。这个寄存器一般被称作段表地址寄存器。

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

概念

1、  线性地址页:从管理和效率的角度出发,线性地址被分为固定长度的组,称为页(page)。例如32位机器,线性地址最大可为4G,如果用4KB为页容量,这样将线性地址划分为2^20个页。

2、  物理页:另一类“页”,称为“物理页”,或者是“页框、页帧”。分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与线性地址页是相同。

如何将两者之间的映射?通过页式管理实现映射。

 

页式管理具体流程:

说明:

1、  分页单元中,页目录的地址放在CPU的CR3寄存器中,是进行地址转换的起始点。

2、  每个进程,都有其独立的虚拟地址空间,运行一个进程,首先需要将它的页目录地址放到CR3寄存器中,将其他进程保存下来。

3、  每一个32位的线性地址被划分三部分:页目录索引(10位):页表索引(10位):偏移(12位)

下面是地址转换的步骤:

第一步:装入进程的页目录地址(操作系统在调度进程时,把这个地址装入CR3)

第二步:根据线性地址前十位,在页目录中,找到对应的索引项 即页表地址。

第三步:根据线性地址中间十位,在页表中,找到对应的索引项 即页的起始地址。

第四步:将页的起始地址与线性地址最后12位相加,等到物理地址。

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

Core i7 MMU 如何使用四级的页表来将虚拟地址翻译成物理地址。36位VPN 被划分成四个9 位的片,每个片被用作到一个页表的偏移量。CR3 寄存器包含Ll页表的物理地址。VPN 1 提供到一个Ll PET 的偏移量,这个PTE 包含L2 页表的基地址。VPN 2 提供到一个L2 PTE 的偏移量,以此类推。

 

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

使用CI(后六位再后六位)进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO(后六位)取出数据返回。

       如果没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU进行替换。

7.6 hello进程fork时的内存映射

所谓的内存映射就是把物理内存映射到进程的地址空间之内,这些应用程序就可以直接使用输入输出的地址空间,从而提高读写的效率。Linux提供了mmap()函数,用来映射物理内存。在驱动程序中,应用程序以设备文件为对象,调用mmap()函数,内核进行内存映射的准备工作,生成vm_area_struct结构体,然后调用设备驱动程序中定义的mmap函数。

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

7.7 hello进程execve时的内存映射

用户空间,放弃原来的用户空间(子进程可能有自己的页面,或者就是通过指针共享了父进程的页面)这些一律放弃,将进程控制块task_struct中对用户空间的描述的数据结构mm_struct的下属结构vma全部置简而言之就是现在子进程的用户空间是个空架子,一个页面也没有,父进程空间被放弃。

 

进程控制块task_struct中有file的指针记录了进程打开的文件信息,子进程对继承到的文件采取关闭应当关闭的信息。file的数据结构中有位图记录了应当关闭的文件,子进程放弃这些文件。一般来说,执行的效果是除了标准输入文件,标准输出文件,标准错误输出文件。其它的文件都不会被子进程继承。(标准输入一般就是键盘,标准输出就是显示器。因此如果子进程有打印语句的话,那么他的打印出来的字符会打印到父进程打印的地方,前面写文章有点错误,我已经改掉了)。

至此我们已经做了的实际动作就是信号处理表,用户空间和文件。但用户空间是个空架子,真正的程序代码没载入,数据段也没载入,堆栈没有开辟,执行参数和环境变量也没有被印射。但可以知道,每个可执行文件的载入是不同的,比如linux下shell文件和a.out文件2个有很明显的不同,你可以对他们采用同样的载入办法吗。下面就是各个代理人自己开始为自己的代理方申请空间,准备用户内存。最后调用 start_thread 开始启动进程。

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

进程线性地址空间里的页面不必常驻内存,在执行一条指令时,如果发现他要访问的页没有在内存中(即存在位为0),那么停止该指令的执行,并产生一个页不存在的异常,对应的故障处理程序可通过从外存加载该页的方法来排除故障,之后,原先引起的异常的指令就可以继续执行,而不再产生异常。

对于用户空间的缺页中断,则会调用函数_do_page_fault.

 

首先从CPU的控制寄存器CR2中读出出错的地址address,然后调用find_vma(),在进程的虚拟地址空间中找出结束地址大于address的第一个区间,如果找不到的话,则说明中断是由地址越界引起的,转到bad_area执行相关错误处理;

 

确定并非地址越界后,控制转向标号good_area。在这里,代码首先对页面进行例行权限检查,比如当前的操作是否违反该页面的Read,Write,Exec权限等。如果通过检查,则进入虚拟管理例程handle_mm_fault().否则,将与地址越界一样,转到bad_area继续处理。

 

handle_mm_fault()用于实现页面分配与交换,它分为两个步骤:首先,如果页表不存在或被交换出,则要首先分配页面给页表;然后才真正实施页面的分配,并在页表上做记录。具体如何分配这个页框是通过调用handle_pte_fault()完成的。

 

handle_pte_fault()函数根据页表项pte所描述的物理页框是否在物理内存中,分为两大类:

 

(1)请求调页:被访问的页框不在主存中,那么此时必须分配一个页框,分为线性映射、非线性映射、swap情况下映射

 

(2)写实复制:被访问的页存在,但是该页是只读的,内核需要对该页进行写操作,此时内核将这个已存在的只读页中的数据复制到一个新的页框中

 

handle_pte_fault()调用pte_non()检查表项是否为空,即全为0;如果为空就说明映射尚未建立,此时调用do_no_page()来建立内存页面与交换文件的映射;反之,如果表项非空,说明页面已经映射,只要调用do_swap_page()将其换入内存即可。

 

7.9动态存储分配管理

动态存储分配方式

动态存储分配方式是不一次性将整个程序装入到主存中。可根据执行的需要,部分地动态装入。同时,在装入主存的程序不执行时,系统可以收回该程序所占据的主存空间。再者,用户程序装入主存后的位置,在运行期间可根据系统需要而发生改变。此外,用户程序在运行期间也可动态地申请存储空间以满足程序需求。由此可见,动态存储分配方式在存储空间的分配和释放上,表现得十分灵活,现代的操作系统常采用这种存储方式。

malloc()函数其实就在内存中找一片指定大小的空间,然后将这个空间的首地址范围给一个指针变量,这里的指针变量可以是一个单独的指针,也可以是一个数组的首地址,这要看malloc()函数中参数size的具体内容。我们这里malloc分配的内存空间在逻辑上连续的,而在物理上可以连续也可以不连续。对于我们程序员来说,我们关注的是逻辑上的连续,因为操作系统会帮我们安排内存分配,所以我们使用起来就可以当做是连续的。

7.10本章小结

本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core7在指定环境下介绍了VA到PA的变换、物理内存访问,还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

这里就是计算机系统中一个基本而持久的思想:如果理解了系统是如何将数据在存储器层次结构中上上下下移动的,那么就可以编写自己的应用程序,使得它们的数据项存储在层次结构中较高的地方,在那里CPU 能更快地访问到它们。

       本章通过对hello在储存结构,高速缓存,虚拟内存涉及到的方面进行了详细的探索,通过对这些结构的了解我们可以以后编写一些对高速缓存友好的代码,或者说运行速度更快的代码,对我们来说都是受益匪浅。

(第7 2分)

 

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

UNIX系统将所有的外部设备都看作一个文件来看待,所有打开的文件都通过文件描述符来引用。文件描述符是一个非负整数,它指向内核中的一个结构体。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。而对于一个socket的读写也会有相应的文件描述符,称为socketfd(socket描述符)。

在UNIX系统中,I/O输入操作(例如标准输入或者套接字的输入)通常包含以下两个不同的阶段:

等待数据准备好

从内核向进程复制数据

例如对于套接字的输入,第一步是等待数据从网络中到达,当所等待的数据到达时,数据被复制到内核中的缓冲区。第二步则是把数据从内核缓冲区复制到应用进程的缓冲区。

根据在这两个不同阶段处理的不同,可以将I/O模型划分为以下五种类型:

阻塞式I/O模型

非阻塞式I/O模型

I/O复用

信号驱动式I/O

异步I/O

8.2 简述Unix IO接口及其函数

read 和 write -- 最简单的读写函数

readn 和 writen -- 原子性读写操作

recvfrom 和 sendto -- 增加了目标地址和地址结构长度的参数

recv 和 send -- 允许从进程到内核传递标志

readv 和 writev -- 允许指定往其中输入数据或从其中输出数据的缓冲区

recvmsg 和 sendmsg -- 结合了其他IO函数的所有特性,并具备接受和发送辅助数据的能力

Unix I/O接口:

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

2.Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。

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

4.读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号” 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。

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

8.3 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的代码实现,这其中有va系列函数,再看看va系列函数的代码。

va_start,函数名称,读取可变参数的过程其实就是在堆栈中,使用指针,遍历堆栈段中的参数列表,从低地址到高地址一个一个地把参数内容读出来的过程·

VA_LIST 是在C语言中解决变参问题的一组宏,所在头文件:#include <stdarg.h>,用于获取不确定个数的参数。

Va_end就是还原堆栈之前的状态,上面三个函数必须配合使用,不然程序可能瘫痪。

       还有就是vsprintf函数,分析函数代码

int vsprintf(char *buf, const char *fmt, va_list args)

   {

    char* p;

    char tmp[256];

    va_list p_next_arg = args;

  

    for (p=buf;*fmt;fmt++) {

    if (*fmt != '%') {

    *p++ = *fmt;

    continue;

    }

  

    fmt++;

  

    switch (*fmt) {

    case 'x':

    itoa(tmp, *((int*)p_next_arg));

    strcpy(p, tmp);

    p_next_arg += 4;

    p += strlen(tmp);

    break;

    case 's':

    break;

    default:

    break;

    }

    }

   

    return (p - buf);

   }

则知道vsprintf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。

       最后就是write函数。

write:

 

    mov eax, _NR_write

 

    mov ebx, [esp + 4]

 

    mov ecx, [esp + 8]

 

int INT_VECTOR_SYS_CALL

其中最后又系统调用了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

显示格式化了的字符串,可以看出代码里面的call是访问字库模板并且获取每一个点的RGB信息最后放入到eax也就是输出返回的应该是显示vram的值,然后系统显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar()是stdio.h中的库函数,它的作用是从stdin流中读入一个字符,也就是说,如果stdin有数据的话不用输入它就可以直接读取了,第一次getchar()时,确实需要人工的输入,但是如果你输了多个字符,以后的getchar()再执行时就会直接从缓冲区中读取了。

实际上是 输入设备->内存缓冲区->程序getchar 

你按的键是放进缓冲区了,然后供程序getchar 

键盘输入的字符都存到缓冲区内,一旦键入回车,getchar就进入缓冲区读取字符,一次只返回第一个字符作为getchar函数的值,如果有循环或足够多的getchar语句,就会依次读出缓冲区内的所有字符直到'\n'.要理解这一点,之所以你输入的一系列字符被依次读出来,是因为循环的作用使得反复利用getchar在缓冲区里读取字符,而不是getchar可以读取多个字符,事实上getchar每次只能读取一个字符.如果需要取消'\n'的影响,可以用getchar();来清除,这里getchar();只是取得了'\n'但是并没有赋给任何字符变量,所以不会有影响,相当于清除了这个字符.

8.5本章小结

本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf函数和getchar函数。了解Unix I/O 将帮助我们理解其他的系统概念。I/O 是系统操作不可或缺的一部分,因此,我们经常遇到I/O 和其他系统概念之间的循环依赖。例如, I/O 在进程的创建和执行中扮演着关键的角色。

结论

Hello的一生:

利用I/O设备,编写出hello.c存在主存中。

利用预处理器,将hello.c处理成hello.i,将调用的外部库展开合并

利用编译器,将hello.i编译成hello.s

利用汇编器,将hello.s汇编成可重定位的hello.o

再利用链接器,将系统中原本的库和hello.o链接,生成一个可执行二进制文件hello

之后Shell调用fork给hello创造一个子进程

Shell接着调用execve创造一个新进程把hello安排进去

CPU给hello分配它的时间片,hello可以执行自己的逻辑流

对于异常信号,中断信号hello都能进行对应的处理

在结束的时候,shell会回收hello的父进程子进程,然后内核删除这期间hello创造的所有数据。

来无影去无踪,这就是hello的一生,这期间花费的时间甚至只够我眨一下眼睛。

 

附件

       Hello            hello.o链接之后生成的可执行文件

       Hello.c          hello程序的源C代码

       Hello.elf       临时文件,用来观察hello的readelf和hello.o的readelf形式

       Hello.i          预处理之后的文件

       Hello.o         汇编之后的文件

       Hello.s          编译之后的文件

       Hello.txt       临时文件hello的反汇编代码

       Helloo.txt     临时文件hello.o的反汇编代码

 

参考文献

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值