计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机
学 号 120L021312
班 级 2003011
学 生 刘旭伟
指 导 教 师 郑贵滨
计算机科学与技术学院
2021年5月
摘 要
本文从预处理、汇编、编译、链接、进程管理、存储管理七个角度介绍了hello的一生------由程序到进程,hello程序从不存在到产生再到被回收的过程。
关键词:程序运行;进程管理;存储管理。
第1章 概述
1.1 Hello简介
P2P: From Program to Process,也即由程序到进程。hello.c源程序经过预处理(cpp)、编译(ccl)、汇编(as)、链接(ld)最终生成可执行目标文件hello.out。最后在shell中输入命令(./hello)后,操作系统(OS)的进程管理为其fork创建子进程,从而实现了从程序到进程的转变。过程图如下:
020: From Zero-0 to Zero-0。Shell通过execve加载并执行该进程。操作系统为程序分配虚拟空间并且映射到物理内存空间。随后CPU为它分配逻辑控制流。随后进程终止,shell回收进程,操作系统释放虚拟空间。hello程序从不存在(0)到产生再到被回收(0)的过程,也即是020的过程。所以进程从0到了0。
1.2 环境与工具
硬件环境:AMD Ryzen5 4600U x64 CPU; 2.10GHz; 16G RAM
软件环境:Ubuntu20.04,Virtual Box
开发与调试工具:gcc,EDB等
1.3 中间结果
文件名 | 作用 |
---|---|
hello.c | 源程序 |
hello.i | 预处理后得到的的文本文件,用来分析预处理器的行为 |
hello.s | 编译后的得到的汇编文件,用来分析编译器行为 |
hello.o | 编译后的得到的汇编文件,用来分析汇编器的行为 |
hello | 链接后得到的可执行文件 |
1.4 本章小结
本章对hello程序的一生进行了简介,并且介绍了本次进行实验的软硬件环境以及我所用到的开发与调试工具。还介绍了一下生成的中间结果文件的名字以及作用。
第2章 预处理
2.1 预处理的概念与作用
-
预处理的概念:所谓预处理是指在进行编译的第一遍扫描(词法和语法)之前预处理器所做的工作。当对一个源文件进行编译时,系统将自动引用预处理程序对程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。
-
预处理的作用:预处理在源代码编译之前对其进行的一些文本性质的操作,如:删除注释、插入被#include指令包含的文件内容、定义和替换由#define指令定义的符号(宏替换或宏展开)、条件编译。在源程序编译之前做一些处理,生成.i文件。
2.2在Ubuntu下预处理的命令
对hello.c文件进行预处理的命令是:gcc -E -o hello.i hello.c。
2.3 Hello的预处理结果解析
hello.c文件截图
hello.i文件部分截图
结果分析:在经过预处理之后,原来23行代码量的hello.c文件变成了3060行代码量的hello.i文件。不难发现,发生了宏替换和宏展开,使得文件内容大大增加,还删除了注释。
2.4 本章小结
本章介绍了预处理的概念与作用,并通过使用Ubuntu下预处理的命令,通过对hello.c文件进行预处理,产生了hello.i文件。并通过解析预处理之后的结果,大致了解了预处理的具体过程。
第3章 编译
3.1 编译的概念与作用
-
编译的概念:编辑器(ccl)将文本文件hello.i翻译成文本文件hello.s。
-
编译的作用:把完成预处理后的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件,为接下来将汇编语言生成机器可识别的机器码做准备。
3.2 在Ubuntu下编译的命令
在Ubuntu下对.i文件进行编译的指令是:gcc -S -o hello.s hello.i。
3.3 Hello的编译结果解析
3.3.0汇编代码
3.3.1数据
①字符串常量:字符串常量存放在.rodata段(只读数据段)中,如下图所示。
②局部变量i:局部变量i被放在栈中,通过寄存器rbp的相对偏移来访问,如下图所示。
③main函数形参argc、argv:根据参数传递时寄存器调用顺序可知,两个参数初始时分别被放在edi和rsi上,并在主函数中将其压栈,同样通过rbp的偏移量来访问。如下图:
3.3.2赋值
hello.c中赋值操作是for循环中i=0;在汇编代码中使用mov指令实现,如下图:
3.3.3算数操作
hello.c文件中的i++;在汇编代码中使用add指令实现,将1加到i上,如下图:
3.3.4关系操作
hello.c文件中的argc!=4;在汇编代码中使用cmp语句实现,比较argc与4的关系,并进行条件跳转。如下图:
3.3.5数组操作
hello.c文件中的数组操作是访问数组argv[],在汇编代码中访问数组是通过数组首地址加偏移量的方式,如下图:
3.3.6控制转移
①if条件语句:hello.c文件中的if(argc!=4),在汇编代码中通过cmp语句判断argc4与4的大小,以此来确定是否发生条件跳转。如下图:
②for循环:本质上也是条件跳转,通过cmp语句比较i是否小于等于7,如果是,则跳转。如下图:
3.3.7函数操作
①函数调用:通过call+函数名实现,如下图:
②函数返回;通过ret指令返回,如下图:
3.4 本章小结
本章主要介绍了编译的概念与作用,并且通过使用gcc工具将hello.i汇编成hello.s文件。并对编译结果从数据,赋值语句,算术操作,关系操作,数组操作控制转移与函数操作这几点进行分析,便于对编译器编译机制的理解。
第4章 汇编
4.1 汇编的概念与作用
-
汇编的概念:把汇编语言翻译成机器语言的过程称为汇编。
-
汇编的作用:将汇编语言翻译成机器语言,也即实现从.s汇编语言文本文件到.o可重定位目标文件,使其在链接后能够被机器识别并执行。
4.2 在Ubuntu下汇编的命令
在Ubuntu下对.s文件汇编的指令是gcc -c -o hello.o hello.s
4.3 可重定位目标elf格式
4.3.1 elf格式
4.3.2 elf头
elf头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。可以用命令readelf -h hello.o查看。如下图:
4.3.3节头表
节头表告诉了我们每个节的大小、名称、类型、读、写、执行权限以及对其方式。由于我们的程序还未进行链接,因此每个节的起始位置都是0,在链接后会为每个节进行重定位以获得起始位置。可以用命令readelf -S hello.o查看,如下图:
4.3.4 符号表
符号表中存储了程序中定义和使用的各种符号,包括函数名,全局变量名等等。其中每一个符号有其对应的值,大小,类型,名字等等内容。可以用命令readelf -s hello.o查看,如下图:
4.3.5重定位节
一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。可以用命令readelf -r hello.o命令查看,如下图:
4.4 Hello.o的结果解析
使用objdump -d -r hello.o命令对hello.o进行反汇编。反汇编代码如下图:
hello.s代码如下:
反汇编代码和汇编代码在指令格式上非常相似,但在以下几个方面存在着不同:
①立即数:立即数在反汇编中是十六进制的,而在汇编代码则是十进制的。如下图:
反汇编代码:
汇编代码:
②函数调用:反汇编代码中子函数调用是通过对主函数地址的相对偏移进行的,而在汇编代码中则是通过call直接加上函数名的方法进行的。如下图:
反 汇编代码
汇编代码
③分支转移:在反汇编代码中,分支转移是通过跳转到以主函数地址为基址的一个偏移地址中,而在汇编代码中则是通过.L4、.L3段名称来跳转的。如下图:
反汇编代码
汇编代码
4.5 本章小结
本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了可重定位目标文件的ELF格式、ELF头、节头部表、符号表和可重定位节。此外,还对hello.o进行了反汇编,并比较了hello.s和hello.o反汇编代码的不同之处,分析了汇编语言与机器语言的对应关系。
第5章 链接
5.1 链接的概念与作用
- 链接的概念:链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(或复制)到内存并执行。
- 链接的作用:将程序从.o文件转化为可执行文件。使分离编译成为可能。
5.2 在Ubuntu下链接的命令
在ubuntu下使用命令ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o进行链接。如下图:
5.3 可执行目标文件hello的格式
5.3.1 elf头
使用readelf -h hello命令,如下图:
由图可以看出节头表的个数由14个增加为27个。Type类型为EXEC说明hello是一个可执行目标文件。同时hello的入口地址非零,说明重定位工作已完成。
5.3.2节头表
可以用命令readelf -S hello查看,如下图:
由图可以看到与hello.o不同,在可执行文件中经过重定位每个节的地址不再是0,说明重定位工作已完成。
5.3.3 符号表
可以用命令readelf -s hello查看,如下图:
5.3.4重定位节
可以通过命令readelf -r hello查看,如下图:
5.4 hello的虚拟地址空间
查看EDB,可以看出虚拟地址空间起始地址为0x400000,结束地址为0x401ff0
如下图:
再根据5.3.2的节头表中各节的偏移量即可得到对应的虚拟地址。
例如下图,.text段地址偏移量为10f0,所以对应的虚拟地址应为0x4010f0,
再查看EDB,发现确实在地址0x4010f0处。
再比如.data段,地址偏移量为3048,说明对应的虚拟地址应为0x403048。
再查看EDB对应,发现确实在地址0x403048处。
其他节头的查看与此类似,这里不再赘述。
5.5 链接的重定位过程分析
使用objdump -d -r hello命令,得到hello的反汇编代码。可以发现hello与hello.o有以下几点不同:
①hello反汇编代码比hello长不少
可以发现puts等函数放入到了hello里,说明链接完成后,引入了其他库里的一些函数和数据。
②hello文件里的函数调用用的都是具体的虚拟地址值,而hello.s用的却是相对地址,这是因为hello的重定位工作已经完成了。如下图:
hello:
hello.s:
hello重定位的过程:
-
重定位节和符号定义:这一步中,链接器将所有类型相同的节合并为同一类型的新的聚合节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
-
重定位节中的符号引用:这一步中,链接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。实际上也就是根据相对地址得到对应的运行地址。
5.6 hello的执行流程
名称 | 地址 |
---|---|
hello!_start | 0x4010f0 |
libc-2.27.so!__libc_start_main | 0x7f8b11973fc0 |
hello!main | 0x401125 |
hello!puts@plt | 0x401030 |
ld-2.31.so | 0x7f8b11b71ae0 |
libc-2.31.so!puts | 0x7f8b119d4450 |
5.7 Hello的动态链接分析
通过查询hello的ELF文件,得到.GOT.PLT的地址
- do_init之前:
- do_init之后:
可以发现表中的数据已经变成了相应的偏移量。
5.8 本章小结
本章讲述了链接的概念与作用,并且通过使用Ubuntu下链接的命令得到了hello可执行文件,然后通过readelf命令分析hello的elf格式,在EDB中查看虚拟地。再利用反汇编指令得到反汇编代码,对链接的重定位过程进行分析。此外,还对hello进行了执行流程、动态链接分析。
第6章 hello进程管理
6.1 进程的概念与作用
-
进程的概念:一个正在运行的程序的实例
-
进程的作用: 进程提供给应用程序两个关键抽象
- 逻辑控制流:每个程序似乎独占地使用处理器,由OS内核通过上下文切换机制来实现。
- 私有地址空间:每个程序似乎独占地使用内存系统,由OS内核的虚拟内存机制实现。
6.2 简述壳Shell-bash的作用与处理流程
-
作用:shell是用户与Linux系统的交互作用界面。shell应用程序提供了一个界面,用户通过访问这个界面访问操作系统内核。
-
处理流程:从终端读入输入的命令,将输入字符串切分获得所有的参数。如果是内置命令则立即执行;否则调用相应的程序执行。shell 应该接受输入信号,并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
打开shell,使用./hello 120L021312 Liuxuwei 1(这里我没有安装中文输入法所以无法输入中文)命令来运行hello程序。
这里shell会分析出我们输入命令的不是一条内置命令,因此为了执行我们的命令,shell会通过fork函数创建一个子进程。这样通过shell,我们就创建了一个hello子进程。
6.4 Hello的execve过程
当调用fork函数创建了一个子进程之后,子进程会调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,需要以下步骤:
-
删除已经存在的用户区域。
-
创建新的代码、数据、堆和栈段:
(1所有这些区域结构都是私有的,写时复制
(2)代码和初始化数据映射到.text和.data区(目标文件提供)
(3).bss和栈堆映射到匿名文件 ,栈堆的初始长度0 -
映射共享区域:共享对象由动态链接映射到本进程共享区域。
-
设置PC:设置当前进程的上下文中的程序计数器指向代码区域的入口点,根据需要换入代码和数据页面。
6.5 Hello的进程执行
-
上下文信息:上下文程序正确运行所需要的状态,由存放在内存中的程序的代码和数据,用户栈、用寄存器、程序计数器、环境变量和打开的文件描述符的集合构成。
-
进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片
-
进程调度:在对进程进行调度的过程,操作系统主要做了两件事:加载保存的寄存器,切换虚拟地址空间。
-
用户态与核心态转换:用户态与核心态区分在于限制了应用程序可执行指令所能访问的地址范围,核心态可以说是拥有最高的访问权限。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中。
hello进程执行分析:
hello起初在用户模式下运行,在hello进程调用sleep之后转入内核模式,内核可能会进行上下文切换。当程序运行到getchar时,内核也会进行上下文切换,让其他进程运行。除了这些,系统还会为hello程序分配时间片,即使没有执行到getchar或者sleep函数,只要hello时间片被用完,系统就会判断出当前程序执行时间足够长了,从而进行上下文切换,将处理器让给其他进程。
6.6 hello的异常与信号处理
6.6.1 产生的异常、信号以及处理方式
-
产生的异常:
- 中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。
- 陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
- 故障:在执行hello程序的时候,可能会发生缺页故障。
- 终止:终止是不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。
-
产生的信号与处理方式:
-
程序的异常往往时通过信号来处理的。
-
在hello程序执行的时候如果从键盘键入ctrl+c这样就会受到SIGINT的终止信号,其他来自键盘的信号也类似。在hello结束的时候会向shell发送SIGCHLD信号告诉shell自己运行结束了。通常会采用专门的信号处理程序来处理对应的信号。
-
6.6.2 通过键盘发送信号
按回车:会发现输入回车后程序的行为几乎是没变,说明回车被无视。
输入ctrl z:会发送一个SIGTSTP信号给前台进程组的每个进程,结果是停止前台作业。
再输入ps命令,列出当前正在运行的进程及其pid
再输入jobs,查看当前正在执行的所有命令
再输入pstree,查看所有进程之间的父子关系,从中可以看到我们的hello进程是shell创建的进程。
再输入fg,让暂停的工作又重新开始前台运行
再输入kill指令,杀死进程
输入ctrl c,会让内核发送一个SIGINT信号给到前台进程组中的每个进程,结果是终止前台进程。
6.7本章小结
本章主要介绍了进程的概念与作用,简述了壳Shell-bash的作用与处理流程、hello的fork进程创建过程、hello的execve过程、hello的进程执行、hello的异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
-
逻辑地址:逻辑地址是指由程序产生的与段相关的偏移地址部分。一个逻辑地址由两部份组成:段标识符和段内偏移量,表示为 [段标识符:段内偏移量]。
-
线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址生成了一个线性地址。如果启用了页式管理,那么线性地址可以再变换产生物理地址。若没有启用页式管理,那么线性地址直接就是物理地址。
-
虚拟地址:虚拟地址是逻辑地址计算后的结果,同样不能直接用来访存,需要通过MMU翻译得到物理地址来访存。在hello反汇编代码计算后就能得到虚拟地址。
-
物理地址:用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符和段内偏移量。
段标识符由一个16位长的字段组成,称为段选择符,其中前13位是一个索引号,后面三位包含一些硬件细节。
索引号就是段描述符的索引。
段描述符具体描述了一个段地址,这样,很多段描述符就组成段描述符表。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。
每个段描述符中包含一个Base字段,它描述了一个段的开始位置的线性地址。将Base字段和逻辑地址中的段内偏移量连接起来就得到转换后的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
计算机利用页表,通过MMU来完成从虚拟地址到物理地址的转换。其中页表是一个页表条目(PTE)的数组,虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个PTE。MMU利用页表实现从虚拟地址到物理地址的变换。
线性地址即虚拟地址,用VA来表示。VA被分为虚拟页号(VPN)与虚拟页偏移量(VPO),CPU取出虚拟页号,通过页表基址寄存器来定位页表条目,通过页表上的PTE有效位来判断是否虚拟内存是否命中,若不命中则执行相应的缺页处理子程序;若命中,则从页表条目中取出信息物理页号(PPN),通过将物理页号与虚拟页偏移量(VPO)结合,得到由物理地址(PPN)和物理页偏移量(PPO)组合的物理地址。具体过程如下图:
7.4 TLB与四级页表支持下的VA到PA的变换
7.4.1 TLB的支持
页表条目 (PTEs) 与其他内存数据字一样缓存在 L1中。PTE 可能被其他数据引用所替换/驱逐,导致不命中;PTE即便cache命中,也仍然需要与L1相当的延迟(1-2周期)。而在这种情况下,就可以利用翻译后备缓冲器(TLB)来加速地址翻译。
TLB是MMU中一个小的具有高相联度的集合,页数很少的页表可以完全放在TLB中,实现虚拟页号向物理页号的映射。具体过程如下图:
TLB命中则减少内存访问,加速地址访问。如不命中则会引发额外的内存访问,好在此情况很少发生。
7.4.2 四级页表的支持
在四级页表中,一到三级页表中存放的数据是指向下一级页表的首地址,而不是物理页号。逐步访问到第四级页表,第四级页表中装的就是物理页号,通过第四级页表读出的物理页号链接上虚拟地址中的VPO就可以获得物理地址。如下图:
下面再给出一个通用的k级页表地址翻译流程:
7.5 三级Cache支持下的物理内存访问
首先要知道CPU的高速缓存机制如下图:
在从TLB或者页表中得到物理地址后,根据物理地址从cache中寻找。首先在L1寻找,如果缓存不命中则紧接着寻找下一级cache L2,接着L3,如果L3也不命中,则需要从内存中将对应的块取出放入cache中,其中可能会发生块的替换等其它操作,这里用到CPU的高速缓存机制,一级一级往下找,直到找到对应的内容。
7.6 hello进程fork时的内存映射
在shell输入命令行后,内核调用fork函数创建子进程,为hello进程创建上下文,并分配一个与父进程不同的PID。通过fork创建的子进程拥有父进程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,为每个进程保持了私有地址空间的抽象概念。同时延迟私有对象中的副本直到最后可能的时刻,充分利用了物理内存。
7.7 hello进程execve时的内存映射
调用exceve函数在当前子进程的上下文加载并运行hello程序,需要以下步骤:
-
删除已经存在的用户区域。
-
创建新的代码、数据、堆和栈段:
⑴所有这些区域结构都是私有的,写时复制
(2)代码和初始化数据映射到.text和.data区(目标文件提供)
(3).bss和栈堆映射到匿名文件 ,栈堆的初始长度0 -
映射共享区域:共享对象由动态链接映射到本进程共享区域。
-
设置PC:设置当前进程的上下文中的程序计数器指向代码区域的入口点,根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
-
缺页故障:引用虚拟内存中的字,不在物理内存中 (DRAM 缓存不命中),会触发缺页故障。
-
缺页中断处理:处理器生成一个虚拟地址,并将其传送给MMU。 MMU生成PTE地址(PTEA),并从高速缓存/主存请求得到PTE。高速缓存/主存向MMU返回PTE。PTE的有效位为零时, 触发缺页异常。缺页处理程序确定物理内存中的牺牲页 (若页面被修改,则换出到磁盘------写回策略)。 缺页处理程序调入新的页面,并更新内存中的PTE。缺页处理程序返回到原来进程,再次执行导致缺页的指令。具体过程如下图:
7.9动态存储分配管理
-
动态内存管理基本方法:在程序运行时可以使用动态内存分配器 (比如 malloc) 获得虚拟内存,动态内存分配器维护着进程的一个虚拟内存区域,称为堆。分配器将堆视为一组不同大小块(blocks)的集合,每个块要么是已分配的,要么是空闲的。分配器可以分为两种类型:
- 显式分配器: 要求应用显式地释放任何已分配的块,例如,C语言中的 malloc 和 free
- 隐式分配器: 应用检测到已分配块不再被程序所使用,就释放这个块,比如Java,ML和Lisp等高级语言中的垃圾收集 (garbage collection)。
-
动态内存分配的基本策略:最大化吞吐量,最大化内存利用率。然而二者是相互矛盾的,所以一个实际的分配器要在吞吐率和利用率之间把握好平衡。
-
放置策略(Placement policy)
- 首次适配, 下一次适配, 最佳适配, 等等.
- 减少碎片以提高吞吐量
-
分割策略(Splitting policy)
-
什么时候开始分割空闲块?
-
能够容忍多少内部碎片?
-
-
合并策略(Coalescing policy)
-
立即合并 (Immediate coalescing): 每次释放都合并
-
延迟合并 (Deferred coalescing): 延迟合并,直到需要才合并,提高释放的性能。例如: 为 malloc扫描空闲链表时合并,外部碎片达到阈值时合并
-
7.10本章小结
本章介绍了hello的存储器地址空间,介绍了逻辑地址、线性地址、虚拟地址、物理地址的具体概念,还介绍了Intel逻辑地址到线性地址的变换-段式管理、 Hello的线性地址到物理地址的变换-页式管理、TLB与四级页表支持下的VA到PA的变换、三级Cache支持下的物理内存访问、hello进程fork时的内存映射、hello进程execve时的内存映射、缺页故障与缺页中断处理,还重点介绍了动态存储管理的基本方法和策略。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
- 一个 Linux 文件 就是一个 m 字节的序列: B0 , B1 , … , Bk, … , Bm-1,所有的I/O设备(网络、磁盘、终端)都被模型化为文件,甚至内核也被映射为文件。
设备管理:unix io接口
- Unix I/O:以将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口。
8.2 简述Unix IO接口及其函数
-
接口:
-
打开和关闭文件
open()and close() -
读写文件
read() and write() -
改变当前的文件位置 (seek)
指示文件要读写位置的偏移量
lseek()
-
-
函数:
- 打开和关闭文件函数
- 打开文件:int open(char* filename,int flags,mode_t mode);
- 关闭文件:int close(fd);
- 读写文件函数
- 读文件:ssize_t read(int fd, void *buf, size_t n);
- 写文件: ssize_t write(int fd, const void *buf, size_t n);
- 打开和关闭文件函数
8.3 printf的实现分析
参考https://www.cnblogs.com/pianist/p/3315801.html
printf函数的函数体如下图所示
首先向printf函数传递参数,然后调用vsprintf函数生成显示信息,再调用write系统函数打印要输出的格式串。调用write函数后,程序进入陷阱,系统调用 int 0x80或syscall等,运行字符显示驱动程序,实现从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片再按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。最后printf函数返回字符串中字符的个数。
8.4 getchar的实现分析
getchar通过read函数从缓冲区中读入一行,并返回读入的第一个字符,若读入失败则返回EOF。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法,简述了Unix IO接口及其函数,进行了printf与 getchar函数的实现分析。
结论
- hello所经历的过程:
- 预处理:hello.c源文件经过预处理,得到了hello.i文本文件
- 汇编:hello.i经过汇编,得到了hello.s汇编文件
- 编译:hello.s经过编译,得到了hello.o可重定位目标文件
- 链接:hello.o经过链接,得到了可执行文件hello
- 进程:在shell中输入命令后,调用fork函数生成子进程,子进程再调用 exceve函数在当前子进程的上下文加载并运行hello程序,并且在执行进程时进行异常与信号处理。最后结束进程,内核回收为其创建的所有信息。
- 内存管理:运行程序时,需要用到很多地址,其中离不开内存管理。
- IO管理:hello程序运行时用到了printf函数,这其中离不开IO管理。
- 感悟:hello的一生短暂却并不平凡,背后满是坎坷与艰辛。正如此次完成大作业的我们,喜悦伴随着辛酸。通过此次的实验,我了解到了小小的hello背后所蕴含的深刻的知识,也清楚的明白了自身的不足。不禁反思:在之前的学习中,自己又忽视了多少微不足道却又蕴含深刻知识的事物呢?除此之外,我还明白了在学习的过程中,不能被事物的表象所迷惑,要透过现象看本质,从本质上来深刻了解事物。在计算机系统设计与实现的过程中,不能只拘泥于结果,更要从底层分析,从本质上探究实现的过程与原理。
附件
文件名 | 作用 |
---|---|
hello.c | 源程序 |
hello.i | 预处理后得到的的文本文件,用来分析预处理器的行为 |
hello.s | 编译后的得到的汇编文件,用来分析编译器行为 |
hello.o | 编译后的得到的汇编文件,用来分析汇编器的行为 |
hello | 链接后得到的可执行文件 |
参考文献
[1]printf函数实现
[2] getchar函数详解
[3] CSAPP电子版
[4]进程
[5]内存管理