剖析 hello 程序的执行过程
前面我们简单的介绍了一下计算机的硬件的组成和操作,现在我们正式介绍运行示例程序时发生了什么,我们会从宏观的角度进行描述,不会涉及到所有的技术细节
刚开始时,shell 程序执行它的指令,等待用户键入一个命令。当我们在键盘上输入了 ./hello
这几个字符时,shell 程序将字符逐一读入寄存器,再把它放到内存中,如下图所示
当我们在键盘上敲击回车键
的时候,shell 程序就知道我们已经结束了命令的输入。然后 shell 执行一系列指令来加载可执行的 hello 文件,这些指令将目标文件中的代码和数据从磁盘复制到主存。
利用 DMA(Direct Memory Access)
技术可以直接将磁盘中的数据复制到内存中,如下
一旦目标文件中 hello 中的代码和数据被加载到主存,处理器就开始执行 hello 程序的 main 程序中的机器语言指令。这些指令将 hello,world\n
字符串中的字节从主存复制到寄存器文件,再从寄存器中复制到显示设备,最终显示在屏幕上。如下所示
高速缓存是关键
上面我们介绍完了一个 hello 程序的执行过程,系统花费了大量时间把信息从一个地方搬运到另外一个地方。hello 程序的机器指令最初存储在磁盘
上。当程序加载后,它们会拷贝
到主存中。当 CPU 开始运行时,指令又从内存复制到 CPU 中。同样的,字符串数据 hello,world \n
最初也是在磁盘上,它被复制到内存中,然后再到显示器设备输出。从程序员的角度来看,这种复制大部分是开销,这减慢了程序的工作效率。因此,对于系统设计来说,最主要的一个工作是让程序运行的越来越快。
由于物理定律,较大的存储设备要比较小的存储设备慢。而由于寄存器和内存的处理效率在越来越大,所以针对这种差异,系统设计者采用了更小更快的存储设备,称为高速缓存存储器(cache memory, 简称为 cache 高速缓存)
,作为暂时的集结区域,存放近期可能会需要的信息。如下图所示
图中我们标出了高速缓存的位置,位于高速缓存中的 L1
高速缓存容量可以达到数万字节,访问速度几乎和访问寄存器文件一样快。容量更大的 L2
高速缓存通过一条特殊的总线链接 CPU,虽然 L2 缓存比 L1 缓存慢 5 倍,但是仍比内存要哦快 5 - 10 倍。L1 和 L2 是使用一种静态随机访问存储器(SRAM)
的硬件技术实现的。最新的、处理器更强大的系统甚至有三级缓存:L1、L2 和 L3。系统可以获得一个很大的存储器,同时访问速度也更快,原因是利用了高速缓存的 局部性
原理。
Again:入门程序细节
现在,我们来探讨一下入门级
程序的细节,由浅入深的来了解一下 C 语言的特性。
#include<stdio.h>
我们上面说到,#include<stdio.h>
是程序编译之前要处理的内容,称为编译预处理
命令。
预处理命令是在编译之前进行处理。预处理程序一般以 #
号开头。
所有的 C 编译器软件包都提供 stdio.h
文件。该文件包含了给编译器使用的输入和输出函数,比如 println() 信息。该文件名的含义是标准输入/输出 头文件。通常,在 C 程序顶部的信息集合被称为 头文件(header)
。
C 的第一个标准是由 ANSI 发布的。虽然这份文档后来被国际标准化组织(ISO)采纳并且 ISO 发布的修订版也被 ANSI 采纳了,但名称 ANSI C(而不是 ISO C) 仍被广泛使用。一些软件开发者使用ISO C,还有一些使用 Standard C。
C 标准库
除了 <sdtio.h> 外,C 标准库还包括下面这些头文件
<assert.h>
提供了一个名为 assert
的关键字,它用于验证程序作出的假设,并在假设为假输出诊断消息。
<ctype.h>
C 标准库的 ctype.h 头文件提供了一些函数,可以用于测试和映射字符。
这些字符接受 int 作为参数,它的值必须是 EOF
或者是一个无符号字符
EOF是一个计算机术语,为 End Of File 的缩写,在操作系统中表示资料源无更多的资料可读取。资料源通常称为档案或串流。通常在文本的最后存在此字符表示资料结束。
<errno.h>
C 标准库的 errno.h 头文件定义了整数变量 errno,它是通过系统调用设置的,这些库函数表明了什么发生了错误。
<float.h>
C 标准库的 float.h 头文件包含了一组与浮点值相关的依赖于平台的常量。
<limits.h>
limits.h 头文件决定了各种变量类型的各种属性。定义在该头文件中的宏限制了各种变量类型(比如 char、int 和 long)的值。
<locale.h>
locale.h 头文件定义了特定地域的设置,比如日期格式和货币符号
<math.h>
math.h 头文件定义了各种数学函数和一个宏。在这个库中所有可用的功能都带有一个 double 类型的参数,且都返回 double 类型的结果。
<setjmp.h>
setjmp.h 头文件定义了宏 setjmp()、函数 longjmp() 和变量类型 jmp_buf,该变量类型会绕过正常的函数调用和返回规则。
<signal.h>
signal.h 头文件定义了一个变量类型 sig_atomic_t、两个函数调用和一些宏来处理程序执行期间报告的不同信号。
<stdarg.h>
stdarg.h 头文件定义了一个变量类型 va_list 和三个宏,这三个宏可用于在参数个数未知(即参数个数可变)时获取函数中的参数。
<stddef.h>
stddef .h 头文件定义了各种变量类型和宏。这些定义中的大部分也出现在其它头文件中。
<stdlib.h>
stdlib .h 头文件定义了四个变量类型、一些宏和各种通用工具函数。
<string.h>
string .h 头文件定义了一个变量类型、一个宏和各种操作字符数组的函数。
<time.h>
time.h 头文件定义了四个变量类型、两个宏和各种操作日期和时间的函数。
main() 函数
main 函数听起来像是调皮捣蛋的孩子故意给方法名起一个 主要的
方法,来告诉他人他才是这个世界的中心。但事实却不是这样,而 main()
方法确实是世界的中心。
C 语言程序一定从 main() 函数开始执行,除了 main() 函数外,你可以随意命名其他函数。通常,main 后面的 ()
中表示一些传入信息,我们上面的那个例子中没有传递信息,因为圆括号中的输入是 void 。
除了上面那种写法外,还有两种 main 方法的表示方式,一种是 void main(){}
,一种是 int main(int argc, char* argv[]) {}
- void main() 声明了一个带有不确定参数的构造方法
- int main(int argc, char* argv[]) {} 其中的 argc 是一个非负值,表示从运行程序的环境传递到程序的参数数量。它是指向 argc + 1 指针数组的第一个元素的指针,其中最后一个为null,而前一个(如果有的话)指向表示从主机环境传递给程序的参数的字符串。如果argv [0]不是空指针(或者等效地,如果argc> 0),则指向表示程序名称的字符串,如果在主机环境中无法使用程序名称,则该字符串为空。
注释
在程序中,使用 /**/ 的表示注释,注释对于程序来说没有什么实际用处,但是对程序员来说却非常有用,它能够帮助我们理解程序,也能够让他人看懂你写的程序,我们在开发工作中,都非常反感不写注释的人,由此可见注释非常重要。
C 语言注释的好处是,它可以放在任意地方,甚至代码在同一行也没关系。较长的注释可以多行表示,我们使用 /**/ 表示多行注释,而 // 只表示的是单行注释。下面是几种注释的表示形式
// 这是一个单行注释 /* 多行注释用一行表示 */ /* 多行注释用多行表示 多行注释用多行表示 多行注释用多行表示 多行注释用多行表示 */
函数体
在头文件、main 方法后面的就是函数体(注释一般不算),函数体就是函数的执行体,是你编写大量代码的地方。
变量声明
在我们入门级的代码中,我们声明了一个名为 number
的变量,它的类型是 int,这行代码叫做 声明
,声明是 C 语言最重要的特性之一。这个声明完成了两件事情:定义了一个名为 number 的变量,定义 number 的具体类型。
int 是 C 语言的一个 关键字(keyword)
,表示一种基本的 C 语言数据类型。关键字是用于语言定义的。不能使用关键字作为变量进行定义。
示例中的 number
是一个 标识符(identifier)
,也就是一个变量、函数或者其他实体的名称。
###变量赋值
在入门例子程序中,我们声明了一个 number 变量,并为其赋值为 11,赋值是 C 语言的基本操作之一。这行代码的意思就是把值 1 赋给变量 number。在执行 int number 时,编译器会在计算机内存中为变量 number 预留空间,然后在执行这行赋值表达式语句时,把值存储在之前预留的位置。可以给 number 赋不同的值,这就是 number 之所以被称为 变量(variable)
的原因。
printf 函数
在入门例子程序中,有三行 printf(),这是 C 语言的标准函数。圆括号中的内容是从 main 函数传递给 printf 函数的。参数分为两种:实际参数(actual argument)
和 形式参数(formal parameters)
。我们上面提到的 printf 函数括号中的内容,都是实参。