关注

程序人生-Hello’s P2P

摘  要

本文通过一个简单的Hello程序,深入剖析了计算机系统中程序从源代码到执行结束的完整生命周期。首先介绍了Hello程序从程序(Program)到进程(Process)的转换过程(P2P),以及从无到有再到无的完整执行过程(020)。随后详细分析了预处理、编译、汇编、链接等编译系统各阶段的工作原理和实现机制。接着探讨了Hello进程在Linux系统中的创建、执行和管理过程,包括进程控制、异常处理和信号机制。进一步深入研究了Hello程序的存储管理机制,包括地址空间转换、页式管理、TLB和Cache系统。最后分析了Hello程序的I/O管理机制,重点解析了printf和getchar的实现原理。通过这个完整的分析过程,本文展现了计算机系统各层次之间的协同工作机制,加深了对计算机系统整体架构的理解。

关键词:Hello程序;编译系统;进程管理;存储管理;I/O管理

目  录

第1章 概述... - 5 -

1.1 Hello简介... - 5 -

1.2 环境与工具... - 5 -

1.3 中间结果... - 5 -

1.4 本章小结... - 5 -

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

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

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

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

2.4 本章小结... - 6 -

第3章 编译... - 7 -

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

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

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

3.4 本章小结... - 7 -

第4章 汇编... - 8 -

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

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

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

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

4.5 本章小结... - 8 -

第5章 链接... - 9 -

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

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

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

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

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

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

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

5.8 本章小结... - 10 -

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

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

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

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

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

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

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

6.7本章小结... - 11 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结... - 13 -

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

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

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

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

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

8.5本章小结... - 14 -

结论... - 15 -

附件... - 16 -

参考文献... - 17 -

第1章 概述

1.1 Hello简介

Hello程序的生命周期可以分为两个阶段:P2P(From Program to Process)和020(From Zero to Zero)。

P2P过程(从程序到进程):

预处理(Preprocessing):hello.c→hello.i:处理#include、#define等预处理指令;展开头文件,删除注释。

编译(Compilation):hello.i → hello.s:将C语言翻译为汇编语言;进行语法分析、语义分析、优化。

汇编(Assembly):hello.s → hello.o:将汇编语言转换为机器语言;生成可重定位目标文件。

链接(Linking):hello.o → hello:合并多个目标文件和库文件;解决符号引用,完成地址重定位。

020过程(从无到有再到无):加载(Loading):Shell调用fork()创建子进程,子进程调用execve()加载hello程序。执行(Execution):CPU执行程序指令,打印信息,调用Sleep等待。终止(Termination):程序执行完毕,进程终止,资源被操作系统回收。

1.2 环境与工具

硬件环境:

处理器:13th Gen Intel(R) Core(TM) i7-13650HX (2.60 GHz)

内存:16GB DDR4

硬盘:512GB NVMe SSD

软件环境:

操作系统:Ubuntu 22.04.3 LTS(Linux内核 5.15.0)

编译器:GCC 11.4.0

调试器:GDB 12.1

链接器:GNU ld 2.38

开发与调试工具:

编译工具链:gcc, as, ld

分析工具:readelf, objdump, nm, size

调试工具:gdb, edb, strace, ltrace

文本编辑器:VS Code 1.85.1

版本控制:Git 2.34.1

其他工具:hexdump, file, ldd

1.3 中间结果

文件名

生成命令

作用说明

hello.c

手工编写

原始C源代码

hello.i

gcc -E hello.c -o hello.i

预处理后的源文件,包含所有头文件内容

hello.s

gcc -S hello.i -o hello.s

汇编语言文件,x86-64汇编代码

hello.o

gcc -c hello.s -o hello.o

可重定位目标文件,包含机器码但地址未确定

hello

gcc hello.o -o hello

可执行文件,可直接运行

hello.elf

readelf -a hello > hello.elf

ELF格式详细分析报告

hello.dis

objdump -d hello > hello.dis

反汇编代码,用于分析机器指令

hello.o.dis

objdump -d -r hello.o > hello.o.dis

hello.o的反汇编,显示重定位信息

hello.sym

nm hello > hello.sym

符号表,显示程序中的符号和地址

hello.map

gcc -Wl,-Map=hello.map hello.o -o hello

链接映射文件,显示链接过程中的详细信息

hello.debug

gcc -g hello.c -o hello.debug

带调试信息的可执行文件

hello.strace

strace -o hello.strace ./hello ...

系统调用跟踪记录

hello.ltrace

ltrace -o hello.ltrace ./hello ...

库函数调用跟踪记录

1.4 本章小结

本章概述了Hello程序的生命周期,从源代码到可执行文件的转换过程(P2P)以及从加载到终止的执行过程(020)。介绍了实验所需的软硬件环境和工具链,并列出了在分析过程中生成的中间文件。这些中间文件将在后续章节中作为分析的基础,帮助我们深入理解计算机系统的工作原理。

第2章 预处理

2.1 预处理的概念与作用

预处理是C语言编译过程的第一阶段,它发生在实际编译之前,由预处理器(C Preprocessor, cpp)完成。预处理的主要作用包括:

头文件包含(#include):将指定的头文件内容插入到源文件中

宏展开(#define):将宏名替换为定义的文本

条件编译(#if, #ifdef, #ifndef, #else, #elif, #endif):根据条件决定是否编译某部分代码

删除注释:移除所有单行注释(//)和多行注释(/* */)

特殊指令处理:如#pragma、#line、#error等

预处理不进行语法检查,只是简单的文本替换和条件判断,为后续的编译阶段做准备。

2.2在Ubuntu下预处理的命令

在Ubuntu系统中,可以使用gcc的-E选项进行预处理:

原本只有25行的hello.c文件经过预处理后扩展到了3061行,这是因为头文件的内容被完整地插入到了源文件中。

2.3 Hello的预处理结果解析

打开hello.i文件,可以看到以下关键变化:

1.头文件展开:

2.源代码保留:

2.4 本章小结

本章详细分析了Hello程序的预处理阶段。通过执行gcc -E命令,我们得到了预处理后的hello.i文件。分析发现,预处理主要完成了以下工作:

将#include <stdio.h>、#include <stdlib.h>和#include <unistd.h>这三个头文件的内容完整插入到源文件中

删除了所有注释(虽然原代码中没有注释)

保留了除预处理指令外的所有C语言代码

预处理阶段不进行语法检查,只是简单的文本处理,为后续的编译阶段准备了一个"纯净"的、包含了所有必要声明的源代码文件。这一阶段的重要性在于它分离了代码的结构和内容,使得头文件可以独立维护和复用。

第3章 编译

3.1 编译的概念与作用

编译是将预处理后的高级语言代码(.i文件)转换为汇编语言代码(.s文件)的过程。这个阶段由编译器(cc1)完成,主要包括以下步骤:

1.词法分析(Lexical Analysis):将源代码分解为一系列的单词(token)

2.语法分析(Syntax Analysis):检查语法结构,生成抽象语法树(AST)

3.语义分析(Semantic Analysis):检查语义正确性,如类型检查

4.中间代码生成(Intermediate Code Generation):生成与机器无关的中间表示

5.代码优化(Code Optimization):对中间代码进行优化,提高效率

6.目标代码生成(Target Code Generation):生成特定机器的汇编代码

编译阶段的主要作用是进行语法和语义检查,并将高级语言转换为低级语言,为后续的汇编阶段做准备。

3.2 在Ubuntu下编译的命令

从预处理文件编译到汇编文件, 查看汇编代码行数

3.3 Hello的编译结果解析

函数定义与栈帧建立

调用帧信息开始;间接分支控制;保存旧的基指针;设置新的基指针;分配32字节栈空间

参数保存

保存argc到栈中[%rbp-20]

保存argv到栈中[%rbp-32]

条件判断(if语句)

比较argc和5;如果相等则跳转到.L2;argc!=5的情况;加载字符串地址到%rdi;调用puts函数;设置exit参数为1;调用exit函数;if条件为false的标签

对应的C代码

循环结构(for循环)

i=0,i存储在[%rbp-4];跳转到循环条件判断;循环体开始;第一个printf调用;加载argv;argv[1];*argv[1];argv[2];argv[3];第二个参数;第三个参数;格式化字符串地址;设置浮点参数数量为0;调用printf;Sleep调用;argv[4];参数argv[4];转换为整数;Sleep参数;调用Sleep;i++;循环条件判断;比较i和9;如果i<=9则继续循环

函数返回

调用getchar;设置返回值0;恢复栈帧;返回

3.4本章小结

本章详细分析了Hello程序的编译阶段。通过编译,预处理后的C代码被转换为x86-64汇编代码。主要发现包括:

1.栈帧管理:编译器为main函数建立了标准的栈帧结构

2.参数传递:遵循x86-64调用约定,参数通过寄存器和栈传递

3.控制流转换:if语句转换为条件跳转;for循环转换为条件判断和跳转标签

4.函数调用:使用PLT(过程链接表)进行函数调用

5.优化:编译器将简单的printf调用优化为puts

6.数据存储:字符串常量存储在只读数据段

编译阶段不仅进行语法转换,还进行了重要的优化工作,生成的汇编代码既保持了程序逻辑,又考虑了执行效率。这个阶段是连接高级语言和机器语言的关键桥梁。

第4章汇编

4.1汇编的概念与作用

汇编是将汇编语言代码(.s文件)转换为机器语言代码(.o文件)的过程。这个阶段由汇编器(as)完成,主要作用包括:

1.符号解析:将标签(label)转换为地址

2.指令编码:将汇编指令转换为机器指令

3.数据编码:将数据定义转换为二进制格式

4.重定位信息生成:标记需要链接时确定的地址

5.生成目标文件:创建符合特定格式(如ELF)的目标文件

汇编阶段产生的是可重定位目标文件,其中的地址还没有最终确定,需要在链接阶段解决。

4.2在Ubuntu下汇编的命令

使用gcc进行汇编; 查看生成的目标文件信息

4.3可重定位目标elf格式

ELF头部信息(readelf -h hello.o):

节头表分析(readelf -S hello.o):

.text节:包含程序的机器代码

.rodata节:包含只读数据,如字符串常量

.rela.text节:包含代码段的重定位信息,需要链接时修正的地址

符号表分析(readelf -s hello.o):

重定位表分析(readelf -r hello.o

4.4Hello.o的结果解析

机器语言与汇编语言的映射关系:

指令编码示例:

endbr64:f3 0f 1e fa

push %rbp:55

mov %rsp,%rbp:48 89 e5

sub $0x20,%rsp:48 83 ec 20

操作数编码:

立即数:如$0x5编码为05

内存地址:如-0x14(%rbp)编码为7d ec

相对地址:如跳转目标2f编码为74 16(16 = 0x2f - 0x19)

重定位项分析:

1c: R_X86_64_PC32 .rodata:需要将.rodata节的地址填入

21: R_X86_64_PLT32 puts-0x4:需要将puts的PLT地址填入

分支和函数调用的机器语言表示:条件跳转(je指令):

机器码:74 16

74:je操作码

16:相对偏移量(0x16 = 22,下一条指令地址0x19 + 22 = 0x2f)

函数调用(call指令):

机器码:e8 00 00 00 00

e8:call操作码

00 00 00 00:占位符,需要链接时填入实际地址

重定位类型:R_X86_64_PLT32(32位PC相对PLT入口)

与第3章hello.s的对比:

1.地址差异:hello.s中使用标签(如.L2、.L3),hello.o中使用相对地址

2.符号未解析:hello.s中的puts@PLT,在hello.o中地址为0,需要重定位

3.完整编码:hello.s是文本表示,hello.o是二进制编码

4.5本章小结

本章详细分析了Hello程序的汇编阶段。通过汇编,汇编代码被转换为机器码,并生成可重定位目标文件。主要发现包括:

1.ELF格式结构:hello.o符合标准的ELF可重定位目标文件格式

2.节区组织:代码、数据、重定位信息分别存储在不同节区

3.符号解析:本地符号已解析,外部符号(如puts、printf)标记为未定义

4.重定位需求:需要链接时确定的地址用0填充,并记录在重定位表中

5.机器码编码:汇编指令被编码为具体的机器指令

汇编阶段生成了包含机器代码但地址未完全确定的目标文件,为链接阶段提供了基础。这个阶段是机器能够直接执行代码的关键步骤,将人类可读的汇编语言转换为计算机可执行的二进制指令。

5章链接

5.1链接的概念与作用

链接是将多个可重定位目标文件合并成一个可执行目标文件的过程。链接器(ld)的主要作用包括:

1.符号解析:将每个符号引用与一个符号定义关联起来

2.重定位:将符号定义与内存地址关联,修改符号引用处的地址

3.节区合并:将不同目标文件的相同类型节区合并

4.地址分配:为合并后的节区分配运行时内存地

5.解析依赖:处理库文件依赖,解决未定义符号

链接使得模块化编程成为可能,可以将程序分解为多个独立的源文件分别编译,最后链接在一起。

5.2在Ubuntu下链接的命令

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

ELF头部信息(readelf -h hello):

重要变化:

文件类型:从REL(可重定位)变为EXEC(可执行)

入口地址:从0x0变为0x401040

程序头表:从0个变为13个段

程序头表(段信息)分析(readelf -l hello):

LOAD段:可加载段,共4个

第一个:代码段(R E),地址0x400000-0x4005e8

第二个:只读数据段(R),地址0x401000-0x401241

第三个:只读数据段(R),地址0x402000-0x402130

第四个:读写数据段(RW),地址0x403df0-0x404010

DYNAMIC段:动态链接信息,地址0x403e00-0x403fe0

INTERP段:指定动态链接器路径/lib64/ld-linux-x86-64.so.2

区节头表分析(readelf -s hello):

动态段信息(readelf -d hello):

5.4hello的虚拟地址空间

LOAD段:可加载段,共4个

第一个:代码段(R E),地址0x555555554000-0x555555555000

第二个:只读数据段(R),地址0x555555555000-0x555555556000

第三个:只读数据段(R),地址0x555555556000-0x555555557000

第四个:读写数据段(RW),地址0x555555557000-0x555555559000

共享库:ld和libc的映射区域

栈段:0x7ffffffde000-0x7ffffffff000

5.5链接的重定位过程分析

重定位是链接器调整可重定位目标文件(hello.o)中指令和数据的地址,使其指向最终虚拟地址的过程。hello.o中调用的 printf、sleep 等函数在编译时仅知道符号名,不知道其最终地址,因此汇编指令中使用 “符号+偏移” 的形式(如call printf@plt),链接器需通过重定位将这些符号引用替换为实际的虚拟地址。

5.5.1 重定位项分析

OFFSET:重定位项在.text 节中的偏移地址,即需要调整的指令位置。

TYPE:重定位类型,R_X86_64_PLT32表示32位PLT重定位,用于动态链接的函数调用。

VALUE:重定位目标符号,-0x4表示指令地址与符号地址的偏移调整。

5.5.2 重定位过程(以printf调用为例)

1.hello.o中的指令:hello.o中调用printf的汇编指令为call 0x0 <printf@plt>,此时 0x0 是相对于 hello.o.text 节起始地址的偏移,并非最终地址。

2.符号解析:链接器查找 C 标准库,找到printf函数的定义,确定其在可执行文件虚拟地址空间中的最终地址。

3.PLT表构建:由于 printf 是动态链接函数,链接器在可执行文件中创建 PLT(过程链接表),PLT 是一个跳转表,存储动态链接函数的跳转地址。printf 对应的 PLT 条目地址为 0x400520。

4.指令调整:链接器将 hello.o 中call 0x0的指令调整为call 0x400520 <printf@plt>,使指令指向 PLT 表中 printf 对应的条目。

5.GOT表关联:GOT(全局偏移表)存储动态链接函数的实际地址,PLT 条目通过 GOT 表间接跳转至函数实际地址。运行时,动态链接器会将GOT表中printf对应的条目填充为其实际地址(0x7ffff7a6b6a0),实现最终跳转。

5.5.3 重定位前后对比

重定位变化分析:对比可见,重定位后,call指令的目标地址从hello.o中的临时偏移调整为可执行文件中PLT表的实际地址,确保程序运行时能正确跳转到printf函数。

5.6hello的执行流程

hello程序的执行流程从操作系统加载程序开始,到程序终止结束,涉及动态链接器初始化、程序入口函数_start、main 函数调用、循环执行、资源清理等环节,具体流程如下:

5.6.1 程序加载

用户在 shell 中执行./hello 2024112825 张若润 18503669307 3,shell 调用execve系统调用,请求加载hello程序。

操作系统验证hello的ELF格式有效性,释放当前 shell 进程的内存空间(保留 PID),根据ELF程序头表分配虚拟地址空间,将hello的代码段、数据段映射到对应虚拟地址。

操作系统启动动态链接器,将其映射到虚拟地址空间的共享库区域。

5.6.2 动态链接器初始化

动态链接器解析hello的动态依赖,加载libc.so到虚拟地址空间,解析其中的符号。

动态链接器填充hello的GOT表,将PLT表中对应的函数条目指向libc.so中函数的实际地址。

动态链接器完成初始化后,跳转到hello的程序入口地址,即_start函数。

5.6.3 _start 函数执行

_start函数由crt1.o提供,负责初始化程序运行环境并调用main函数:

初始化栈空间,设置argc、argv、envp。

调用__libc_start_main函数(libc.so提供),该函数负责:

初始化 C 标准库。

调用main函数,传入argc和argv参数。

捕获main函数的返回值,调用exit函数终止程序。

5.6.4 main函数执行

参数校验:判断argc是否等于5,若不等于,调用printf输出用法提示,调用exit(1) 终止程序。

循环执行:初始化局部变量i=0,进入循环(i从0到9):

调用 printf 函数,输出 "Hello 2024112825 张若润 18503669307"。

调用 sleep 函数,休眠 argv[4]指定的秒数。

等待输入:循环结束后,调用 getchar 函数,等待用户输入一个字符。

返回结果:getchar 返回后,main 函数返回 0。

5.6.5 程序终止

__libc_start_main捕获 main 函数的返回值 0,调用exit(0)函数。

exit函数执行资源清理:关闭所有打开的文件描述符、刷新 stdio 缓冲区、释放动态内存等。

调用_exit系统调用,通知操作系统终止当前进程,回收进程占用的资源(内存、CPU 等)。

5.6.6 gdb 跟踪执行流程

5.7Hello的动态链接分析

hello程序采用动态链接方式依赖C标准库,动态链接的核心是“运行时链接”,即可执行文件不包含libc.so的代码,仅存储依赖信息,程序运行时由动态链接器(ld-linux.so.2)加载libc.so并完成符号解析和重定位,实现函数调用。

hello 的动态链接依赖信息:

动态链接前,GOT表条目:

动态链接后,GOT表条目:

动态链接采用延迟绑定机制,即函数第一次被调用时才进行符号解析和地址填充,而非程序启动时一次性解析所有动态符号,减少程序启动时间。以printf函数为例,延迟绑定流程如下:

1.第一次调用printf时,main函数中的call 0x400428 <printf@plt>跳转到PLT表的 printf条目。

2.PLT 条目执行jmpq *0x401008(%rip),此时GOT表中0x401008 条目指向PLT条目的下一条指令。

3.延迟绑定代码调用动态链接器的_dl_runtime_resolve函数,解析printf符号,查找其在libc.so中的实际地址。

4._dl_runtime_resolve将printf的实际地址写入GOT表的0x401008条目,更新GOT表。

5.跳转到printf函数的实际地址,执行printf功能。

6.后续调用printf时,PLT条目直接通过GOT表中的实际地址跳转,无需再次解析,提高执行效率。

5.8本章小结

本章详细分析了Hello程序的链接阶段。通过使用-no-pie选项编译,我们得到了一个静态链接的可执行文件。主要发现包括:

1.静态链接特性:所有库代码合并到可执行文件中,无动态依赖

2.固定地址加载:由于-no-pie选项,程序加载到固定虚拟地址

3.简化内存布局:只有程序本身的内存映射,无共享库映射

4.直接函数调用:无需PLT/GOT机制,函数调用使用直接地址

这种静态链接方式使得程序分析更加简单直观,但牺牲了现代操作系统的某些安全特性(如ASLR)。对于学习和理解计算机系统原理来说,这种简化是有益的。

6章hello进程管理

6.1进程的概念与作用

进程是计算机科学中的核心概念,是程序的一次执行实例。在操作系统中,进程具有以下特性:

1.独立性:每个进程拥有独立的地址空间和系统资源

2.并发性:多个进程可以并发执行,共享CPU时间片

3.动态性:进程有创建、执行、暂停、终止等生命周期

4.结构性:进程由程序代码、数据、栈、堆和进程控制块(PCB)组成

进程的作用:

1.资源分配的基本单位:操作系统以进程为单位分配CPU、内存等资源

2.程序执行的载体:程序只有被加载为进程才能执行

3.实现并发:通过进程切换实现多任务并发执行

4.提供保护:进程间的地址空间隔离提供系统稳定性

进程控制块(PCB):包含进程ID、状态、优先级、寄存器值、内存映射等信息,是操作系统管理进程的数据结构。

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

Shell是用户与操作系统内核之间的命令行接口,bash(Bourne Again SHell)是最常用的Shell之一。Shell的主要作用:

1.命令解释:解析用户输入的命

2.进程创建:为命令创建执行进程

3.I/O重定向:管理输入输出重定向

4.管道连接:连接多个进程的输入输出

5.作业控制:管理前台和后台作业

6.环境管理:维护环境变量

7.脚本执行:执行Shell脚本

Shell-bash的处理流程:

命令读取:bash通过read系统调用从标准输入读取用户输入的命令行字符串。

命令解析:

分词:将命令行字符串拆分为单词(token),即["./hello","2024112825","张若润","18503669307","3"]。

路径解析:判断命令./hello是否为绝对路径或相对路径,此处为相对路径,bash根据当前工作目录拼接为绝对路径(如/home/user/hello)。

检查内置命令:判断./hello是否为bash内置命令(如cd、echo),此处为外部可执行文件,继续下一步。

创建子进程:bash调用fork系统调用创建一个子进程,子进程是bash进程的副本,继承 bash 的环境变量、打开的文件描述符等。

加载并执行程序:子进程调用execve系统调用,将自身的代码段、数据段替换为 hello 程序的对应内容,加载hello程序并执行,传入参数数组(argv)和环境变量数组(envp)。

父进程等待:bash父进程调用waitpid系统调用,阻塞等待子进程(hello 进程)终止。

回收资源与反馈结果:hello 进程终止后,内核向 bash 父进程发送 SIGCHLD 信号,bash 父进程回收子进程的资源(如进程控制块 PCB),获取子进程的退出状态码,若退出状态码为 0 表示执行成功,否则表示失败,bash 准备接收下一条用户命令。

6.3Hello的fork进程创建过程

fork系统调用是Linux中创建新进程的核心接口,bash通过fork创建子进程,再由子进程执行hello程序。fork创建进程的核心特点是Copy-On-Write,COW,即子进程初始时与父进程共享所有内存空间,仅当其中一方修改内存数据时,才会为修改方分配新的内存页并复制数据,避免创建进程时的大量内存复制开销。

hello进程的fork 创建过程如下:

1.父进程准备:bash父进程执行fork系统调用,陷入内核态。

内核初始化子进程PCB:

2.内核为子进程分配唯一的进程ID,其他进程标识符。

复制父进程的PCB信息,包括进程状态、优先级、打开的文件描述符表、信号掩码、环境变量等,但子进程的进程状态初始化为就绪态。

3.内存空间共享:内核不为子进程分配新的物理内存,而是让子进程共享父进程的页表,指向相同的物理内存页。将父进程和子进程共享的物理内存页的权限设置为“只读”,并在页表项中标记COW标志。

4.子进程资源调整:

子进程的程序计数器(PC)、栈指针(SP)等寄存器状态与父进程fork调用时一致,即子进程从fork系统调用的返回处开始执行。

子进程的文件描述符表是父进程的副本,指向相同的文件对象,文件引用计数加 1。

5.返回用户态:

父进程从fork系统调用返回,返回值为子进程的PID。

子进程从fork系统调用返回,返回值为 0。

6.进程调度:内核将父进程和子进程加入就绪队列,由进程调度器根据优先级和调度算法分配CPU时间片,此时hello的子进程已创建完成,等待执行execve系统调用加载hello程序。

6.4Hello的execve过程

execve系统调用的作用是 “替换进程映像”,即子进程(fork创建的bash子进程)通过execve加载hello程序,将自身的代码段、数据段、BSS段、堆、栈等内存空间替换为hello程序的对应内容,进程 PID 保持不变,但进程的执行代码和数据完全替换,实现从bash子进程到hello进程的转变。

hello进程的execve过程如下:

1.参数准备:子进程准备execve的三个参数:

路径名:hello程序的绝对路径(如/home/user/hello)。

argv数组:["./hello", "2024112825", "张若润", "18503669307", "3", NULL]。

envp数组:继承自bash父进程的环境变量。

2.系统调用陷入内核态:子进程执行execve系统调用,CPU从用户态切换到内核态,内核开始处理execve请求。

3.验证程序合法性:

内核检查路径名对应的文件是否存在、是否具有执行权限。

解析文件的 ELF 头部,验证是否为有效的 ELF 可执行文件,确认目标架构与当前系统一致。

4.释放旧进程映像:

内核释放子进程当前的内存空间,包括bash子进程的代码段、数据段、堆、栈等,回收对应的虚拟地址空间。

关闭所有标记为“执行exec时关闭”(FD_CLOEXEC)的文件描述符,保留其他文件描述符(如标准输入、输出、错误)。

5.加载新进程映像(hello程序):

内核解析 hello 的 ELF 程序头表,根据程序头表中的LOAD段信息,将hello的代码段(.text)、数据段(.data)、BSS 段(.bss)等加载到虚拟地址空间的对应位置。

分配并初始化栈空间,将argv、envp数组压入栈中,设置栈指针(SP)指向栈顶。

加载动态链接器(ld-linux.so.2),若hello为动态链接程序,内核将动态链接器映射到虚拟地址空间,由其负责加载依赖的共享库(如libc.so)。

6.设置程序入口地址:

内核将程序计数器(PC)设置为hello的ELF头部中e_entry字段指定的地址。

7.返回用户态:内核完成execve处理,从内核态切换回用户态,CPU开始执行hello程序的_start函数,子进程正式转变为hello进程,执行hello的代码逻辑。

6.5Hello的进程执行

hello进程执行的核心是操作系统的进程调度机制,即内核通过调度算法分配 CPU时间片给hello进程,使其在CPU上执行指令,同时涉及用户态与核心态的切换、进程上下文切换等关键过程。

6.5.1进程调度与时间片分配

Linux 采用完全公平调度算法(CFS),根据进程的优先级和累计运行时间,为每个进程分配公平的CPU时间片。hello进程的调度流程如下:

1.hello进程创建后,内核将其加入就绪队列,CFS调度器计算hello进程的虚拟运行时间,虚拟运行时间越小,进程优先级越高,越容易获得CPU。

1.当CPU空闲时,CFS调度器选择就绪队列中vruntime最小的进程,将CPU分配给hello进程。

3.内核执行进程上下文切换:保存当前运行进程(如bash父进程)的上下文信息(寄存器状态、PC、SP等),恢复hello进程的上下文信息,将CPU的控制权交给hello进程。

4.hello进程在CPU上执行指令,直到时间片用完或发生阻塞(如调用sleep、getchar)。

5.时间片用完时,时钟中断触发,内核保存hello进程的上下文,将其放回就绪队列,重新计算vruntime,调度器选择下一个高优先级进程执行。

6.5.2 用户态与核心态转换

hello 进程执行过程中,会频繁发生用户态与核心态的切换,主要场景包括系统调用(如printf、sleep、getchar)和中断处理:

1.用户态执行:hello进程的_start函数、main函数、printf函数的用户态逻辑(如字符串格式化)等在用户态执行,CPU只能访问用户态内存(如hello的代码段、数据段、栈),不能直接访问内核态内存,权限较低。

2.核心态转换触发:

当hello进程调用printf时,printf底层会调用write系统调用,CPU执行int 0x80指令(32位)或syscall指令(64位),触发陷阱,切换到核心态。

当用户按下键盘按键时,键盘中断触发,CPU切换到核心态,执行中断处理程序。

3.核心态执行:内核在核心态处理系统调用或中断,如write系统调用对应的内核函数sys_write,负责将数据写入显示器设备,核心态下CPU可访问所有内存(用户态+内核态),执行特权指令(如修改页表、控制IO设备)。

4.返回用户态:内核完成系统调用或中断处理后,通过iret指令恢复用户态上下文,切换回用户态,hello进程继续执行后续指令。

6.5.3 hello 进程的执行流程与调度交互

hello进程的执行流程与调度机制的交互如下:

1.hello进程执行main函数中的循环,调用printf函数输出字符串,printf底层调用 write 系统调用,切换到核心态,内核处理write请求,完成输出后返回用户态。

2.调用sleep函数,sleep底层调用nanosleep系统调用,内核将hello进程的状态从运行态改为阻塞态,移出就绪队列,放入睡眠队列,此时CPU可调度其他进程执行。

3.sleep指定的时间到达后,内核定时器触发,将hello进程从睡眠队列移回就绪队列,状态改为就绪态,等待调度器分配CPU。

4.调度器为hello进程分配CPU时间片,hello进程恢复执行,继续循环输出,直到循环结束。

5.调用getchar函数,getchar底层调用read系统调用,若键盘缓冲区无数据,内核将hello进程设置为阻塞态,等待用户输入。

6.用户按下键盘按键后,键盘中断触发,内核将hello进程改为就绪态,调度器分配CPU,hello进程读取按键数据,getchar返回,main函数返回0,进程执行结束。

6.6hello的异常与信号处理

正常运行hello:

Ctrl-Z暂停进程:

使用fg恢复运行:

使用Ctrl-C终止进程

在后台运行

6.7本章小结

本章深入分析了Hello程序的进程管理机制。通过分析,我们了解到:

1.进程的本质:进程是程序执行的实例,拥有独立的资源和上下文

2.Shell的作用:作为用户接口,解析命令并创建进程

3.进程创建:通过fork系统调用创建新进程,采用写时复制优化

4.程序加载:通过execve系统调用加载hello程序,替换进程映

5.进程执行:操作系统通过调度器管理进程执行,涉及上下文切换和状态转换

6.异常与信号:hello程序会处理多种异常和信号,操作系统提供了相应的处理机制

进程管理是操作系统的核心功能,它使得多个程序能够并发执行,共享系统资源,同时保证系统的稳定性和安全性。hello程序的执行过程完整地展示了从进程创建到终止的完整生命周期。

7章hello的存储管理

7.1hello的存储器地址空间

在计算机系统中,hello程序作为一个用户进程,其存储器地址空间是一个逻辑上的连续地址范围,并非直接对应物理内存的实际地址。该地址空间按照功能被划分为多个区域,从低地址到高地址依次包括:代码段(.text)、数据段(.data)、BSS段(.bss)、堆、共享库区域和栈。

1.代码段存储hello程序的机器指令,具有只读属性,确保程序指令不会被意外修改,hello中main函数的汇编指令、printf和sleep等函数的调用指令均存放于此。

2.数据段用于存储已初始化且非零的全局变量和静态变量,若hello程序中存在初始化的全局变量(如intglobal_init=10;),则会被分配到该区域。

3.BSS段存放未初始化或初始化为零的全局变量和静态变量,由操作系统在程序加载时初始化为零,hello中若定义staticintglobal_uninit;,会位于此段。

4.堆区域用于程序运行时的动态内存分配,hello中printf函数内部可能通过malloc申请动态内存,其内存空间就来自堆。堆的地址由低向高增长,通过内存分配函数(如malloc)和释放函数(如free)管理。

5.共享库区域加载程序依赖的动态链接库,如hello依赖的libc.so库(包含printf、sleep等函数的实现),会被映射到该区域。

6.栈区域用于存储函数调用过程中的局部变量、函数参数、返回地址等,hello中main函数的局部变量i、argc、argv,以及函数调用时的参数传递和返回地址均通过栈实现。栈的地址由高向低增长,遵循“先进后出”的原则,函数调用时栈帧创建,函数返回时栈帧销毁。

逻辑地址是程序代码中使用的地址(如hello汇编指令中操作数对应的地址),是相对于进程地址空间起始位置的偏移量;线性地址是逻辑地址经过段式变换后得到的地址,是平坦的地址空间;虚拟地址与线性地址在Linux系统中通常视为同一概念(x86-64架构下),是进程视角下的地址;物理地址是实际内存硬件的地址,是数据在物理内存中的真实存储位置。hello程序运行时,CPU执行指令使用的是虚拟地址,通过地址变换机制转换为物理地址,从而访问实际内存。

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

Intel x86 架构中,段式管理是地址变换的第一步,负责将逻辑地址转换为线性地址。逻辑地址由“段选择符”和“偏移量”两部分组成,段选择符存放在段寄存器(如CS、DS、SS等)中,偏移量由指令给出。

段式管理的核心是段描述符表(GDT或LDT),其中每个段描述符对应一个内存段,包含段的基地址、段的大小、段的访问权限等信息。hello程序运行时,CPU会根据段寄存器中的段选择符,从GDT或LDT中找到对应的段描述符,提取段基地址,然后将段基地址与逻辑地址中的偏移量相加,得到线性地址。

以hello程序的代码段为例,CS寄存器中存放代码段的段选择符,该选择符指向GDT中代码段的段描述符。段描述符中记录了代码段的基地址(如0x0000000000400000),hello程序中某条指令的逻辑地址偏移量为0x1234,则线性地址为0x0000000000400000+0x1234=0x0000000000401234。

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

页式管理是将线性地址转换为物理地址的核心机制,其核心思想是将线性地址空间和物理地址空间均划分为固定大小的块,称为“页”(通常为4KB),线性地址空间的页称为“虚拟页”,物理地址空间的页称为“物理页”。页式管理通过页表实现虚拟页到物理页的映射,从而将线性地址(虚拟地址)转换为物理地址。

hello程序运行时,CPU会将线性地址拆分为页目录索引、页表索引和页内偏移三部分(x86-64架构下为四级页表,拆分后包含更多索引部分,核心原理一致)。以32位系统的两级页表为例,线性地址的高10位为页目录索引,中间10位为页表索引,低12位为页内偏移。

地址变换过程如下:

CPU通过控制寄存器CR3获取页目录表的物理基地址,根据线性地址的页目录索引,在页目录表中找到对应的页目录项。

页目录项中存储了页表的物理基地址,根据线性地址的页表索引,在该页表中找到对应的页表项(PTE)。

页表项中存储了虚拟页对应的物理页基地址,将该物理页基地址与线性地址的页内偏移相加,得到最终的物理地址。

例如,hello程序中某变量的线性地址为0x0000000000401234,页大小为4KB,则页内偏移为0x234,剩余的高地址部分0x0000000000401为虚拟页号。通过页表查询,该虚拟页对应的物理页基地址为0x12345000,则物理地址为0x12345000+0x234=0x12345234。

页式管理的优势在于可以实现虚拟内存,多个进程可以共享物理内存,同时通过页表项的权限位实现内存保护,防止hello程序越权访问其他进程的内存或修改只读内存区域。

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

在x86-64架构中,为了支持庞大的虚拟地址空间(2^64),采用四级页表结构,从高到低依次为PML4(页映射四级目录)、PDPT(页目录指针表)、PD(页目录表)、PT(页表)。线性地址(虚拟地址)被拆分为PML4索引、PDPT索引、PD索引、PT索引和页内偏移五部分,各部分位数根据页大小而定(4KB页时,各索引部分为9位,页内偏移为12位)。

TLB是CPU中的高速缓存,用于缓存最近使用的虚拟页号到物理页号的映射关系,避免每次地址变换都需要遍历四级页表,提高地址变换效率。因为遍历四级页表需要多次访问物理内存,而TLB的访问速度接近CPU寄存器,能显著降低地址变换的延迟。

在TLB与四级页表支持下,hello程序的VA(虚拟地址)到PA(物理地址)的变换过程如下:

1.CPU首先将虚拟地址的虚拟页号(PML4索引+PDPT索引+PD索引+PT索引)与TLB中的条目进行比对,查看是否存在命中的映射关系。

2.若TLB命中,直接从TLB中获取对应的物理页基地址,与页内偏移相加得到物理地址,完成地址变换。

3.若TLB未命中,CPU会启动四级页表遍历:

根据CR3寄存器中的PML4基地址和虚拟地址的PML4索引,找到对应的PML4条目,获取PDPT的物理基地址。

根据PDPT索引和PDPT物理基地址,找到PDPT条目,获取PD的物理基地址。

根据PD索引和PD物理基地址,找到PD条目,获取PT的物理基地址。

根据PT索引和PT物理基地址,找到PT条目,获取物理页基地址。

4.将物理页基地址与页内偏移相加得到物理地址,同时将该虚拟页号与物理页号的映射关系存入TLB,以便后续访问时命中。

hello程序运行过程中,频繁访问的代码段和数据段的地址映射会被缓存到TLB中,后续访问时无需遍历四级页表,大幅提升了内存访问效率。

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

Cache(高速缓冲存储器)是位于CPU与物理内存之间的高速存储部件,用于缓存CPU近期可能访问的内存数据,解决CPU运算速度与物理内存访问速度不匹配的问题。三级Cache(L1、L2、L3)采用分级架构,L1 Cache集成在CPU核心内部,访问速度最快但容量最小;L2 Cache同样位于CPU核心内部,容量和访问速度介于L1和L3之间;L3 Cache为多核CPU共享,容量最大,访问速度相对较慢,但仍远快于物理内存。

hello程序的物理内存访问过程在三级Cache支持下如下:

1.CPU通过地址变换得到物理地址后,首先访问L1 Cache,根据物理地址的索引部分查找L1 Cache中的缓存行。

2.若L1 Cache命中(缓存行中存在所需数据),直接从L1 Cache读取数据到CPU寄存器,访问结束,该过程延迟极低(通常为几个CPU时钟周期)。

3.若L1 Cache未命中,访问L2 Cache,同样根据物理地址索引查找L2 Cache的缓存行。

4.若L2 Cache命中,将数据读取到CPU寄存器,同时将该数据写入L1 Cache,以便后续快速访问。

5.若L2 Cache未命中,访问L3 Cache,查找对应的缓存行。

6.若L3 Cache命中,将数据读取到CPU寄存器,并逐级写入L2和L1 Cache。

7.若L3 Cache未命中,CPU会向内存控制器发送请求,访问物理内存,将所需数据从物理内存读取到CPU寄存器,同时逐级写入L3、L2和L1 Cache,该过程延迟最高(通常为几十到几百个CPU时钟周期)。

Cache的缓存策略通常采用“局部性原理”,即CPU近期访问的数据往往集中在某个连续的内存区域。hello程序中循环执行printf函数时,会反复访问代码段中的printf调用指令和数据段中的字符串常量,这些数据会被缓存到三级Cache中,后续访问时直接从Cache读取,显著提升了程序的执行效率。

7.6hello进程fork时的内存映射

fork系统调用用于创建一个新进程,新进程(子进程)是原进程(父进程,即hello进程)的副本。在fork调用过程中,操作系统采用“写时复制”(Copy-On-Write,COW)技术管理内存映射,避免直接复制父进程的全部内存空间,提高创建进程的效率。

hello进程fork时的内存映射过程如下:

1.父进程(hello进程)调用fork后,操作系统为子进程创建新的进程控制块(PCB),并复制父进程的页表,此时子进程的页表与父进程的页表完全一致,指向相同的物理内存页。

2.操作系统将父进程和子进程页表中对应物理页的权限设置为“只读”,同时在PCB中记录这些共享的物理页。

3.子进程创建完成后,父进程和子进程继续执行,此时两者共享所有物理内存数据(代码段、数据段、BSS段、堆、栈等)。

4.当父进程或子进程尝试修改某共享物理页的数据时(如修改堆中的动态内存或栈中的局部变量),CPU会触发页故障(因为页权限为只读)。

5.操作系统处理该页故障时,会为修改方分配新的物理内存页,将原共享页的数据复制到新页中,然后更新修改方的页表,使其指向新的物理页,并将该页的权限恢复为“可写”。此时,父进程和子进程的该内存页不再共享,各自拥有独立的副本。

对于hello程序而言,fork创建的子进程初始时与父进程共享代码段(hello的机器指令)、数据段、BSS段、堆和栈中的数据。若子进程仅执行读操作(如读取argv参数),则始终共享这些内存页;若子进程修改局部变量i或调用malloc分配并修改动态内存,则会触发写时复制,为这些内存页创建独立副本。这种机制既保证了进程的独立性,又减少了fork时的内存复制开销,提升了系统性能。

7.7hello进程execve时的内存映射

execve系统调用用于加载一个新的程序替换当前进程的内存空间,hello进程若调用execve(如在shell中执行另一个程序),则当前hello进程的代码段、数据段、BSS段、堆、栈等内存区域会被新程序的对应区域替换,进程的PID保持不变,但进程的内存映射发生根本性变化。

hello进程execve时的内存映射过程如下:

1.操作系统首先验证execve参数指定的新程序文件(如另一个可执行文件test)是否有效(是否为ELF格式、是否具有执行权限等)。

2.若程序有效,操作系统释放当前hello进程的所有内存映射,包括代码段、数据段、BSS段、堆、栈以及共享库的映射区域,回收对应的虚拟地址空间。

3.操作系统解析新程序的ELF文件头和程序头表,根据程序头表中的信息,为新程序分配虚拟地址空间,并将新程序的代码段、数据段、BSS段等加载到对应的虚拟地址区域。

代码段被映射到只读的虚拟地址区域,数据段和BSS段被映射到可写的虚拟地址区域。

若新程序依赖动态链接库(如libc.so),操作系统会加载这些动态链接库,将其映射到新程序的虚拟地址空间的共享库区域。

4.操作系统初始化新程序的栈空间,设置argc(参数个数)、argv(参数数组)、envp(环境变量数组)等参数,将其压入栈中。

5.操作系统设置程序计数器(PC)指向新程序的入口地址(通常为ELF文件头中的e_entry字段指定的地址,即_start函数的地址),随后CPU开始执行新程序的指令。

例如,hello进程执行execve("./test",NULL,NULL)后,hello的内存空间被test程序替换,test的代码段、数据段等被加载到虚拟地址空间,栈中被初始化test的argc、argv等参数,CPU开始执行test的指令,hello程序的代码和数据不再存在于该进程的内存空间中。

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

缺页故障(Page Fault)是指CPU访问某虚拟地址时,对应的虚拟页未被映射到物理内存(即页表项中标记为“未Present”),此时CPU会触发缺页中断,由操作系统进行处理。在虚拟内存机制中,程序的所有虚拟页并非全部加载到物理内存,而是根据需要动态加载,缺页故障是实现虚拟内存的关键机制。

hello程序运行过程中可能触发缺页故障的场景包括:程序启动时,代码段和数据段的部分虚拟页未加载到物理内存;访问堆或栈的扩展区域(如栈溢出时的栈扩展、malloc分配的动态内存对应的虚拟页未映射物理内存);访问动态链接库中未加载的代码或数据页。

缺页中断的处理过程如下:

1.CPU检测到缺页故障后,暂停当前指令的执行,保存当前进程的上下文(如寄存器状态、程序计数器等),并将缺页中断的原因(如虚拟地址、故障类型)存入相关寄存器。

2.CPU根据中断向量表,跳转到操作系统的缺页中断处理程序。

3.缺页中断处理程序首先检查引发故障的虚拟地址是否合法(是否在当前进程的虚拟地址空间内)。

若虚拟地址非法(如访问超出进程地址空间的地址),则向当前进程发送SIGSEGV信号(段错误信号),hello进程若未处理该信号,会终止运行并输出段错误信息。

若虚拟地址合法,则操作系统为该虚拟页分配空闲的物理内存页。

4.操作系统将该虚拟页对应的磁盘数据(如hello程序文件中的代码段数据、交换分区中的数据)加载到分配的物理内存页中。

5.操作系统更新当前进程的页表,将该虚拟页与分配的物理页建立映射,并标记页表项为“Present”。

6.恢复进程的上下文,让CPU回到触发缺页故障的指令处重新执行,此时该虚拟地址已映射到物理内存,指令可正常执行。

例如,hello程序启动时,其代码段的部分虚拟页未加载到物理内存,当CPU执行这些虚拟页对应的指令时,会触发缺页中断。操作系统分配物理内存页,将磁盘上的代码段数据加载到物理内存,更新页表后,指令继续执行。缺页中断机制使得程序可以在物理内存不足的情况下运行,仅加载当前需要的内存页,提高了内存的利用率。

7.9动态存储分配管理

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

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

7.10本章小结

本章围绕hello程序的存储管理展开,详细分析了其存储器地址空间的构成,以及从逻辑地址到线性地址、线性地址到物理地址的两级地址变换机制。在段式管理中,x86-64架构下Linux 系统将段基地址设为0,段式管理主要用于权限检查;页式管理通过页表实现虚拟页到物理页的映射,是地址变换的核心。TLB作为高速缓存,缓存了频繁使用的地址映射关系,四级页表则支持了x86-64架构的庞大虚拟地址空间,两者结合大幅提升了地址变换效率。

三级Cache通过缓存近期访问的内存数据,解决了CPU与物理内存的速度不匹配问题,显著提升了hello程序的内存访问效率。hello进程fork时采用的写时复制技术,避免了不必要的内存复制,提高了进程创建效率;execve时则会替换进程的内存映射,加载新程序的内存区域。缺页故障与缺页中断处理是虚拟内存机制的核心,使得程序可以在物理内存不足的情况下正常运行,提高了内存利用率。

通过本章的分析,深入理解了计算机系统中存储管理的核心机制,以及这些机制如何支撑hello程序的正常运行,体现了存储管理在计算机系统中的重要性。

8章hello的IO管理

8.1Linux的IO设备管理方法

Linux系统采用“一切皆文件”的理念管理IO设备,将所有IO设备(如键盘、显示器、磁盘、网卡等)都抽象为文件,通过统一的文件系统接口进行管理。这种抽象机制使得用户程序无需关注具体设备的硬件细节,只需通过标准的文件操作函数(如open、read、write、close等)即可实现对不同IO设备的访问,简化了IO编程的复杂性。

Linux的IO设备管理主要通过以下关键组件实现:

8.1.1设备文件

设备文件是IO设备在文件系统中的抽象表示,存储在/dev目录下,分为字符设备文件和块设备文件:

1.字符设备文件:对应字符设备(如键盘、显示器、串口等),以字符为单位进行数据传输,不具备缓存功能,数据传输是实时的。hello程序中使用的键盘(getchar函数读取输入)和显示器(printf函数输出)对应的设备文件分别为/dev/tty(终端设备文件),属于字符设备文件。

2.块设备文件:对应块设备(如磁盘、U盘等),以块(通常为512字节或4KB)为单位进行数据传输,具备缓存功能,数据传输效率较高。

设备文件通过主设备号和次设备号标识具体的设备:主设备号用于标识设备驱动程序(同一类设备共享同一个主设备号),次设备号用于标识同一类设备中的具体设备(如多个磁盘分区对应不同的次设备号)。例如,/dev/tty的主设备号为5,次设备号为0,通过这两个编号,系统可以找到对应的设备驱动程序和具体设备。

8.1.2设备驱动程序

设备驱动程序是操作系统内核中与具体硬件设备交互的软件模块,负责将操作系统的抽象IO请求转换为硬件设备能够理解的命令,同时处理设备的中断请求,完成数据的传输。设备驱动程序隐藏了设备的硬件细节,为上层提供统一的接口。

Linux系统的设备驱动程序分为内核态驱动和用户态驱动,大多数设备采用内核态驱动。当hello程序通过文件操作函数访问IO设备时,系统调用会陷入内核态,内核根据设备文件的主设备号找到对应的设备驱动程序,由驱动程序与硬件设备交互,完成数据的读取或写入。

8.1.3文件系统接口

Linux通过VFS(虚拟文件系统)提供统一的文件系统接口,VFS屏蔽了不同文件系统(如ext4、xfs、tmpfs等)和设备文件的差异,为用户程序提供了一致的文件操作接口(open、read、write、close、lseek等)。当hello程序调用这些函数访问设备文件时,VFS会将请求转发给对应的设备驱动程序或文件系统,完成实际的IO操作。

8.1.4中断处理

IO设备的操作通常是异步的(如键盘按键、磁盘IO完成),设备完成操作后会向CPU发送中断请求,通知CPU处理结果。Linux内核通过中断控制器管理中断请求,为每个设备分配唯一的中断号,当收到中断请求时,CPU会暂停当前任务,跳转到对应的中断处理程序。

hello程序调用getchar函数等待键盘输入时,CPU可以执行其他任务,当用户按下键盘按键时,键盘设备会发送中断请求,内核的键盘中断处理程序会读取按键的扫描码,转换为ASCII码,存入系统的键盘缓冲区,随后getchar函数通过read系统调用读取该ASCII码并返回。中断处理机制提高了CPU的利用率,避免了CPU对IO设备的忙等待。

8.2简述UnixIO接口及其函数

Unix IO接口是一套标准的文件操作接口,适用于所有Unix/Linux系统,遵循“一切皆文件”的理念,提供了统一的IO操作方式,hello程序中的printf和getchar函数底层均依赖这些接口实现。核心的Unix IO函数如下:

8.2.1open函数

功能:打开或创建一个文件(包括设备文件),返回一个文件描述符(file descriptor,fd),后续的IO操作通过该文件描述符标识文件。

原型:int open(const char*pathname , int flags , mode_t mode);

参数说明:

pathname:文件路径(如"/dev/tty"、"test.txt")。

flags:打开方式标志,如O_RDONLY(只读打开)、O_WRONLY(只写打开)、O_RDWR(读写打开)、O_CREAT(文件不存在时创建)等。

mode:创建文件时的权限(如0644,表示所有者可读可写,其他用户可读),仅当flags包含O_CREAT时有效。

返回值:成功返回非负整数(文件描述符),失败返回-1。

hello程序若要向显示器输出数据,底层会打开/dev/tty设备文件,使用O_WRONLY标志,返回的文件描述符用于后续的write操作。

8.2.2read函数

功能:从文件描述符对应的文件中读取数据到缓冲区。

原型:ssize_tread(intfd,void*buf,size_tcount);

参数说明:

fd:文件描述符(由open函数返回)。

buf:存储读取数据的缓冲区指针。

count:请求读取的字节数。

返回值:成功返回实际读取的字节数(可能小于count,如到达文件末尾),失败返回-1,读取到文件末尾返回0。

hello程序中的getchar函数底层会调用read函数,从键盘对应的文件描述符(如0,标准输入)读取1个字节的ASCII码数据。

8.2.3write函数

功能:将缓冲区中的数据写入文件描述符对应的文件。

原型:ssize_twrite(int fd,const void*buf, size_t count);

参数说明:

fd:文件描述符。

buf:存储要写入数据的缓冲区指针。

count:要写入的字节数。

返回值:成功返回实际写入的字节数(通常等于count,若发生错误可能小于count),失败返回-1。

hello程序中的printf函数底层会调用write函数,将格式化后的字符串写入显示器对应的文件描述符(如1,标准输出)。

8.2.4close函数

功能:关闭文件描述符,释放与该文件相关的资源(如缓冲区、文件锁等)。

原型:int close(int fd);

参数说明:fd为要关闭的文件描述符。

返回值:成功返回0,失败返回-1。

当hello程序完成IO操作后(如程序退出时),会关闭所有打开的文件描述符,释放系统资源。

8.2.5lseek函数

功能:调整文件的读写指针位置,仅适用于可随机访问的文件(如磁盘文件),字符设备文件(如键盘、显示器)不支持该操作。

原型:off_tlseek(intfd,off_toffset,intwhence);

参数说明:

fd:文件描述符。

offset:偏移量,可正可负。

whence:偏移基准,如SEEK_SET(从文件开头计算)、SEEK_CUR(从当前指针位置计算)、SEEK_END(从文件末尾计算)。

返回值:成功返回调整后的文件指针相对于文件开头的偏移量,失败返回-1。

8.2.6select/poll/epoll函数

功能:IO多路复用函数,用于同时监控多个文件描述符的IO状态(如是否可读、可写),避免对单个文件描述符的忙等待,提高程序的IO效率,适用于需要处理多个IO设备的场景。

Unix IO接口的特点是简单、统一、高效,所有IO设备均通过文件描述符进行操作,用户程序无需关注设备的硬件细节,只需调用标准的IO函数即可完成数据传输。

8.3printf的实现分析

printf函数是C语言标准库中的格式化输出函数,用于将格式化后的字符串输出到标准输出(默认是显示器)。hello程序中多次调用printf函数输出"Hello % s % s % s\n"格式的字符串,其底层实现依赖Unix IO接口和系统调用,整个流程可分为以下几个关键步骤:

8.3.1格式化字符串处理

printf函数首先解析格式化字符串(如"Hello % s % s % s\n")和可变参数(argv[1]、argv[2]、argv[3]),将可变参数按照格式化字符串指定的格式(如%s表示字符串)替换到对应的位置,生成一个完整的字符串。这一步骤由标准库中的vsprintf函数实现:

vsprintf函数接收格式化字符串、可变参数列表(va_list)和输出缓冲区,将格式化后的字符串写入缓冲区。

hello程序中printf("Hello % s % s % s\n",argv[1],argv[2],argv[3]),vsprintf会将argv[1]、argv[2]、argv[3]对应的字符串替换%s,生成"Hello 2024112825 张若润18503669307\n"(假设输入参数正确),并存储到缓冲区中。

8.3.2系统调用准备

格式化字符串生成后,printf函数需要将缓冲区中的字符串输出到显示器,这一步骤需要通过Unix IO接口的write函数实现。在此之前,需要确认标准输出对应的文件描述符:

Linux系统中,每个进程启动时会默认打开三个文件描述符:0(标准输入,stdin)、1(标准输出,stdout)、2(标准错误,stderr),其中标准输出默认对应显示器设备(/dev/tty)。

printf函数默认向标准输出写入数据,因此使用文件描述符1。

8.3.3系统调用与内核态处理

printf函数调用write(1,buf,len),其中buf是存储格式化后字符串的缓冲区指针,len是字符串的长度。该调用会触发系统调用,从用户态切换到内核态,内核执行以下操作:

1.内核根据文件描述符1找到对应的文件对象(struct file),该文件对象关联到显示器对应的设备文件(/dev/tty)的inode节点。

2.inode节点中存储了设备的主设备号和次设备号,内核根据主设备号找到对应的终端设备驱动程序。

3.内核将write函数传递的缓冲区数据和长度传递给终端设备驱动程序,驱动程序将数据转换为硬件设备能够理解的命令(如向显示器的显存写入数据)。

8.3.4硬件设备输出

终端设备驱动程序与显示器硬件交互,完成数据的输出:

1.显示器的显示原理是通过显存(Video RAM,VRAM)存储每个像素的RGB颜色信息,显示芯片按照刷新频率(如60Hz)逐行读取VRAM中的数据,通过信号线传输到液晶显示器,驱动显示器的像素发光,形成可见的字符和图像。

2.驱动程序将格式化后的字符串对应的ASCII码转换为字模库中的字符点阵数据(每个字符由若干个像素组成),并将这些点阵数据写入VRAM的对应位置。

3.显示芯片读取VRAM中的点阵数据,控制显示器的对应像素发光,最终在屏幕上显示出"Hello 2024112825 张若润 18503669307"字符串。

8.3.5状态返回与用户态切换

数据写入完成后,驱动程序向内核返回操作结果(如实际写入的字节数),内核将该结果返回给用户态的printf函数,printf函数最终返回实际输出的字符数,完成整个输出过程。

8.4getchar的实现分析

getchar函数是C语言标准库中的字符输入函数,用于从标准输入(默认是键盘)读取一个字符(ASCII码),hello程序在循环输出10次后调用getchar函数等待用户输入。其底层实现依赖Unix IO接口、中断处理机制和系统调用,整个流程如下:

8.4.1函数调用与阻塞等待

hello程序调用getchar函数后,getchar函数会调用标准库中的read函数,向标准输入(文件描述符0)发起读取1个字节的请求。此时,若系统的键盘缓冲区中没有未读取的字符,read函数会进入阻塞状态,等待用户输入。

8.4.2键盘中断触发与处理

当用户按下键盘上的某个按键时,键盘设备会产生一个扫描码(对应按键的物理位置),并向CPU发送异步中断请求(键盘中断):

1.CPU收到键盘中断后,暂停当前正在执行的任务(如hello程序的其他代码),保存当前进程的上下文(寄存器状态、程序计数器等)。

2.CPU根据中断向量表找到对应的键盘中断处理程序(内核态代码),并跳转到该程序执行。

8.4.3扫描码转换与缓冲区存储

键盘中断处理程序执行以下操作:

1.读取键盘发送的扫描码,根据键盘映射表(如ASCII码映射表)将扫描码转换为对应的ASCII码(如按下字母'a',扫描码转换为ASCII码97)。

2.将转换后的ASCII码存入系统的键盘缓冲区(内核维护的一个环形缓冲区),用于缓存用户输入的字符。

8.4.4阻塞解除与数据读取

当键盘缓冲区中有数据后,内核会唤醒之前阻塞的read系统调用,read函数从键盘缓冲区中读取1个字节的ASCII码,存入用户程序提供的缓冲区:

1.read函数从内核态切换回用户态,将读取到的ASCII码返回给getchar函数。

2.getchar函数将该ASCII码转换为对应的字符(如ASCII码97转换为字符'a'),并返回给hello程序。

getchar函数默认是行缓冲的,即用户需要按下回车键后,键盘缓冲区中的字符才会被read函数读取。这是因为终端设备默认工作在规范模式(canonical mode),内核会将用户输入的字符缓存起来,直到收到回车键(ASCII码13)后,才将整行字符送入键盘缓冲区,供read函数读取。若用户按下Ctrl+C、Ctrl+Z等组合键,内核会将其解析为信号(如SIGINT、SIGTSTP),并发送给hello程序,而非作为普通字符存入键盘缓冲区。

8.4.5异常情况处理

若用户在getchar函数等待输入时按下Ctrl+C,内核会向hello程序发送SIGINT信号(中断信号),若hello程序未自定义该信号的处理函数,会默认终止程序运行;若按下Ctrl+Z,会发送SIGTSTP信号(暂停信号),hello程序会被暂停,转入后台运行,可通过fg命令恢复前台运行。

8.5本章小结

本章详细分析了hello程序的IO管理机制,围绕Linux系统“一切皆文件”的核心思想,阐述了IO设备的抽象管理方式。Linux通过设备文件、设备驱动程序、文件系统接口和中断处理四大组件,实现了对不同IO设备的统一管理,用户程序无需关注硬件细节,即可通过标准接口访问设备。

Unix IO接口提供了open、read、write、close等核心函数,为hello程序的IO操作提供了基础,这些函数遵循统一的调用规范,适用于所有文件(包括设备文件)。printf函数的底层实现依赖vsprintf进行字符串格式化,再通过write系统调用将数据写入标准输出,最终由终端驱动程序将数据转换为显示器的像素信息,完成输出;getchar函数则通过read系统调用读取标准输入,依赖键盘中断处理程序将扫描码转换为ASCII码并缓存,实现字符输入。

通过本章的分析,明确了用户程序IO操作的底层流程,理解了系统调用、中断处理、设备驱动等关键机制在IO管理中的作用,体现了Linux系统IO管理的统一性、高效性和灵活性。

结论

通过深入分析Hello程序的完整生命周期,我们获得了对计算机系统设计与实现的深刻理解:

Hello所经历的过程总结:

编写与预处理(P2P开始):编写C源代码hello.c;预处理展开头文件,生成hello.i

编译与汇编:编译器将C代码转换为汇编代码hello.s;汇编器生成机器码目标文件hello.o

链接与加载:链接器合并目标文件和库,生成可执行文件hello;Shell调用fork创建进程,execve加载程序

进程执行(020过程):操作系统调度进程执行;CPU执行指令,访问内存;

处理异常和信号

IO交互:printf输出到屏幕;getchar从键盘读取输入;Sleep系统调用实现等待

进程终止(020结束):程序执行完毕;进程终止,资源回收;返回Shell

对计算机系统的深切感悟:

分层抽象的力量:从晶体管到应用程序的多层抽象;每层隐藏复杂性,提供简洁接口;使得系统既高效又易于使用

软硬件协同设计:硬件提供基础能力(CPU、内存、IO);操作系统管理和虚拟化资源;应用程序专注于业务逻辑

性能与功能的平衡:缓存层次平衡速度与容量;虚拟内存平衡物理限制与地址空间需求;进程调度平衡响应时间与吞吐量

可靠性与安全性:进程隔离保护系统稳定性;权限控制保护系统安全;异常处理保证程序健壮性

创新理念与思考:新的设计与实现方法:

智能化内存管理:基于机器学习预测内存访问模式,优化页面置换和缓存策略

自适应调度算法:根据应用特性和用户行为动态调整调度策略

安全优先的架构:硬件支持的内存安全特性,防止缓冲区溢出等漏洞

跨层优化机会:编译器了解硬件特性,生成优化代码;操作系统了解应用模式,优化资源分配;应用了解系统特性,优化算法实现

面向未来的思考:异构计算:CPU、GPU、TPU协同工作;量子计算:全新的计算范式;神经形态计算:仿脑计算架构

通过这次对Hello程序的深入分析,我们不仅理解了计算机系统的工作原理,更体会到了系统设计中蕴含的智慧和美感。每一个简单的"Hello World"背后,都是一个极其复杂而精巧的系统在支撑。这种从简单现象深入复杂本质的探索过程,正是计算机科学的魅力所在。

附件

列出所有的中间产物的文件名,并予以说明起作用。

文件名

生成命令

作用说明

hello.c

手工编写

原始C源代码

hello.i

gcc -E hello.c -o hello.i

预处理后的源文件,包含所有头文件内容

hello.s

gcc -S hello.i -o hello.s

汇编语言文件,x86-64汇编代码

hello.o

gcc -c hello.s -o hello.o

可重定位目标文件,包含机器码但地址未确定

hello

gcc hello.o -o hello

可执行文件,可直接运行

hello.elf

readelf -a hello > hello.elf

ELF格式详细分析报告

hello.dis

objdump -d hello > hello.dis

反汇编代码,用于分析机器指令

hello.o.dis

objdump -d -r hello.o > hello.o.dis

hello.o的反汇编,显示重定位信息

hello.sym

nm hello > hello.sym

符号表,显示程序中的符号和地址

hello.map

gcc -Wl,-Map=hello.map hello.o -o hello

链接映射文件,显示链接过程中的详细信息

hello.debug

gcc -g hello.c -o hello.debug

带调试信息的可执行文件

hello.strace

strace -o hello.strace ./hello ...

系统调用跟踪记录

hello.ltrace

ltrace -o hello.ltrace ./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]KANAMORIH.ShakingWithoutQuaking[J].Science,1998,279(5359):2063-2064.

[6]CHRISTINEM.PlantPhysiology:PlantBiologyintheGenomeEra[J/OL].Science,1998,281:331-332[1998-09-23].http://www.sciencemag.org/cgi/collection/anatmorp.

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2402_88640405/article/details/156571338

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--