计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021112503
班 级 2103103
学 生 王桓烨
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年5月
摘 要
本文通过分析hello程序从代码编辑器到运行进程的过程,对计算机系统进行了深刻的分解和剖析。当我们在IDE上编辑完代码后,gcc调用cpp/cc1/as/ld,将C语言源文件进行预处理,编译,汇编,链接,最终形成可执行目标文件存储在磁盘当中。而当我们运行此可执行目标文件时,操作系统为其创建进程并管理进程分配内存,又由CPU控制程序逻辑流的运行,上下文切换并处理异常,最后hello程序走向了生命的尽头。
关键词:计算机系统,CSAPP,C语言,进程
目 录
第1章 概述
1.1 Hello简介
P2P:
(1).首先由用户在代码编辑器上编写hello.c源程序
(2).调用C预处理器 cpp,得到hello.i
(3).经ccl编译后得到汇编语言文件hello.o
(4).由as汇编后得到可重定位目标文件hello.o(5).最后由ld链接后得到可执行目标文件hello我们可以在shell中进入对应目录并输入./hello运行hello程序,shell会为这个程序fork子进程,此时由Program转变成了Process。
020:
(1).我们在shell中创建一新进程,接着调用execve并映射虚拟内存(mmp)。
(2).接着OS将虚拟内存对应到物理内存,Unix I/O 提供输入输出方式。
(3).程序从main函数中返回,进程结束,其父进程也就是shell会回收其它,操作系统内核删除其相关信息,释放它所占用的内存空间。
1.2 环境与工具
硬件环境:AMD Ryzen 7 Mobile 4800H
软件环境:Windows11 64位,Ubuntu 20.04LTS
调试工具:Vim,edb-debugger,gdb等
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
表1 中间文件及其作用
中间文件 |
文件作用 |
hello.i |
预处理得到的文件 |
hello.s |
经过ccl汇编之后的汇编文件 |
hello.o |
可重定位文件 |
hello |
可执行目标文件 |
1.4 本章小结
本章我们对hello的一生做了分解和剖析,介绍了p2p和020的过程和此论文的实验环境,包括硬件环境,软件环境,调试工具等。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。
作用:根据#include,将相应的头文件内容插入C语言代码中,将#define所定义的字符串进行替换,同时根据#if后的条件决定要编译的代码,最后删除注释。
2.2在Ubuntu下预处理的命令
gcc hello.c -E -o hello.i
图1 预处理操作及结果
2.3 Hello的预处理结果解析
查看hello.i的部分内容,找到main函数部分:
图2 预处理结果文件
可以发现代码前的注释和#include都被删去,且在代码前添加了3000多行头文件代码。
2.4 本章小结
本章介绍了预处理的概念以及作用,分析了hello.c到hello.i的过程和代码变化。合理的使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
第3章 编译
3.1 编译的概念与作用
概念:编译器ccl将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。
作用:对预处理完后的代码进行翻译,将其转换为更贴近计算机的汇编语言。同时还具有代码语法检查,代码优化等功能。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
图3 编译命令及结果
3.3 Hello的编译结果解析
3.3.1:
(1).常量:
.string声明了一个字符串,此处声明了两个字符串常量,分别是两个printf中要输出的字符串。
(2).变量:
main函数中声明了一个循环计数器变量i,观察汇编代码会发现它被储存在栈(%rbp-4)位置处:
还有传入main函数的两个参数argv数组和数组长度argc变量,通过寻找存储函数变量的寄存器rdi,rsi即可确定其在栈中的位置:
可见edi寄存器中存储的是0a0rgc变量,而argv数组头存储于寄存器rsi中。
3.3.2:赋值运算
在汇编代码中主要为mov操作,根据需要传递的数据类型的不同会选择 movq(8个字节),movl(4个字节),movb(2个字节),movw(1个字节)等。也有leaq操作可以实现赋值运算。
这句代码即对应for循环内的i=0。
3.3.3:类型转换
hello.c中有:atoi(argv[3]),将argv[3]转换为整形,在汇编语言中需要调用函数:
3.3.4:算术操作
hello.c中出现了i++,在汇编语言中对应为:
3.3.5:关系操作
通过cmp命令即可,例如hello.c中判断i<8:
在执行cmp时会设置条件码,根据条件码即可知道i与8的大小关系。
3.3.6:数组
由3.3.1中我们已知传入的数组argv头存储在栈%rbp-32的位置,而argv数组为一个char*型数组,每一个argv中均存储一个字符串,也就是我们的输入。由于64位系统,指针类型占8个字节,可以判断argv[1/2]首地址存储在M[%rbp-32+8/%rbp-32+16] 的位置。也可由以下汇编代码得知:
将M[%rbp-32+16] 和 M[%rbp-32+8] 作为函数参量传入了printf函数,然后call printf输出argv[1]和argv[2]。
3.3.7:控制转移
通过cmp和jmp命令即可实现,比如判断是否跳出循环:
若i小于等于8,则跳转至L4处接着进行循环内操作,否则不跳转,即跳出循环。
3.3.8:函数操作
除main函数外,多使用call命令实现调用函数。
(1).main:
传入的argc存于%edi中,argv存储于%rsi中
(2).printf:
于3.3.6中分析过了,不过还传入了LC1处的字符串:
.LC1 .string “Hello %s %s\n”
(3).atoi:
传入M[%rbp-32+24]也就是argv[3]
(4).sleep:
传入%eax,eax是atoi函数的返回值,也就是atoi(argv[3])
(5).exit:
传入数值1,代表非正常结束进程。
(6).getchar:
吞一个缓冲区字符,无传入变量。
3.4 本章小结
详细分析了hello.s中的汇编语言及其作用同时介绍了编译的概念和作用。
第4章 汇编
4.1 汇编的概念与作用
概念与作用:汇编器as将hello.s翻译成机器语言指令,将这些指令打包成一个可重定位目标程序,并将结果保存在目标文件hello.o中。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
图4汇编命令及结果
4.3 可重定位目标elf格式
4.3.1:ELF header
图5 ELF文件头
前16个字节的序列描述了生成该文件的系统的字的大小和字节顺序,剩下部分交代了目标文件类型(REL),机器类型(X86-64),字节头部表的偏移(start of section headers)以及节头部表中条目的大小和数量。
4.3.2:section header
图6 ELF节头
记录各节名称,大小,类型,地址,偏移量等信息。
4.3.3:重定位节
.rela.text,保存的是.text节中需要被修正的信息。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。具体有:printf,exit,getchar()等外部函数和.rodata中的模式串。
图7 ELF可重定位段
4.3.4:符号表
图8 ELF符号表段
.symtab,存储着程序中被引用的函数名称和全局变量的信息
4.4 Hello.o的结果解析
图9 反汇编命令及结果
查看objdump得到的汇编语言与hello.s对照,发现整体逻辑框架以及命令几乎没改变,但是:1. 立即数全变为16进制,如32变0x20;2. hello.s调用函数使用call接函数名称而反汇编后全部为下一条指令,相当于不跳转,这是由于还未进行链接器链接;3. 分支跳转时,反汇编使用的是具体的代码位置而不是段名称。
4.5 本章小结
本章介绍汇编的概念与作用。as汇编器将汇编语言转换为机器语言得到hello.o可重定位目标文件,通过readelf命令,剖析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
/usr/lib/x86_64-linux-gnu/libc.so
/usr/lib/x86_64-linux-gnu/crtn.o
hello.o
图10 链接命令及结果
5.3 可执行目标文件hello的格式
5.3.1:ELF header
图11 ELF文件头
类型为EXEC可执行文件,大小为64B,可知节头表起始位置为14208B,共27个节。
5.3.2:节头表
图12 ELF节头表
从这里我们可以看到各节的大小和偏移量。从elf header中可知节头表起始位置为14208B,大小为64B。
5.3.3:段头部表
图13 ELF段头部表
描述了可执行文件连续的片到连续的内存段的映射关系,同时还包括各片大小以及对应内存段大小,offset显示出了各片在目标文件的位置。
5.3.4:符号表
图14 ELF符号表
5.3.5:重定位节
图15 ELF重定位节
5.4 hello的虚拟地址空间
通过查看edb可以看出程序从0x401000开始:
图16 虚拟地址空间
从5.3.1的ELF头中可以看出程序的入口地址为0x4010f0,对应于节头表中.text节的起始地址。通过edb查看如下:
5.5 链接的重定位过程分析
分别查看经objdump得到的汇编代码和hello.o的反汇编代码:
图17 反汇编结果
我们可以发现在hello.o中地址从0开始,而链接后我们有了确定的地址,hello已经实现了重定位,反汇编代码地址也从0x400000开始。
hello的反汇编代码中加入了很多新的函数,如puts,printf,getchar,atoi,exit,sleep等。
hello的反汇编代码中调用函数都已经变成函数确切的地址了,而条件跳转中需要跳转到的位置也已经变成了确切的地址。
5.6 hello的执行流程
表2 执行流程程序名称
程序名称 |
ld-2.31.so!_dl_start |
ld-2.31.so!_dl_init |
hello!_start |
libc-2.31.so!_libc_start_main |
libc-2.31.so!_cxa_atexit |
hello!_libc_csu_int |
libc-2.31.so!_setjmp |
libc-2.27.so!exit |
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
执行init之前PLT中的内容:
图18 执行前内容
执行init之后PLT中的内容:
图19 执行后内容
初始时GOT里面存的都是PLT的第二条指令,随后链接器修改GOT,下一次再调用PLT时,指向的就是正确的内存地址。PLT就能跳转到正确的区域。
5.8 本章小结
本章介绍了链接的概念与作用,详细介绍了hello.o是如何链接生成一个可执行文件的并展示了ELF各节的内容,并通过edb查看了虚拟地址空间,研究了链接过程中的重定位过程。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程时计算机上的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
作用:向用户提供了一种假象,似乎我们的程序是当前系统运行的唯一程序,我们的程序好像独占处理器和内存。
6.2 简述壳Shell-bash的作用与处理流程
shell是Linux系统中的一个交互型应用程序,其基本功能是解释并运行用户指令。
处理流程如下:
1.终端进程读取用户输入的命令行
2.命令行解释器构造argv和envp
3.判断是否为内置命令,是便立即执行,否则寻找可执行文件
4.调用fork创建子进程后调用exeecve函数在子进程中加载并运行可执行文件
5.调用程序的main函数,程序在一个进程的上下文中运行
6.3 Hello的fork进程创建过程
在shell中键入./Hello,由于不是内置命令,终端会将其看成一个可执行文件,调用fork创建一个新进程并在其中执行。新创建的子进程几乎但不完全与父进程相同,包括代码、数据段、堆、共享库以及用户栈。父进程和新创建的子进程之间最大的区别在于他们有不同的PID。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。
6.4 Hello的execve过程
创建子进程后,子进程调用execve函数在当前子进程的上下文加载并运行hello程序:
1.删除已经存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。
3.映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
4.设置程序计数器(PC)。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
1.逻辑控制流
一系列程序计数器 PC 的值的序列叫做逻辑控制流。由于进程是轮流使用处理器的,同一个处理器执行一个进程后轮到另一个进程使用,操作系统会对进程的运行进行调度:执行进程A->上下文切换->执行进程B->上下文切换->执行进程A->… 如此循环往复。
2.用户模式和内核模式
处理器使用一个寄存器提供两种模式的区分。用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据;内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
3.上下文
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。当内核调度一个新的进程抢占当前进程后,我们需要进行上下文切换,保存以前进程的上下文,恢复新恢复进程被保存的上下文。
4.调度
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。在对进程进行调度的过程,操作系统主要做了两件事:加载保存的寄存器,切换虚拟地址空间。
5.用户态和核心态的转换
进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.6 hello的异常与信号处理
程序正常运行:
图20 正常运行
1. 发送信号ctrl+Z:进程收到SIGTSTP信号
进程只是被暂停并未被终止
图21 程序暂停
可以输入fg 1让其返回前台
图22 返回前台
2. 发送信号ctrl+C:进程收到信号SIGINT
图23 程序终止
可以发现进程被终止,被彻底结束:
3. 乱按键盘
无关输入被送入缓存区,输出到屏幕上
图24 无关输入
4. kill命令
我们可以使用kill传输信号SIGKILL将进程杀死,或传输其他信号
图25 杀死进程
6.7本章小结
本章我们了解了进程的概念和作用,hello进程的执行过程以及内核的调度过程和异常信号处理过程。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。
线性地址:也叫虚拟地址,和逻辑地址类似,也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件也是内存的转换前地址。
虚拟地址:也就是线性地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
1、段式管理的基本思想
段式存储管理以段为单位分配内存,每段分配一个连续内存区,但各段之间不要求连续,内存的分配和回收类似于动态分区分配,由于段式存储管理系统中作业的地址空间是二维的,因此地址结构包括两个部分:段标识符和段内偏移量。
段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。
索引号就是段描述符的索引。段描述符具体描述了一个段地址,这样,很多段描述符就组成段描述符表。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。
Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。
2、段式管理的特点
优点:1、提供了内外存统一管理的虚拟实现方案。
2、段式虚存每次交换的是一个程序段或数据段。
3、在段式管理中,段长可以根据需要动态扩充。
不足:1、要求更多的硬件支持,提高了机器的成本。
2、由于内存空闲区管理方式上与分区式管理相同,因而存在碎片问题。
3、每段的长度受内存可用空闲区大小的限制。
7.3 Hello的线性地址到物理地址的变换-页式管理
MMU利用页表来完成线性地址到物理地址的变换。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。操作系统负责维护页表的内容 ,以及在磁盘与DRAM之间来回传送页。
如下图所示,页表就是一个页表条目(Page Table Entry)的数组。虚拟地址空间中每个页在页表中一个固定偏移量处都有一个PTE。此图中展示了一个有N个虚拟页和M个物理页的系统的页表:
图26 页表
CPU从虚拟地址的前n-p位中,提出虚拟页号,页表基址寄存器通过这个虚拟页号定位到相应的页表条目,但这个页表条目的有效位是1时,就从该页表条目中提出对应的物理页号,将它和刚刚的虚拟地址的后p位也就是过去的虚拟地址偏移量,现在的物理页偏移量相结合,这样,我们就得到了物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
(以下格式自行编排,编辑时删除)
虚拟地址被划分成为4个VPN和1个VPO。每个VPNi都是一个到第i级页表的索引,其中1≤i≤3。第j级页表中的每个PTE,1≤j≤3,都是指向第j+1的某个页表的基址。为了构造物理地址,在能够确定PPN之前,MMU必须访问4个PTE,同时,和只有一级的页表结构一样,PPO和VPO是相同的。
图27 k级页表的地址翻译
MMU将虚拟地址VA做处理,取出VPN1、VPN2、VPN3、VPN4(VPNi为指向第i级页表的索引)及VPO(虚拟页面偏移量)。接着用VPN1在一级页表中匹配,若匹配PTE不为空,则用一级页表PTE的内容到二级页表中继续匹配;若匹配为空,则代表该页未分配,产生缺页,需要跳转至缺页处理子程序处理。在访问完4个页表之后,获得物理页面的PPN,再配合PPO(与VPO相等),可以获得物理地址VP。
7.5 三级Cache支持下的物理内存访问
由于CPU和主存之间逐渐增大的差距,系统设计这被迫在CPU寄存器文件和主存之间插入了一个小的SRAM高速缓存存储器,称为L1高速缓存(一级缓存)。L1高速缓存的访问速度几乎和寄存器一样快,典型的是大约4个时钟周期。
随着CPU和主存之间的性能差距不断增大,系统设计这在L1高速缓存和主存之间有插入了一个更大的高速缓存,称为L2高速缓存,可以在大约10个时钟周期内访问到它。有些现代系统还包括一个更大的高速缓存,称为L3高速缓存,在存储器层次结构中,它位于L2高速缓存和主存之间,可以在大约50个周期内访问到它。
根据第一级Cache的相关参数解析PP的索引位,具体说来,根据B大小解析PP的低b位作为块内偏移,根据S的大小解析紧跟着的s位作为组索引,剩下的位全部作为tag标记位,即如下图所示:
图28组索引
MMU将物理地址发给L1缓存,缓存从物理地址中取出缓存偏移CO、缓存组索引CI以及缓存标记CT。若缓存中CI所指示的组有标记与CT匹配的条目且有效位为1,则检测到一个命中,读出在偏移量CO处的数据字节,并把它返回给MMU,随后MMU将它传递给CPU。若不命中,则需到低一级Cache(若L3 cache中找不到则到主存)中取出相应的块将其放入当前cache中,重新执行对应指令,访问要找的数据。
7.6 hello进程fork时的内存映射
内存映射:Linux通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个内存区域的内容,这个过程称为内存映射。一个虚拟页面一旦被初始化了,它就在一个由内核维护的专门的交换文件之间换来换去。交换文件也叫交换空间或者交换区域,在任何时刻,交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数。
当CPU读取数据时,是由内存管理单元(MMU)管理的。MMU位于CPU与物理内存之间,它包含从虚地址向物理内存地址转化的映射信息。当CPU引用一个内存位置时,MMU决定哪些页需要驻留(通常通过移位或屏蔽地址的某些位)以及转化虚拟页号到物理页号。
首先,内核会为fork出来的进程创建对应的数据结构,并为它分配一个唯一的pid;接着,它创建了当前hello进程的mm_struct、区域结构和页表的原样副本来给其创建虚拟内存。它将两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间。
图29私有的写时复制
7.7 hello进程execve时的内存映射
1. 删除已存在的用户区域。
2. 其次创建新的区域结构,将私有区域映射到这一块地方。所有的.text、.data、.bss区域都是新创建的,这些区域是私有的、写时拷贝。.bss是匿名文件区域,初始化为二进制0,栈、堆也都是初始化为0.
3. 接着映射共享区域以及一部分动态链接库。其中这些共享区域是动态链接到程序然后映射到虚拟地址空间的共享区域。
4. 最后设置上下文中的程序计数器PC,使得它指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
产生缺页中断的几种情况
1、当内存管理单元(MMU)中确实没有创建虚拟物理页映射关系,并且在该虚拟地址之后再没有当前进程的线性区(vma)的时候,可以肯定这是一个编码错误,这将杀掉该进程;
2、当MMU中确实没有创建虚拟页物理页映射关系,并且在该虚拟地址之后存在当前进程的线性区vma的时候,这很可能是缺页中断,并且可能是栈溢出导致的缺页中断;
3、当使用malloc/mmap等希望访问物理空间的库函数/系统调用后,linux并未真正给新创建的vma映射物理页,此时若先进行写操作,将和2产生缺页中断情况一样;若先进行读操作也会产生缺页异常,将被映射给默认零页,等再进行写操作时,仍会产生缺页中断,这次必须分配1物理页了,进入写时复制的流程;
4、当使用fork等系统调用创建子进程时,子进程不论有无自己的vma,它的vma都有对于物理页的映射,但它们共同映射的这些物理页属性为只读,即linux并未给子进程真正分配物理页,当父子进程任何一方要写相应物理页时,导致缺页中断的写时复制;
缺页中断的处理:处理函数为do_page_fault函数,大致流程中为:
(一)地址为内核空间:
1. 当地址为内核地址空间并且在内核中访问时,如果是非连续内存地址,将init_mm中对应的项复制到本进程对应的页表项做修正;
2. 地址为内核空间时,检查页表的访问权限;
3. 如果1,2没有处理完全,跳到非法访问处理;
(二)地址为用户空间:
1. 如果使用了保留位,打印信息,杀死当前进程;
2. 如果在中断上下文中火临界区中时,直接跳到非法访问;
3. 如果出错在内核空间中,查看异常表,进行相应的处理;
4. 查找地址对应的vma,如果找不到,直接跳到非法访问处,如果找到正常,跳到good_area;
5. 如果vma->start_address>address,可能是栈太小,对齐进行扩展;
6. good_area处,再次检查权限;
7. 权限正确后分配新页框,页表等;
图30 页面缺页的操作图
7.9动态存储分配管理
创建和删除虚拟内存的区域需要用到动态内存分配器。
动态内存分配器维护一个进程的虚拟内存区域,称为堆。系统间细节不同,但不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化数据区域后开始,并向上生长。对于每个进程,内核维护者一个变量brk,它指向堆的顶部。
图31 堆
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格,它们都要求应用显示地分配块。
1. 显式分配器:应用显式地释放任何已分配的块。例如,C标准库提供叫malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。
2. 隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的巳分配的块的过程叫做垃圾收集。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
而动态内存分配主要有以下两种策略:
1. 带边界标签的隐式空闲链表:
简单堆块:
图32 简单堆块
隐式空闲链表组织堆:
图33 隐式空闲链表组织堆
2. 显示空闲链表:
使用双向空闲链表的堆块的形式:
图34 双向空闲链表的堆块
7.10本章小结
本章简述了在计算机中的虚拟内存管理,虚拟地址、物理地址、线性地址、逻辑地址的区别以及它们之间的变换模式,重新认识了共享对象、fork和execve,也简单介绍了动态内存分配的方法与原理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
1. 设备的模型化:文件,所有的输入和输出都能被当做相应文件的读和写来执行。
2. 设备管理:unix io接口,使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
(1)打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
Linux shell创建的每个进程开始时都有三个打开的文件: 标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可以用来代替显式的描述符值。
(2)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
(3)读写文件: 一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的而文件,当k>=m时执行读操作会触发一个成为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
(4)关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
函数格式如下:
1.打开文件:int open (char *filename, int flags, mode_t mode); 返回:若成功则为新文件描述符,若出错为-1。
2.关闭文件:int close (int fd); 返回:若成功则为0,若出错则为-1。
3.读文件:ssize_t read (int fd, void *buf, size_t n); 返回:若成功则为读的字节数,若EOF则为0,若出错为-1。
4.写文件:ssize_t write (int fd, const void *buf, size_t n); 返回:若成功则为写的字节数,若出错则为-1。
printf函数:
int printf(const char *fmt, ...)
{
int i;
va_list arg = (va_list)((char *)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
所引用的vsprintf函数
int vsprintf(char *buf, const char *fmt, va_list args)
{
char *p;
chartmp[256];
va_listp_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函数将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 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;
}
该函数调用了read函数,会将用户接下来输入的字符存放到键盘缓冲区直到遇到回车才返回输入的字符串。
其中,用户输入字符时,键盘接口会得到该按键对应的键盘扫描码,产生中断请求。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章节简述了Linux系统下I/O的机制,了解了有关打开、关闭与读写文件的操作,说明了linux的IO设备管理方法,讲述了Unix IO接口并介绍了它的五个函数。除此之外,还对照代码分析了printf和getchar的实现。
结论
“Hello World.”
这是我学习代码时第一条映入我眼帘的输出语句。但那时的我并不知道这条语句是如何通过代码和程序,在计算机内部经历了怎样的流程输出到了屏幕上。
如今在学习了计算机系统这门课程后,我才知晓,一个简单的C语言程序需要经过诸多复杂的步骤才能真正执行起来,在操作系统中运行:
1.hello.c经cpp预处理得到hello.i文本文件
2.hello.i经ccl编译得到汇编语言文件hello.s
3.hello.s经as翻译成了一个可重定位目标文件hello.o
4.hello.o经ld与动态链接库链接形成可执行文件
5.shell中输入命令 ./hello 7203610321 lhz
6.shell调用fork函数创建子进程
7.shell调用execve函数加载进程上下文并运行hello
8.hello最终被shell父进程回收,内核回收其全部信息
经过了一学期的学习,配合着CSAPP这本经典教材和CMU的课程实验,我对现代计算机系统的基础框架和地层设计有了一个大致的认识。有效运用这些知识,我们能够编写出更快,更易理解,移植适应性更高,更加适配我们电脑硬件的代码。也通过这门课程,我了解到了计算机构造的美丽与巧妙,让我对计算机这门科学有了更加浓厚的兴趣。
附件
表3 附件中的文件及作用
文件名 |
作用 |
hello.c |
源文件 |
hello.i |
预处理后的文件 |
hello.s |
经编译得到的汇编文件 |
hello.o |
汇编之后的可重定位目标文件 |
hello |
链接后得到的可执行目标文件 |
hello_elf.txt |
可执行文件hello的ELF格式 |
hello_o_elf.txt |
hello.o的ELF格式 |
hello_o_asm.txt |
hello.o的反汇编代码 |
hello_asm.txt |
hello的反汇编代码 |
参考文献
[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.