> 技术文档 > 计算机系统大作业——程序人生_13th gen intel(r) core(tm) i7-13700h 2.40 ghz的吞吐量达

计算机系统大作业——程序人生_13th gen intel(r) core(tm) i7-13700h 2.40 ghz的吞吐量达

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业      医学与健康         

学     号      2023111309         

班     级      2352001            

学       生      鞠畅                

指 导 教 师      刘松波                

计算机科学与技术学院

2025年5月

摘  要

本报告详细分析了一个名为“程序人生-Hello’s P2P”的C程序从源代码到可执行文件的整个生命周期。通过逐步跟踪程序的预处理、编译、汇编、链接等阶段,深入理解了程序构建的各个环节。报告还探讨了进程管理、存储管理和IO管理等计算机系统核心概念,并通过实际操作和分析,展示了这些概念在实际程序中的应用。通过对“hello”程序的深入剖析,揭示了程序在操作系统中的执行流程,包括进程创建、内存映射、动态链接、异常与信号处理等关键环节。此外,报告还讨论了Linux系统的IO设备管理方法和Unix IO接口,以及printf和getchar函数的实现分析。本研究不仅加深了对程序构建和执行过程的理解,而且提供了对计算机系统设计与实现的深刻见解。

关键词:

程序生命周期;预处理;编译;汇编;链接;进程管理;存储管理;动态链接                           

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

目  录

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

P2P即Program to Process过程,是计算机系统将静态的程序代码转换为动态运行的进程的完整生命周期。过程始于源代码到可执行文件的转化:预处理器cpp对hello.c展开头文件与宏定义,生成中间文本文件hello.i;编译器ccl将hello.i转换为汇编代码文件hello.s;汇编器将hello.s翻译为机器指令,生成可重定位目标文件hello.o;链接器合并hello.o与标准库,解析符号引用,生成最终可执行文件hello。随后,操作系统通过fork()创建子进程,并由execve()将hello加载到进程内存空间,初始化堆栈、代码段等资源,完成从静态程序到动态进程的转化。

O2O即From Zero-0 to Zero-0过程,调了进程从创建到结束的完整生命周期,以及在不同状态下的转换。操作系统为hello分配内存、CPU时间片等资源,使其从“零状态”开始运行,执行输出、休眠及等待输入等操作;在进程终止后,操作系统回收所有已分配资源,清除页表项及缓存数据,最终使系统回归初始“零状态”,完成从资源无到有、再从有到无的完整生命周期。

1.2 环境与工具

(列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。)

13th Gen Intel(R) Core(TM) i7-13700H   2.40 GHz

Windows 11  64 位操作系统

VMware Workstation Pro

Ubuntu 20.04.4 64位

开发工具:gcc,gdb,edb,objdump

1.3 中间结果

(列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。)

文件名

说明

hello.c

源代码文件,包含程序的C语言源代码。

hello.i

预处理后的文件,包含经过宏定义展开和头文件包含后的代码。

hello.s

汇编代码文件,包含编译器将C代码转换成的汇编指令。

hello.o

目标文件,包含汇编器将汇编代码转换成的机器代码。

hello

可执行文件,包含链接器将目标文件和库文件链接后的可执行代码。

1.4 本章小结

本章概述了“程序人生-Hello’s P2P”的整个研究过程,包括研究目的、使用的工具和环境以及中间结果文件的说明。通过本章,可以对整个实验的流程和结构有一个清晰的认识,为后续章节的深入分析打下基础。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理是C语言编译过程的初始阶段,由C预处理器(cpp)对源代码进行文本级处理,其核心作用是为后续编译步骤准备规范化的代码。具体而言,预处理器根据指令,如#include、#define、#ifdef等执行头文件展开、宏替换、条件编译以及删除注释和添加调试信息。

通过预处理,原始代码被转化为无宏定义、无指令且包含完整依赖内容的中间文本文件,如hello.i ,为编译器提供清晰、可直接翻译为汇编代码的输入,同时增强代码的可维护性与跨平台适应性。

2.2在Ubuntu下预处理的命令

命令:gcc -E hello.c -o hello.i

Bb

                   图1 生成预处理文件

2.3 Hello的预处理结果解析

“hello.i”文件共有3061行。在文件的前面部分,涉及对“stdio.h”“unistd.h”“stdlib.h”头文件的引入和替换,以及对#define内容的替换。首先,要找到头文件对应的位置,将其引入,然后删除对应的#include语句,同时对#define内容也进行相应的替换,以完成预处理。程序的最后是“hello.c”中的main函数,可以看出,在第3046行之后,内容与“hello.c”文件中#include之后的行完全相同,这表明预处理阶段并没有对其做任何改动。

图2  hello.i文件内容

                    图3  hello.i文件内容

2.4 本章小结

在本章中,我们详细讨论了预处理的概念和作用,并通过Ubuntu环境下的具体命令展示了预处理的过程。通过对“hello.i”文件的解析,我们理解了预处理阶段如何将源代码转化为编译器能够进一步处理的形式,为编译阶段做好准备。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译是C语言程序构建的核心阶段,由编译器将预处理后的中间代码转换为汇编语言代码,并进一步优化代码结构。其核心作用在于:语法与语义分析,检查代码是否符合C语言规范,并生成抽象语法树以描述程序逻辑;代码优化,通过常量折叠、循环优化、冗余消除等手段提升程序执行效率;生成汇编代码,将高级语言逻辑翻译为与目标硬件架构匹配的低级汇编指令,输出汇编文件。

作用是将文本文件转换为汇编语言文件,便于之后生成二进制文件。编译过程衔接预处理与汇编阶段,既是高级语言到机器指令的桥梁,又通过静态错误检测与优化为程序性能奠定基础,是构建可执行程序的关键环节。

3.2 在Ubuntu下编译的命令

     命令:gcc -S hello.i -o hello.s

图4 编译生成hello.s

3.3 Hello的编译结果解析

                                                      图5 hello.s文件内容

3.3.1数据

movl      $0, -4(%rbp): 可以看出,编译器在处理局部变量时,将其放在栈中,int i 整型局部变量在栈中占据了4字节。

movl      %edi, -20(%rbp) : int argc 整型变量是主函数的一个参数,被保存在栈中,由寄存器传入。

movq     %rsi, -32(%rbp) : char *argv[] 字符型数组变量也是主函数的一个参数,它的首地址被保存在栈中,由寄存器传入

3.3.2赋值

movl      $0, -4(%rbp): int i初始未赋值,默认赋值为0。

3.3.3类型转换

atoi(argv[4])将字符型argv[4]转换为整型: 首先从栈中取出 argv的初始地址,保存到 %rax 中。因为每个指针8个字节,因此移动32个字节到达argv[4]的地址,再将此地址传给rdi,然后调用atoi函数进行类型转换。

3.3.4算术操作

addl $1, -4(%rbp) : i++,实现i=i+1

3.3.5 关系操作

argc!=5: cmpl     $5, -20(%rbp) 将argc和5做比较,使用了cmpl这一命令。

i<10: cmpl   $9, -4(%rbp) 同样使用了cmpl命令判断i是否小于10,所以将i和9比较。

3.3.6 数组操作

printf(\"Hello %s %s %s\\n\",argv[1],argv[2],argv[3]):

首先将argv的首地址存入rax,再将rax加上对应偏移(8*n),找到对应的地址,再将其存在另一个寄存器中(rcx,rdx,rsi)。

3.3.7 控制转移

if(argc!=5): cmpl $5, -20(%rbp)

je     .L2        

将argc和5比较,je表示相等则执行,即如果argc=5就跳转到L2,否则执行下方命令,实现了控制转移。

for(i=0;i<10;i++): 先将i和9进行比较,jle是不相等则执行,即如果不相等,就会跳转到L4,执行循环内容,循环末尾addl $1, -4(%rbp)实现i++,实现循环计数。L3下是进行循环条件判断,L4下是循环主体。

3.3.8 函数操作

exit(1):call exit@PLT使用call调用exit函数。

sleep(atoi(argv[4])):call sleep@PLT使用call调用sleep函数。

getchar():call   getchar@PLT使用call调用getchar函数。

return 0:movl   $0, %eax将0赋给eax,即0为返回值,然后退出。

3.4 本章小结

本章深入探讨了编译的概念和作用,并通过具体的命令和示例展示了如何在Ubuntu下进行编译操作。通过对编译结果的分析,我们理解了编译器如何将预处理后的代码转换成汇编代码,为后续的汇编阶段奠定基础。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

汇编是C语言程序构建的关键阶段,由汇编器将编译器生成的汇编代码(如hello.s)转换为机器语言指令,并生成可重定位目标文件(如hello.o)。其核心作用包括:指令翻译、符号解析、生成目标文件。

汇编过程实现了从低级汇编语言到硬件可执行指令的精确映射,为链接器提供标准化输入,是程序从抽象逻辑向物理机器执行过渡的核心环节。

4.2 在Ubuntu下汇编的命令

命令:gcc -c hello.s -o hello.o

                    图6 汇编生成.o文件

4.3 可重定位目标elf格式

(分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。)

通过readelf -a hello.o 生成elf格式

             图7 查看ELF文件头部信息的结果

这个输出表明,该ELF文件是一个64位的可重定位文件,适用于AMD64架构,没有程序头(因为它是一个目标文件),包含14个节头。这一部分包含了magic,类别,数据,程序头起点,头大小,节头大小和数量等基本信息。

               图8 查看ELF文件的节头信息

其中列出了文件中各个节的详细属性,包括节的名称、大小、类型、在文件中的偏移量、对齐要求等,图中还解释了不同标志位的含义,如写入(W)、分配(A)、可执行(X)等,这些信息对于理解ELF文件的结构和内容至关重要,特别是在进行系统编程、调试或逆向工程时。

               图9 查看ELF文件的重定位表信息

图中列出了 .rela.text 节中包含的8个条目和 .rela.eh_frame 节中的1个条目。每个条目都包含了偏移量、信息、类型、符号值以及符号名称加附加值。这些信息用于在链接过程中将代码中的符号引用解析为内存中的实际地址。

               图10 查看ELF文件的符号表信息

符号表包含了18个条目,每个条目描述了一个符号的信息,包括其编号、值、大小、类型、绑定、可见性、索引和名称。这些信息对于理解程序中符号的定义和引用非常重要,尤其是在进行链接和调试时。

4.4 Hello.o的结果解析

通过objdump -d -r hello.o 分析hello.o的反汇编,并与第3章的 hello.s进行对照分析。

                     图11 反汇编信息

(说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。)

1.机器语言的构成:

图中左侧显示的是机器指令,这些是由二进制或十六进制代码组成的指令,它们是计算机硬件可以直接执行的指令。图中右侧显示的是汇编指令,它们是机器指令的文本表示形式,更易于人类阅读和理解。汇编指令通常由操作码和操作数组成。

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

指令映射:每条机器指令对应一条汇编指令。例如,图中55机器指令对应 push %rbp 汇编指令,这条指令用于将基指针寄存器的值压入栈中。

寄存器映射:汇编语言中的寄存器名称(如 %rbp、%rsp、%rdi 等)映射到机器指令中使用的寄存器。

操作映射:汇编指令中的操作(如 mov、add、call 等)映射到机器指令中的相应操作。

地址映射:汇编指令中的地址(如 %rsi、(%rbp) 等)映射到机器指令中的具体内存地址或寄存器偏移量。

数据映射:汇编指令中的立即数(如 0x20)直接映射到机器指令中的数据部分。

3. 与第三章的hello.s对比,首先在指令前增加了其十六进制表示,即机器语言。其次在操作数上,hello.s中操作数为十进制,而hello.o的反汇编中操作数为十六进制。在条件跳转语句上,hello.o的反汇编文件用的是相对偏移量,而hello.s中是函数名。调用函数方面,在汇编语言中,函数调用call后直接加调用函数的名称,而在机器语言中,在callq调用函数时,后面跟着的是下一条指令的地址,而不是所要调用函数的地址hello.o的反汇编文件中采用重定向的方式进行跳转,链接时根据重定位条目来获得地址信息。分支转移指令后面的是具体地址,直接跳转到具体地址执行;而在汇编语言中,则是跳到对应的段,如L2、L4等。

4.5 本章小结

在本章中,我们讨论了汇编的概念和作用,并展示了如何在Ubuntu下进行汇编操作。通过对“hello.o”文件的分析,我们了解了汇编器如何将汇编代码转换成机器语言指令,这些指令是计算机能够直接执行的代码。

(第41分)

第5章 链接

5.1 链接的概念与作用

链接是将一个或多个编译后产生的目标文件以及所需的库文件组合起来,解析符号引用、合并代码与数据段、进行地址重定位,最终生成一个可直接加载运行的可执行文件的关键步骤。

链接器就像一个“装配工”和“地址分配员”。它把分散编译好的零件(hello.o等目标文件)和标准零件(库)收集起来,解决零件间相互调用(符号解析)的问题,把所有零件合理地组装(段合并)到一个大机器(可执行文件hello)的框架里,并给每个零件在机器中的位置精确标号(重定位),最终使得这个机器(hello)能被操作系统准确地加载到内存中并执行。没有链接,hello.o只是一个无法独立运行的半成品。

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/9/crtbegin.o \\

    hello.o \\

    /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o \\

    /usr/lib/x86_64-linux-gnu/crtn.o \\

    -lc \\

    -z relro \\

    -o hello

                 图12 生成hello可执行文件

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

(分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。)

使用readelf -a hello生成hello的ELF格式

     图13 对hello可执行文件进行分析后得到的ELF文件头信息

输出包含了多个方面的信息,如:文件的魔数、文件的体系结构等文件识别信息;文件类型和目标机器;程序头和节头信息。入口点地址、程序头起点、节头大小和节头数量都被改变。说明程序获得了入口地址。

图14 节头表信息

头表详细列出了文件中各个节的属性,包括节的大小、类型、入口大小、地址、标志、链接信息、信息、偏移量和对齐方式。这些节涵盖了程序的多种数据和代码,对于理解程序的结构和行为至关重要,它们定义了程序的代码段、数据段、符号信息、重定位信息等,操作系统和动态链接器会根据这些信息加载和执行程序。通过分析这些节,开发者可以深入了解程序的编译和链接细节,这对于调试、优化和逆向工程等任务非常有用。

   图15 程序头表信息

程序头表详细列出了文件中各个段的属性,包括段的类型、偏移量、文件大小、内存大小、虚拟地址、物理地址、标志和对齐方式。这些段对于理解程序的内存布局和行为至关重要,它们定义了程序的代码段、数据段、堆栈段、动态链接信息等,操作系统和动态链接器会根据这些信息加载和执行程序。通过分析这些段,开发者可以深入了解程序的编译和链接细节。

图16 动态段信息

动态段是 ELF 文件中的一个重要部分,它包含了程序运行时动态链接器需要的信息。图中列出了25个动态段条目,每个条目包含标签、类型和名称/值。这些条目提供了关于动态链接、动态加载和程序运行时行为的重要信息。这些信息对于理解程序的动态链接和加载过程至关重要,它们定义了程序如何与操作系统和共享库交互,以及如何在运行时解析符号和重定位代码。

   图17 重定位表信息

重定位表是 ELF 文件中用于在程序加载到内存时调整代码和数据地址的部分。图中列出了两个重定位段:.rela.dyn 和 .rela.plt。.rela.dyn 段包含两个条目,分别用于全局数据的重定位。rela.plt 段包含六个条目,用于过程链接表的重定位,指向 puts、printf、getchar、atol、exit 和 sleep 这些在C标准库中定义的函数。

   图18 符号表信息

符号表包含两个部分:.symtab 和 .strtab,分别列出了程序中的符号及其对应的字符串表索引。符号表中显示了重定位时使用的符号,用于记录函数、变量等符号的名称、位置与类型,是链接器链接时候的重要导航。

5.4 hello的虚拟地址空间

(使用gdb/edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。) 

         图19 说明载程序的程序头LOAD是从地址0x400000开始

                      图20  hello的虚拟地址空间

5.5 链接的重定位过程分析

(通过objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。)

                   图21 生成hello反汇编代码

                           图22

不同之处——函数数量增加:与hello.o相比,hello的部分多了很多函数,如.plt,puts@plt,sleep@plt等函数的反汇编代码,说明链接器hello将hello中所用到的函数和.o文件链接了起来。

                           图23

函数调用:callq后面跟着的是对应函数的地址,说明在连接时进行了重定位,将相对地址转换为在内存中真实地址。

                             图24

条件转移:链接之后,跳转指令,会跳转到对应语句在内存中的地址,进行执行。

连接过程:链接器在处理目标文件时,主要执行符号解析和重定位两个任务。重定位分为两个步骤:首先确定符号的最终内存地址,然后更新程序中对这些符号的所有引用。以 hello.o 文件为例,假设其中包含对 puts 函数的调用。在未链接的 hello.o 中,puts 调用指令的二进制编码为 e8 00 00 00 00,其中 00 00 00 00 是待填充的地址占位符。在链接过程中,链接器确定 puts 函数的实际地址为 0x401080。然后,链接器计算 puts 调用指令的引用地址 0x401126,并计算偏移量:0x401080 - 0x401126 = -0xaa。将该偏移量转换为小端序 56 ff ff ff 并填入指令中,更新后的指令为 e8 56 ff ff ff。

5.6 hello的执行流程

(使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。)

                           图25  gdb调试

子程序名

     程序地址

_init

0x401000

puts@plt;

0x401090

printf@plt;

0x4010a0

getchar@plt;

0x4010b0

atoi@plt;

0x4010c0

exit@plt;

0x4010d0

sleep@plt;

0x4010e0

_start

0x4010f0

main

0x4011d6

_fini

0x4012f8

5.7 Hello的动态链接分析

(分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。)

图26  GOT表

函数在运行时经过动态链接器处理,通过延时绑定,避免修改运行代码段。通过PLT和GOT表就可以实现对函数进行动态链接,加载时动态链接器重定位GOT表中的内容,就可以得到函数的正确地址。以exit函数为例来说明。首先通过objdump -R hello查看GOT表。得到exit的GOT表地址0x404038。

                        图27 gdb调试

gdb运行hello,并在主函数处设置断点后运行。这时查看0x404038地址处对应的内容,可以发现内容为0x401070,这说明此时还没有解析真实地址,指向的是.plt表中的跳板。此时尚未解析,需要调用动态链接器。之后在exit处设置断点,continue执行,再次查看0x404030地址处对应的内容,得到0x00007ffff7e06a40,说明exit的GOT项已经被替换为 libc.so 中真正的exit地址,动态链接器完成绑定。

5.8 本章小结

本章详细介绍了链接的概念和作用,并通过具体的命令和示例展示了链接过程。通过对链接结果的分析,我们理解了链接器如何将目标文件和库文件合并,生成最终的可执行文件,以及重定位过程如何确保程序的正确执行。

(第51分)

第6章 hello进程管理

6.1 进程的概念与作用

进程是操作系统进行资源分配和调度的基本单位,它是程序的一次动态执行过程。 当一个程序被加载到内存中运行时,操作系统会为其创建一个独立的进程。这个进程拥有自己独立的虚拟地址空间、代码、数据、堆栈、打开的文件描述符、环境变量以及执行状态等系统资源。其主要作用体现在:资源隔离与保护、实现并发执行、资源分配与管理、提供执行环境。

进程就像一个拥有独立办公室、专属电话线、文件柜和任务清单的员工。操作系统(老板)把程序(工作任务书)交给这个员工去执行。每个员工(进程)在各自隔离的办公室(地址空间)里工作,互不干扰(隔离),老板通过快速切换关注点(调度)让所有员工看起来都在同时干活(并发),并根据需要给他们分配办公用品(资源)。没有进程,程序只是一份静止的说明书;进程赋予了程序动态执行的生命和能力。

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

Shell是操作系统的命令解释器与用户界面核心,其核心作用是作为用户与内核之间的“翻译官”和“任务调度管家”,主要处理流程如下:读取命令、解析与分割、执行扩展、查找与执行命令、报告结果。

Shell就像一个高效的“任务指挥中心”。用户下达自然语言指令(命令),它负责精准拆解(解析)、预先处理(扩展)、寻找执行者(查找程序/内置命令)、派遣工人(fork子进程)、更换工具包(exec加载程序)、管理输入输出管道(重定向)、监控任务完成(wait),最后汇报结果。它让用户无需直接面对复杂的内核系统调用,通过简洁命令即可灵活操控整个系统。

6.3 Hello的fork进程创建过程

fork()函数用于创建一个与当前执行进程几乎完全相同的子进程。这个过程包括几个关键步骤:首先,父进程调用fork()函数请求创建子进程;接着,操作系统内核为子进程分配必要的资源,包括内存空间和文件描述符,并复制父进程的进程描述符信息,如进程ID(PID)、用户ID、组ID等。内核为子进程分配一个新的、唯一的PID,并复制父进程的地址空间到子进程,通常采用写时复制技术以优化资源使用。随后,内核设置子进程的状态为就绪,并将其加入到调度队列中。fork()函数在父进程中返回子进程的PID,在子进程中返回0,如果创建失败则返回-1。最后,父进程和子进程从fork()调用之后的指令继续独立执行,由于地址空间的复制,它们可以独立地修改变量和数据,而不会影响对方。

                    图28 程序正常执行

6.4 Hello的execve过程

xecve 函数用于替换当前进程映像为一个新的程序。这个过程通常发生在父进程通过 fork 创建子进程后,子进程需要执行一个完全不同的程序。以下是 execve 函数执行过程的完整描述:execve 函数的调用通常包括三个参数:一个指向新程序可执行文件路径的指针,一个指向命令行参数数组的指针,以及一个指向环境变量数组的指针。当 execve 被调用时,它首先关闭子进程的所有文件描述符,然后解绑任何挂起的信号处理函数,接着将新的程序加载到子进程的地址空间中,替换掉原有的程序代码和数据。新的程序从其入口点开始执行,而子进程的PID、用户ID、组ID等属性保持不变。

6.5 Hello的进程执行

在操作系统中,进程执行是一个复杂的过程,涉及到进程上下文信息、进程时间片、进程调度以及用户态与核心态的转换。以下是对这一过程的完整描述:

当一个进程开始执行时,操作系统内核首先为其分配必要的资源,包括内存空间、文件描述符等,并设置进程的上下文信息,如进程状态、优先级、寄存器值等。进程调度器根据进程的优先级和时间片等信息,决定哪个进程将获得CPU时间片并开始执行。

进程时间片是操作系统分配给每个进程的CPU时间单元,用于控制进程的执行时间。当一个进程的时间片用完时,操作系统会暂停该进程的执行,将其状态从运行态变为就绪态,并调度其他就绪态的进程开始执行。这个过程称为上下文切换,涉及到保存当前进程的上下文信息和加载下一个进程的上下文信息。

在进程执行过程中,用户态与核心态的转换是一个重要的概念。用户态是指进程执行用户程序代码的状态,而核心态是指进程执行内核代码的状态。当进程需要访问系统资源或请求系统服务时,它会通过系统调用从用户态切换到核心态。系统调用是用户程序与操作系统内核之间的接口,用于执行特权操作,如文件操作、进程控制等。

6.6 hello的异常与信号处理

(hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。)

1.非法指令:  信号:SIGILL(Illegal Instruction)

处理:默认情况下,程序终止。

2.浮点异常:  信号:SIGFPE(Floating Point Exception)

处理:默认情况下,程序终止。

3.访问违规:  信号:SIGSEGV(Segmentation Fault)

处理:默认情况下,程序终止。

4.终止请求:  信号:SIGTERM(Termination Request)

处理:默认情况下,程序终止。

5.用户中断:  信号:SIGINT(Interrupt from keyboard)

处理:默认情况下,程序终止。

6.挂起请求:  信号:SIGTSTP(Stop request from terminal)

处理:默认情况下,前台进程暂停执行。

(程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。)

1. 回车:通常不会产生信号,但可能触发程序中的特定逻辑。

2. Ctrl-C: 导致内核发送一个SIGINT信号到前台进程组中的每个进程,默认情况下,程序终止。

                               图29

3. Ctrl-Z: 导致内核发送一个SIGTSTP信号到前台进程组的每个进程,前台进程暂停执行,可以恢复继续执行。

                               图30

4. ps命令:显示当前所有进程的状态

                               图31

5. jobs命令:显示当前shell会话中的作业状态。

                               图32

6. pstree命令:以树形结构显示进程关系。

                            图33(部分)

7. fg命令:将后台作业带回前台继续执行。

                              图34

8. kill命令:向进程发送信号,默认为 SIGTERM

                                 图35

6.7本章小结

在本章中,我们探讨了进程管理的概念,包括进程的创建、执行和终止。通过对“hello”程序的分析,我们了解了fork和execve函数在进程创建和执行新程序中的作用,以及进程在运行过程中如何响应各种信号和异常。

(第62分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

1.逻辑地址:

逻辑地址是程序中使用的地址,它是相对于程序的起始地址的偏移量。在编写程序时,程序员通常不需要关心逻辑地址对应的物理地址,因为这些地址是由编译器和链接器处理的。在 hello 程序源代码编译和汇编后生成的机器指令中直接使用的地址。它是 相对于某个段基址的偏移量。在 hello 的汇编代码,如 movl $0x804a000, %eax或目标文件 hello.o 的符号表中看到的地址都是逻辑地址。

2.线性地址:

    线性地址是虚拟内存系统中的一个概念,它是虚拟地址空间中的地址。在保护模式下的现代处理器中,线性地址通过段选择符和偏移量来表示。线性地址不是实际的物理内存地址,它需要通过内存管理单元(MMU)转换为物理地址。链接器将 hello.o、库文件等合并生成可执行文件 hello 时,会对所有逻辑地址进行 重定位,确定它们在 hello 进程虚拟地址空间中的最终位置,例如,.text 段可能从 0x8048000 开始,.data 段从 0x804a000 开始。当 hello 进程运行时,CPU 指令指针、栈指针、数据访问指令中的地址,以及 printf 输出的地址,指的都是这个进程虚拟地址空间中的地址,即线性地址/虚拟地址。

3.虚拟地址:

虚拟地址是操作系统为每个进程提供的地址空间中的地址。每个进程都有独立的虚拟地址空间,这使得每个进程可以安全地运行而不会相互干扰。虚拟地址通过操作系统的内存管理机制(如分页或分段)映射到物理地址。

4.物理地址:

物理地址是实际内存芯片上的地址。它是内存管理单元(MMU)或内存控制器直接使用的地址。物理地址是实际存储数据的内存单元的位置。每一次内存访问都依赖于虚拟地址到物理地址的映射,操作系统通过页表将虚拟地址转化为对应的物理地址。

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

段式存储管理是一种内存管理技术,它将程序划分为多个段,每个段代表程序的一个逻辑部分,例如代码段、数据段或共享段。这种划分方式允许程序的不同部分独立地进行存储和访问。

在段式管理中,程序的地址空间由段表定义,段表包含了段的选择符(也称为段名)、起始地址、加载位和段长度等信息。程序的逻辑地址由两部分组成:段选择符和段内偏移量。段选择符是一个16位的字段,用于在段描述符表中索引具体的段描述符。段描述符表包含了系统中所有段的描述信息,其中全局描述符表(GDT)是整个系统共用的,而局部描述符表(LDT)则是每个任务或程序独有的。

全局描述符表(GDT)存储了操作系统使用的段描述符以及各任务和程序的局部描述符表(LDT)的描述符。每个任务或程序的局部描述符表(LDT)包含了该任务或程序私有的段描述符,以及它们使用的门描述符,如任务门和调用门。

逻辑地址是程序编译后的地址,它与实际的物理内存地址没有直接关系。即使在不同的机器上使用相同的编译器编译同一个源程序,生成的逻辑地址也是相同的。然而,由于不同的机器可能有不同的内存布局,因此生成的线性地址(即逻辑地址到物理地址的中间层)可能会有所不同。

通过段式管理,操作系统能够更灵活地管理内存,允许程序的不同部分根据需要进行独立地加载和卸载,同时也支持了内存的共享和保护。这种管理方式在现代操作系统中仍然有着广泛的应用,尤其是在需要处理复杂内存布局和多任务环境的情况下。

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

操作系统通过页表来跟踪虚拟页和物理页之间的映射关系。页表由一系列的页表项组成,每个页表项包含两个关键部分:一个有效位和一个地址字段。有效位指示对应的虚拟页是否已经加载到物理内存中,而地址字段则存储着对应的物理页在内存中的起始地址。当程序访问一个虚拟地址时,如果该虚拟页尚未加载到物理内存(即缺页),系统将从磁盘中加载所需的数据到物理内存中。一旦数据加载完成,页表将更新以反映新的虚拟页到物理页的映射关系。

在页命中(即所需数据已经在物理内存中)的情况下,处理器通过以下步骤完成虚拟地址到物理地址的转换:处理器生成虚拟地址并发送给内存管理单元。

MMU根据虚拟地址计算出需要访问的页表项的位置,并从高速缓存或主存中检索该页表项。高速缓存或主存将请求的页表项返回给MMU。MMU使用页表项中的地址字段构造出完整的物理地址,并将其发送给高速缓存或主存。高速缓存或主存根据物理地址返回处理器请求的数据。

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

以Intel Core i7处理器为例,来阐述其内存地址转换流程。

该处理器生成一个虚拟地址(VA),这个地址由虚拟页号(VPN)和页内偏移(VPO)组成。虚拟页号进一步被划分为多个部分,以便在多级页表中进行查找。内存管理单元(MMU)利用虚拟页号的高位部分来索引转换后备缓冲区(TLB),这是一个快速访问的缓存,存储了最近使用的页表项。

如果TLB查找成功(即TLB命中),MMU可以直接从TLB中获取物理页号(PPN),并与页内偏移组合形成完整的物理地址(PA)。这是理想的情况,因为它避免了访问主内存以获取页表项,从而加快了地址转换速度。

然而,如果TLB未命中,MMU必须查询页表来解析虚拟地址。在Intel Core i7中,这涉及到一个四级页表的查找过程。MMU首先使用虚拟页号的第一个部分(VPN1)来索引一级页表,找到对应的页表项,该项包含了二级页表的基地址。如果该页表项有效,MMU将继续使用VPN的下一个部分(VPN2)来索引二级页表,依此类推,直到在四级页表中找到对应的页表项,从而获得物理页号。

一旦获得了物理页号,MMU将其与页内偏移组合,形成完整的物理地址。为了提高未来地址转换的效率,这个虚拟地址到物理地址的映射会被添加到TLB中。

这种多级页表和TLB的组合使用,使得Intel Core i7处理器能够有效地管理大量内存,同时保持快速的地址转换速度。通过减少对主内存的访问次数,系统能够更高效地运行,尤其是在处理大量数据和复杂任务时。

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

在现代处理器架构中,例如Intel Core i7,三级缓存(L1、L2、L3 Cache)的设计是为了减少CPU访问主存所需的时间,从而提高数据访问的效率。当CPU需要数据时,它首先检查L1 Cache,这是最快的缓存级别,通常集成在CPU核心内部。如果所需数据在L1 Cache中(即缓存命中),CPU可以直接从L1 Cache获取数据,这一过程非常快速。

如果数据不在L1 Cache中(缓存未命中),CPU接下来会检查L2 Cache。L2 Cache通常比L1 Cache大,但速度稍慢。如果数据在L2 Cache中,CPU将数据从L2 Cache加载到L1 Cache,以便下次访问时可以更快地获取。

如果L2 Cache中也没有所需数据,CPU将继续检查L3 Cache。L3 Cache是所有核心共享的,比L1和L2 Cache更大,但访问速度也更慢。如果数据在L3 Cache中,CPU将数据加载到L1和L2 Cache中,以优化多级缓存的使用效率。

如果三级缓存中都没有找到所需数据(即三级缓存均未命中),CPU必须访问主存。从主存中获取数据通常需要较长时间,因为主存的访问速度远低于缓存。获取到数据后,CPU会将数据块加载到L1、L2和L3 Cache中,以备后续访问。

当缓存中没有足够的空间来存储新加载的数据块时,需要决定替换哪一块数据。这通常基于缓存替换策略,如最近最少使用(LFU)策略。LFU策略会选择最长时间未被访问的数据块进行替换,以期望未来该数据块不太可能被访问。

通过这种多级缓存结构和缓存替换策略,CPU能够有效地管理数据访问,减少对主存的依赖,从而提高整体系统性能。这种机制在现代计算机系统中至关重要,它使得CPU能够快速处理大量数据,支持复杂的计算任务。

7.6 hello进程fork时的内存映射

当`hello`程序调用`fork()`系统调用时,操作系统内核会进行一系列操作来创建一个新的子进程。内核首先为子进程创建一个新的进程控制块(PCB),该结构包含了进程的状态信息,如进程ID(PID)、进程状态、寄存器集合等。接着,内核为子进程分配一个唯一的进程标识符(PID),以区分系统中的不同进程。

子进程会继承父进程的内存映射,包括代码段、数据段、堆和栈。这意味着子进程最初拥有与父进程相同的虚拟地址空间和页表。为了提高效率,内核通常使用写时复制(Copy-On-Write, COW)技术。在这种技术下,父进程和子进程最初共享相同的物理内存页。只有当进程尝试修改某一页时,内核才会创建一个新的物理内存页,并将修改后的数据复制到新页中。这样,每个进程最终都有自己的独立地址空间,而不需要立即复制所有内存页。

内核更新子进程的页表,使其指向正确的物理内存页。在写时复制的情况下,页表最初指向相同的物理页,但在发生写操作时,页表会更新为指向新的物理页。内核复制父进程的寄存器和栈到子进程,但子进程的栈会稍微偏移,以确保`fork()`返回不同的值(0表示子进程,子进程的PID表示父进程)。

最后,内核将子进程添加到调度队列中,使其有机会运行。子进程从`fork()`调用之后的指令开始执行。通过这种方式,`fork()`系统调用能够高效地创建新的进程,而不需要立即复制大量的内存页。这使得进程创建更加快速和高效,特别是在创建大量短生命周期的子进程时。写时复制技术确保了每个进程最终都有自己的独立地址空间,从而实现了进程隔离和保护。

7.7 hello进程execve时的内存映射

当`execve`函数被调用后,进程的内存空间会经历一系列的变化。在`execve`执行前,用户空间中的代码段、数据段、堆、栈等内存区域都是由当前程序所占用的。然而,一旦`execve`被调用,这些内存区域会被释放,文件映射区域、共享内存等也会被清除。尽管如此,内核会保留一些关键的进程标识信息,如PID,但会清空页表并重新建立虚拟内存结构。

在新的虚拟内存结构建立之后,操作系统会进行新的内存映射。这时,`hello`程序会被加载到进程的虚拟地址空间中,替换原有的内存内容。新的程序代码、数据、堆和栈等内存区域会被创建,并且新的程序开始执行。这一过程实际上是在当前进程的上下文中启动了一个全新的程序,而旧程序的执行被完全终止。

通过`execve`调用,进程能够改变其执行的程序,这在动态加载和执行程序时非常有用。例如,在Shell脚本中,可以通过`execve`来启动一个新的程序,而不需要创建新的进程。这样,当前Shell进程的资源可以被新程序重用,从而节省了系统资源。同时,这也使得进程能够灵活地改变其执行的程序,以适应不同的任务需求。

总的来说,`execve`函数在进程的内存映射中扮演着重要的角色。它不仅能够释放旧程序的内存资源,还能够建立新的内存映射,加载新的程序,并开始新的执行流程。这一过程是操作系统内存管理和进程调度的重要组成部分,对于实现程序的动态加载和执行具有重要意义。

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

当程序尝试访问一个不在物理内存中的虚拟地址时,就会发生缺页故障,这会触发一个缺页中断。缺页中断是操作系统内存管理中的一个重要机制,用于处理虚拟内存和物理内存之间的映射问题。当中断发生时,操作系统会调用内核的缺页处理程序来解决这个问题。

缺页处理程序的第一步是寻找空白的物理页面。物理内存中的空白页面是指那些尚未被任何虚拟页面映射的页面。如果有足够的空白页面可用,操作系统会将所需数据加载到这些空白页面中。然而,如果系统中没有足够的空白页面,操作系统就需要选择一个牺牲页,即一个当前不在使用的页面,将其内容(如果已被修改)写回到磁盘上的交换区,然后将该页面标记为空白,以便用于加载新的数据。

一旦找到了空白页面或牺牲页,系统就会将所需的数据从磁盘装载到这个物理页面中。接下来,操作系统会更新页表,将发生缺页的虚拟地址映射到新的物理地址。这样,当程序再次尝试访问该虚拟地址时,就可以通过页表找到对应的物理地址,从而访问到所需的数据。

完成这些步骤后,缺页处理程序会返回到程序,程序会重新执行导致缺页中断的指令。由于此时虚拟地址到物理地址的映射已经建立,指令可以成功执行,所需的数据可以被送入CPU进行处理。

缺页中断处理是虚拟内存管理中的关键环节,它确保了程序可以访问比物理内存更大的地址空间,同时也为操作系统提供了对内存使用的精细控制。通过有效地管理虚拟地址到物理地址的映射,操作系统能够提高内存的使用效率,支持多任务处理,并为程序提供隔离和保护。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。对每个进程,内核维护一个变量brk,指向堆的顶部。分配器将堆视作一组不同大小的块的集合,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格:显式分配器和隐式分配器。显式分配器要求应用显式地释放任何已分配的块,隐式分配器要求分配器检测一个已分配块何时不再被程序所用,那么就释放这个块。隐式分配器又叫垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。为实现动态内存分配器,可以使用隐式空闲链表。当一个应用请求k字节的块时,分配器搜索空闲链表,查找一个足够大可以放置所请求块的空闲块。分配器执行这种搜索的方式是由放置策略确定的。一些常见的策略是首次适配、下一次适配和最佳适配。一旦分配器找到一个匹配的空闲块,就需要决定分配这个空闲块中多少空间。一个选择是用整个空闲块,但这样会造成内部碎片。如果匹配不太好,那么分配器会将这个空闲块分割,第一部分变成分配块,剩下的变成一个新的空闲块。利用边界标记,可以允许在常数时间内进行对前面块的合并。这种思想是在每个块的结尾添加一个脚部,其中脚部就是头部的一个副本。这样分配器就可以通过检查它的脚部,判断前面一个块的起止位置和状态,这个脚部总是在距离当前块开始位置一个字的距离。但是这种方法也存在潜在缺陷,就是在应用程序操作许多个小块时,会产生显著的内存开销。

7.10本章小结

    本章深入分析了存储管理的各个方面,包括虚拟地址空间、段式管理和页式管理。通过对“hello”程序的内存映射和动态链接的分析,我们理解了操作系统如何管理内存资源,以及这些管理机制如何支持程序的执行和资源的有效利用。

(第7 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

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

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

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

8.3 printf的实现分析

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

[转]printf 函数实现的深入剖析 - Pianistx - 博客园

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

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

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

8.4 getchar的实现分析

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

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

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

8.5本章小结

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

(第8 选做 0分)

结论

以下是该程序所经历的关键阶段:

1. 预处理:源代码文件hello.c被预处理器处理,生成了预处理文件hello.i。

2. 编译:预处理文件hello.i被编译器转换成汇编代码,生成了汇编文件hello.s。

3. 汇编:汇编器将汇编代码转换成机器指令,生成了目标文件hello.o。

4. 链接:链接器将目标文件hello.o与库文件合并,生成了最终的可执行文件hello。

5. 进程创建:操作系统通过fork()系统调用创建了子进程,子进程继承了父进程的地址空间。

6. 内存映射:子进程通过execve()加载了新程序,其内存空间被重新映射,原有的代码和数据段被新程序替换。

7. 进程执行:新程序在子进程中开始执行,操作系统为该进程分配了CPU时间片,程序按指令顺序执行。

8. IO管理:程序执行过程中,通过系统调用来执行输入输出操作,例如printf和getchar。

9. 进程终止:程序执行完毕后,通过exit()系统调用终止进程,操作系统回收了该进程的所有资源。

    通过对“hello”程序生命周期的分析,我深刻认识到计算机系统是一个复杂而精密的整体,每一部分都必须精确设计和实现以确保系统稳定和高效。内存管理和进程调度是操作系统设计中的关键挑战。为了提高系统性能和资源利用率,我们可以探索新的内存分配策略和进程调度算法,例如采用更智能的内存预分配和回收策略,以及基于机器学习的进程调度算法,以动态适应不同的负载情况。

此外,随着多核处理器的普及,如何有效利用多核资源实现真正的并行计算成为未来计算机系统设计的重要方向。我们可以设计新的并发控制机制和同步方法,以减少锁的竞争和等待时间,提高多线程程序的性能。

总之,计算机系统的设计与实现是一个不断发展的领域,需要不断地创新和优化以适应不断变化的需求和技术进步。通过本研究,我对未来在这一领域的研究和开发工作充满期待,并希望能够为推动计算机系统的发展做出自己的贡献。

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

附件

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

文件名

说明

hello.c

源代码文件,包含程序的C语言源代码。

hello.i

预处理后的文件,包含经过宏定义展开和头文件包含后的代码。

hello.s

汇编代码文件,包含编译器将C代码转换成的汇编指令。

hello.o

目标文件,包含汇编器将汇编代码转换成的机器代码。

hello

可执行文件,包含链接器将目标文件和库文件链接后的可执行代码。

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

参考文献

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

[1]张志源.“Linux操作系统”课程建设的实践探索[J].晋城职业技术学院学报,2024,17(06):41-44.

[2]徐振宇,李征,张飞絮,等.基于指令集映射的汇编语言教学探索[J].实验室科学,2024,27(04):1-6.

[3]冯晓兵,郝丹,高耀清,等.编译技术与编译器设计专题前言[J].软件学报,2024,35(06):2583-2584.DOI:10.13328/j.cnki.jos.007103.

[4]蔡婧怡.计算机数据库管理技术探析[J].计算机产品与流通,2019,(08):138.

[5]李洋,庞立滨.Linux系统进程管理的分析与探究[J].科技与企业,2015,(16):87.DOI:10.13751/j.cnki.kjyqy.2015.16.088.

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