计算机系统漫游:执行hello.c,计算机都干了些什么?

CSAPP这本书,放在我的书架上已经一年多了,在过去的时间里几乎没有花大块、很长的时间来看过它,始终没有坚持看完。但是作为一个学计算机的,操作系统垃圾就真的很说不过去了。所以还是要重新系统的学习这本书,当然,我知道,学习这本书仅仅只是一个开始。嗯,还有一个点我想我需要说明一下,关于这本书的博客我会根据自己的理解尝试以「教授者」的角度去描述,它可能会和真理会有偏差,甚至会因为我的粗心或理解不清导致说法错误。如果你认为我说的不对,非常真诚的恳请您能够发个issues告知我,我会及时更正过来。再一次真诚的谢谢您。我也不知道会不会有人看到这篇文章,但是我想做个说明总是好一点,避免不必要的误会,到时候落得个「不懂装懂」就很不好了。最后,开心学习,开心成长~ 加油加油~~

这是CSAPP这本书的第一章,主要讲述了,当我们运行一个hello.c文件时,从输入到显示结果,计算机都干了些什么?

开篇

在学习这本书之前呢,我们先要弄清楚三个问题:

  1. 为什么学习这本书?
  2. 学习这本书,我们能获得什么?
  3. 这本书是以什么样的路线来讲解「操作系统」这门课程的?

引用书中的话,这本书适用于一些希望深入了解软硬组件是如何工作的以及这些组件是如何影响程序的正确性和性能的,以此来提高自身的技能。这本书就是想我们了解,当在系统上执行hello程序时,系统到底发生了什么以及为什么会这样。通过限制hello程序的生命周期来开始对系统的学习。

hello.c 的一生

下面我们正式开始整章的学习。

1.写出一个hello.c程序

先看一个我们每个人的入门程序 (偷笑,当初打出这段代码小黑框蹦出来的时候真的是超惊喜欸!):

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("hello, world\n");
return 0;
}

现在我们编辑完了c程序,那当我们点下”run”按钮到最后控制台跳出输出和结果这个过程,计算机会发生什么呢?

2.程序的翻译(启动)

为了在系统上运行c程序,每条c语句都必须被其它程序转化为一些列的低级机器语言指令。然后这些指令按照一种称为可执行目标程序(可执行目标文件)的格式打好包,并以二进制磁盘文件的形式存放起来。

在Unix系统上,从源文件到目标文件的转化是由 编译器驱动程序完成的:
linux> gcc -o hello hello.c
在这里,gcc编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行目标文件hello。这个翻译过程可分为四个阶段完成,如下图。执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)一起构成了 编译系统

complier

简单说下各个阶段都干了些什么:

  • 预处理阶段:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。直接将头文件中的内容插入到程序文本中。
  • 编译阶段:编译器(ccl)将文本文件.i编译成文本文件.s的汇编文件,它包含一个汇编语言程序。这个程序包含main的定义:

    1
    2
    3
    4
    5
    6
    7
    main:
    subq $8, %rsp
    movl $.LCO, %edi
    call puts
    movl $0, %eax
    addq $8, %rsp
    ret
  • 汇编阶段:汇编器(as)将.s编译成机器语言指令,并将结果保存在目标文件.o(二进制文件)中。

  • 链接阶段:链接器(ld)负责将各种目标文件合并到hello 文件中。它是一个可执行文件,可以被加载到内存中,由系统执行。

所以呢,经过这个阶段,我们的hello.c程序已经被翻译成一个可执行的二进制文件存储在磁盘里了。接下来就是,处理器是怎么读这个文件的呢?

3.处理器读并解释存储在内存中的指令

Unix系统上运行可执行文件,我们可以在shell里这样写:

1
2
3
linux> ./hello
hello, world
linux>

我们看到,打印出内容了。有必要解释一下,shell是一个命令行解释器。它输出一个提示符,等待一个命令行,然后执行这个命令。如果这个命令行的第一个单词不是内置的shell命令,那么shell就会假设这是一个可执行文件的名字,将加载运行这个文件。

好啦,总结一下这个过程,编辑 -> 编译 -> 读取文件加载到内存运行 -> 输出到控制台。是不是觉得很简单?哈哈,我们讲的太泛啦。接下来的几个章节里,我们将会更加细粒度的看清楚这些阶段背后的本质。通过更多了解计算机的底层,让我们以后遇到莫名奇妙的链接错误时不会手足无措,需要提高程序性能的时候也不会无从下手。

讲完了这么多,现在我们需要了解一些其它的基础知识。

你必须知道的一些基础知识

1.信息就是上下文

了解位、字节的概念。计算机的所有数据都是以一串比特(位)表示的,因此学习这个是有必要的。原文源程序实际上就是一个由值0和1组成的位序列,8个位被组织成一组,称为字节。每个字节表示程序中的某些文本字符。于是有:(每个字节表示某些文本字符)1字节 = 8个位

在计算机中,所有的数据都是以比特的形式表示,那么我们怎么区分不同数据呢?唯一的方法就是读到这些数据对象的上下文。

2.我们为什么要了解编译系统是怎么工作的呢?

  • 优化程序性能
  • 理解链接时的错误
  • 避免安全漏洞
  • 等等

3. 系统硬件

  • 总线:总线是贯穿整个系统的一组电子管道。它携带着信息字节,负责各个部件间的传递。通常总线被设计成传送定长的字节块,也就是字。字中的字节数(字长)就是一个基本的系统参数。我们现在的大多数机器要么是4个字节(32位),要么就是8个字节(64位)。
  • I/O设备:I/O设备是系统与外界的联系通道(显示器(输出设备)、键盘(输入设备)啥的都是)。每个I/O设备都通过一个控制器或适配器与I/O总线相连。控制器和适配器之间的在于它们的封装方式。控制器是I/O设备本身或者系统的主印制电路板(主板)上的芯片组。而适配器则是一块插在主板插槽上的卡。但无论怎么样,它们的功能都是在I/O总线和I/O设备之间传递信息。
  • 主存:一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。从物理上来说,主存是一组动态随机存取存储器(DRAM)芯片组成。从逻辑上来说,存储器是一个线性的字节数组,每个字节都有唯一的索引。
  • 处理器:中央处理单元,也叫处理器,是解释存储在主存中指令的引擎。处理器的核心是一个大小为一个字的存储设备(或寄存器),称为程序计数器(PC)。在任何时候,PC都指向主存中的某条机器语言指令。

4. 缓存

从上面“运行程序”中,我们知道,计算机工作时,处理器从主存中取出数据放入寄存器中进行计算,但是,处理器从寄存器文件中读取数据比从主存中读取几乎要快100倍。并且,随着这些年半导体技术的进步,这样的速度差还在持续加大。针对处理器与主存之间的差异,系统设计者采用了更小更快的存储设备,称为高速缓存存储器。通过让高速缓存存放可能经常访问的数据,大部分的内存操作都能在快速的高速缓存中完成。

意识到高速缓存存储器存在的应用程序员能够利用告诉缓存将程序的性能提高一个数量级。

5. 存储设备的层次结构

看张图就大概明白了:

memory-1

6. 操作系统

首先,操作系统是一个系统软件。它有两个基本功能:1.防止硬件被失控的应用程序滥用。2.向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备。它通过几个基本的抽象概念(进程、虚拟内存和文件)来实现这两个功能。看图:

cs-1

7. 进程

进程是操作系统对一个正在运行的程序的一种抽象。当我们运行hello程序时,操作系统会提供一种假象,就好像系统上只有这一个程序在运行。我们现在来讨论一下一个CPU单核处理器的情况。当我们在shell中输入命令到运行hello程序,这实际上有两个程序,那么操作系统是怎么做的呢?

thread-1

我们看到,从一个进程到另一个进程时有操作系统内核管理的。它负责对程序上下文的切换。

操作系统内核是操作系统代码常驻主存的部分。当应用程序需要操作系统的某些操作时,比如读写文件,它就执行一条特殊的系统调用指令,将控制权传递给内核,然后内核执行被请求的操作并返回应用程序。不过需要注意的是,内核它不是一个独立的进程。相反,他是系统管理全部进程所用代码和数据结构的集合。

8. 线程

尽管通常我们认为一个进程只有单一的控制流,但是在现代系统中,一个劲曾实际上可以由多个称为线程的执行单元完成。每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。因为多线程之间比多进程之间更容易共享数据,也因为线程一般来说都比进程跟高效,所以以线程作为编程模型对于网络服务器应用变得越来越重要。

9. 虚拟内存

从名字看就知道,它是一个抽象出来的概念。它为每一个进程提供了一个假象,即每个线程都独占的使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间。下面这张图是Linux进程的虚拟地址空间。

vm-1

我们来从下到上大致的解释一下吧:

  • 程序代码和数据:对所有的进程来说,代码是从同一固定地址开始,紧接着的是和C全局变量相对应的数据位置。代码和数据区是直接按照可执行目标文件的内容初始化的。第7章研究链接和加载时,会学习到更多有关地址空间的内容。
  • 堆:运行时堆。代码和数据区在进程一开始运行时就被指定了大小,与此不同,当调用向malloc和free这样的c标准库函数时,堆可以在运行时动态的扩展和收缩。在第9章学习管理虚拟内存时,我们将更详细的研究堆。
  • 共享库:大约在地址空间的中间部分是一块用来存放像C标准库和数学库这样的共享库的代码和数据的区域。共享库如何工作的呢?第7章介绍动态链接时会介绍到。
  • 栈:位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用。和堆一样,用户栈在程序执行期间也可以动态扩展和收缩。第3章将学习编译器是如何使用栈的。
  • 内核虚拟内存:地址空间顶部的区域是为内核保留的。不允许应用程序读写这个区域的内容或直接调用内核代码定义的函数。相反,它们必须调用内核来执行这些操作。

10. 文件

文件就是字节序列。每个I/O设备,包括磁盘、键盘、显示器,甚至网络都可以看成是文件。不要小瞧文件这个概念,它向应用程序提供了一个统一的视图,来看待可能含有的所有各式各样的I/O设备。

11. 系统之间利用网络通信

正如我们知道的那样,系统与系统之间通常是通过网络连接到一起的。当然,如果我们从一个单独的系统来看,网络也可被视为一个I/O设备。

12. amdahl定律

定律主要思想是:当我们对系统的某个部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度。若系统执行某应用程序需要时间为T1,假设系统某部分所需要执行时间与该时间的比例为α,而该部分性能提升比例为k。则总的执行时长为:
T = (1 - α)T1 + (αT1)/k = T1[(1 - α) + α/k],
所以,计算加速比s = T/T1 为:
S = 1 / ((1 - α) + α/k)

13. 并发和并行

  1. 线程级并发
  2. 指令集并行

14. 计算机系统中抽象的重要性(这是一个很重要的思维方式)

抽象的使用是计算机科学中最为重要的概念之一。计算机系统中的一个重大主题就是,提供不同层次的抽象表示,来隐藏实际发现的复杂性。例如,为一组函数规定一个简单的应用程序接口就是一个很好的变成习惯,程序员无须关注内部实现就可以使用代码。

有问题?发送 issues 给我~

0%