计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 2023111184
班 级 23L0513
学 生 刘俊毅
指 导 教 师 史先俊
计算机科学与技术学院
2025年5月
本文详细分析了Hello程序从源代码到进程执行的完整生命周期,揭示了计算机系统各层次的工作原理与协同机制。通过预处理、编译、汇编和链接四个阶段(P2P过程),Hello程序从高级语言逐步转换为可执行文件;在进程管理阶段(020过程),操作系统通过fork和execve加载程序,CPU执行指令并处理异常与信号,最终完成资源回收。文章结合Ubuntu环境下的实际操作,探讨了ELF文件格式、虚拟内存管理、动态链接、进程调度及I/O设备管理等关键技术,展现了硬件与软件在程序执行中的深度协同。
关键词:Hello程序;ELF格式;进程管理;虚拟内存;动态链接;I/O管理
目 录
第1章 概述
1.1 Hello简介
Hello程序的生命周期完美诠释了计算机系统从静态代码到动态执行的完整过程。
在P2P阶段(Program to Process),源代码经过预处理、编译、汇编和链接四个关键步骤,逐步从人类可读的C语言转化为机器可执行的二进制文件。预处理阶段处理宏定义和头文件包含,编译阶段进行语法分析和代码优化生成汇编代码,汇编阶段转换为机器指令,最终链接阶段将所有模块和库函数整合成完整的可执行程序。
在020阶段(Zero to Zero),操作系统通过fork创建新进程,execve加载程序代码,建立完整的进程执行环境。CPU按照指令顺序执行程序逻辑,操作系统负责内存管理、进程调度和I/O处理。程序执行过程中可能遇到各种异常和信号,如用户中断或系统错误,操作系统会进行相应处理。最终程序执行完毕,操作系统回收所有分配的资源,包括内存、文件描述符等,进程完全消失。整个过程展现了计算机系统如何将静态程序转化为动态进程,并在执行结束后完全清理,实现从无到有再到无的完整生命周期。
1.2 环境与工具
Cpu:Intel Core i9-13900HX Memory:16GB RAM
泰山服务器
Compiler & Debugger: gcc, objdump, readelf, gdb, edb
1.3 中间结果
hello.i 预处理之后的文本文件
hello.s hello的汇编代码
hello_o_obj.s hello.o的反汇编代码
hello_obj.s hello的反汇编代码
hello.o hello的可链接重定位文件
hello hello的可执行文件
1.4 本章小结
本章概述了hello,首先介绍了P2P和O2O的含义及其过程,接着介绍了作业中使用的硬件环境、软件环境和开发工具,最后简要说明了从.c文件到可执行文件的转换过程及所出现的文件。
第2章 预处理
2.1 预处理的概念与作用
预处理的基本概念:预处理是C/C++程序编译过程中的第一个阶段,由预处理器负责执行。预处理器在编译器正式编译源代码之前,对源代码进行一系列文本级别的处理和转换。
预处理的主要作用:
宏展开:将程序中所有的宏定义(#define)进行替换。
头文件包含:处理#include指令,将被包含的头文件内容插入到源文件中。
条件编译:根据条件决定哪些代码参与编译,使用#ifdef、#ifndef、#if、#else、#elif和#endif等指令。
2.2在Ubuntu下预处理的命令
预处理命令:gcc -E hello.c -o hello.i


2.3 Hello的预处理结果解析
经过预处理,源程序预处理后变成了3057行。
开始展示的是源程序的基本信息:

随后依次完成对头文件的展开
以stdio.h为例展示预处理的具体过程:

cpp会到Linux系统的环境变量下寻找stdio.h,打开/usr/include/stdio.h。


随后cpp发现stdio.h使用了“#define”、“#include” 等,故cpp对它们进行递归展开替换,最终的hello.i文件中删除了原有的这部分;对于其中使用的“#ifdef”、“#ifndef”等条件编译语句,cpp会对条件值进行判断来决定是否对此部分进行包含。
预处理文件的最后,是源代码,除去头文件以外的内容保持不变

2.4 本章小结
本章介绍了预处理的概念和作用,以及预处理的指令,随后分析了预处理的过程与结果。
第3章 编译
3.1 编译的概念与作用
编译的基本概念:编译是将预处理后的高级语言代码(.i文件)转换为汇编语言代码(.s文件)的过程。这个阶段由编译器完成,是程序构建过程中从高级语言向机器语言转换的关键步骤。
编译的主要作用:
语法分析:检查C语言语法正确性,同时构建抽象语法树。
语义分析:检查类型匹配并验证函数调用参数。
代码优化:常量表达式计算和无用代码消除。
生成中间代码:生成与机器无关的中间表示,为不同目标平台提供统一优化接口。
目标代码生成:将中间代码转换为目标架构的汇编代码 。
3.2 在Ubuntu下编译的命令
编译命令:gcc -S hello.i -o hello.s


3.3 Hello的编译结果解析
3.3.1对文件信息的记录
.file为源文件 .text代码段 .section .radata只读代码段 .string字符串 .global全局变量 .type表明main是函数类型。

3.3.2对局部变量的操作
局部变量进入函数main时,会根据需求在栈上申请预留一段空间以使用。当局部变量的生命周期结束时,会在栈上释放这段空间。在下图中可以看到在L2处局部变量被存到-4(%rbp)上,初始赋值为0。

3.3.3赋值操作
赋值操作用movq指令完成。下图展示了对于局部变量初始化中的赋值操作。

3.3.4对字符串常量的操作
在main函数中使用到字符串时,会得到字符串的首地址

3.3.5对立即数的操作
立即数在使用时会直接用$符号加上数字表示

3.3.6main函数参数的传递
main函数开始部分,会将%rbp保存起来,下图21行栈指针减少32位,就是将%rdi和%rsi的值存入栈中。

由此可知,%rbp-20和%rbp-32分别存放了argv数组和argc的值。
3.3.7对数组的操作
对数组的操作固定为找到数组的首地址,加上偏移量。在汇编代码中,每次将%rbp-32的值传%rax,然后加上偏移值,得到数组中的值,随后分别存入寄存器作为对应参数,提供调用printf函数时的使用需求。

3.3.8对函数的调用与返回
函数参数有寄存器传参,返回值存在%rax中。在函数调用时,先将相应值存入相应寄存器,然后使用call指令和ret指令调用函数和返回函数。


3.3.9for循环
for循环中,将循环变量存入一个寄存器中,当执行完一个循环体之后,更新循环变量,用cmp指令将其与条件进行比较,满足则继续,否则退出循环。

3.4 本章小结
本章展示了编译的概念和过程,通过示例介绍了汇编代码通过指令如何实现各类操作。
第4章 汇编
4.1 汇编的概念与作用
汇编是指将汇编语言代码(.s文件)转换为机器可执行的二进制目标文件(.o文件)的过程。该过程由汇编器完成,是程序从人类可读代码到机器可执行代码的关键转换阶段。
汇编的主要作用:
指令转换:将汇编代码转换为机器码。
符号解析:确定标签的地址,并生成符号表。
生成可重定位目标文件:输出.o文件,包含代码段、数据段等,供链接器使用。
处理伪指令:如.section、.globl,指导汇编器如何组织代码和数据。
4.2 在Ubuntu下汇编的命令
汇编命令:gcc hello.s -c -o hello.o


4.3 可重定位目标elf格式
ELF Header:
读取指令:readelf -h hello.o
ELF Header开始的十六字节序列描述了生成该文件的系统的字的大小和字节顺序,剩下的部分包含了大小、目标文件类型、机器类型等信息。

节头
读取指令:readelf -S hello.o
节头部分记录了各个节的名称、类型、地址、偏移量和大小等信息。

重定位节
读取指令:readelf -r hello.o
重定位节中包含了.text中需要重新定位的信息,诸如.rodata、.puts等。

符号表
读取指令:readelf -s hello.o
符号表存放了程序中定义和引用的函数和全局变量的信息。

4.4 Hello.o的结果解析
反汇编指令:objdump -d -r hello.o
可以看见不仅有.s文件中出现的代码,还包含了其对应的机器语言代码

在hello.s文件中的对局部变量赋值

在hello.o中表示为

.o文件需要真实地址才能下一步操作,而.s文件可以不用。
函数调用方面,.s文件在call后可直接跟上函数名称,如 call printf@PLT,但是.o文件call后跟的是重定位条目指引的信息,如 call 68 <main+0x68>。
4.5 本章小结
本章首先介绍了汇编的概念和作用,然后对hello.s文件进行汇编,生成ELF可重定位目标文件hello.o,查看了hello.o的ELF头、节头表、可重定位信息和符号表等。最后将结果与.s文件相比较,得到机器语言与汇编语言的对应关系。
第5章 链接
5.1 链接的概念与作用
链接的基本概念:
链接是将多个可重定位目标文件和库文件合并,生成最终可执行文件的过程。链接器负责解析符号引用、合并代码段和数据段,并确定最终的运行时内存布局。
链接的作用:
符号解析:查找所有未定义的符号(如printf),并在其他目标文件或库中找到其定义。
重定位:合并所有目标文件的代码段和数据段。将相对地址转换为绝对地址。
库文件处理:静态链接:将库代码直接嵌入可执行文件。动态链接:运行时加载共享库。
生成可执行文件:输出符合操作系统要求的可执行格式。
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头
读取指令:readelf -h hello

节头表
读取指令:readelf -S hello

程序头表
读取指令:readelf -l hello

重定位节
读取指令:readelf -r hello

符号表
读取指令:readelf -s hello

5.4 hello的虚拟地址空间
各段的虚拟地址起始位置与5.3的节头部表得到的虚拟地址一致,大小也基本一致。
用edb的SymbolViewers工具查看各段信息

用edb查看.interp段的内容

用edb查看.rodata段的内容

5.5 链接的重定位过程分析
以下是对hello反汇编的结果:

相较于对hello.o的反汇编,代码变多了。而且每条数据和指令已经确定了虚拟地址。
以下是对hello.o的反汇编:

可以看出对于跳转指令和call指令,hello不再是绝对地址,而已经是重定位之后的虚拟地址。
下图是hello.o的重定位信息:

可知,对于0x401148的call指令,应该绑定0x5个符号,这里是相对寻址。再查看hello.o符号表,找到第五个符号,绑定其地址。

再hello中找到puts的地址,为0x401090

下一条指令地址为0x40114d,减去puts地址,得到0x16b。所以PC需要减去这个值,也就是加上0xfffffe95,由于为小端法,所以重定位目标处应该为95feffff。
5.6 hello的执行流程
启动顺序:_start→__libc_start_main→main→用户代码
终止顺序:main返回→exit →_dl_fini→syscall
动态链接:通过PLT/GOT机制实现运行时函数地址解析
各个子程序和子程序地址:
_abi_tag 0x400330
_init 0x401000
puts@plt 0x401030
printf@plt 0x401040
getchar@plt 0x401050
atoi@plt 0x401060
exit@plt 0x401070
sleep@plt 0x401080
_start 0x4010f0
_dl_relocate_static_pie 0x401120
main 0x401125
_fini 0x4011c0
_IO_stdin_used 0x402000
_DYNAMIC 0x403e50
_GLOBAL_OFFSET_TABLE_ 0x404000
_data_star t0x404048
data_start 0x404048
_bss_start 0x40404c
_edata 0x40404c
_end 0x40405
5.7 Hello的动态链接分析
在hello节头表查询到PLT和GOT的位置:

可知.got.plt起始位置为0x404000


发现该地址的信息发生改变。由此可知,对于变量,我们可以使用代码和数据段的相对位置不变原则计算正确的地址;对于库函数,我们需要got和plt合作,才能指向正确的内存地址。
5.8 本章小结
本章介绍了可重定位目标文件hello.o经过链接生成可执行目标文件hello的过程,分析了hello的执行流程,对hello程序进行了动态链接分析。
第6章 hello进程管理
6.1 进程的概念与作用
进程的基本概念:进程是操作系统进行资源分配和调度的基本单位,是程序的一次动态执行实例。每个进程拥有独立的地址空间、文件描述符、环境变量等系统资源。
进程的核心作用:
资源封装:通过task_struct(Linux内核数据结构)管理内存、文件、信号等资源。
并发执行:由CPU调度器分配时间片,通过上下文切换实现多任务 安全隔离 基于MMU的地址空间隔离,系统调用接口控制权限。
通信协调:通过管道、共享内存、信号等IPC机制交互。
6.2 简述壳Shell-bash的作用与处理流程
用户接口:提供命令行交互界面解析用户输入
进程创建:通过fork()+execve()加载程序
环境管理:维护环境变量
作业控制:支持前后台切换
管道/重定向:重定向文件描述符
Shell的处理流程:
1.从终端读入输入的命令;
2.将输入字符串切分获得所有的参数;
3.如果是内置命令则立即执行;
4.若不是则调用相应的程序执行;
5.shell随时接受键盘输入信号,并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
Hello程序的fork进程创建过程深刻展现了现代操作系统的进程管理机制。当用户在Shell中输入"./hello"命令时,系统首先通过fork()系统调用创建一个与Shell完全相同的子进程,这个看似简单的操作背后蕴含着精妙的设计:
进程复制阶段:内核调用do_fork()函数,为新进程分配唯一的PID。采用写时复制技术优化性能:仅复制父进程的页表,不立即复制物理内存,共享相同的物理页框,标记为只读,当任一进程尝试写入时触发页错误,再实际复制内存页
进程描述符初始化:创建新的task_struct结构体,继承父进程打开的文件描述符表,复制信号处理函数表,初始化新的内存描述符mm_struct
执行环境准备:复制寄存器状态,包括程序计数器PC
设置返回值区分父子进程:父进程获得子进程PID,子进程获得0返回值,将新进程加入就绪队列,等待调度
6.4 Hello的execve过程
Hello程序的execve过程是操作系统加载和执行新程序的核心机制,其实现过程精密而复杂。当fork创建的子进程调用execve("./hello", argv, environ)时,内核将执行以下关键操作:
程序加载准备阶段:内核首先验证可执行文件的权限和格式(通过ELF头部的魔数0x7F+'ELF'识别)。建立新的内存地址空间,清空原有进程的代码段、数据段和堆栈段。解析程序的.interp段获取动态链接器路径。
段映射与重定位:按程序头表(Program Header)将各段映射到虚拟内存:.text段映射为可读可执行(R+X)、.data和.bss段映射为可读写(RW)。动态链接器处理重定位项,修正GOT中的外部符号地址。建立初始堆栈结构,压入环境变量和参数向量。
执行环境初始化:设置入口点为动态链接器(而非直接跳转到main)。动态链接器完成延迟绑定:首次调用库函数时通过.got.plt项跳转到链接器、链接器解析实际地址并回填.got.plt。初始化线程局部存储(TLS)区域,注册atexit处理函数。
用户态执行开始:控制权转移至_start函数,初始化全局对象,准备main函数参数:argc存入RDI寄存器、argv指针存入RSI、envp指针存入RDX。最终跳转到main函数执行。
6.5 Hello的进程执行
Hello程序的进程执行过程展现了现代操作系统调度管理的精妙设计。当execve完成程序加载后,进程进入动态执行阶段,其核心机制可分解为以下几个层面:
1.进程上下文管理:
任务状态段(TSS)保存关键寄存器状态:RIP指向当前指令地址、RSP维护用户栈指针、CR3控制页表基址。
浮点寄存器状态通过FXSAVE指令保存。
内核维护的thread_struct包含:系统调用号、错误码、信号掩码。
- 时间片:一个进程执行它的控制流的一部分的每一个时间段叫做时间片,多任务也叫时间分片。
- 状态转换典型场景:
系统调用陷入内核:CPU自动切换GS寄存器指向内核栈,保存用户态SS/RSP/EFLAGS/CS/RIP到内核栈,跳转到entry_SYSCALL_64处理程序。
4.性能优化机制:
快速系统调用(SYSCALL/SYSRET):使用MSR寄存器(IA32_LSTAR)存储系统调用入口、避免传统的int 0x80软中断开销。
TLB保持策略:PCID(进程上下文ID)标记TLB项、在CR3切换时保留部分TLB条目。
6.6 hello的异常与信号处理
出现的异常类别:
1.中断:异步发生的。在执行hello程序的时候,由处理器外部的I/O设备的信号引起的。I/O设备通过像处理器芯片上的一个引脚发信号,并将异常号放到系统总线上,来触发中断。这个异常号标识了引起中断的设备。在当前指令完成执行后,处理器注意到中断引脚的电压变高了,就从系统总线读取异常号,然后调用适当的中断处理程序。在处理程序返回前,将控制返回给下一条指令。结果就像没有发生过中断一样。
2.陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
3.故障:由错误引起,可能被故障处理程序修正。在执行hello时,可能出现缺页故障。
4.终止:不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者 SRAM位被损坏时发生的奇偶错误
Hello执行过程中的异常:
1.正常运行:

2.运行时乱按:
输入的字符串只是被缓存到缓存区,进程结束后作为命令行输入。

3.运行时输入回车:
回车前的字符串当作shell输入的命令。

4.运行时输入Ctrl+C
使用SIGINT信号终止前台进程。

5.运行时输入Ctrl+Z
使用SIGTSTP信号停止前台作业。

6.输入Ctrl+Z后运行ps、jobs、pstree、fg、kill等命令
6.6.1.首先输入Ctrl+Z,进程收到SIGTSTP信号,信号的动作是将hello挂起;
6.6.2.通过ps命令看到hello进程没有被回收,其进程号是1917881;
6.6.3.用jobs命令看到job ID是1,状态是“已停止”;
6.6.4.接着输入pstree,以树状图形式显示所有进程;
6.6.5.输入fg,使停止的进程收到SIGCONT信号,重新在前台运行;
6.6.6.输入kill,-9表示给进程1917881发送9号信号,即SIGKILL,杀死进程。





6.7本章小结
本章介绍了进程的概念及作用,阐述了shell的作用和处理流程。以hello为例,分析了fork和execve函数的执行过程,对进程的创建、执行等做了详细分析。最后给出了异常状态的类别和对hello进行一些异常操作的处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
概念:由程序生成的地址,在程序中直接使用的地址。
在hello.c中的体现:
程序中的变量i、argc、argv等都有自己的逻辑地址
函数调用(如printf、sleep、getchar)时,返回地址也是逻辑地址
代码中的指令地址(如main函数的开始地址)也是逻辑地址
2. 线性地址/虚拟地址 (Linear Address/Virtual Address)
概念:逻辑地址经过段式内存管理转换后得到线性地址(在x86架构中);在平坦内存模型中,逻辑地址通常直接对应线性地址。虚拟地址是从进程视角看到的统一地址空间。
在hello.c中的体现:
当程序运行时,操作系统为hello进程创建一个独立的虚拟地址空间
argv[1]、argv[2]等字符串指针的值就是虚拟地址
函数代码、全局变量、堆栈等都位于这个虚拟地址空间中
3. 物理地址 (Physical Address)
概念:实际在内存硬件上使用的地址,由MMU将虚拟地址转换得到。
在hello.c中的体现:
程序本身不知道物理地址,这是由操作系统和硬件管理的
当printf输出字符串时,字符串数据最终会通过物理地址访问内存
sleep函数调用时,进程的上下文信息会保存在物理内存中
7.2 Intel逻辑地址到线性地址的变换-段式管理
1. 段寄存器
CS (代码段), DS (数据段), SS (堆栈段), ES, FS, GS
存储16位段选择符,指向GDT或LDT中的段描述符
2. 段描述符表
GDT (Global Descriptor Table):全局描述符表,系统唯一
LDT (Local Descriptor Table):局部描述符表,每个任务可有自己的LDT
每个描述符8字节,描述段的特性
3.段描述符结构
63 56 55 52 51 48 47 40 39 32
| Base 31:24 | Flags | Limit 19:16 | Access Byte | Base 23:16 |
31 16 15 0
| Base 15:0 | Limit 15:0 |
关键字段:
Base Address:32位段基址
Limit:20位段界限(粒度由G位决定)
Type:段类型(代码/数据,可读/可写等)
DPL:描述符特权级(0-3)
P:段存在位
G:粒度位(0=字节,1=4KB页)
7.3 Hello的线性地址到物理地址的变换-页式管理
1. 基本参数
页大小:通常4KB
地址划分:
31 22 21 12 11 0
| 页目录索引 | 页表索引 | 页内偏移 |
2. 核心数据结构
(1) 页目录
每个进程一个,由CR3寄存器指向
包含1024个页目录项(PDE),每个4字节
每个PDE指向一个页表
(2) 页表(Page Table)
每个页表1024个页表项(PTE),每个4字节
每个PTE指向物理页框
- 页表项结构
31 12 11 9 8 7 6 5 4 3 2 1 0
| 页基址 | AVL |G|0|D|A|C|W|U|R|P|
关键标志位:
P (Present):页是否在物理内存中
R/W:读写权限
U/S:用户/超级用户权限
A (Accessed):是否被访问过
D (Dirty):是否被修改过
3. 地址转换过程(以4KB页为例)
(1)CR3寄存器获取当前页目录物理基址
(2)解析线性地址:
页目录索引(10位)→在页目录中的位置
页表索引(10位)→在页表中的位置
页内偏移(12位)→在物理页中的字节位置
(3)多级查找:
物理地址 = ((CR3 + 页目录索引*4)读出PDE →
(PDE_base + 页表索引*4)读出PTE →
(PTE_base + 页内偏移)
(4)TLB加速:最近使用的转换结果保存在TLB缓存中
7.4 TLB与四级页表支持下的VA到PA的变换
翻译后备缓存器(TLB,也叫快表)
每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目)以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。许多系统都试图消除即使是这样的时间开销,它们在MMU中包括了一个关于PTE的小缓存,也就是TLB。
TLB是一个小的、虚拟寻址的缓存、其中的每一行都保存着一个由单个PTE组成的块。TLB通常具有高度的相联度,TLB的速度快于一级cache。
TLB通过虚拟页号VPN部分进行索引,分为TLBT(TLB标记)和TLBI(TLB索引),这样每次MMU会从TLB中取出相应的PTE(页表条目),当TLB不命中时,MMU又从L1缓存中取出相应的PTE,新取出的PTE会存放在TLB中此时可能会覆盖一个已存在的条目。
多级页表
使用层次结构的页表来压缩页表,形成相应的k级页表。那么虚拟地址被划分为VPO(虚拟页偏移量)以及k个VPN(虚拟页号),每个VPN i都是一个到第i级页表的索引,其中1≤i≤k。第j级页表中的每个PTE,1≤j≤k-1,都指向第j+1级的某个页表的基址。第k级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问k个PTE。对于只有一级的页表结构,PPO和VPO是相同的
7.5 三级Cache支持下的物理内存访问
当一条加载指令指示CPU从主存地址A中读一个字时,它将地址A发送到高速缓存。如果高速缓存正保存着地址A处那个字的副本,它就立即将那个字发回给CPU。
根据PA、L1高速缓存的组数和块大小确定高速缓存块偏移(CO)、组索引(CI)和高速缓存标记(CT),使用CI进行组索引,对组中每行的标记与CT进行匹配。如果匹配成功且块的valid标志位为1,则命中,然后根据CO取出数据并返回数据给CPU。
若未找到相匹配的行或有效位为0,则L1未命中,继续在下一级高速缓存(L2)中进行类似过程的查找;若仍未命中,则继续在L3高速缓存中进行查找;三级Cache均未命中则需访问主存获取数据。
若进行了上一步,说明至少有一级高速缓存未命中,则需在得到数据后更新未命中的Cache。首先判断其中是否有空闲块,若有空闲块(有效位为0),则直接将数据写入;若不存在,则需根据替换策略(如LRU、LFU策略等)驱逐一个块再写入。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任何一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效替代当前程序。加载并运行hello需要以下四个步骤:
删除当前进程虚拟地址中已存在的用户区域;
映射私有区域,为新程序的代码、数据、bss和栈创建新的区域结构,所有这些新的区域都是私有的、写时复制的;
映射共享区域,将hello与libc.so动态链接,然后再映射到虚拟地址空间中的共享区域;
设置当前进程上下文程序计数器(PC),使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
在虚拟内存中DRAM缓存不命中称为缺页。缺页异常时会调用内核中的缺页异常处理程序。具体流程如下:
处理器生成一个虚拟地址,并把它传送给MMU;
MMU生成PTE地址,并从高速缓存/主存请求得到它;
高速缓存/主存向MMU返回PTE;
若PTE中的有效位是零,则MMU触发一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序;
缺页异常处理程序确定物理内存中的牺牲页,如果这个页面已经被修改,则把它换出到磁盘;
缺页处理程序页面调入新的页面(内核从磁盘复制所需的虚拟页面到内存中),更新内存中的PTE;
缺页处理程序返回到原来的进程中,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU,因为虚拟页面已经现在在物理内存中了,所以就会命中。
7.9动态存储分配一、基本动态内存分配方法
1. 显式分配器(手动管理)
malloc/free(C)、new/delete(C++)
特点:
程序员显式控制内存分配和释放
灵活性高但容易导致内存泄漏和悬垂指针
2. 隐式分配器(自动管理)
垃圾回收(GC)机制
特点:
系统自动回收不再使用的内存(如Java、Python)
减轻程序员负担但可能引入不可预测的停顿
主要算法:
标记-清除(Mark-Sweep)
引用计数(Reference Counting)
分代收集(Generational)
二、动态内存分配策略
1. 连续内存分配策略
首次适应(First Fit):
从空闲列表头部开始搜索,选择第一个足够大的块
简单快速但容易产生外部碎片
最佳适应(Best Fit):
搜索整个空闲列表,选择最小的足够大的块
减少浪费但可能产生许多小碎片
最差适应(Worst Fit):
总是分配最大的空闲块
适合中等大小分配请求
2. 非连续内存分配策略
分页(Paging):
内存划分为固定大小的页
通过页表实现虚拟地址到物理地址的映射
分段(Segmentation):
按逻辑单元(代码、数据、堆栈等)划分
每个段有不同大小
段页式:
结合分段和分页的优点
现代操作系统常用方案
7.10本章小结
本章介绍了hello的存储地址空间,intel的段式管理、hello的页式管理,以及在TLB和四级页表的支持下完成VA到PA的变换过程,三级Cache支持下的物理内存访问。解释了hello进程的fork与execve时的内存映射,缺页故障及其处理,以及进程的动态存储分配的管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件。例如:/dev/sda2文件是用户磁盘分区,/dev/tty2文件是终端。
设备管理:unix io接口
将设备模型化为文件的方式允许Linux内核引入一个简单、低级的应用接口,称为Unix IO,这使得所有的输入和输出都能以一种统一的方式来执行。
8.2 简述Unix IO接口及其函数
Unix I/O 是操作系统提供的一组用于输入输出操作的基础接口,这些接口以文件抽象为核心,提供统一的访问方式。
Unix I/O 核心概念
文件描述符(File Descriptor):非负整数,标识打开的文件
三个标准描述符:0(stdin), 1(stdout), 2(stderr)
文件类型:普通文件、目录文件、字符设备文件、块设备文件、FIFO(命名管道)、套接字、符号链接。
统一设备接口:所有I/O设备都抽象为文件,使用相同的系统调用接口访问。
函数:
int open(char* filename,int flags,mode_t mode),进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
int close(fd),fd是需要关闭的文件的描述符,close返回操作结果。
ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n字节到描述符为fd的当前文件位置。
lseek函数:off_t lseek(int fd, off_t offset, int whence),应用程序显示地修改当前文件的位置。
stat函数:int stat(const char *filename,struct stat *buf),以文件名作为输入,并填入一个stat数据结构的各个成员。
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;
}
在代码的四到六行中,第四行目的是让argv指向第一个字符串;第二句的作用是格式化,并返回要打印的字符串的长度,第三句的作用是调用write函数将buf的前i个字符输出到终端,调用了unix I/O。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用int 0x80或syscall等。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar函数内部调用了read函数,通过系统调用read读取存储在键盘缓冲区的ASCII码,直到读到回车符才返回。不过read函数每次会把所有的内容读进缓冲区,如果缓冲区本来非空,则不会调用read函数,而是简简单单的返回缓冲区中最前面的元素。
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCⅡ码,保存到系统的键盘缓冲区中。
8.5本章小结
本章介绍了Linux的I/O设备的基本概念和管理方法,Unix I/O接口及其函数,最后分析了printf和getchar函数的实现。
结论
在计算机系统中执行一个简单的hello程序,其背后经历的过程却蕴含着极为精妙的系统设计与实现哲学。从用户键入命令到屏幕输出结果,整个过程涉及硬件与软件的深度协同,每一层抽象都在隐藏复杂性的同时暴露恰到好处的控制力。
当用户在终端输入./hello时,shell首先会通过系统调用接口触发操作系统的加载器机制。内核会解析这个ELF格式的可执行文件,检查其头部信息以确认架构兼容性和动态链接需求。接着,内存管理单元(MMU)开始工作,为进程分配虚拟地址空间,建立页表映射,并将代码段、数据段等加载到物理内存中。这个过程体现了计算机系统"欺骗的艺术"——通过虚拟内存技术让每个进程独占整个地址空间的假象,而背后是页表、TLB和缺页异常处理机制的精密配合。
当CPU开始执行hello的main函数时,指令流水线开始工作。编译时由printf转换的write系统调用会触发从用户态到内核态的上下文切换,这个过程涉及寄存器保存、权限级别切换和内核栈切换。文件子系统随后根据标准输出的文件描述符找到对应的tty设备驱动,最终通过显示控制器将字符送入帧缓冲区。在此期间,CPU的缓存层次结构(L1/L2/L3)和预取机制始终在静默地优化内存访问延迟,而超标量架构可能正在并行执行多条无关指令。
这个过程让我深刻认识到,优秀的系统设计应当像分形几何一样——在不同尺度上呈现自相似的清晰结构。当前的操作系统架构虽然成熟,但在异构计算时代面临新的挑战。
附件
大作业的中间产物:

hello:链接后的可执行目标文件
hello.c:源程序
hello.i:预处理后的文本文件
hello.o:汇编后的可重定位目标文件
hello.s:编译后的汇编文件
参考文献
[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.
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/bujiangfenghua/article/details/148216265



