计算机系统

大作业

题 目

程序人生-Hello’s P2P

专 业

人工智能领域方向(2+X模式)

学 号

班 级

学 生

指导教师

计算机科学与技术学院

2025年5月

摘 要

本报告以“程序人生 - Hello’s P2P”为主题,围绕 Hello 程序从源代码到进程运行的完整生命周期展开研究。首先介绍了 Hello 程序在 Ubuntu 环境下通过预处理、编译、汇编和链接生成可执行文件的过程,详细分析了各阶段的操作及生成的中间资料。接着探讨了 Hello 程序作为进程在架构中的管理机制,包括进程创建、加载、执行流程以及异常与信号处理。之后从存储管理角度,阐述了 Hello 进程的存储器地址空间、逻辑地址到物理地址的变换过程,以及 TLB、四级页表和三级 Cache 在地址变换和内存访问中的作用,还分析了 fork 和 execve 时的内存映射及缺页故障处理。末了研究了 Linux 的 IO 管理方法,分析了 printf 和 getchar 的实现过程。借助对 Hello 工具全生命周期的研究,深入理解了计算机系统各核心模块的协同工作机制,为计算机系统的设计与实现提供了有益的思考。

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

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

目 录

第1章 概述................................................................................... - 4 -

1.1 Hello简介............................................................................ - 4 -

1.2 环境与应用........................................................................... - 4 -

1.3 中间结果............................................................................... - 5 -

1.4 本章小结............................................................................... - 5 -

第2章 预处理............................................................................... - 6 -

2.1 预处理的概念与作用........................................................... - 6 -

2.2在Ubuntu下预处理的命令................................................ - 6 -

2.3 Hello的预处理结果解析.................................................... - 6 -

2.4 本章小结............................................................................... - 7 -

第3章 编译................................................................................... - 8 -

3.1 编译的概念与作用............................................................... - 8 -

3.2 在Ubuntu下编译的命令.................................................... - 8 -

3.3 Hello的编译结果解析........................................................ - 8 -

3.4 本章小结............................................................................. - 10 -

第4章 汇编.................................................................................. - 11 -

4.1 汇编的概念与作用............................................................. - 11 -

4.2 在Ubuntu下汇编的命令.................................................. - 11 -

4.3 可重定位目标elf格式...................................................... - 11 -

4.4 Hello.o的结果解析........................................................... - 13 -

4.5 本章小结............................................................................. - 15 -

第5章 链接................................................................................. - 16 -

5.1 链接的概念与作用............................................................. - 16 -

5.2 在Ubuntu下链接的命令.................................................. - 16 -

5.3 可执行目标材料hello的格式......................................... - 16 -

5.4 hello的虚拟地址空间....................................................... - 19 -

5.5 链接的重定位过程分析..................................................... - 20 -

5.6 hello的执行流程............................................................... - 20 -

5.7 Hello的动态链接分析...................................................... - 21 -

5.8 本章小结............................................................................. - 21 -

第6章 hello进程管理.......................................................... - 23 -

6.1 进程的概念与作用............................................................. - 23 -

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

6.3 Hello的fork进程创建过程............................................ - 23 -

6.4 Hello的execve过程........................................................ - 23 -

6.5 Hello的进程执行.............................................................. - 24 -

6.6 hello的异常与信号处理................................................... - 24 -

6.7本章小结.............................................................................. - 28 -

第7章 hello的存储管理...................................................... - 29 -

7.1 hello的存储器地址空间................................................... - 29 -

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

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

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

7.5 三级Cache拥护下的物理内存访问................................ - 31 -

7.6 hello进程fork时的内存映射......................................... - 32 -

7.7 hello进程execve时的内存映射..................................... - 32 -

7.8 缺页故障与缺页中断处理................................................. - 32 -

7.9动态存储分配管理.............................................................. - 33 -

7.10本章小结............................................................................ - 33 -

第8章 hello的IO管理....................................................... - 34 -

8.1 Linux的IO设备管理方法................................................. - 34 -

8.2 简述Unix IO接口及其函数.............................................. - 34 -

8.3 printf的实现分析.............................................................. - 35 -

8.4 getchar的实现分析.......................................................... - 36 -

8.5本章小结.............................................................................. - 36 -

结论............................................................................................... - 37 -

附件............................................................................................... - 39 -

参考文献....................................................................................... - 40 -

第1章 概述

1.1 Hello简介

1.1.1. P2P

从源代码到内存中运行进程的完整生命周期,这一过程涉及计算机系统多个核心模块的协同工作。就是在Hello的自白中,P2P指的是“Program to Process”,描述的

1. Program阶段

首先将代码保存为.c文件,此时它只是存储在硬盘上的文本材料,也就是Program。接下来,编译器(如GCC)对程序进行预处理,包括展开头文件、根据条件保留或删除相应代码段、消除注释等,生成中间文件(.i);下一步是编译,这一步将预处理后的代码转换为汇编语言(.s);接下来是汇编,在这一步中汇编器将汇编语言转换为机器可识别的可重定位的目标二进制文件(.o);最后链接器将目标文件与系统库链接,生成可执行文件,至此应用就具备了在操作系统中运行的能力。

2. Process阶段

在Shell中输入./hello,操作系统会通过fork创建子进程,再通过调用execve将可执行文件加载到进程地址空间。接着,操作系统凭借mmap将可执行文件的代码段、数据段映射到进程的虚拟地址空间(VA),同时分配栈和堆空间。然后,内存管理单元通过TLB和页表将VA转换为PA,结合Cache加速内存访问,保证进程能读取指令和数据。CPU从内存中读取指令,利用流水线工艺(如取指、译码、执行、访存、写回等)执行操作,把结果输出到屏幕。在输出过程中,IO管理模块协调键盘输入和显卡输出,通过中断机制处理硬件请求,确保结果正确显示在屏幕上。

3.进程生命周期的终结

程序执行完毕后,操作系统通过调用exit回收进程占用的资源,如内存、文件句柄等,进程状态变为“终止” ,Shell处理进程退出状态,完成“收尸”过程,这时进程从内存中消失,只剩下磁盘上的可执行文件。

1.1.2.020

在Hello的自白中,020是“Zero to Zero”的隐喻,描述了工具从无到有再到无的完整闭环,体现了计算机系统中资源的动态管理。

1.从无到有

“0”状态,代码尚未存在。进程加载时,操作系统为变量分配内存,未初始化的全局/静态变量被置为0,栈空间也初始化为0,保证程序运行环境的干净。就是在hello.c创建前,磁盘对应位置是未启用的存储空间,从逻辑上看

2. 从有到无

进程结束后,操作系统将其占用的虚拟地址空间标记为空闲,页表项清空,内存内容被释放,回到未使用状态,即逻辑上的“0”。代码输出后,除了可能的返回值0,没有持久化数据留存,其运行过程对系统的影响,如CPU时间片、IO操作等,仅为短暂消耗,最终回归“无痕迹”状态。

1.2 环境与设备

环境:Ubuntu 64位,

工具:VScode,gcc,edb,ld,as

1.3 中间结果

hello.i

处理源代码中的预处理指令(如#include、#define、#ifdef等),生成扩展后的纯文本文件。为下一步编译提供操作对象。

hello.s

将预处理后的代码转换为汇编语言,方便后续生成机器码

hello.o

将汇编代码转换为机器码,生成可重定位的二进制文件。方便进行链接。

1.4 本章小结

本章主要介绍了Hello的生命全过程,以及我所使用的环境与工具,及中间结果。

(第1章0.5分)

第2章 预处理

2.1预处理的概念与作用

在编译过程中,预处理是将源代码转换为可执行程序的第一个关键阶段。其负责展开头文件、宏替换、处理源代码中的特殊指令对源代码进行文本替换和条件处理,为正式编译做准备。就是(以#开头的预处理指令)、删除注释,生成一个中间文件(.i),供后续编译阶段使用。预处理的主要目标

2.2在Ubuntu下预处理的命令

gcc -E hello.c-o hello.i

-E:仅执行预处理,不进行编译、汇编和链接。

-o hello.i:指定输出文件名为hello.i。

图1 hello.i的生成

2.3 Hello的预处理结果解析

图2 hello.i的具体内容

经过预处理,.c转化为.i,打开文件可以发现文件内容增加很多,这是因为它进行了宏展开,头文件中的内容也被包含在该档案中。

2.4 本章小结

本章主导介绍了预处理的概念和作用,以及生成预处理材料的命令,对Hello的与处理结果的解析。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

将高级语言的抽象表达(如函数调用、循环结构)转换为低级语言(汇编语言)。就是编译指将预处理后的文件转换为汇编语言(.s)的过程。其主要有四个主要作用,一是词法分析(将源代码分解为词法单元),二是语法分析,构建抽象语法树,验证代码结构;三是语义分析,检查类型合法性、作用域等语义规则;四是代码生成与优化。主要

3.2 在Ubuntu下编译的命令

gcc -S hello.i-o hello.s

-S:仅执行预处理,不进行编译、汇编和链接。

-o hello.s:指定输出文件名为hello.s。

图3 hello.s的生成

3.3 Hello的编译结果解析

图4 hello.s的具体内容

.file:声明源文件

.text:代码节

.section:

.rodata:只读代码段

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

.string:声明一个字符串(.LC0,.LC1

.global:声明全局变量(main)

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

3.3.1数据

程序中有两个字符串,由上图可知,这两个字符串都在只读数据段中,而且main函数声明了一个局部变量i,编译器进行编译时会将其放在堆栈中,参数argc作为main的参数也在堆栈中,各种立即数则体现在汇编代码中,数组中的每个元素都是一个指向字符类型的指针,同时由hello.c可知,hello.c声明了一个全局函数int main(int argc,char *argv[]),经过编译之后,main函数中启用的字符串常量也被存放在材料区。.global main说明函数是全局函数。

3.3.2赋值操作

程序中的赋值执行主要有:i=0这条赋值处理在汇编代码主要采用mov指令来构建,i是int类型,有32位,两个字,所以使用的是movl。

3.3.3算数操作

int类型的,因此汇编代码只用addl就能实现。就是hello.c中的算数操控有:i++,由于是i

3.3.4关系操作

(1)argc!=3;是在一条件语句中的条件判断:argc!=3,进行编译时,这条指令被编译为:cmpl $3,-20(%rbp),同时这条cmpl的指令还有设置条件码的作用,当根据条件码来判断是否需要跳转到分支中。

(2)i<8,在hello.c作为判断循环条件,在汇编代码被编译为:cmpl $7,-4(%rbp),计算 i-7然后设置 条件码,为下一步 jle 利用条件码进行跳转做准备。

3.3.5控制转移指令

先设定好条件码,之后按照条件码的状态来完成控制转移操作。就是在汇编语言里,会先设置条件码,再依据这些条件码来控制程序的转移。就像在hello.c中,有两种典型的控制转移情况:其一,判断变量i是否等于3,当i等于3时就不执行if语句,反之则执行;其二,for循环语句for(i=0;i<8;i++),每次都会判断i是否小于8,以此来决定是否跳转到循环语句里继续执行。这两种情况都

3.3.6函数操作

调用函数(如函数P调用函数Q)时主要涉及三方面管理:一是传递控制,即执行Q时程序计数器需设为Q代码的起始地址,返回时则设为P中调用Q之后指令的地址;二是传递数据,P要能向Q供应一个或多个参数,Q需能向P返回一个值;三是分配和释放内存,Q开始执行时可能要为局部变量分配空间,返回前还得释放这些空间。

hello.C涉及的函数操作有:

main函数,printf,exit,sleep ,getchar函数

那两个字符串就是main函数的参数是argc和argv;两次printf函数的参数恰好

exit参数是1,sleep函数参数是atoi(argv[3])

函数的返回值存储在%eax寄存器中。

3.4 本章小结

本章主要介绍了编译的概念和作用,在Ubuntu下的编译命令,及编译结果的解析。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

编译系统的关键环节,负责将人类可读的低级语言翻译为计算机硬件能直接理解的机器码。其他机制有地址解析、符号表生成以及重定位信息的生成。就是汇编是将汇编语言代码转换为机器可执行的二进制指令(目标文件)的阶段。它

4.2 在Ubuntu下汇编的命令

gcc -chello.s -c -o hello.o

assource.s -ooutput.o

图5 hello.o的生成

4.3 可重定位目标elf格式

4.3.1readelf -h hello.o

ELF Header是ELF格式目标文件的元数据头部,以16字节Magic序列标识文件的基本属性(字长、字节序等),后续字段涵盖文件类型、机器架构、节头部表偏移等链接所需信息。通过解析 ELF Header,可确定hello.o为可重定位目标文件,涵盖13个节,这些信息帮助链接器正确分析和组合目标文件的各部分内容。

图6 hello.o elf头文件的内容

4.3.2readelf -S hello.o

查看节头部表,含有了文件中出现的各个节的语义,包括节的类型、位置和大小等信息,由于是可重定位目标文件,于是每个节都从0开始,用于重定位。在记录头中得到节头表的信息,继而再使用节头表中的字节偏移信息得到各节在记录中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;素材段和只读数据段都不可执行,而且只读数据段也不可写。

图7 hello.o elf节头部表

4.3.3readelf -s hello.o

查看符号表,存放程序中定义和引用的函数和全局变量的信息。value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。

图8 hello.o elf的符号表

4.4 Hello.o的结果解析

objdump -d -r hello.o查看hello.o的反汇编,具体如下:

图9 hello.o的反汇编的内容

通过对比反汇编代码与hello.s可知,汇编语言指令与反汇编代码中的汇编部分并无本质差异,但反汇编代码同时展示了机器代码。机器语言由二进制机器指令构成,是计算机可直接识别的语言,指令由操作码和操作数组成;汇编语言则用人们熟悉的词句表述CPU动作,与CPU运行原理最接近,且每条汇编指令的操作码都能以二进制机器数据表示,二者可建立一一映射关系。

因为hello.c调用的共享库函数地址需动态链接器确定,汇编成机器语言时,对这类地址不确定的函数调用,先将call指令后的相对地址设为全0(目标地址为下一条指令),并在.rela.text节添加重定位条目,待静态链接时进一步确定。就是从反汇编代码分析,其一,分支转移时跳转指令用确定地址而非汇编语言中的段名称(段名称仅是助记符,汇编成机器语言后不存在);其二,函数调用在.s材料中直接跟函数名,反汇编中call的目标地址是当前下一条指令,这

4.5 本章小结

本章主要介绍了编译相关的知识,可重定位目标的elf格式以及hello.o的结果解析。

(第41分)

第5章 链接

5.1 链接的概念与作用

在程序编译过程中,链接是将多个目标记录及所需的库文件组合成一个可执行文件的过程。链接的具体作用在于,它首先通过符号解析找到目标文件中函数或变量引用的实际定义位置,并重定位符号地址,确保所有引用准确无误;接着将各目标文件相同类型的段合并,并为其分配内存起始地址和空间大小;接着根据静态或动态链接方式,整合库函数与依赖,保障程序运行时所需库的正确加载;最终,链接器依照操作系统的可执行文件格式,将合并后的代码和数据组织成特定结构,生成可供系统加载执行的文件。

5.2 在Ubuntu下链接的命令

图10 ld运行界面

具体命令如下: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的格式

5.3.1readelf -h hello

ELF Header是ELF格式目标材料的元内容头部,以16字节Magic序列标识文件的核心属性(字长、字节序等),后续字段包含文件类型、机器架构、节头部表偏移等链接所需信息。通过解析 ELF Header,可确定hello为可执行的文件,包含27个节。

图11 hello的elf文件的头文件

5.3.2readelf -S hello

图12 hello的elf的节头文件

5.3.3readelf -s hello

图13 hello的elf的符号表

5.4 hello的虚拟地址空间

图14 edb查看虚拟空间地址

通过使用edb加载hello,能够看出hello的虚拟地址空间开始于0x401000,结束于0x402000.

5.5 链接的重定位过程分析

5.5.1hello与hello.o的不同

说已经完成了重定位,而hello.o反汇编代码中代码的虚拟地址均为0,未完成可重定位的过程,如图5.5.1和5.5.2的比较可知.就是(1)hello反汇编的代码有确定的虚拟地址,也就

(2)hello反汇编的代码中多了很多的节以及很多函数的汇编代码,如图5.5.3.所示,这些节都具有一定的功能和含义

5.5.2链接的重定位过程分析

hello重定位的过程:

(1)重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,软件中每条指令和全局变量都有唯一运行时的地址。

(2)重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。

(3)重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。代码的重定位条目放在.rel.txt

5.6 hello的执行流程

程序名称

程序地址

Id-2.27.so! _dl_start

0x7fce 8cc38ea0

1d-2.27.so! _dl_mit

0x7fce 8cc47630

hello! _start

0x400500

libc-2.27.so! _libc_start_main

0x7fce 8c867ab0

1ibc-2.27.s0!_cxa_atexit

0x7fce 8c889430

1ibc-2.27.s0!_libe_csu_mit

0x4005c0

libc-2.27.so! _setjmp

0x7fce 8c884c10

lbc-2.27.so!exit

0x7fce 8-889128

表1 hello的执行过程

5.7 Hello的动态链接分析

动态链接的核心思想是将程序按模块拆分,在运行时才将这些模块链接成完整代码,而非像静态链接那样提前整合为单一可执行文件。虽然链接过程被推迟到运行时,但生成可执行文件时仍需依赖动态链接库。

由于共享库在运行时可能加载到任意内存地址,编译器无法预先确定函数的运行时地址,因此会生成重定位记录,交由动态链接器在加载时解析。GNU编译系统采用延迟绑定机制,进一步将函数地址的绑定推迟到首次调用时。

延迟绑定通过GOT(全局偏移量表)和PLT(过程链接表)协同完成:

PLT:位于代码段,是一个16字节代码数组。其中,PLT[0]负责跳转到动态链接器,其他每个条目对应一个被调用的库函数,负责触发函数调用流程。

GOT:位于数据段,是一个8字节地址数组。GOT[0]和GOT[1]存储动态链接器所需的元信息,GOT[2]指向动态链接器入口点,其余条目与PLT一一对应,存储运行时解析后的函数实际地址。

这种设计将函数地址解析延迟到首次调用,显著提升了程序启动速度。

5.8 本章小结

本章围绕程序链接展开,首先阐述链接是将多个目标文件及库文件组合为可执行文件的过程,其作用包括符号解析、地址重定位、段合并、库函数整合等。之后通过readelf命令可查看可执行目标文件hello的格式,使用edb加载hello可知其虚拟地址空间范围为0x401000至0x402000。链接的重定位过程包括合并相同类型的节并分配运行时内存地址、修改符号引用使其指向正确地址,编译器会生成重定位条目辅助这一过程。动态链接则是将程序按模块拆分,运行时再链接,采用延迟绑定机制,通过GOT和PLT协同达成,提升了代码启动速度。

(第51分)

第6章 hello进程管理

6.1 进程的概念与作用

进程是操作系统中对正在运行程序的动态抽象,是程序的一次执行过程,包含程序代码、数据、堆栈及进程控制块等核心组件。其作用在于构建多任务并发执行,通过调度让多个进程交替使用CPU以提升资源利用率;为程序提供独立执行环境,各进程地址空间隔离以保障系统稳定安全;作为资源分配基本单位,负责内存、I/O设备等资源的申请与管理;支持程序动态管理,可对进程进行创建、终止等操作,是现代操作系统实现任务管理的核心基础。

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

Shell是操作系统中用户与内核交互的桥梁,它充当着命令解释器的角色,能够将用户输入的指令转化为环境能够执行的执行。用户经过Shell输入命令,比如查看文档、复制数据、运行程序等,Shell会对这些命令进行解析、处理,并调用相应的系统功能来完成操作。此外,Shell还具备环境变量管理、任务控制等功能,同时帮助脚本编程,让用户可以通过编写脚本实现批量任务的自动化处理,是用户操控操作系统的重要软件。

Bash是Shell的一种具体实现,全称为Bourne Again SHell,它是Linux和Unix系统中最为常用的Shell之一。Bash在兼容传统Shell能力的基础上,增加了许多实用的特性,比如命令补全、历史命令查询、管道和重定向功能的增强等,让用户操作更加便捷高效。在处理用户命令时,Bash会按顺序完成读取、解析、执行等步骤,对于内置命令直接处理,外部程序则通过创建子进程来运行,并且支持复杂的脚本逻辑,如条件判断、循环结构等,是系统管理和日常操作中不可或缺的组件。

6.3 Hello的fork进程创建过程

在Linux系统中,当应用调用`fork()`函数时,内核会为父进程创建一个子进程,通过复制进程控制块、采用写时复制机制共享内存资源、继承文件描述符等方式,使子进程成为与父进程几乎相同的独立执行单元;`fork()`在父进程中返回子进程PID,在子进程中返回0,以此区分父子进程执行逻辑,最终两者按调度策略并发执行,构建多进程并发的基础模型,同时通过写时复制优化资源利用效率,为后续程序加载(如`exec`)提供进程创建基础。

6.4 Hello的execve过程

在Linux系统中,当程序调用execve加载"Hello"程序时,内核会根据指定的可执行文件路径,查找并验证文件权限,然后清空当前进程的地址空间,将新程序的代码段、数据段加载到内存中,同时设置环境变量和命令行参数,最后将程序计数器指向新程序的入口点(通常是`_start`)开始执行,完成从旧代码到"Hello"程序的彻底替换,原进程上下文(除PID外)被完全覆盖,新程序以当前进程的身份运行并输出"Hello"。

6.5 Hello的进程执行

在多任务操作系统环境下,hello进程的完整执行流程涵盖创建、调度、运行、切换及终止多个阶段,具体如下:

1.进程创建与加载:用户在终端输入./hello后,Shell 调用fork()创建子进程,子进程再通过execve平台调用加载 hello 程序。此时,操作系统为新进程分配进程控制块(PCB),记录进程 ID、寄存器状态等关键信息,并为其分配虚拟内存空间,完成代码段、数据段加载及堆栈初始化。

  1. 进入就绪队列:新进程被标记为就绪状态,加入调度器(如 Linux 的 CFS 调度器)的就绪队列,调度器依据优先级、历史运行时间等因素计算进程权重,等待 CPU 调度。
  2. 进程调度与执行:调度器从就绪队列选中 hello 进程,分配 CPU 时间片。若采用时间片轮转算法,时间片耗尽后进程将被抢占。CPU 从 PCB 加载进程上下文(寄存器、应用计数器等),使其进入运行状态。
  3. 用户态与架构调用:hello 进程在用户态执行指令,如执行printf时,内部调用write(),凭借软中断触发用户态到核心态切换。操作系统检查参数合法性,将信息从用户空间复制到内核,调用驱动输出,完成后通过iret或sysret指令返回用户态继续执行。
  4. 中断与上下文切换:时间片耗尽时,硬件定时器触发时钟中断,CPU 进入核心态。内核将当前进程上下文保存到 PCB,标记为就绪状态并放回调度队列,随后选择新进程加载上下文,完成切换。
  5. 进程终止与资源回收:hello 进程执行exit()系统调用时正常终止,释放资源后变为僵尸进程,等待父进程经过wait()回收;若发生段错误等异常,内核发送信号终止进程。

上述流程展示了操作系统如何在多任务环境下高效管理进程,保障资源隔离、分配公平性及系统安全。

6.6 hello的异常与信号处理

6.6.1乱打字符不产生影响

图15 乱打字符对执行的影响

6.6.2ctrl+z,ps,pstree

由于进程过多,反而不如ps直观。就是输入ctrl-z默认结果是挂起前台的作业,hello进程并没有回收,而是运行在后台下,用ps命令许可看到,hello进程并没有被回收。此时hello的PID为8288.之后可以用fg重新唤起hello进程,行看到hello重新开始进行。还可以用pstree来打印后台所有的进程,但

图16 ctrl+z对执行结果的影响,以及ps结果

图17 用fg重新唤起进程

图18 pstree的运行结果

6.6.3ctrl+c

在键盘上输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业,之后使用ps看不到hello了,可见这个进程确实终止了。

图19 ctrl+c对结果的影响

6.7本章小结

本章聚焦hello进程在Linux架构中的管理,阐述进程作为脚本动态执行实例的概念,其由代码、数据、堆栈和PCB构成,可实现多任务并发等功能;说明Shell(bash)作为交互接口的作用;介绍进程创建时调用`fork()`的相关操作及`fork()`返回值的作用;讲述`execve`平台调用加载hello程序的过程;梳理hello进程从Shell触发到终止的执行流程;提及`Ctrl+Z`和`Ctrl+C`对进程的影响及查看进程状态的方式。

(第62分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

7.1.1逻辑地址

逻辑地址是程序直接使用的地址,由段选择子和偏移量组成。编译器在编译时生成的地址均为逻辑地址,尚未关联物理内存。

在hello工具中,main函数中的局部变量i、argv数组的引用以及printf函数的调用地址均为逻辑地址。例如,argv[1]在代码中的地址引用由编译器生成,属于逻辑地址。

7.1.2线性地址

线性地址是逻辑地址经过段式转换后得到的地址,或直接由程序使用的连续地址空间(现代操作系统通常禁用分段,逻辑地址直接映射为线性地址)。

7.1.3虚拟地址

虚拟地址是进程视角的独立地址空间,每个进程拥有完整的地址范围。比如hello进程的代码段、堆栈、全局变量等均位于虚拟地址空间中。

7.1.4物理地址

物理地址是实际RAM芯片上的地址,由MMU(内存管理单元)通过页表将虚拟地址转换而来。当hello程序执行printf时,虚拟地址中的指令地址被转换为物理地址,CPU从物理内存读取指令。

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

最初根据段选择子的TI位选择GDT或LDT。例如:TI=0时,启用GDT的基地址(由GDTR寄存器存储),然后计算描述符地址:描述符在表中的位置 = GDT/LDT基地址 + Index × 8(每个描述符占8字节)。之后,从段描述符中获取段基址、段限长和DPL(段的访问权限级别)。最后,计算线性地址 = 段基址 + 偏移量。

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

在操作系统的内存管理中,页式管理是一种重要的内存管理方式,它经过将逻辑地址(线性地址)转换为物理地址,实现了程序对内存的高效访问。以下将详细介绍线性地址到物理地址在页式管理下的变换过程:

7.3.1页式管理的基本概念

1.页与页框

页:操作系统将进程的逻辑地址空间划分为大小相等的块,称为页(通常大小为4KB、8KB等)。

页框:物理内存被划分为与页大小相同的块。

2. 页表

否在内存中(高效位)等信息。就是每个进程维护一张页表,记录逻辑页与物理页框的映射关系。页表项包含:物理页框号、访问权限、

7.3.2地址变换过程

单级页表地址变换先将线性地址拆分为页号和页内偏移,借助页号在页表中查找对应的物理页框号,若页表项有效则结合页内偏移生成物理地址,无效则触发缺页中断。

多级页表(以二级页表为例)把线性地址划分为页目录索引、页表索引和页内偏移,先利用页目录基址与页目录索引找到页表物理地址,再借助页表索引在页表中获取物理页框号,最后与页内偏移组合成物理地址,相比单级页表减少了内存占用。

快表 TLB 缓存常用页表项,地址变换时优先查询 TLB,命中则直接获取物理页框号生成物理地址,未命中时按多级页表流程访问内存页表,并将结果存入 TLB,从而加快地址变换速度。

7.3.3页式管理的关键机制

页式管理中有三项关键机制。当页表项管用位为0时会触发缺页中断,操作系统需从外存加载对应页到物理内存并更新页表;页表项中的访问权限位能控制进程对物理内存的访问,起到内存保护作用,防止越权操作;页式管理支撑部分页面在内存、部分在磁盘,实现虚拟内存扩展,突破了物理内存的限制。

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

7.4.1 TLB与四级页表的协同

CPU进行VA到PA变换时,先查TLB,命中则直接用缓存的物理页框号与页内偏移生成PA;未命中则通过CR3寄存器中的PML4基址,经PML4表、PDPT表、PD表、PT表四级查找获取物理页框号,再与页内偏移组合成PA,最后将映射关系存入TLB。具体过程如下:

1. TLB优先查询(快速路径)

当CPU获取VA时,最初将VA的页索引字段(如PML4索引、PDPT索引等组合)作为键,查询TLB。

若TLB命中:直接获取缓存的物理页框号,与页内偏移组合生成PA(物理地址),无需访问内存页表。若TLB未命中:进入四级页表查询流程(慢速路径)。

2. 四级页表查询(慢速路径)

1、查找PML4表:PML4基址存于CR3寄存器,通过VA的PML4索引计算PML4表项地址= CR3 + PML4索引 × 8字节(页表项大小)读取PML4表项,获取PDPT表的物理地址(若表项有效位为0,触发缺页中断)。

2、查找PDPT表:用VA的PDPT索引计算PDPT表项地址= PDPT表基址 + PDPT索引 × 8字节,之后读取表项,获取PD表的物理地址。

3、查找PD表:同理,用VA的PD索引计算PD表项地址,获取PT表的物理地址。

4、查找PT表:用VA的PT索引计算PT表项地址,读取物理页框号(若表项有效位为0,触发缺页中断)。

5、生成PA:物理页框号左移12位(页内偏移位数),与VA的页内偏移组合,得到最终PA = (物理页框号 << 12) | 页内偏移

3. TLB更新

四级页表查询完成后,将VA到PA的映射关系存入TLB,以便后续同地址或相邻地址的快速转换。

7.5 三级Cache帮助下的物理内存访问

CPU访问数据时,会先进入L1 Cache查询的优先迅速路径,将物理地址低位作为索引和标记来查找,若命中则直接读取数据,未命中则进入L2 Cache查询。L2 Cache查询方式与L1相同,命中后会读取数据并写入L1 Cache以预取信息,未命中则进入L3 Cache查询。L3 Cache查询依旧通过物理地址索引和标记进行,命中后读取数据并依次写入L2和L1 Cache,未命中则进入物理内存访问的慢速路径。当三级Cache均未命中时,CPU通过内存总线访问DRAM物理内存,读取数据块后按层级写入L3、L2、L1 Cache,返回数据给CPU的同时,利用空间局部性预取相邻地址数据存入Cache。

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

execve函数通过调用内核区域的启动加载器代码,在当前进程环境中加载并运行可执行目标文件hello,实现以hello程序完整替代当前程序的过程。这一加载运行流程包括以下关键步骤:

1.清除原有用户空间区域:系统会先删除当前进程虚拟地址空间中用户部分已存在的区域结构,为新程序的加载释放空间。

2.映射私有内存区域:为新应用的代码、资料、bss段及栈空间创建全新的区域结构,这些区域均为私有且采用写时复制机制。其中,代码段和数据段直接映射到hello文件的.text和.data区块;bss段属于“请求二进制零”区域,映射至匿名文件,其空间大小由hello文件定义;栈和堆同样为“请求二进制零”区域,初始状态下长度为零,会随程序运行动态扩展。

3.映射共享库区域:由于hello程序与共享对象libc.so存在动态链接关系,体系会将libc.so映射到用户虚拟地址空间的共享区域内,确保程序运行时能正确调用共享库资源。

4.重置程序执行入口:execve函数执行的末了一步,是将当前进程上下文的程序计数器(PC)设置为hello代码区域的入口点,使CPU从新程序的起始位置开始执行指令,完成应用的无缝替换。

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

缺页故障是指CPU访问虚拟地址时,若对应物理页面未在内存中,内存管理单元就会触发异常。其触发条件包括页面未加载、权限错误以及写时复制等情况。

当缺页故障发生后,CPU会中断当前程序,跳转至操作系统的缺页中断处理程序。处理流程首先是保存现场并获取相关信息,比如保存寄存器状态,判断是硬缺页、软缺页还是写保护错误等缺页类型。接着进行合法性检查,验证虚拟地址是否属于进程合法地址空间,若非法则触发段错误终止进程,合法则继续处理。然后进行页面加载与映射,根据页面是匿名页、资料映射页还是交换页来查找位置,再分配物理页帧,若内存不足还需触发页面置换算法,最后更新页表。完成这些后刷新TLB,重新执行触发缺页的指令,让程序恢复执行。

在该过程中,页表、页框数据库、交换空间和反向映射等关键数据结构与机制发挥着重要作用。页面置换算法如LRU、Clock算法和工作集模型等,能帮助框架更好地管理内存。

还有一些特殊场景应该特殊处理,像写时复制,当进程尝试写入共享只读页面时,会分配新物理页并复制内容,更新页表;内存映射文件在首次访问时会触发缺页从磁盘加载,脏页则会回写。

为了优化性能,系统采用预取、大页和延迟加载等策略。缺页中断会带来性能影响,硬缺页的磁盘I/O耗时很多,可通过减少缺页率和降低缺页代价来优化,比如增大内存、使用SSD等。

总之,缺页故障与中断处理是虚拟内存系统的核心,它通过按需加载和页面置换实现内存高效利用与进程隔离,操作系统的一系列处理步骤平衡了性能与资源管理,理解这一过程对系统调优、工具性能分析及内核编写都很关键。

7.9动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。(此节课堂没有讲授,选做,不算分)

7.10本章小结

本章围绕程序运行中的内存地址转换与管理机制展开。首先介绍逻辑地址、线性地址、虚拟地址和物理地址的概念及转换过程,包括段式管理到页式管理的映射,TLB加速与四级页表协作实现虚拟地址到物理地址的变换。接着阐述三级Cache对物理内存访问的优化,以及fork时的写时复制机制、execve时的内存映射重建。最后分析缺页故障的触发条件与中断处理流程,涵盖页面加载、置换算法及性能优化策略。

(第7 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

Linux的I/O设备管理方法基于设备的模型化和统一的Unix I/O接口,通过抽象和标准化简化了用户程序与硬件设备的交互。

8.1.1设备模型化为档案

通过Linux 将所有 I/O 设备抽象为文件,统一存放在/dev目录下,称为设备档案。例如,键盘、鼠标被抽象为字符设备,拥护按字节流读写;磁盘等存储设备则被抽象为块设备,协助按数据块读写。这种抽象使得用户程序能够通过处理文件的标准接口(如 open()、read()、write())访问设备,无需关注底层硬件差异,实现了“一切皆档案”的设计哲学。

8.1.2UnixI/O接口

设备管理凭借Unix I/O接口实现,核心系统调用包括:

open():打开设备文档,返回记录描述符(File Descriptor)。

read()/write():从设备读取数据或向设备写入数据。

ioctl():控制命令(如调整终端参数、配置网络接口)。

close():释放设备资源。

这些接口屏蔽了设备的物理特性,用户仅需执行档案描述符,而具体硬件交互由内核中的设备驱动程序完成。

Linux 通过虚拟文件系统(VFS)统一管理设备档案、普通文件和其他文件系统。VFS 定义了通用接口,设备驱动通过实现这些接口将硬件操作注册到内核。这种设计使 Linux 的设备管理兼具灵活性与一致性:开发者可编写通用代码操作各类设备,而硬件厂商仅需提供符合内核接口的驱动,无需修改上层应用。

8.2 简述Unix IO接口及其函数

Unix I/O接口是操作系统提供的一组标准化系统调用,用于统一管理文件、设备和网络等输入输出操作。其核心思想是将所有 I/O 资源抽象为文件,通过材料描述符(整数标识符)进行访问,主要函数如下:

8.2.1基本操作函数

名称

作用

open()

打开或创建文件/设备,返回档案描述符

read()

从文件描述符读取数据到缓冲区

write()

将缓冲区材料写入文件描述符

close()

关闭文件描述符,释放资源

表2 基本操作函数及作用

8.2.2控制与扩展函数

名称

作用

lseek()

调整文件偏移量

ioctl()

向设备发送控制命令,用于调整或获取设备参数

表3 控制与扩展函数及作用

Unix I/O 接口通过资料描述符和统一函数,隐藏了设备差异,使程序能以相同方式操作磁盘文档、键盘、网络套接字等资源。其简洁性和扩展性是 Unix 设计哲学的关键体现。)

8.3printf的实现分析

printf的达成过程可分为用户态格式化、内核态系统调用、字符显示驱动与硬件渲染三个阶段,具体流程如下:

8.3.1用户态格式化

用户态格式化阶段是 printf实现的核心预处理步骤,主要在用户空间完成格式化字符串的解析与内容转换。具体流程为:printf接收可变参数(如整数、浮点数、字符串等)后,调用vsprintf函数,逐字符扫描格式字符串,遇到%格式符时,根据类型标识(如d表示十进制整数)从参数列表中提取对应变量,将其转换为字符串形式,并按格式拼接到用户态缓冲区中。此过程完全在用户态执行,避免内核切换开销,最终生成符合显示要求的连续字符流,供后续系统调用write统一写入设备。

8.3.2内核态系统调用

内核态架构调用阶段始于printf调用write函数),其中记录描述符1指向标准输出设备(如终端);随后通过软中断或syscall指令从用户态切换至内核态,触发内核中的sys_write函数,由内核将用户缓冲区的材料复制到内核空间,并根据文件描述符定位到对应的设备驱动(如终端驱动),最终将数据传递给底层硬件处理。

8.3.3字符显示驱动与硬件渲染

字符显示驱动与硬件渲染阶段负责将字符数据转换为屏幕上的可视图像:驱动程序首先根据ASCII字符从字模库中提取对应的像素点阵,随后将位图数据按坐标写入显存(VRAM),其中每个像素点的颜色以RGB值存储(如白色`0xFFFFFF`);显示芯片以固定刷新率逐行读取显存内容,经过信号线将RGB分量传输至显示器,LCD控制器根据时序将像素点映射到屏幕物理位置,最终结束图像渲染并呈现给用户。

此过程体现了分层抽象:用户程序仅关注格式化逻辑,内核负责数据传递,驱动程序处理硬件差异,最终由显示硬件完成光信号转换。

8.4 getchar的实现分析

getchar的实现涉及多个层次,从硬件中断处理到系统调用,再到用户空间的库函数缓冲管理。以下是详细的完成分析:

当用户按下键盘时,硬件触发中断,CPU跳转至键盘中断处理程序:

1.扫描码转换:程序读取扫描码,结合Shift/Caps Lock状态转换为ASCII码;

2.内核缓冲:字符存入内核环形缓冲区,规范模式下等待回车标记行结束;

3. 控制处理:退格键等修改缓冲区内容,回车符触发数据就绪。

用户调用getchar()时:

1.环境调用read:若内核缓冲区存在换行符,复制整行素材到用户空间;否则进程阻塞;

2.标准库优化: stdio预读整行到用户缓冲区,后续getchar直接从中取字符,减少内核切换;

3. 逐字返回:从用户缓冲区逐个返回字符,读完再触发新`read`,直到遇`\n`结束。

8.5本章小结

Linux的I/O管理以“一切皆文件”为核心,借助将设备抽象为/dev目录下的字符/块设备文件,并借助虚拟文件系统(VFS)统一管理,用户可经过open、read、write等标准接口执行硬件,屏蔽底层差异。其分层设计结合用户态、内核态及硬件驱动的分工协作,以文件描述符和ioctl构建灵活控制,最终通过抽象化、标准化与分层机制,在高效管理硬件的同时,践行了Unix“机制与策略分离”的设计哲学,确保简洁性、扩展性与跨平台兼容性。

(第8选做 0分)

结论

一、hello程序的完整生命周期

1.从源代码到可执行文件

预处理阶段:对hello.c进行预处理,展开头文件、宏替换、处理条件编译指令等,生成hello.i档案,为后续编译做准备。

编译阶段:将hello.i转换为汇编语言hello.s,此过程进行词法分析、语法分析、语义分析及代码生成与优化。

汇编阶段:把hello.s汇编成可重定位目标文件hello.o,生成机器码并包含重定位信息。

链接阶段:将hello.o与相关库文件链接,生成可执行文件hello,完成符号解析、地址重定位和段合并。

2.从可执行文件到运行进程**

进程创建:在Shell中输入./hello后,通过fork创建子进程,子进程复制父进程的资源。

应用加载:子进程调用execve将hello代码加载到内存,替换自身的地址空间,初始化代码段、数据段、栈和堆。

进程执行:CPU从hello程序的入口点开始执行,经历指令 fetch、decode、execute 等阶段,完成各种操作。

IO操作:执行printf时,通过系统调用将数据输出到标准输出设备;执行getchar时,从标准输入设备读取数据。

进程终止:程序执行完毕后,调用exit回收资源,进程状态变为终止,Shell处理退出状态。

3.内存管理与地址变换

虚拟地址空间:hello进程拥有独立的虚拟地址空间,包括代码段、数据段、BSS段、栈和堆等区域。

地址变换:从逻辑地址到线性地址通过段式管理变换,从线性地址到物理地址通过页式管理变换,TLB和四级页表加速地址变换过程。

Cache优化:三级Cache(L1、L2、L3)缓存物理内存数据,提高内存访问速度。

内存映射:fork时采用写时复制机制共享内存,execve时重新映射内存区域。

4.进程管理与异常处理

进程调度:hello进程在就绪队列中等待CPU调度,获得时间片后执行,时间片耗尽或被中断时进行上下文切换。

异常处理:遇到Ctrl+Z时进程被挂起,Ctrl+C时进程被终止,乱打字符等异常输入由程序或系统进行处理。

二、对计算机系统设计与实现的感悟

1. 分层抽象的设计思想:计算机系统从硬件到软件采用分层抽象的设计,每层只关注本层的能力和接口,降低了平台的复杂度,提高了系统的可扩展性和可维护性。例如,IO管理将设备抽象为资料,用户程序借助标准接口操作设备,无需了解底层硬件细节。

2. 机制与策略分离的原则:平台设计中区分机制和策略,机制供应基本功能,策略由上层决定。例如,内存管理中的页式管理机制提供地址变换和内存分配功能,而页面置换策略可以根据不同的应用场景进行调整。

3. 高效的资源管理:计算机系统经过各种技术实现资源的高效管理,如虚拟内存技术扩展物理内存,Cache技巧提高内存访问速度,写时复制技术减少进程创建时的内存开销。

4. 错误处理与可靠性:系统设计中考虑了各种错误情况,并给出了相应的处理机制,如缺页中断处理、信号处理等,保证系统的可靠性和稳定性。

5. 创新的可能性:利用对计算机系统的学习,认识到环境设计中存在许多创新的空间。例如,可以研究更高效的内存管理算法、更智能的进程调度策略、更优化的IO处理方式等,以提高系统的性能和用户体验。

(结论0分,缺失-1分)

附件

hello.i

处理源代码中的预处理指令(如#include、#define、#ifdef等),生成扩展后的纯文本文件。为下一步编译提供操作对象。

hello.s

将预处理后的代码转换为汇编语言,方便后续生成机器码

hello.o

将汇编代码转换为机器码,生成可重定位的二进制材料。方便进行链接。

hello_objdump

对hello进行反汇编与hello.s对比,更好的理解汇编。

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

参考文献

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

[1] Bryant, Randal E., and David R. O'Hallaron. "Computer Systems: A Programmer's

Perspective." Pearson, 2016.

[2]伍之昂.Linux Shell编程从初学到精通[M].北京:电子工业出版社.

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