2024春哈工大计算机系统大作业

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业        人工智能      

学     号        2022111713      

班     级         2203601      

学       生         单卓然    

指 导 教 师         吴锐        

计算机科学与技术学院

2024年5月

摘  要

HelloWorld几乎是每个程序员的第一个项目。我们逐步输入几行代码,运行后惊喜地看到输出中的“Hello, World!”

然而,从代码编辑器输入代码到程序运行的这个简单过程实际上反映了计算机科学的核心概念。在这个过程中,我们需要理解语法规则、编译原理以及操作系统的运行机制。 当我们点击运行按钮时,编写的代码首先经过编译器的处理,被转换成机器可以执行的指令。

然后,操作系统负责将这些指令加载到内存中,并安排CPU执行它们。整个过程涉及许多底层细节,如内存管理、进程调度和指令执行等。

通过跟踪HelloWorld程序的生命周期,本文深入介绍了从代码编辑器到程序最终运行并结束的完整过程,从而对计算机底层原理进行了深入分析。

关键词:CSAPP;计算机系统;汇编;进程;程序                           

目  录

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

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

写出.c;预处理,编译,汇编,链接

Fork+execve运行

存储,IO管理;结束,被回收

P2P过程:

图表 1过程总览


1.编写hello.c

2.gcc生成目标文件

1.预处:cpp理根据#得到.i

2.编译:ccl翻译为汇编.s

3.汇编:as把.s翻译为机器指令,打包在.o的二进制文件

4.链接:hello.o与printf.o合体

3.shell加载,fork,execve加载运行。

1.2 环境与工具

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

图表 2硬件参数1

图表 3硬件参数2

1.3 中间结果

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

.i预处理后的文件

.s汇编文件

.o可重定位文件

hello(无后缀)可执行文件

.asm 反汇编

.elf .out的elf文件

elf.txt .o的elf文件

1.4 本章小结

简介Hello的P2P的过程,列出了使用环境的配置,也列出了所使用的文件

第2章 预处理

2.1 预处理的概念与作用

预处理根据#提示修改原始程序。也就是引用了哪些头文件,如stdio.h的头文件,并把他们插入到程序中。最后处理之后就能得到一个.i文件。

2.2在Ubuntu下预处理的命令

       用gcc -E hello.c -o hello.i

       或 cpp hello.c hello.i

图表 4输入命令

2.3 Hello的预处理结果解析

       1.把头文件内容替换

       2.宏定义替换

       3.删除注释

图表 5.i一部分如图

2.4 本章小结

       本章讲述了预处理的相关内容,并给出实操过程,附上截图

第3章 编译

3.1 编译的概念与作用

编译:生成汇编程序的过程。产物是.s(原文:编译器(cc1)将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。该程序包含函数 main 的定义)

3.2 在Ubuntu下编译的命令

       用gcc -S hello.c -o hello.s

       或cc1 hello

图表 6编译示意图

图表 7用cc1

       用cc1可能宅找不到路径,所以得用补全的唯一的路径

3.3 Hello的编译结果解析

图表 8.s大致内容

1.LC0:中文字符串,局部符号

图表 9.LC0如图

       2.LC1:英文局部符号,对应前两个参数(学号和姓名)

图表 10.LC1

3.if函数判断

图表 11 if函数

       4.不满足则退出

图表 12不满足if的情况

              对puts和exit的调用

图表 13 puts和exit

       5.for循环

图表 14 for循环

              For循环的结尾转换数据格式,并sleep

       6.读取字符串

图表 15getchar

.      7.元数据部分

LFE6: 局部标签,用于标记函数或代码段的结束。

.size main, .-main: 定义main函数的大小。

.ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0": 编译器及其版本信息。

剩余都是定义不可执行堆栈,对齐等要求的定义。

3.4 本章小结

本章探寻了.s文件的格式,分析了汇编语言所代表的含义,对理解程序最底层的执行过程有了更详细的了解。

第4章 汇编

4.1 汇编的概念与作用

       把.s文件翻译为机器语言指令,并打包为一种叫可重定位文件的格式,生成.o文件

4.2 在Ubuntu下汇编的命令

使用指令 gcc -c hello.s -o hello.o

       或者 as hello.s -o hello.o

图表 16命令截图

4.3 可重定位目标elf格式

指令adelf -a hello.o > elf.txt把elf重定位到txt里

图表 17 elf总述

       以16字节的魔数开头,描述了类别,大小端等信息。其他的部分是保存了连接语法分析和解释目标文件的息。如节头部表的文件偏移,大小,数量。

图表 18节头部表

记录了每一个节的起始位置,大小

图表 19重定位节

包含一部分外部变量的信息,在链接时根据重定位节信息来计算地址进行重定位。

可以看见,重定位信息包括puts,exit,printf,atoi,sleep,getchar

图表 20符号表

定义了此模块中的符号信息,包括全局符号,外部符号(其他模块定义的全局符号)和局部符号(static,不能被其他模块所使用)

用一个整数数组来唯一的标识每个节

4.4 Hello.o的结果解析

objdump -d -r hello.o  分析hello.o的反汇编

这里重定位到.asm里objdump -d hello.o >hello.asm

图表 21反汇编

更加清晰明了,使用call加相对地址来调用函数

跳转指令由段名称变为了地址

全局变量由于没有rodata所以只能在后续的重定位链接中获取

格式上:hello.s前没有一串二进制数,即相应的机器码,而反汇编代码前面有与之对应的机器码。

重定位条目:汇编代码仍然采用直接声明的方式,即通过助记符;而反汇编代码采用重定向的方式进行跳转,机器代码在此处留下一些地址以供链接时重定向,链接时根据标识、重定位条目自动填充地址。

4.5 本章小结

本章展示了elf文件与汇编的结果,并且与编译结果进行了对比,展示了汇编的特性。

5章 链接

5.1 链接的概念与作用

链接是指把各个数据,代码片段整合成单一文件的过程,以便于更好的加载运行。

链接通常是自动执行的,不过这时对于小型程序而言的。大型的应用程序往往可以被分解为更小的模块独立地编译修改。当我们向改变一个模块是,只需要编译这模块即可。

5.2 在Ubuntu下链接的命令

可以用gcc hello.o -o hello

用ld则需要详细写明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/11/crtbegin.o

hello.o

-lc /usr/lib/gcc/x86_64-linux-gnu/11/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello

图表 22 ld链接示例

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

图表 23 可执行文件结构

图表 24还是那个elf头

图表 25重定位节

图表 26符号表

头部表内容:偏移vaddr/paddr:内存地址align:对齐要求filesz:目标文件中的段大小memsz:内存中的段大小flags:运行时访问权限

5.4 hello的虚拟地址空间

图表 27edb查看

5.5 链接的重定位过程分析

在程序的编译和链接过程中,涉及到了以下关键变化和步骤:

新增的函数:

添加了多个函数及其对应的汇编实现,例如常见的 printf、atoi、exit 等,还包括 _init 初始化函数。

新增的节(Section): 引入了新的节(Section),包括但不限于:

.init 节:包含程序的初始化代码,这些代码在程序启动时执行。

.plt 节(Procedure Linkage Table):用于延迟绑定(lazy binding)的函数调用,动态链接器在运行时将真正的函数地址填充到这些调用点上。

.plt.sec:可能是特定程序自定义的一些节,具体含义根据程序的需求而定。

函数调用的改变:

在链接阶段,函数调用不再依赖于重定位类型和偏移量,而是直接使用函数的绝对地址和函数名进行调用。

链接过程的作用:

链接器(如 ld)负责将各个目标文件(.o 文件)整合在一起,形成一个完整的可执行文件。 链接器通过处理来自目标文件的重定位条目,将函数调用和控制流跳转的地址填写为最终的地址,以确保程序在执行时能够正确地访问和执行各个函数。

5.6 hello的执行流程

函数调用如图

图表 28 hello所调用函数

5.7 Hello的动态链接分析

 在进行动态链接之前,首先进行静态链接,生成部分链接的可执行目标文件 hello。这时,共享库中的代码和数据并没有被合并到 hello 中。

只有在加载 hello 时,动态链接器才会对共享目标文件中的相应模块进行重定位,并加载共享库,从而生成完全链接的可执行目标文件。

看.plt段

图表 29先EDB查看.plt段

图表 30运行后EDB查看.plt段

5.8 本章小结

链接过程中包含了许多提高程序执行效率、减小空间浪费的措施,例如静态链接库和动态链接共享库等,这为编写高效的程序提供了有益的思路。 经过链接,程序已经变成了一个可执行文件。接下来,只需在 shell 中输入命令即可创建进程并执行该文件。

6章 hello进程管理

6.1 进程的概念与作用

进程:执行中的程序实例。系统中的每个程序都运行在某个进程的上下文(context)中。

上下文:由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

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

Shell 是一个命令行解释器,它输出一个提示符,等待用户输入命令行并执行该命令。如果命令行的第一个单词不是内置的 Shell 命令,Shell 假设它是一个可执行文件名,会加载并运行这个文件(Shell 会创建一个新进程,并在新进程的上下文中运行该可执行文件)。

例如,运行 Hello 程序时,Shell 会加载并运行 Hello 程序,并等待程序终止。程序终止后,Shell 会输出提示符,等待下一个命令行输入。

6.3 Hello的fork进程创建过程

pid_t fork(void); // 返回:子进程返回 0,父进程返回子进程的 PID,如果出错,则返回 -1

fork一次,却会返回两次:一次在父进程中,一次在新创建的子进程中。

在父进程中,fork 返回子进程的 PID;在子进程中,fork 返回 0。由于子进程的 PID 总是非零,返回值提供了区分父进程和子进程的方法。

父进程和子进程有以下特点:

1. 并发: - 父进程和子进程是并发运行的独立进程。内核可以以任意方式交替执行它们的指令。 - 例如,在某些系统上,父进程可能先执行完 printf 语句,然后是子进程;在其他系统上可能相反。 - 程序员不能对进程间指令的执行顺序做任何假设。

2. 相同但独立的地址空间: - 在 fork 返回后,父进程和子进程的地址空间看似相同。每个进程有相同的用户栈、局部变量、堆、全局变量和代码。 - 例如,当 fork 在第 6 行返回时,父进程和子进程中的本地变量 x 都是 1。 - 然而,父进程和子进程是独立的,它们各自对 x 的修改不会影响对方。

3. 共享文件: - 父进程和子进程共享文件描述符。例如,当父进程调用 fork 时,stdout 文件是打开的并指向屏幕。 - 子进程继承了这个文件描述符,因此它的输出也会显示在屏幕上。

6.4 Hello的execve过程

execve 函数在当前进程的上下文中加载并运行一个新程序。

int execve(const char *filename, const char *argv[], const char *envp[]);

// 如果成功,则不返回;如果出错,则返回 -1。

execve 函数加载并运行可执行文件 filename,传递参数列表 argv 和环境变量列表 envp。只有在发生错误时(例如找不到 filename),execve 才会返回调用程序。

因此,与 fork 函数(调用一次返回两次)不同,execve 调用一次并且通常不会返回。

6.5 Hello的进程执行

内核为每个进程维持一个上下文(context),这是内核重新启动一个被抢占进程所需的状态。

上下文包括以下内容:通用寄存器,浮点寄存器,程序计数器,用户栈,状态寄存器,内核栈各种,内核数据结构(如描述地址空间的页表、进程表、文件表)

在某些时刻,内核可能决定抢占当前进程并恢复一个先前被抢占的进程。这种决策称为调度(scheduling),由内核中的调度器(scheduler)代码处理。当内核选择一个新进程运行时,我们称之为调度该进程。

调度新进程后,内核通过上下文切换机制将控制转移到新进程。上下文切换过程包括:保存当前进程的上下文。恢复先前被抢占进程的上下文。将控制传递给恢复的进程。每个进程执行其控制流的一段时间称为进程时间片。

图表 31上下文切换示意图

上下文切换的原因有很多,以下是几个例子:内核代表用户执行系统调用时。中断引发的上下文切换。

6.6 hello的异常与信号处理

异常可以分为四类:中断(interrupt),陷阱(trap)、故障(fault)和终止(abort)。

中断:自 I/O 设备的信号,异步,总是返回到下一条指令

陷阱:意的异常,同步,总是返回到下一条指令

故障:在可恢复的错误,同步,可能返回到当前指令

终止:可恢复的错误,同步,不会返回

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

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

6.7本章小结

本章通过Linux中的进程操作命令,学习了计算机系统中的异常、进程、信号等相关知识,同时还探讨了用户态和内核态之间的上下文切换。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址(Logical Address): 包含在机器语言指令中,用于指定操作数或指令的地址。 促使程序员将程序分成若干段。 每个逻辑地址由一个段(segment)和偏移量(offset)组成,偏移量表示从段开始到实际地址的距离。 对应于 hello.o 中的相对偏移地址。

线性地址(Linear Address): 逻辑地址到物理地址转换之间的中间层。 程序代码产生逻辑地址(段内偏移地址),加上段的基地址生成线性地址。 如果启用了分页机制,线性地址会转换成物理地址;否则,线性地址直接作为物理地址。

虚拟地址(Virtual Address): 程序运行时使用的逻辑地址,通常运行在虚拟地址空间中。 由于采用段式存储模式,虚拟地址是二维的,由段基址和段内位移表示。

物理地址(Physical Address): 线性地址经过分页机制转换得到的实际内存地址,用于定位实际要访问的内存单元。 由内存管理单元(MMU)执行此操作。 计算机系统的存储组织为一个由 M 个连续字节组成的数组,每个字节都有唯一的物理地址。

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

程序按照自身的逻辑关系划分为若干段,每个段都有一个段名。在低级语言中,程序员使用段名来编程,每段从0开始编址。

分段系统的逻辑地址由段号(段名)和段内地址(段内偏移量)组成。段地址结构,号(段描述符):由16位字段组成,称为段选择符。前13位是索引号,用于在段描述符表中查找具体段描述符。后3位包含硬件细节。段选择符的前13位直接指向段描述符表中的一个具体段描述符,描述该段的信息。

段描述符的分类:

全局段描述符:存放在全局段描述符表(GDT)中。

局部段描述符:存放在局部段描述符表(LDT)中。

中断描述符:存放在中断描述符表(IDT)中。

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

在虚拟寻址中,CPU 生成一个虚拟地址(Virtual Address, VA,即线性地址)来访问主存。这个虚拟地址在送到内存之前会被转换成适当的物理地址。

这种地址转换需要 CPU 硬件和操作系统之间的紧密合作,类似于异常处理的机制。 CPU 芯片上有一个称为内存管理单元(Memory Management Unit, MMU)的专用硬件,MMU 利用存放在主存中的查询表来动态翻译虚拟地址,这些表由操作系统管理。 数据在磁盘(较低层)上被分割成块,这些块作为磁盘和主存(较高层)之间的传输单元。

虚拟内存(VM)系统通过将虚拟内存分割为固定大小的虚拟页(Virtual Page, VP)来对磁盘块进行映射。

VM 系统通过页表将虚拟页映射到物理页,从而实现虚拟地址到物理地址的转换。

图表 32寻址过程

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

TLB:(Translation Lookaside Buffer)翻译后备缓冲器,用于加速地址的翻译 使用TLB时翻译地址的一般处理流程:

CPU 产生一个虚拟地址。

MMU 从 TLB 中取出相应的 PTE。

MMU 将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存。

高速缓存/主存将所请求的数据字返回给 CPU。

还可以衍生出多级页表。

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

物理地址由块偏移(CO)、组索引(CI)、标记(CT)三部分组成。

首先在一级Cache找,不命中则到下一级缓存即二级Cache找,若不命中则到三级Cache下访问。

7.6 hello进程fork时的内存映射

当当前进程调用 fork 函数时,内核会为新进程创建各种数据结构,并分配一个唯一的 PID。

为了创建新进程的虚拟内存,内核复制当前进程的 mm_struct、区域结构和页表。

内核将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

图表 33一个私有的写时复制对象

7.7 hello进程execve时的内存映射

当执行 execve 函数加载并运行 a.out 时,以下步骤会发生:

删除已存在的用户区域: 删除当前进程虚拟地址空间中的用户区域结构。

映射私有区域: 为新程序的代码、数据、bss 和栈区域创建新的区域结构。 这些新区域都是私有的,并且是写时复制的。 代码和数据区域映射到 a.out 文件中的 .text 和 .data 区。 bss 区域映射到匿名文件,其大小在 a.out 中指定。 栈和堆区域也映射到匿名文件,初始长度为零。

映射共享区域: 如果 a.out 与共享对象(如标准 C 库 libc.so)链接,这些对象会动态链接到程序,并映射到用户虚拟地址空间中的共享区域内。

设置程序计数器(PC): execve 的最后一步是设置当前进程上下文中的程序计数器,使其指向代码区域的入口点。

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

图表 34缺页

当发生缺页故障时(即 DRAM 缓存不命中),缺页处理程序会被调用。

处理过程如下:

内核确定一个牺牲页。

如果页面被修改,则将其换出到磁盘。

将新的目标页替换牺牲页,并将其写入内存。

更新内存中的页表项(PTE)。

缺页处理程序返回到原进程,重新启动导致缺页的指令。

7.9动态存储分配管理

动态内存分配器维护着进程的虚拟内存区域,称为堆。堆从未初始化的数据区域之后开始,并向上增长。内核为每个进程维护一个变量 brk,指向堆的顶部。

1.堆的管理:

堆被视为一组不同大小的块,每个块是一个连续的虚拟内存片。

块可以是已分配的或空闲的。

已分配的块保留给应用程序使用,空闲块等待分配。

空闲块保持空闲,直到被应用程序显式分配。已分配的块保持已分配状态,直到被释放。

2.分配器的要求:

处理任意请求序列:每个释放请求必须对应一个当前已分配的块。

立即响应请求:分配器必须立即响应分配请求,不得重新排列或缓冲请求。

只使用堆:分配器使用的任何非标量数据结构都必须保存在堆中。

对齐块:分配器必须对齐块,使其能够保存任何类型的数据对象。

不修改已分配的块:分配器只能操作或改变空闲块。

不同性能的分配器:

隐式空闲链表:使用单向链表实现。

显式空闲链表:使用带前驱和后继的双向链表,实现更快的查找。

分离的空闲链表:提高分配效率。

3.垃圾回收机制:

使用 free 函数回收已申请但未显式回收的空间。 垃圾回收机制使用有向可达图来判断内存块是否需要回收。

7.10本章小结

本章介绍了计算机中的各类地址及其转换,探讨了虚拟内存系统的内部工作原理,包括 fork 和 execve 函数的实现,最后讨论了动态内存分配的相关知识。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:在Linux中,所有的I/O设备(例如网络、磁盘和终端)都被抽象为文件,所有的输入和输出操作都视为对相应文件的读写。

设备管理:Linux内核提供了一个简单、底层的接口,称为Unix I/O,使所有的输入和输出操作都能以统一且一致的方式执行。

8.2 简述Unix IO接口及其函数

1. Unix I/O接口:

(1) 打开文件:应用程序通过请求内核打开文件来访问I/O设备。内核返回一个小的非负整数,称为描述符,该描述符标识此文件并用于后续操作。内核会记录所有关于该文件的信息,应用程序只需记住描述符。Linux shell创建的每个进程在开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。这些常量在 <unistd.h> 头文件中定义:

  1. #define STDIN_FILENO 0 
  2. #define STDOUT_FILENO 1 
  3. #define STDERR_FILENO 2 

(2) 改变当前文件位置:对于每个打开的文件,内核维护一个文件位置k,初始为0。文件位置是从文件开头起的字节偏移量。应用程序可以通过执行seek操作显式设置文件位置k。

(3) 读写文件:读操作从当前文件位置k开始,将n字节复制到内存,并将k增加n。当k >= 文件大小时,读操作触发EOF条件,应用程序可以检测到EOF。在文件末尾没有明确的“EOF符号”。写操作从内存复制n字节到文件,从当前位置k开始,然后更新k。

(4) 关闭文件:当应用程序完成对文件的访问后,它会通知内核关闭文件。内核释放打开文件时创建的数据结构,并将描述符返回到可用描述符池中。无论进程为何原因终止,内核都会关闭所有打开的文件并释放它们的资源。

2. Unix I/O函数:

(1) open:打开文件

  1. #include <sys/types.h> 
  2. #include <sys/stat.h> 
  3. #include <fcntl.h> 
  4. int open(const char *filename, int flags, mode_t mode); // 返回:若成功则为新文件描述符,若出错为 -1 

flags 参数指明进程如何访问文件:

O_RDONLY:只读

O_WRONLY:只写

O_RDWR:可读可写

mode 参数指定新文件的访问权限位。

(2) close:关闭文件

  1. #include <unistd.h> int close(int fd); // 返回:若成功则为0,若出错则为-1 

(3) 读写文件

  1. #include <unistd.h> 
  2. ssize_t read(int fd, void *buf, size_t n); // 返回:若成功则为读的字节数,若EOF则为0,若出错为-1 
  3. ssize_t write(int fd, const void *buf, size_t n); // 返回:若成功则为写的字节数,若出错则为-1 

read 函数从描述符fd的当前文件位置复制最多n字节到内存位置buf,返回值为实际读取的字节数,-1表示错误,0表示EOF。write 函数从内存位置buf复制最多n字节到描述符fd的当前文件位置。

8.3 printf的实现分析

printf 函数包含在 <stdio.h> 头文件中:         

  1. int printf(const char *fmt, ...) {
  2.     int i;
  3.     char buf[256];
  4.     va_list arg = (va_list)((char*)(&fmt) + 4);
  5.     i = vsprintf(buf, fmt, arg);
  6.     write(buf, i);     
  7.     return i;
  8. }

函数原型中的 (char*)(&fmt) + 4) 表示获取可变参数的地址。vsprintf 用格式字符串对可变参数进行格式化,产生格式化输出,然后通过 write 系统调用将输出写到终端。

8.4 getchar的实现分析

getchar 函数包含在 <stdio.h> 头文件中:

  1. __STDIO_INLINE int getchar(void) {
  2.     return getc(stdin);
  3. }

异步异常-键盘中断的处理:当用户按键时触发键盘终端,操作系统将控制转移到键盘中断处理子程序,中断处理程序执行,接受按键扫描码转成ascii码,保存到系统的键盘缓冲区,显示在用户输入的终端内。当中断处理程序执行完毕后,返回到下一条指令运行。

getchar 实际上调用的是Unix I/O函数 getc。键盘中断处理程序接收按键扫描码,将其转换为ASCII码,并保存到系统的键盘缓冲区。getchar 调用 read 系统函数读取按键ASCII码,直到接受到回车键才返回。

8.5本章小结

本章介绍了系统级IO和Unix IO的基本知识,IO是系统操作不可或缺的一部分,理解系统IO有助于掌握其他系统概念。最后分析了 printf 和 getchar 两个标准化IO函数的实现。

结论

Hello的整个生命周期如上。总览如下:

创建源文件:通过文本编辑器将hello.c的内容写入文件。

编译过程:

预处理:使用GCC进行预处理,生成预处理后的C程序文件hello.i。

编译:将预处理后的C文件编译成汇编语言文件hello.s。

汇编:将汇编文件转换为可重定位目标文件hello.o。

链接:将目标文件与必要的模块和函数库链接,生成可执行文件hello。

运行程序:

在命令行中输入 ./hello 调用hello文件时,shell会首先调用 fork 创建子进程。

在子进程中,execve 创建虚拟内存空间映射,翻译虚拟内存地址为物理地址,并通过高速缓存和缺页处理将hello加载进内存和CPU。

执行程序:

CPU获取指令后,控制hello程序的逻辑流程。

期间,程序调用 printf、getchar 等函数与I/O设备交互,实现屏幕显示和键盘输入。

异常处理:

当程序运行过程中用户输入 ctrl-c、ctrl-z 等指令时,系统中断并调用相应的信号处理程序进行处理。

回顾hello程序的完整执行流程,我们不仅了解了现代计算机的各种先进硬件和精妙的设计思想,还认识到程序设计之外广阔的计算机系统世界,从而加深了对计算机学习的兴趣。

附件

hello.i        预处理生成的文本文件

hello.s        .i文件编译后得到的汇编语言文件

hello.o        .s文件汇编后得到的可重定位目标文件

hello          .o经过链接生成的可执行目标文件

hello.asm      hello.o的反汇编代码文件

hello.elf       hello.out的elf文件

elf.txt         hello.o的elf文件

参考文献

[1]  《深入理解计算机系统(原书第三版)》 Randal E.Bryant David R.O’Hallaron 机械工业出版社

[2]  CSAPP:第八章——异常控制流http://t.csdnimg.cn/hFQGj

  • 20
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值