因开始使用博客框架,原图片资源尚未导入,图片显示将不正常
大作业要求
Hello 的自白
我是Hello,我是每一个程序猿的初恋(羞羞……)
却在短短几分钟后惨遭每个菜鸟的无情抛弃(呜呜……),他们很快喜欢上sum、sort、matrix、PR、AI、IOT、BD、MIS……,从不回头。
只有我自己知道,我的出身有多么高贵,我的一生多么坎坷!
多年后,那些真懂我的大佬(也是曾经的菜鸟一枚),才恍然感悟我的伟大!
……………………想当年:
俺才是第一个玩 P2P的: From Program to Process
懵懵懂懂的你笨笨磕磕的将我一字一键敲进电脑存成hello.c(Program),无意识中将我预处理、编译、汇编、链接,历经艰辛,我-Hello一个完美的生命诞生了。
你知道吗?在壳(Bash)里,伟大的OS(进程管理)为我fork(Process),为我execve,为我mmap,分我时间片,让我得以在Hardware(CPU/RAM/IO)上驰骋(取指译码执行/流水线等);
你知道吗?OS(存储管理)与MMU为VA到PA操碎了心;TLB、4级页表、3级Cache,Pagefile等等各显神通为我加速;IO管理与信号处理使尽了浑身解数,软硬结合,才使我能在键盘、主板、显卡、屏幕间游刃有余, 虽然我在台上的表演只是一瞬间、演技看起来很Low、效果很惨白。
感谢 OS!感谢 Bash!在我完美谢幕后回收了我。 我赤条条来去无牵挂!
我朝 CS(计算机系统-Editor+Cpp+Compiler+AS+LD + OS + CPU/RAM/IO等)挥一挥手,不带走一片云彩! 俺也是 O2O: From Zero-0 to Zero-0。
历史长河中一个个菜鸟与我擦肩而过,只有CS知道我的生、我的死,我的坎坷,“只有 CS 知道……我曾经……来…………过……”————未来一首关于Hello的歌曲绕梁千日不绝 !!
摘要
计算机伴随我们每个人,在生活中无处不见,对于程序员更是如此。每个程序员第一个接触到的程序便是hello——这个打印出"hello, world"的简单程序。它虽然简单,但是包含了无穷的奥秘,本文以此以此贯穿始终,从程序猿的角度来理解,完整讲述了hello程序的一生的每个重大阶段,分析了每个阶段的过程和意义。
关键词:计算机系统;编译;异常控制流;虚拟内存
第1章 概述
1.1 Hello 简介
当一个文本文件被敲上了那段代码,储存成了hello.c
文件后,经过预处理变成了hello.i
,经过编译变成了hello.s
,经过汇编器变成了hello.o
,最后由链接器链接成为二进制的可执行文件——hello
。自此,Program诞生了。当我们在shell中输入./hello
时,shell
将字符逐一读入寄存器,再把它放到内存中,敲下回车时,这个program
hello被运行了!它从0开始,随后内核为其分配空间,加载到内存,分配处理器资源后,它就成了一个进程Process,这时它有了许多资源!在短暂的时间之后,它执行了main
函数返回或exit
。它,终止了,在被shell
回收之后,还被内核清楚了痕迹!它又变成了0。
1.2 环境与工具
硬件组成:
处理器:2.5 GHz Intel Core i7-7660U
内存:16 GB 2133 MHz LPDDR3
硬盘:512GB SSD
软件和环境:
VMware Fusion 专业版10.1.3
Ubuntu 18.04.1
开发调试工具:
Xcode
CLion
Sublime Text 3
GDB
Hexedit
1.3 中间结果
名称 | 类型 |
---|---|
hello.c | 源文件 |
hello.i | 修改了的源程序 |
hello.s | 汇编程序 |
hello.o | 可重定位目标程序 |
hello | 可执行目标程序 |
1.4 本章小结
hello虽然简单,但是产生的过程是十分伟大的,理解hello执行的过程能够帮助我们理解程序的一生。
第2章 预处理
2.1 预处理的概念与作用
预处理器(cpp
) 根据以字符#开头的命令,修改原始的C程序。主要处理#开始的预编译指令,如宏定义(#define
)、文件包含(#include
)、条件编译(#ifdef
)等。合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
2.2 在Ubuntu 下预处理的命令
命令:
gcc -E hello.c > hello.i
生成的hello.i
如图(节选):

图2-1 hello.i节选
2.3 Hello 的预处理结果解析
预处理之后生成了.i
文件,执行了这些操作:将引用的头文件的内容复制进来、进行宏替换,定义和替换了由#define
指令定义的符号、删掉注释的内容、条件编译。
2.4 本章小结
预处理是一个程序能够执行的前提,它让我们写的简单的程序更加完整,正价严谨,增加了必要的内容,也删去了不需要的内容,为下一步操作做好了铺垫。
第3章 编译
3.1 编译的概念与作用
编译器(ccl
)将文本文件hello.i
翻译成文本文件hello.s
,它包含一个汇编语言程序,是一个将高级语言翻译成低级语言的过程。
3.2 在Ubuntu下的编译命令
命令:
gcc -S hello.c -o hello.s

图3-1 编译命令

图3-2 hello.s节选
3.3 hello的编译结果解析
数据解析:
int sleepsecs: 在C程序中声明为
int
类型,但赋值为2.5
,小数点后内容被舍去,所以实际编译后为:_sleepsecs: .long 2 ## 0x2
字符串"Usage: Hello 1173710229 xxx!\n":
L_.str: ## @.str .asciz "Usage: Hello 1173710229 \xxx\xxx\xxx\xxx\xxx\n"
int i
: 程序为它保留了空间,是-20(%rbp)
,在为其赋值时能够体现:LBB0_2: movl $0, -20(%rbp)
int argc
: 被储存到-8(%rbp)
中
运算符和操作解析:
i++
: 程序先将变量i
(即-20(%rbp)
)放入寄存器%eax
,然后执行addl $1, %eax
:## %bb.5: ## in Loop: Header=BB0_3 Depth=1 movl -20(%rbp), %eax
addl $1, %eax ## i++操作
movl %eax, -20(%rbp)
jmp LBB0_3i=0
:LBB0_2: movl $0, -20(%rbp) ## i=0
if (argc!=3)
:!=
被翻译为cmpl xx, xx
和je
,是一个条件跳转,它修改了“不等于”为“等于则跳转”:## %bb.0: pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $48, %rsp
movl $0, -4(%rbp)
movl %edi, -8(%rbp)
movq %rsi, -16(%rbp)
cmpl $3, -8(%rbp) ## != 操作
je LBB0_2i<10: 此处的小于号被编译为
compl xxx, xxx
和jge
:LBB0_3: ## =>This Inner Loop Header: Depth=1 cmpl $10, -20(%rbp) ## i和10比较
jge LBB0_6for
语句: 循环语句靠比较和控制转移来实现,例如:LBB0_3: cmpl $10, -20(%rbp)
jge LBB0_6 ## 上面将10与i比较,此条指令执行跳转
3.4 本章小结
编译是很关键的一步,它能够将高级语言翻译成低级语言,编译器的出现使得用高级编写程序成为可能,除此以外,编译时编译器还会执行一系列的优化操作,让程序更加高效的执行。
第4章 汇编
4.1 汇编的概念与作用
汇编器(as
)将hello.s
翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件hello.o
中。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o

图4-1 汇编命令

图4-2 hello.o节选
4.3 可重定位目标elf格式
使用
readelf -S hello.o
可以得到如下内容:
[号] 名称 类型 地址 偏移量 大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040 0000000000000081 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000348 00000000000000c0 0000000000000018 I 10 1 8
[ 3] .data PROGBITS 0000000000000000 000000c4 0000000000000004 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 000000c8 0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 000000c8 0000000000000032 0000000000000000 A 0 0 8
[ 6] .comment PROGBITS 0000000000000000 000000fa 000000000000002b 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 00000125 0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 00000128 0000000000000038 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 00000408 0000000000000018 0000000000000018 I 10 8 8
[10] .symtab SYMTAB 0000000000000000 00000160 0000000000000198 0000000000000018 11 9 8
[11] .strtab STRTAB 0000000000000000 000002f8 000000000000004d 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 00000420 0000000000000061 0000000000000000 0 0 1
节 | 作用 |
---|---|
.text | 已编译程序的机器代码。 |
.rodata | 只读数据。 |
.data | 已初始化的全局和静态C变量。 |
.bss | 未初始化对的全局和静态C变量,以及所有被初始化为0的全局或静态变量。 |
.symtab | 符号表,存放在程序中定义和引用的函数和全局变量的信息。 |
.rel.text | text节中位置的列表。 |
.rel.data | 被模块引用或定义的所有全局变量的重定位信息。 |
.debug | 调试符号表。 |
.line | 原始C源程序中的行号和.text节中机器指令之间的映射。 |
.strtab | 字符串表,包括.symtab和.debug节中的符号表,以及节头部中的节名字。 |
4.4 hello.o的结果解析
hello.o
文件内容如图所示:

图4-3 hello.o节选
hello.o
的反汇编文件内容包含十六进制代码和汇编代码,mov
、sub
、cmpl
等与跳转、调用无关的操作与hello.s
相同,并且十六制代码和汇编代码存在唯一(或多种)映射,但是与地址相关的内容会存在区别,例如原来的je
LBB0_2
变为了je 2b <main+0x2b>
,对应十六进制代码74 16
。
总结起来,有如下不同:
.o
反汇编文件中的每条指令都对应了一个地址;.s
文件中的跳转指令和call指令后面跟随了偏移地址;.o
文件中的数都是十进制的,.s反汇编文件中都是十六进制的。
4.5 本章小结
汇编将低级语言翻译为机器语言并且将原文件打包成可重定位目标程序,为链接做好准备。
第5章 链接
5.1 链接的概念与作用
链接器将库函数或者其他引用与当前C程序合并到一起,然后保存为可执行目标文件。例如我们的hello程序就使用了printf()
函数,这是标准C库的函数,链接器将它们链接起来,这样hello程序就能够使用printf()
函数了。
5.2 在Ubuntu下链接的命令
ld /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/7/crtbeginT.o -L/usr/lib/gcc/x86_64-linux-gnu/7 hello.o -lc -lgcc -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/gcc/x86_64-linux-gnu/7/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -o hello
ld /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/7/crtbeginT.o -L/usr/lib/gcc/x86_64-linux-gnu/7 hello.o -lc -lgcc -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/gcc/x86_64-linux-gnu/7/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -o hello

图5-1 链接操作
5.3 可执行目标文件hello的格式
使用
readelf -S hello
可以得到如下内容:
There are 28 section headers, starting at offset 0x19c0:
节头:
[号] 名称 类型 地址 偏移量 大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400200 00000200 000000000000001e 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400220 00000220 0000000000000020 0000000000000000 A 0 0 4
[ 3] .hash HASH 0000000000400240 00000240 0000000000000034 0000000000000004 A 5 0 8
[ 4] .gnu.hash GNU_HASH 0000000000400278 00000278 000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 0000000000400298 00000298 00000000000000c0 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400358 00000358 0000000000000057 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 00000000004003b0 000003b0 0000000000000010 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 00000000004003c0 000003c0 0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 00000000004003e0 000003e0 0000000000000030 0000000000000018 A 5 0 8
[10] .rela.plt RELA 0000000000400410 00000410 0000000000000078 0000000000000018 AI 5 21 8
[11] .init PROGBITS 0000000000400488 00000488 0000000000000017 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 00000000004004a0 000004a0 0000000000000060 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000400500 00000500 0000000000000212 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 0000000000400714 00000714 0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 0000000000400720 00000720 000000000000003a 0000000000000000 A 0 0 8
[16] .eh_frame PROGBITS 0000000000400760 00000760 0000000000000100 0000000000000000 A 0 0 8
[17] .init_array INIT_ARRAY 0000000000600e00 00000e00 0000000000000008 0000000000000008 WA 0 0 8
[18] .fini_array FINI_ARRAY 0000000000600e08 00000e08 0000000000000008 0000000000000008 WA 0 0 8
[19] .dynamic DYNAMIC 0000000000600e10 00000e10 00000000000001e0 0000000000000010 WA 6 0 8
[20] .got PROGBITS 0000000000600ff0 00000ff0 0000000000000010 0000000000000008 WA 0 0 8
[21] .got.plt PROGBITS 0000000000601000 00001000 0000000000000040 0000000000000008 WA 0 0 8
[22] .data PROGBITS 0000000000601040 00001040 0000000000000014 0000000000000000 WA 0 0 8
[23] .bss NOBITS 0000000000601060 00001054 0000000000000050 0000000000000000 WA 0 0 32
[24] .comment PROGBITS 0000000000000000 00001054 000000000000002a 0000000000000001 MS 0 0 1
[25] .symtab SYMTAB 0000000000000000 00001080 0000000000000630 0000000000000018 26 43 8
[26] .strtab STRTAB 0000000000000000 000016b0 000000000000022d 0000000000000000 0 0 1
[27] .shstrtab STRTAB 0000000000000000 000018dd 00000000000000e2 0000000000000000 0 0 1
5.4 hello的虚拟地址空间

图5-2 hello虚拟地址空间展示
5.5 链接的重定位过程分析
hello.o
与hello
对比和分析:
图5-3 hello.o反汇编内容

图5-4 hello反汇编内容
可见有如下不同:- hello中指令的地址已经不再是
hello.o
中的偏移量了,已经变成了虚拟地址; jmp
、call
指令后面的地址也变成了虚拟地址;- hello中多了许多外部函数,例如
puts@plt
,printf@plt
,getchar@plt
,链接之后,hello才是一个完整的程序。
- hello中指令的地址已经不再是
重定位:
重定位包含两个操作,首先重定位节和符号定义,然后重定位节中的符号引用。

图5-5 重定位展示
5.6 hello的执行流程
1. _init
2. __libc_csu_init ()
3. _dl_elf_hash ()
4. __vdso_platform_setup ()
5. _dl_vdso_vsym ()
6. _dl_lookup_symbol_x ()
7. dl_new_hash ()
8. do_lookup_x ()
9. check_match ()
10. strcmp ()
11. dl_symbol_visibility_binds_local_p ()
12. _dl_vdso_vsym ()
13. __vdso_platform_setup ()
14. __init_misc ()
15. __strrchr_avx2 ()
16. __GI___ctype_init ()
17. call_init ()
18. init_cacheinfo ()
19. handle_intel ()
20. intel_check_word ()
21. __GI_bsearch ()
22. intel_02_known_compare ()
23. handle_intel ()
24. init_cacheinfo ()
25. _dl_init ()
26. __libc_start_main ()
27. __GI___cxa_atexit ()
28. __new_exitfn ()
29. _init()
30. _setjmp ()
31. __sigsetjmp__sigjmp_save ()
32. _IO_puts ()
33. *ABS*+0x9dc70@plt ()
34. __strlen_avx2 ()
35. IO_validate_vtable ()
36. _IO_new_file_xsputn ()
37. _IO_new_file_overflow ()
38. __GI__IO_doallocbuf ()
39. __GI__IO_file_doallocate ()
40. __GI___fxstat ()
41. main()
42. puts@plt ()
43. exit@plt ()
5.8 本章小结
链接在编译时由静态编译器完成,也可以在加载和运行时由动态连接起来完成。链接的主要任务是符号解析和重定位,符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义,而重定位确定每个符号的最终内存地址,并修改对那些目标的引用。链接的机制使得程序的规模能够越来越大,同时还能方便调试、解决问题。
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中程序的实例,系统中的每个程序都运行在某个进程的上下文中。
进程总是处于下面三种状态之一:
•运行。进程要么在CPU上执行,要么在等待被执行且最终会被内核调度。
•停止。进程的执行被挂起,且不会被调度。当收到SIGSTOP
、SIGTSTP
、SIGTTOU
信号时,进程就会停止,并且保持停止直到收到SIGCONT
信号。
•终止。进程永远地停止了。
6.2 简述壳Shell-bash的作用与处理流程
- 作用:
shell
是一个交互型的应用级程序,它代表用户运行其他程序。 - 处理流程:
shell
执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行,求值步骤解析命令行,并代表用户运行程序。
6.3 Hello的fork进程创建过程
①
--->printf--->exit
| |
shell--->fork------------------->
②
--->printf--->sleep--->printf--->
|
shell--->fork----------------------------------->
6.4 hello的execve过程
--->execve--->Hello--->exit
| |
shell--->fork--------------------waitpid--->exit
6.5 hello对的进程执行
Linux系统中的每个程序都运行在一个进程上下文中,有自己的虚拟地址空间。当shell运行一个程序时,会fork出一个子进程,子进程通过execve调用加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用应用程序的main
函数。
其中用户模式和内核模式之间的转换成为上下文切换。
6.6 hello的异常与信号处理
- hello在运行中会遇到的信号:
SIGTSTP
:挂起SIGINT
:终止SIGCONT
:继续运行被挂起的进程 - 挂起hello
图6-1 挂起hello ps
图6-2 ps命令jobs
图6-3 jobs命令pstree
图6-4 pstree命令- 发送
SIGCONT
图6-5 发送SIGCONT - 杀死进程
图6-6 杀死进程
6.7本章小结
异常控制流发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制。
在硬件层,异常是由处理器中的时间出发的控制流中的突变。控制流传递给一个软件处理程序,该处理程序进行一些处理,然后返回控制给被中断的控制流。
在操作系统层,内核用异常控制流提供进程的基本概念。进程提供给应用两个抽象:1)逻辑控制流,它提供给每个程序一个假象,好像它是在独自占用处理器,2)私有地址空间,它给每个应用一个假象,好像它在独自占用主存。
第7章 hello的存储管理
7.1 hello的存储器地址空间
首先明白地址空间的概念,地址空间是一个非负整数地址的有序集合。
逻辑地址:一个逻辑地址由两部份组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。
线性地址:是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。
虚拟地址:在一个带虚拟内存的系统中,CPU从有一个N=2^n
个虚拟地址的空间生成虚拟地址,虚拟地址的概念和逻辑地址没有明显区别,都在一个虚拟环境下的地址。例如,在已经链接好的可执行目标文件hello中,指令已经全部被赋予了虚拟地址,所有控制转移语句和常量的引用都依靠虚拟地址。
物理地址:计算机系统的主存被组织成一个由M个连续字节大小的单元组成的数组。每个字节都有唯一的物理地址。物理地址用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相相应。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址必须加上隐含的DS 数据段的基地址,才能构成线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址转物理地址通过分页机制,在保护模式下,控制寄存器©0的最高位PG位控制着分页管理机制是否生效,如果PG=1,分页机制生效,需通过页表查找才能把线性地址转换物理地址。如果PG=0,则分页机制无效,线性地址就直接做为物理地址。
32位的线性地址被分成3个部分:
最高10位Directory
页目录表偏移量,中间10位 Table是页表偏移量,最低12位Offset
是物理页内的字节偏移量。
页目录表的大小为4k(刚好是一个页的大小),包含1024项,每个项4字节(32位),项目里存储的内容就是页表的物理地址。如果页目录表中的页表尚未分配,则物理地址填0。
页表的大小也是4k,同样包含1024项,每个项4字节,内容为最终物理页的物理内存起始地址。
以我们程序中遇到的callq 4004c0 <printf@plt>
为例,内核先将0x4004c0
转换为二进制,即0000
0000 0000 0100 0000 0000 1100 0000
,最高10位是0,查看页表目录第0项,里面存放的是页表的物理地址。线性地址中间十位是00 0100
0000
是十进制64,查看页表的第64项,那里存放的是最终物理页的物理起始地址。物理页基地址加上线性地址中最低12位的偏移量,CPU就找到了线性地址最终对应的物理内存单元。
7.4 TLB与四级页表支持下的VA到PA的变换
下图给出了Core i7 页表翻译过程:

图7-1 Core i7 页表翻译
7.5 三级Cache支持下的物理内存访问

图7-2 Core i7 Cache支持下的物理内存访问

图7-3 Core i7 处理器封装
7.6 hello进程fork时的内存映射
当fork
函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID
。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct
、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中每个区域结构都标记为私有时写复制。
当fork
在新进程中返回时,新进程在现在的虚拟内存刚好和调用fork
时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
执行execve时需要以下几个步骤:
- 删除已存在的用户区域。
- 映射私有区域。
- 映射共享区域。
- 设置程序计数器。
下图展示了加载器是如何映射用户地址空间的区域的:

图7-4 加载器映射用户地址空间区域
7.8 缺页故障与缺页中断处理
DRAM缓存不命中时称为缺页,当缺页发生后,会触发一个缺页异常,需要调用缺页处理程序,该程序会选择一个牺牲页(如果所选的牺牲页已经被修改了,内核会先将其复制回磁盘),接下来,内核会从磁盘复制请求的页到物理内存,然后更新PTE随后返回。返回后,会重新启动刚才导致出现缺页异常的指令,但这时请求的页已经缓存在主存中了。
7.9动态存储分配管理
- 概念
动态内存分配器维护着一个进程的虚拟内存区域,称为堆,它进阶在未初始化的数据区域后开始,内核维护着一个变量brk指向堆的顶部。分配器将堆视为一组不同大小的块的集合来维护,每个块就是一个连续的虚拟内存片,要么已分配,要么空闲。 - 分配器,分配器有两种基本风格:
- 隐式分配器,要求显式地分配,但是释放可以交给垃圾收集器自动完成。
- 显式分配器,要求应用显式地分配和释放内存,C语言就是使用这样的分配器,而
printf()
函数调用的malloc()
函数就是分配过程。
- 实现问题,动态存储分配需要考虑如下问题:
- 空闲块组织。
- 放置策略。
- 分割空闲块。
- 合并空闲块。
通常可以采用组织方式有:隐式空闲链表、带边界标记的隐式空闲链表、显式空闲链表、分离的空闲链表。隐式空闲链表使用头部来标记这个块的大小和是否空闲,由它的大小可以计算出下一个块的位置;带边界标记的隐式空闲链表是对隐式空闲链表合并效率低的一个改进方式,为每个块加入脚部,这样就可以从中间开始查找上一个块是否空闲了;显式空闲链表的块中有前驱和后继块的指针,可以直接找到前一个块和后一个块,使得首次适配和释放的速度更加快;分离的空闲链表是维护多个空闲链表,其中每个链表的块有大致相等的大小。
当请求块时,分配器搜索空闲链表,找到一个足够大的可以放置所请求的空闲块,执行这种搜索的方式是由放置策略决定的,常见的策略是首次适配、下一次适配、最佳适配。首次适配从头开始搜索空闲链表,选择第一个合适的空闲块,下一次适配从刚才搜索结束的地方开始继续搜索;最佳适配会检查每个块,选择合适所需请求大小的最小空闲块。
在释放内存时会执行空闲块合并,来节省内存空间,当所有块都被搜索过并且块已经最大程度合并了,仍然没有找到足够大小的块来放置时,分配器会调用sbrk函数,向内核请求额外的堆内存。
7.10本章小结
虚拟内存是对主存的一个抽象,支持虚拟内存的处理器通过虚拟寻址间接引用主存。虚拟内存提供三个重要的功能,第一,它在主存中自动缓存最近使用的存放在磁盘上的虚拟地址空间的内容;第二,它简化了内存管理,进而简化了链接、在进程之间共享数据、进程的内存分配以及程序的加载。最后虚拟内存通过在每条页表条目中加入保护位,从而简化了内存保护。
现代系统通过将虚拟内存片和磁盘上的文件片关联起来,来初始化虚拟内存片,这个过程即内存映射,它为共享数据、创建新的进程、加载程序提供了高效的机制。
第8章 hello的IO管理
8.1 Linux的IO
设备管理方法
设备的模型化:文件
设备管理:Unix IO
接口
一个Linux文件就是一个m
个字节的序列,所有的I/O
设备都被模型化为文件,而所有的输入和输出都被大动作对相应文件的读和写来执行,Linux内核引出的接口为Unix
I/O
,使得所有的输入和输出都能以一种统一的方式来执行:
- 打开文件。
- Linux
shell
创建的每个进程开始都有三个打开的文件:标准输入、标准输出、标准错误 - 改变当前文件的位置,对于每个打开的文件,内核保持着一个文件位置
k
,初始为0
,这个文件位置是从文件开头起始的字节偏移量。应用程序能通过执行seek操作显式地设置文件当前的位置为k
。 - 读写文件。
- 关闭文件,当应用完成了对文件的访问后就通知内核关闭这个文件。
8.2 简述Unix IO
接口及其函数
int open(char *filename, int flags. mode_t mode)
打开一个文件或者创建一个新文件。int close(int 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
的实现分析
首先看Linux下的printf
的实现:
static int printf(const char *fmt, ...) {
va_list args;
int i;
va_start(args, fmt);
write(1,printbuf,i=vsprintf(printbuf, fmt, args));
va_end(args);
return i;
}
这里fmt
指向字符串的第一个字符,其中,调用write
函数时,第一个参数1
表示输出设备,由上面的内容可知,Linux下所有设备都是以文件形式看待的。里面的vsprintf
实现如下:
int vsprintf(char *buf, const char *fmt, va_list args) {
char *p;
char tmp[256];
va_list p_next_arg = args;
for (p = buf; *fmt; fmt++) {
if (*fmt != '%') {
*p++ = *fmt;
continue;
}
fmt++;
switch (*fmt) {
case 'x':
itoa(tmp, *((int *) p_next_arg));
strcpy(p, tmp);
p_next_arg += 4;
p +=strlen(tmp);
break;
case 's':
break;
default:
break;
}
}
return (p - buf);
}
下面是write
函数的实现:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
最后一条语句表示要通过系统来调用sys_call
这个函数。sys_call
的内容较复杂,但是它实现了一个功能:显示格式化了的字符串。
字符显示驱动子程序:从ASCII
到字模库到显示vram
(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram
,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar
的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ASCII
码,保存到系统的键盘缓冲区。
getchar
等调用read
系统函数,通过系统调用读取按键ASCII
码,直到接受到回车键才返回。
8.5本章小结
Linux提供了少量基于Unix I/O
模型的系统级函数,它们允许应用程序打开、关闭、读和写文件,提取文件的元数据,以及执行I/O
重定向,为程序设计带来更大一步发展空间。
结论
hello经历了些什么?
- 被写成hello.c
- 被预处理后变成hello.i
- 被编译器编译为hello.s
- 被汇编成了hello.o
- 由链接器链接成为可执行目标文件hello
- shell为hello程序fork,然后execve
- 内核通过上下文切换,使hello程序运行起来像独占CPU,通过虚拟内存,让hello程序看起来像独占主存
- MMU将虚拟地址翻译为物理地址完成访存
- hello调用printf函数,printf调用malloc,执行动态内存分配
- shell回收hello,内核删除hello的所有痕迹,hello运行完毕
附件
hello.c——C语言源文件
hello.i——修改了的源程序
hello.s——汇编程序
hello.o——可重定位目标程序
hello——可执行目标程序
参考文献
[1]兰德尔.深入理解计算机系统[M]. 北京:机械工业出版社, 2016.
[2]百度百科.逻辑地址[EB/OL]. https://baike.baidu.com/item/%E9%80%BB%E8%BE%91%E5%9C%B0%E5%9D%80/3283849?fr=aladdin#3.2018-2018.
[3]phlsheji.[EB/OL]. https://www.cnblogs.com/bhlsheji/p/4868964.html.2015-2018.
[4]佚名.[EB/OL]. https://zhidao.baidu.com/question/1835973961391339380.html.2017-2018.
[5]斐然成章.[EB/OL]. https://blog.csdn.net/l_nan/article/details/51188290.2016-2018.
评论区