程序人生-Hello’s P2P(大作业)

摘  要

本论文研究hello这个简单的c语言程序在Linux系统下的生命周期,从编译、链接、加载、运行、终止、回收的完整过程。在Ubuntu系统下对hello程序的整个生命周期进行了研究,通过对hello.c程序的深入研究,把本学期计算机系统课程所学知识梳理与回顾了一遍,加深了对计算机系统的了解。

关键词:计算机系统;程序的生命周期                           

目  录

第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简介

1.1.1 hello文本代码编写

通过一些文本编辑器,系统会将我们键盘输入的代码读入寄存器,然后存入内存,在我们进行保存后,系统就把编写好的文本文件存入磁盘中。

1.1.2使用shell程序生成可执行文件hello

我们打开shell程序并输入指令gcc hello.c -o hello,这个指令会让编译器 一次完成预处理、编译、汇编、链接四个步骤,然后你就可以看到生成好的可 执行文件了。

1.1.3使用shell运行可执行文件

打开shell程序并输入指令./hello,当然在shell的内置指令中是没有这个 指令的,shell就会将其视作一个可执行文件,使用fork函数创造一个子进程, 并利用execve函数将hello的内容加载到子进程的地址空间中,然后hello就 会在这个进程中运行。

1.1.4进程回收

当子进程运行到代码中的return语句后,进程此时是终止状态,它会向shell 程序发送信号SIGCHLD,等待系统回收这个进程,shell会调用waitpid是操 作系统回收进程,到这里hello程序的整个生命就结束了。

1.2 环境与工具

1.2.1硬件环境

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

1.2.2软件环境

Windows10 64位;Vmware 11;Ubuntu 16.04

1.2.3开发工具

Visual Studio 2010 64位;vim;gcc 9.4.0;edb1.3.0

1.3 中间结果

1.hello.c

hello程序的c语言源文件,文本文件

2.hello.i

hello.c经过预处理得到的文件,文本文件

3.hello.s

编译后产生的汇编文件,储存文件的汇编代码,文本文件

4.hello.o

汇编产生的可重定位文件目标文件,二进制文件

5.hello

链接产生的可执行文件,二进制文件

1.4 本章小结

hello world作为我们学习程序语言的第一个程序,对每一个程序员都有着非同一般的意义,这个例程在 Brian Kernighan 和 Dennis M. Ritchie合著的The C Programme Language使用而广泛流行。因为它的简洁,实用,并包含了一个该版本的C程序首次在1974年 Brian Kernighan 所撰写的 Programming in C: A Tutorial 出现。接下来我们会从各个方面去了解这个闻名世界的程序。


第2章 预处理

2.1 预处理的概念与作用

预处理(preprocessing),在程序设计领域中,一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。在c语言中,预处理一般会将以#开头的行视作预处理指令,如#include、#define等,

预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。帮助程序员更简单的阅读维护程序

2.2在Ubuntu下预处理的命令

命令:cpp hello.c hello.i

2.3 Hello的预处理结果解析

打开hello.i可以看到短短23行的程序被扩展到了3000多行,拉到文本文件最下端可以看到函数的主题并没有做更改,只是扩展了头文件等,在文件中可以看到很多函数的原型包括printf等,这些都是标准C的函数原型。

2.4 本章小结

预处理可以简化程序员工作量,并且是程序拥有一定的可移植性,通过预编译文件,我们也可以简单了解到头文件包含的函数。


第3章 编译

3.1 编译的概念与作用

编译(compilation , compile)编译器将预处理文件转换为汇编文件,将程序语言转换为汇编语言。

汇编语言代码简短,占用内存少并且易于访问执行速度快,能够访问所有可以访问的软硬件资源,其更贴近于底层,方便汇编器将其转换为机器码供器执行。

3.2 在Ubuntu下编译的命令

命令:/usr/gcc/x86_64-linux-gnu/10/cc1 hello.i

3.3 Hello的编译结果解析

打开hello.s可以看到原来的程序语言已经被转换为了汇编语言,上图即是main函数的主体,接下来对它进行解析。

3.3.1判断语句

if(argc!=4){

printf("用法: Hello 学号 姓名 秒数!n");

exit(1);

}

这是main函数的第一个语句块,是一个判断语句,判断输入是否合法,如果不合法,则提示正确的输入格式然后退出程序。既然是判断语句,那很简单就能找到判断的汇编语句:cmpl $4,-20(%rbp),说明现在的函数输入argc是储存在-20(%rbp)处,如果不等于4则不跳转进入语句:leap .LC0(%rip),%rdi,就是要将LC0的数据传入rdi中作为下面函数puts的参数传入。

LC0的内容可以在文本文件的最上面找到:

.LC0:

.string "347224250346263225: Hello 345255246345217267 345247223345220215 347247222346225260357274201"

很显然,这个就对应需要输出的字符串。中文使用几个字节表示。

输出完后就接着调用exit函数结束程序了。

3.3.2循环语句

for(i=0;i<8;i++){

printf("Hello %s %sn",argv[1],argv[2]);

sleep(atoi(argv[3]));

}

这是main函数的第二个语句块,是一个循环语句,循环每次输出argv[1],argv[2]分别对应学号姓名,然后睡眠argv[3]的时间,一共要循环八次。循环就要用到判断和跳转,这里是在.L3进行实现,语句:cmpl $7 ,-4(%rbp);jle .L4;显然是与7进行小于等于判断,如果是则跳转到.L4,那么也就是说.L4就是循环主体。

在循环主体中,我们再一次见到了语句:leap .LC1(%rip),%rdi,同样是将LC1的数据传入作为printf的参数。下面是LC1的内容:

LC1:

.string "Hello %s %sn"

这个就十分浅显易懂了,接下来进行printf函数的调用,等结束后又调用了atoi函数,并将其返回值传输给了edi作为sleep的输入。再往后读入就又来到了循环判断条件语句,形成闭环。

3.4 本章小结

汇编是系统通过预处理和编译器将原本程序语言转换后的结果,它更加贴近底层,我们需要学习汇编语言用于理解编译器的功效,去找到程序隐含的优化可能,以及了解程序错误的原因以便进行代码修改。


第4章 汇编

4.1 汇编的概念与作用

汇编一般是指汇编程序将汇编语言书写的程序翻译为与之等价的机器语言程序的过程。汇编器接受.s程序输入,一可重定位程序作为输出。可重定位文件包含二进制代码和数据,其形式可以在编译时与其他可重定位文件合并,创建一个可执行文件,加载到内存并执行。

4.2 在Ubuntu下汇编的命令

命令:as hello.s -o hello.o

4.3 可重定位目标elf格式

命令:readelf -a hello.o

4.3.1ELF头

ELF头描述生成该文件的系统的字的大小和字节顺序、帮助连接器语法分析和解释目标文件的信息。上图拥有的信息有:ELDF64(ELF 64位的可执行程序);补码表示,小端法;可重定位文件;系统架构为AMD x86-64;节头开始为文件开始处1040字节偏移处。

4.3.2节头

上图列出了节头,包括节名、类型、地址、偏移量等属性。

4.3.3符号表

Value是符号在对应节中的偏移量。

4.4 Hello.o的结果解析

命令:objdump -d -r hello.o

结果:

可以看到在反汇编中最大的区别就是调用函数没有填入有效的数字(如1c处第二个字节开始是四字节的0).这些要留到链接阶段进行重定位符号引用,才会填上相对偏移量。此外,由于在编译阶段没有保留符号的名字,函数调用都被写为了<main+offset>的形式。

机器语言用特定的字节表示各种操作。只要给定了文件的开始位置,就可以把合法的字节序列唯一地解释为有效的指令。汇编语言的操作数直接用人能够读懂的字符(%rax、$10)来表示;而机器代码的操作数会被映射为特定的字节或大/小端法表示的十六进制。分支转移和函数调用由标签、符号(.L1,sum等)变为了相对偏移量,更适合被加载到内存中工作。

4.5 本章小结

汇编器接受汇编代码文件并将其转化为可重定位目标文件。它可以和其他可重定位文件合并产生一个可直接加载被运行的可执行目标文件。但由于它并没有完全包含程序的全部信息,所以它的符号是用0占位的。


5链接

5.1 链接的概念与作用

链接将多个可重定位目标文件整合到一起,修改符号应用,输出一个可以执行的目标文件。

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/10/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/10/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro

5.3 可执行目标文件hello的格式

    命令:readelf -a a.out

5.3.1ELF头

从上图可以看到,与4.3相比有了一些不同,如文件类型已经变成了EXEC可执行文件,程序的入口位置也出现了变化,是因为连接上了一些库文件使main函数不再是从0x0开始。

5.3.2节头

从上图可以看到节头数目变多了,而且每个节的地址也不再是0,而是经过一定计算后得到的偏移量。

5.3.3符号表

5.4 hello的虚拟地址空间

根据表头信息知道ELF从0x400000开始

.text节从0x401090开始

在反汇编代码寻址可以看到确实使函数入口,如下图

5.5 链接的重定位过程分析

命令:objdump -d -r a.out

在上图的主函数中,我们可以看到原来反汇编代码中main加上相对偏移的跳转已经被重新计算,对子函数的call也在重定位后被计算出来了。

5.6 hello的执行流程

地址后六位与子函数名

401000 <_init>

401030 <puts@plt>

401040 <printf@plt>

401050 <getchar@plt>

401060 <atoi@plt>

401070 <exit@plt>

401080 <sleep@plt>

401090 <_start>

4010c0 <_dl_relocate_static_pie>

4010c1 <main>

401150 <__libc_csu_init>

4011b0 <__libc_csu_fini>

4011b4 <_fini>

使用edb调试查看一步步进入的函数

5.7 Hello的动态链接分析

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

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

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

从ELF头找到.got

在edb中查看got表在调用dl_init之前的内容

调用后

从图中可以看出,在dl_init调用之后,该处的两个8字节的数据都发生了改变。

和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]是动态链接器ld-linux.so模块中的入口点。

5.8 本章小结

链接为程序编写以及版本管理(利用动态链接)提供了一定的便利。程序员不必将所有函数同时写在一个文件中,而是可以分别工作,最后将可重定位目标文件链接在一起

以下格式自行编排,编辑时删除


6hello进程管理

6.1 进程的概念与作用

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

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

shell是一种传统的用户界面,本质上也是一个程序。而bash是shell的一种,在1989年发布第一个正式版本,现在许多Linux发行版都把它作为默认shell。shell每次读取用户输入的指令,并检查其是否为内置命令。若是,则shell直接按用户指令执行;否则它会认为这是一个可执行程序,在文件系统中查找并为其fork一个子进程并执行。

6.3 Hello的fork进程创建过程

当我们输入./hello时,shell发现它不是一个内置命令,于是将其判定为可执行程序。shell为它fork一个子进程:内核为新进程创建各种数据结构,并分配给它一个唯一的PID。它创建当前进程的mm_struct、区域结构和页表的原样副本。

6.4 Hello的execve过程

在shell为hello创建子进程后,shell调用execve函数。execve获取参数中的filename,将命令行作为新进程的argv,并传入环境变量。

6.5 Hello的进程执行

6.5.1相关概念

1.逻辑控制流

系列程序计数器 PC 的值的序列叫做逻辑控制流。由于进程是轮流使用处理器的,同一个处理器每个进程执行它的流的一部分后被抢占,然后轮到其他进程。

2.用户模式和内核模式

处理器使用一个寄存器提供两种模式的区分。用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据;内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

3.上下文

上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。

6.5.2进程调度的过程

 在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。

 以执行sleep函数为例,sleep函数请求调用休眠进程,sleep将内核抢占,进入倒计时,当倒计时结束后,hello程序重新抢占内核,继续执行。

初始时,控制流在hello内,处于用户模式

调用系统函数sleep后,进入内核态,此时间片停止。

2s后,发送中断信号,转回用户模式,继续执行指令。

6.5.3用户态与核心态转换

为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核心态可以说是“创造模式”,拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。

6.6 hello的异常与信号处理

在程序执行过程中可能会收到SIGINT、SIGTSTP等信号。

6.6.1异常处理

当进程收到一个信号且该信号未被阻塞,该进程就会调用异常处理程序来处理该信号。如果未用signal函数指定信号处理程序,该进程会执行默认行为。否则,进程的控制会被内核转移到该信号处理程序处。

6.6.2SIGINT/SIGTSTP

当hello执行过程中用户在键盘上按下Ctrl+C/Z,会使得操作系统给所有前台进程(此时仅有shell)发送一个SIGINT/SIGTSTP信号。当shell捕获到这个SIGINT/SIGTSTP信号,它检查前台进程组并给hello进程发送(通过kill函数)一个SIGINT/SIGTSTP,由于hello没有设置对于这两种信号的处理程序,它执行默认操作:终止/停止,并向父进程shell发送一个SIGCHLD信号。

6.6.3SIGCHLD

当hello向shell发送一个SIGCHLD,shell会调用自己的SIGCHLD处理程序来回收子进程(通过waitpid函数)。shell需要获取子进程的退出状态(用waitpid中修改过的status参数),并且根据其退出状态做不同的处理:当子进程停止,shell仅仅是将它的运行状态改为停止;否则,shell直接将其回收,并视情况输出提示信息。

下图是一些信号测试

对运行的进程发送SIGTSTP信号

用fg让进程恢复前台运行并用jobs查看运行的作业

6.7本章小结

本章介绍了进程的概念与作用,以及Shell-bash的基本概念。针对进程,在这一章中根据hello可执行文件的具体示例研究了fork, execve函数的原理与执行过程。在hello运行过程中,内核有选择对其进行管理,决定何时进行上下文切换。并且当接受到不同的异常信号时,异常处理程序将对异常信号做出相应,执行相应的代码,每种信号都有不同的处理机制,对不同的异常信号,hello也有不同的处理结果。


7hello的存储管理

7.1 hello的存储器地址空间

7.1.1逻辑地址

逻辑地址(Logical Address)是指由程序hello产生的与段相关的偏移地址部分(hello.o)。

7.1.2线性地址

线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址,或者说是(即hello程序)段中的偏移地址,它加上相应段的基地址就生成了一个线性地址。

7.1.3虚拟地址

有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。

7.1.4物理地址

物理地址(Physical Address)是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就直接成为物理地址了。

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

一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。

索引号就是段描述符的索引。段描述符具体描述了一个段地址,这样,很多段描述符就组成段描述符表。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。

Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。

一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],

看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。

拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。把Base + offset,就是要转换的线性地址了。

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

无论是IA32的段式管理还是页式管理,都需要查询页表将线性地址转换为物理地址。线性地址被分为数个部分(在Linux内存系统中为2部分,但思想是相同的)。MMU(Memory Management Unit,内存管理单元)取得线性地址的前面一部分并以其为索引(虚拟页号)查询页表的表项,得到物理页号,再将物理页号与线性地址的后面一部分拼接到一起,得到物理地址,CPU就可以通过这个物理地址访问到内存。

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

TLB(Translation Lookaside Buffer,翻译后备缓冲器)用于加速地址翻译。每次CPU产生一个虚拟地址,就必须查询PTE(页表条目),在最差情况下它在内存中读取PTE,耗费几十到几百个周期。TLB是一个对PTE的缓存,每当查询PTE时,MMU先询问TLB中是否存有该条目,若有,它可以很快地得到结果;否则,MMU需要按照正常流程到高速缓存/内存中查询PTE,把结果保存到TLB中,最后在TLB中取得结果。

多级页表用于减少常驻于内存中的页表大小。由于在同一时间并非所有虚拟内存都被分配,那么操作系统可以只记录那些已被分配的页来减小内存开销,这通过多级页表实现。对于一个k(k>1)级页表,虚拟地址被分为k个虚拟页号和一个虚拟页偏移量。为了将虚拟地址转换为物理地址,MMU首先用第一段虚拟页号查询常驻于内存的一级页表,获取其二级页表的基址,再用第二段虚拟页号查询三级页表的基址,直到对第k级页表返回物理地址偏移,MMU就得到了该虚拟地址对应的物理地址。对于那些没有分配的虚拟地址,对应的多级页表根本不存在,只有当分配到它们时才会创建这些页表。因此,多级页表能够减少内存需求。

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

在MMU获得物理地址后,它请求对应内存单元的数据,使高速缓存/内存将它返回给CPU。如果在寄存器和主存之间没有任何其他存储设备,那么每次内存读都需要花费几十个到几百个时钟周期。为了加速对数据的访问,现在几乎所有的计算机都配备多级的高速缓存。高速缓存不同于基于DRAM的主存,它采用基于SRAM的更小而快速的存储方式。存储器层次结构的主要思想是上一层的存储器作为低一层存储器的高速缓存。每当MMU发起一次对内存的取数据请求,L1缓存需要检查是否缓存了该内存处的数据。若有,CPU可以很快地取得该数据(仅花费几个时钟周期);否则,L1缓存需要从L2处请求数据并将L2返回的数据缓存在L1中。以此类推,最终一层层地将数据返回给CPU。

此外,缓存以块为基本单元进行,也就是对于一个单元的请求会把它附近的一个特定大小的块也缓存到更高层的存储器。这依赖于程序拥有的空间局部性和时间局部性。空间局部性指一个被引用的内存单元附近的内存很有可能在不久后被引用;时间局部性指一个被引用的内存单元很有可能在不久后被多次引用。因此,一个具有良好局部性的程序会减少花费在访问内存上的时间:一次缓存可以供后来的多次内存访问使用。因此,程序员应该根据具体问题对程序的写法进行适当的调整,以利用缓存提供的性能提升。

7.6 hello进程fork时的内存映射

当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve的加载步骤如下:

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

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

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

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

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

缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。

缺页中断处理:通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面 到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成。

7.9动态存储分配管理

动态内存管理的基本方法与策略介绍如下:

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

 具体而言,分配器分为两种基本风格:显式分配器、隐式分配器。

 显式分配器:要求应用显式地释放任何已分配的块。

 隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。

7.10本章小结

本章介绍了程序是如何组织储存器的。先从程序所使用的不同地址开始,分别介绍了逻辑地址、虚拟地址(线性地址)以及物理地址。并介绍了计算机是怎么一步步将地址从逻辑地址变化到虚拟地址再从虚拟地址变化到物理地址的。


8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

一个linux文件就是一个m个字节的序列。所有的I/O设备(如网络、磁盘和终端)都被模型话为文件,而所有的输入和输出都被当做相应的文件的读和写来执行。这种将设备优雅地映射为文件的方式,运行linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

Unix I/O接口:

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 I/O函数:

1.int open(char* filename,int flags,mode_t mode) ,进程通过调用 open 函 数来打开一个存在的文件或是创建一个新文件的。open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。

2.int close(fd),fd 是需要关闭的文件的描述符,close 返回操作结果。

3.ssize_t read(int fd,void *buf,size_t n),read 函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置 buf。返回值-1 表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。

4.ssize_t wirte(int fd,const void *buf,size_t n),write 函数从内存位置 buf 复制至多 n个字节到描述符为 fd的当前文件位置。

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程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。printf用了两个外部函数,一个是vsprintf,还有一个是write。

vsprintf函数作用是接受确定输出格式的格式字符串fmt(输入)。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

write函数将buf中的i个元素写到终端。

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

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;

}

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf和getchar函数的实现。

结论

hello经历的过程:

首先我们用高级语言,如c语言,为hello写下源代码,然后交由编译器进行编译成可执行文件。一般来说我们使用的都是像CodeBlock这样的集成环境来进行编译的。然而这个编译的过程实际上又分为很多步,首先使用cpp进行预处理,生成后缀名为.i的文件。然后用cc1编译器进行编译,将预处理过的.i文件翻译成.s汇编文件。之后通过as汇编器进行汇编,将汇编文件生成.o可重定位文件。最后通过链接将其与库文件链接并进行重定位生成可执行文件hello。这是一个hello成为可执行文件的过程。

接下来要让hello成为一个运行中的进程。我们通过shell键入./hello 1190200416 陈睿奕 1的命令,其中./hello是让shell运行hello,而后面的参数则是我们要通过命令行传递给hello的参数。shell接受到命令后会解析命令,当确定./hello是一条来自外部的命令而不是内置命令时会创建为hello创建一个子进程。此时内核还会为其分配一段虚拟内存。最后再通过execve来执行hello函数。这样一个hello进程就被创建出来了,当hello进入函数入口开始运行时,虚拟内存会产生缺页异常,从而将hello程序所需要使用的信息交换到主存,并为其分配物理地址.

最后是hello生命的终结,当hello程序运行结束,通过return返回后会向shell发送一个SIGCHLD信号。通过这个信号,shell获悉了hello的结束,并从主存中删除与其相关的所有数据,一个hello的进程也彻底宣告消亡。


附件

1.hello.c

hello程序的c语言源文件,文本文件

2.hello.i

hello.c经过预处理得到的文件,文本文件

3.hello.s

编译后产生的汇编文件,储存文件的汇编代码,文本文件

4.hello.o

汇编产生的可重定位文件目标文件,二进制文件

5.hello

链接产生的可执行文件,二进制文件

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>