CSAPP-1:计算机系统漫游
本周开始了CSAPP的读书计划,规划已久,终于要开动了。之前自己也零散看过,不得不说这本书有难度都没坚持下去,这次跟着码农翻身小伙伴们一起打卡这本书。
CSAPP这本书不用多说绝对经典中的经典,如果想成为一名知道计算机硬件和软件如何工作,了解其如何影响程序的正确性和性能的程序员,那么一定得看啊。
如果能完全理解本书讲解的计算机系统以及它对应用程序的影响,那么恭喜你,你走上了一条为数不多的大牛道路。
在开始之前,我们先看一个最常见的程序:
#include <stdio.h> int main() { printf("hello,world\n"); return 0; }
本文就从上面这个最简单的hello程序展开,沿着它的生命周期展开学习.
程序的保存格式
上面的hello程序其实就是一个由值 0 和 1 组成的位(即比特)序列,8个位成一组,称为字节。我们输入文本hello.c文件中的字符即用每个字节来表示(大部分计算机系统都是用ASCII标准来表示字符,即把字节转为整数值)。
总结: 信息=位+上下文
程序是如何运行的
hello程序的诞生使用C语言来编写的,好处是人可以读懂,但是为了在系统上运行,还是得转化为一系列低级的机器语言指令。
在Unix系统上,从源文件到目标文件的转化得靠编译器, 下面记录编译过程:
- hello.c需要经过预处理器读取系统头文件内容并且插入到程序文本中得到一个新的C程序,一般以.i作为扩展名;
- 然后编译器将文本文件hello.i 翻译成汇编语言文本文件hello.s
- 接下来,就该汇编器登场,将汇编语言翻译成机器语言指令,并保存到hello.o文件中,此时它是一个二进制文件了
- 最后链接阶段,将程序中调用的c标准库的函数合并到我们的hello.o程序中,结果就是一个可执行文件,可以被加载到内存中,由系统执行。
系统的硬件组成
要真正了解程序时如何运行的,首先要对系统的硬件组成有一个了解:
-
总线:贯穿整个系统的电子管道,可以理解为所有的数据设备以及系统之间的数据流转都要接到总线上。
-
I/O设备:系统与外部世界的联系通道;比如键盘、鼠标、磁盘、显示器等;
所有的I/O设置都要通过一个控制器或者适配器与I/O总线相连;
-
主存:也就是我们常说的内存,这是一个临时存储设备,在处理器执行程序时,用来存放程序和处理的数据;
-
处理器:也就是我们常说的CPU,是解释存储在主存中指令的引擎;其核心是一个大小为一个字(定长的字节,根据系统不同确定)的寄存器,称为程序计数器(PC)。在程序运行的过程中,PC都是指向主存中的一条机器语言指令。
从系统通电开始,直到系统断电,处理器一直在不断的执行PC指向的指令,在更新PC,使其指向下一条指令;
下面列举几个CPU在指令要求下可能执行的操作:
- 加载:从主存复制一个字到寄存器,已覆盖原有寄存器的内容;
- 存储:从寄存器复制一个字到主存的某个位置,以覆盖这个位置的原有值;
- 操作:把两个寄存器的内容复制到ALU(算数/逻辑单元),ALU对这两个字做算数运算,并将结果存放到一个寄存器中覆盖原有的内容;
- 跳转:从指令本身抽取一个字,并将这个字复制到PC中,以覆盖PC中原来的值;
处理器看上去是它的指令集架构的简单实现,但是现代处理器采用非常复杂的机制来加速程序的运行。因此我们在理解的时候要将处理器的指令集架构 和处理器的 微体系结构分来:指令集架构描述的是每条机器代码指令的效果;微体系结构描述的是处理器的实现;
运行程序
当我们在执行 ./hello
后,其实发生的过程是:
刚开始,shell程序执行它的指令,等待我们输入一个命令,当我们输入./hello
后,shell程序将字符读入寄存器,在把它存放到内存中;
当你在敲回车时,shell程序就知道我们已经结束了命令的输入,然后shell执行一系列指令来加载可执行文件,将目前文件的代码和数据复制到主存。注:利用直接存储器(DMA)技术,数据可以不到处理器直接从磁盘到主存。
一旦加载到内存中,处理器就开始执行程序的main机器指令,这些指令将“hello,world\n” 字符串中字节从主存复制到寄存器文件。再从寄存器文件复制到显示设备,最终展示在屏幕上。
高速缓存
从上面的例子,我们可以总结出,hello程序经历了从开始在磁盘上,加载时被复制到主存,处理器运行时又从主存复制到处理器,最后又从处理器复制到显示器。
这里从我们程序员角度讲,这些复制就是开销,那么问题来了,如何减小开销提高处理器效率呢???
从机械原理角度来看,存储设备越大运行越慢;处理器读磁盘比读内存开销大1000万倍,而寄存器文件的读取速度又比内存块几乎100倍,加快处理器的运行速度比加快主存运行速度要容器的多。
针对处理器与主存之间的差异,系统设计采用了更小更快的存储设备,称之为高速缓存,存放处理器近期可能会需要的信息。这个其实和我们平时开发程序是一样的,采用多级缓存,存放热点数据,提高系统处理能力。 这里的原理是利用程序具有访问局部区域里的数据和代码的趋势,所以高速缓存中存放了可能经常访问的数据,这样大部分操作就能在告诉缓存中完成。
请看下面的存储器层次的结构,相信你会一目了然:
如图所示,上一层存储器是下一层的高速缓存。
操作系统管理硬件
我们写的程序,没有直接访问键盘、显示器、磁盘等硬件,而是依赖操作系统提供的服务,所以可以把操作系统看成是应用程序和硬件之间一层软件。
操作系统有两大功能:
- 防止硬件被滥用;
- 对应用程序屏蔽底层复杂而通常又大不相同的硬件设备,提供简单一致的机制;
操作系统通常抽象出几个概念:进程、虚拟内存、文件;
进程
进程是操作系统对一个正在运行的应用程序的抽象,一个系统可以同时运行多个进程。
单核处理器一个时刻只能执行一个程序,而目前的多核处理器能同时执行多个程序。无论单核还是多核,一个CPU看上去都是在并发执行多个进程,这是通过处理器在进程间切换来实现的,这种切换被称为 上下文切换;
进程之间的切换是由操作系统内核管理的,内核是操作系统常驻主存的部分。当应用程序需要操作系统的某些操作时,比如读写文件,它就执行一条系统调用指令,将控制权传递给内核。然后内核执行被请求的操作并返回应用程序。 注意,内核不是一个独立的进程,它是系统管理所有进程所用代码和数据结构的集合。
线程
一个进程实际上是由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。
优点:比进程之间更容易共享数据;一般来讲也比进程更高效;
虚拟内存
这是一个抽象概念,它为每个进程提供了一个假象,即每个进程都在单独使用主存,每个进程看到的内存都是一致的,称为虚拟地址空间,如下图所示,地址是从小往上增大的:
文件
文件就是字节序列,所有的I/O设备,甚至网络都可以看成是文件;
并发
多核处理器是将多个CPU集成到一个集成电路芯片上。多核处理器组织架构如下:
超线程:称为同时多线程,允许一个CPU同时执行多个并发流的技术。Intel Core i7 处理器可以让每个核执行两个线程。
计算机系统中的抽象
在处理里,指令集架构提供了对实际处理器硬件的抽象,使用这个抽象,机器代码表现的好像运行在一个一次只执行一条指令的处理器上。不管底层多复杂精细,哪怕可以并发的执行多条指令,担又总是与那个简单有序的模型保持一致。只要模型一样,不同的处理器实现也能执行同样的机器代码,而又提供不同的开销和性能。这种抽象思想简直太重要了,在整个计算机科学中也随处可见,比如java类的生命和C语言的函数原型,以及计算机网络的分层。
看了上面这副图可以总结为:
- 文件是对I/O设置的抽象;
- 虚拟内存是对主存和磁盘的抽象;
- 进程是对处理器、主存和I/O设备的抽象;
至此,本章的学习就结束了,主要对计算机系统的组成和程序运行有了大框架的认知,后续继续进行深入学习。
扩展问题
- 信息=位+上下文,什么是上下文?工作中有哪些例子?
每一段程序都有很多外部变量。只有像Add这种简单的函数才是没有外部变量的。一旦你的一段程序有了外部变量,这段程序就不完整,不能独立运行。你为了使他们运行,就要给所有的外部变量一个一个写一些值进去。这些值的集合就叫上下文。 比如:C++的lambda表达式里面,[写在这里的就是上下文](int a, int b){ ... }
- RISC指令集和CISC指令集有什么区别,它们的典型CPU有哪些?
- CSIC(Complex Instruction Set Computer) 复杂指令集的CPU; CISC体系的设计理念是用最少的指令来完成任务(譬如计算乘法只需要一条MUL指令即可),因此CISC的CPU本身设计复杂、工艺复杂,但好处是编译器好设计。CISC出现较早,至今Intel还一直采用CISC设计;
- RSIC(Reduced Instruction Set Computer) 精简指令集的CPU; RISC的设计理念是让软件来完成具体的任务,CPU本身仅提供基本功能指令集,即:指令集中指令的数量相对很少。这种设计理念相对于CISC的设计理念,CPU的设计和工艺简单了,但是编译器的设计变复杂了
- 典型CPU: 一般典型CISC的CPU指令数在300条左右。ARM的CPU(作为典型的RISC的CPU)常用指令数在30条左右。 一般来说,CISC的CPU的功耗更高,一般用在PC机和笔记本电脑中。相对来说,RISC的CPU的功耗更低,一般用在嵌入式领域。
- 基于栈的CPU和基于寄存器的CPU有什么区别?
这个问题可以将JVM看成是一个基于栈的CPU,它在运行程序的时候都是用栈,代码必须使用这些指令来移动变量(即push和pop);

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Python 内存分配时的小秘密
Python 中的sys 模块极为基础而重要,它主要提供了一些给解释器使用(或由它维护)的变量,以及一些与解释器强交互的函数。 本文将会频繁地使用该模块的getsizeof() 方法,因此,我先简要介绍一下: 该方法用于获取一个对象的字节大小(bytes) 它只计算直接占用的内存,而不计算对象内所引用对象的内存 这里有个直观的例子: importsys a=[1,2] b=[a,a]#即[[1,2],[1,2]] #a、b都只有两个元素,所以直接占用的大小相等 sys.getsizeof(a)#结果:80 sys.getsizeof(b)#结果:80 上例说明了一件事:一个静态创建的列表,如果只包含两个元素,那它自身占用的内存就是 80 字节,不管其元素所指向的对象是什么。 好了,拥有这把测量工具,我们就来探究一下 Python 的内置对象都藏了哪些小秘密吧。 1、空对象不是“空”的! 对于我们熟知的一些空对象,例如空字符串、空列表、空字典等等,不知道大家是否曾好奇过,是否曾思考过这些问题:空的对象是不是不占用内存呢?如果占内存,那占用多少呢?为什么是这样分配的呢?...
- 下一篇
Node.js 使用 MongoDB 的 ObjectId 作为查询条件
当往MongoDB中插入一条数据时,会自动生成ObjectId作为数据的主键。 那么如何通过ObjectId来做数据的唯一查询呢? 在MongoDB中插入一条数据 在MongoDB中插入一条如下结构的数据: { _id: 5d6a32389c825e24106624e4, title: 'GitHub 上有什么好玩的项目', content: '上个月有水友私信问我,GitHub 上有没有比较好玩的项目可以推荐?我跟他说:"有,过两天我整理一下"。\n' + '\n' + '然而,一个月过去了,我把这件事情忘了精光,直至他昨天提醒我才记起2_05.png。\n', creation: 2019-08-31T08:39:20.384Z } 其中,上述_id的值“5d6a32389c825e24106624e4”,是MongoDB自动分配的。 使用 MongoDB 的 ObjectId 作为查询条件 须知,_id的值“5d6a32389c825e24106624e4”并非是字符串,而是ObjectId对象类型。因此,如下查询是行不通的: // 查询指定文档 const findNews =...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS关闭SELinux安全模块
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Linux系统CentOS6、CentOS7手动修改IP地址
- Hadoop3单机部署,实现最简伪集群
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS7,8上快速安装Gitea,搭建Git服务器