本文为极客时间徐文浩老师 - 深刻浅出计算机组成原理学习笔记html
现代计算机的基本组成部分其实主要由三部分组成:CPU,内存,主板。java
你撰写的程序,打开的任何PC端应用。都要加载到内存中才能运行,存放在内存中的程序及其数据须要被CPU读取,CPU计算完以后还要把对应的数据写回到内存。主板的做用就是承载两者,由于他们不能互相嵌入到对方中。程序员
CPU读取内存中的二进制指令,而后译码,经过控制信号操做对应的运算原件以及存储单元进行操做。算法
英特尔创始人之一 戈登丶摩尔 曾说:当价格不变时,集成电路上可容纳的元器件的数目每隔18-24个月便会增长一倍,性能也将提高一倍。编程
咱们如今所使用的机器既叫图灵机也叫冯诺依曼机,二者是不一样的计算机抽象。冯诺依曼侧重于硬件抽象,图灵机则侧重于计算抽象。数组
起源于冯诺依曼发表的一片文章即“第一份草案”,文中描述了他心目中的一台计算机应该长什么样。进而确立了当代计算机的体系结构,即:运算器,控制器,存储器,输入设备,输出设备五部分。缓存
他认为现代计算机应该是一个“可编程”的计算机,是一个“可存储”的计算机。即也叫存储程序计算机。由于过去的计算机电路是焊死在电路板的,如同如今的计算器,若是要作其余的操做,那么就须要从新焊接电路,它是不可编程的计算机。再后来的计算机是“插拔式”的计算机,须要用什么程序必须得插入该程序的组件才可。这样程序没法保存在计算机上,每次使用的时候还须要插拔的方式才能够。它是不可存储的计算机。bash
图灵是一位数学家,他并无考虑计算机的硬件基础,而是只考虑了计算机在数学计算模型上的可行性。即只思考了做为一个“计算机”,他应该作的工做,和怎么工做。图灵只思考了计算机的计算模型,及计算机所谓的“计算”的理论逻辑的实现方法。服务器
图灵机能够看作由一条两端可无限延长的带子,它有一个读写头,以及一组控制读写头工做的命令组成,是一种抽象的计算模型,即将人们使用纸笔进行的数学运算由一个虚拟的机器代替。多线程
它证实了通用计算理论,确定了计算机实现的可能性。同时它给出了计算机应有的主要架构,引入了读写,算法,程序语言的概念。极大的突破了过去的计算机的设计理念。
其实图灵机本质上是状态机,计算机理论模型,冯诺依曼体系则更像是图灵机的具体物理实现。包括运算,控制,存储,输入,输出五个部分。冯诺依曼体系相对以前的计算机最大的创新在于程序和数据的存储。今后实现机器的内部编程。图灵机的纸带应对冯诺依曼体系中的存储,读写头对应输入输出规则及(读取一个符号后,作了什么)运算,纸带怎么移动则对应着控制。
理论上图灵机能够模拟人类的全部计算过程,不管复杂与否。
芯片组
芯片组是主板的核心组成部分,联系CPU及其余周边设备的运做。主板上最重要的芯片组就是南北桥芯片组。
南北桥芯片组->主板上的两个主要芯片组,靠上方的叫北桥,下方的叫南桥。北桥负责与CPU通讯,而且连接告诉设备(内存,显卡),而且与(I/O操做)南桥通讯,南桥负责与低俗设备(硬盘/外部IO设备,USB等设备)通讯,而且与北桥通讯。
总线
主板的芯片组和总线解决了CPU和内存通讯的问题(北桥),芯片组控制数据传输的流转(从哪来,到哪儿去),总线则是实际数据传输的高速公路。
CPU的好坏决定 -> 主频高,缓存大,核心数多。CPU通常安装在主板的CPU插槽中。
数据通路:其实就是链接了整个运算器与控制器,方便咱们程序的运转和计算,并最终组成了CPU。
CPU通常被叫作超大规模集成电路,由一个个晶体管组合而成,CPU的计算过程其实就是让晶体管中的“开关”信号不断的去“打开”和“关闭”。来组合完成各类运算和功能。这里的“打开”及“关闭”操做的快慢就是由CPU主频来影响。
控制器
一条条指令执行的控制过程,就是由计算机五大组件之一的控制器来控制的。
CPU即中央处理器,GPU即图形处理器,如今的电脑,大部分GPU都集成在了CPU中也叫集成显卡,后来本来的GPU即属于北桥的内存控制器等做为一支独立的芯片封装到了CPU基板上。因此后来的及其的主板上没有南北桥之分了,只剩下了PCH芯片即过去的南桥。
固然若是你的PC机要运行一些大型游戏,或者有一些对GPU要求较高的工做的话,也能够配置独立的GPU卡到主板上。
过去:
要看一台PC机的具体CPU核数以及线程数能够经过任务管理器界面看到,也能够经过计算机右键属性的设备管理器中看到(仅能看到线程数)。或者经过以下命令看到
wmic
cpu get *
-----------对应属性
NumberOfCores
NumberLogicProcessors
复制代码
CPU 的主频即内核工做的时钟频率,一般所说的***CPU是多少兆赫的,这里所谓的兆赫就是描述的CPU主频,CPU型号后面跟着的2.4 GHZ
即主频的数字描述。
主频并不直接表明CPU的运算速度,因此也会有CPU主频高可是CPU的运算速度慢的状况,主频仅是CPU性能表现的一方面。
Java 中的全部线程均在JVM进程中,CPU调度的是进程中的线程。
CPU线程数和Java线程数并无直接关系,CPU采用分片机制执行线程,给每一个线程划分很小的时间颗粒去执行,可是真正的项目中,一个程序要作不少的的操做,读写磁盘、数据逻辑处理、出于业务需求必要的休眠等等操做,当程序正在执行的线程进入到I/O操做的时候,线程随之进入阻塞状态,此时CPU会作上下文切换,以便处理其余线程的任务;当I/O操做完成后,CPU会收到一个来自硬盘的中断信号,并进入中断处理例程,手头正在执行的线程则可能所以被打断,回到 ready 队列。而先前因 I/O 而阻塞等待的线程随着 I/O 的完成也再次回到 就绪队列,这时 CPU 在进行线程调度的时候则可能会选择它来执行。
参考:
线程是操做系统最小的调度单位,进程则是操做系统资源分配的对小单位。
进程:进程是操做系统分配资源的基本单位,每隔进程拥有虚拟后的独立的内存空间,存储空间,CPU资源。各类PC端应用均是一个独立的进程。 线程:是CPU调度的基本单位,赞成进程的各个线程共享进程内部的资源,线程间的通信远小于进程间的。由于(各个线程共享进程内部的资源)。因此在多线程并发的状况下,须要额外关注对于共享资源的保护问题,尤为是全局变量。
Intel的超线程技术,目的是为了更充分地利用一个单核CPU的资源。CPU在执行一条机器指令时,并不会彻底地利用全部的CPU资源,并且实际上,是有大量资源被闲置着的。 超线程技术容许两个线程同时不冲突地使用CPU中的资源。好比一条整数运算指令只会用到整数运算单元,此时浮点运算单元就空闲了,若使用了超线程技术,且另外一个线程恰好此时要执行一个浮点运算指令,CPU就容许属于两个不一样线程的整数运算指令和浮点运算指令同时执行,这是真的并行。 我不了解其它的硬件多线程技术是怎么样的,但单就超线程技术而言,它是能够实现真正的并行的。但这也并不意味着两个线程在同一个CPU中一直均可以并行执行,只是刚好碰到两个线程当前要执行的指令不使用相同的CPU资源时才能够真正地并行执行。
本质上是一个物理核在跑一个线城时,同时利用闲置的晶体管跑其余指令,这样就能够提高效能。
参考:
计算机的两个核心指标:性能,功耗。具体的体现则是响应时间和吞吐率。响应时间即单位任务执行运算的快慢,吞吐量即单位时间处理任务的多少。
程序运行时间: 程序在用户态运行指令的时间+内核态运行指令的时间。
但受线程调度的影响,CPU在同一时间会有不少的Task在执行,不是只执行特定程序的指令,而且同一台计算机可能CPU满载执行,也能会降频执行。而且程序运行时间也会受到相应的主板和内存的影响。
程序的CPU执行时间 = CPU时钟周期数 * 时钟周期时间 - 能够当作处理每一个Task所需时间。
好比Intel Core - i7 - 7700HQ 2.8GHZ
,这里的2.8GHZ
粗浅理解即CPU在一秒里能够执行的简单指令数是2.8G条。准确说即CPU的一个“钟表”可以识别出来的最小时间间隔。
**时钟周期时间:**在CPU内部,和咱们戴的电子石英表相似,有一个叫晶体振荡器的东西简称“晶振”,晶振的每一次“滴答”即电子石英表的时钟周期时间(晶振时间)。在2.8GHZ
主频的CPU上,这个时钟周期时间就是1/2.8GHZ
。CPU就是按照这个“时钟”提示的时间来进行本身的操做,主频越高意味着这个表走的越快,CPU也就“被逼”着走的也快,CPU越快散热压力固然也越大。
这里能够得出,晶振时间与CPU执行固定指令耗时成正比,越小耗时越少。
CPU时钟周期数 = 指令数 * 每条指令的平均时钟周期数(CPI)。 - 能够当作共有多少个Task。
这里说了每条指令的平均时钟周期数,因此咱们就知道不一样的指令执行时间是不一样的,即所花费的时钟周期数是不一样的,可能别人的Task简单花1秒钟就能作完,你的Task比较复杂须要5秒才行。具体到计算机,乘法的时钟周期数就要多于加法。不过现代的CPU经过流水线技术可使得单个命令的执行须要的CPU时钟周期数更少了。
一个程序包含多条语句,一条语句可能对应多条指令,一条CPU指令可能须要多个CPU时钟周期才能完成。
程序的CPU执行时间: 指令数 * 每条指令的平均时钟周期数(CPI) * 时钟周期时间
由上面的公式咱们知道,若是想要减小程序的CPU执行时间的话那么就要从以上三点着手。可是指令数是由不一样编译器所决定的,时钟周期时间则是由CPU主频的高低来决定的,而每条指令的平均时钟周期数咱们则能够经过流水线技术来优化。
CPU功耗 ~= 1/2 * 负载电容 * 电压的平方 * 开关平率 * 晶体管数量
制程:
纳米制程,以14nm为例,其制程是指在芯片中,线最小能够作到14纳米的尺寸,缩小晶体管能够减小耗电量(晶体管必定的单位面积中),同时能够提高信号量在电路间的传输速度,缩小制程后,晶体管之间的电容也会更低,从而提高他们之间的开关频率。可知功耗与电容成正比,因此传输速度更快,还更省电。
阿姆达尔定律:
并行优化,并非全部的问题均可以经过并行去优化。
优化后的执行时间 = 受优化影响的执行时间/加速倍数(并行处理数) + 不受影响的执行时间
汇编器是一种工具程序,用于将汇编语言源程序转换为机器语言。机器语言是一种数字语言, 专门设计成能被计算机处理器(CPU)理解。全部 x86 处理器都理解共同的机器语言。
汇编语言包含用短助记符如 ADD、MOV、SUB 和 CALL 书写的语句。汇编语言与机器语言是一对一的关系:每一条汇编语言指令对应一条机器语言指令。 这就意味着不一样型号的处理器若是所使用的机器语言不一样的话,那么他们的汇编语言也毫不相同。
高级语言如 Python、C++ 和 Java 与汇编语言和机器语言的关系是一对多。好比,C++的一条语句就会扩展为多条汇编指令或机器指令。 一种语言,若是它的源程序可以在各类各样的计算机系统中进行编译和运行,那么这种语言被称为是可移植的。
汇编语言不是可移植的,由于它是为特定处理器系列设计的。目前普遍使用的有多种不一样的汇编语言,每一种都基于一个处理器系列。 对于一些广为人知的处理器系列如 Motorola 68×00、x8六、SUN Sparc、Vax 和 IBM-370,汇编语言指令会直接与该计算机体系结构相匹配,或者在执行时用一种被称为微代码解释器的处理器内置程序来进行转换。
要让一段C语言程序在一个 Linux 操做系统上跑起来,咱们须要把整个程序翻译成一个汇编语言的程序,这个过程咱们通常叫编译成汇编代码。针对汇编代码,咱们能够再用汇编器翻译成 机器码。这些机器码由“0”和“1组成的机器语言表示。这一条条机器码,就是一条条的计算机指令。这样一串串的 16 进制数字,就是咱们 CPU 可以真正认识的计算机指令。为了读起来方便,咱们通常把对应的二进制数,用 16 进制表示
解释型语言,是经过解释器在程序运行的时候逐句翻译,而 Java 这样使用虚拟机的语言,则是由虚拟机对编译出来的中间代码进行解释,或者即时编译(JIT)成为机器码来最终执行。
咱们平常用的 Intel CPU,有 2000 条左右的 CPU 指令。常见的指令能够分红五大类:
来看一段汇编代码:
#include <time.h>
#include <stdlib.h>
int main()
{
srand(time(NULL));
int r = rand() % 2;
int a = 10;
if (r == 0)
{
a = 1;
} else {
a = 2;
}
==================
if (r == 0)
3b: 83 7d fc 00 cmp DWORD PTR [rbp-0x4],0x0
3f: 75 09 jne 4a <main+0x4a>
{
a = 1;
41: c7 45 f8 01 00 00 00 mov DWORD PTR [rbp-0x8],0x1
48: eb 07 jmp 51 <main+0x51>
}
else
{
a = 2;
4a: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2
51: b8 00 00 00 00 mov eax,0x0
}
复制代码
该段代码的具体释义可参考深刻浅出计算机组成原理第六节。
机器语言,汇编语言,编译器:
过去编写程序是经过纸带打孔的方式,那么就只能经过“0,1”机器码来进行程序的编写,后来进不出了汇编语言,汇编语言是一种更接近人类语言的语言,用汇编器能够将汇编语言转为机器语言。汇编器则至关于翻译机的存在,能够根据具体的汇编指令转为计算机能识别的二进制码,即各CPU开发商提供的机器码。
咱们知道当下所流行的各类汇编语言都是与处理器所一对一的,即当初汇编语言的设计人员在编写汇编语言的时候是经过CPU开发商提供的指令集手册来对应开发定义的汇编语言,而各类型号不一样的CPU所独有的指令集则是烧录到了CPU中。
参考:
能够先读:
基本概念:
寄存器 寄存器是中央处理器内的组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和地址。在中央处理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序计数器(PC)。
各种存储器按照到cpu距离由近到远(访存速度由高到低)排列分别是寄存器,缓存,主存,辅存。
其余优质解答:
逻辑上,咱们能够认为,CPU 其实就是由一堆寄存器组成的。而寄存器就是 CPU 内部,由多个触发器或者锁存器组成的简单电路。
N 个触发器或者锁存器,就能够组成一个 N 位(Bit)的寄存器,可以保存 N 位的数据。比方说,咱们用的 64 位 Intel 服务器,寄存器就是 64 位的。
一个 CPU 里面会有不少种不一样功能的寄存器。其中有三个比较特殊的
CPU 里面还有更多用来存储数据和内存地址的寄存器。这样的寄存器一般一类里面不止一个。咱们一般根据存放的数据内容来给它们取名字,好比整数寄存器、浮点数寄存器、向量寄存器和地址寄存器等等。有些寄存器既能够存放数据,又能存放地址,咱们就叫它通用寄存器。
一个程序执行的时候,CPU 会根据 PC 寄存器里的地址,从内存里面把须要执行的指令读取到指令寄存器里面执行,而后根据指令长度自增,开始顺序读取下一条指令。一个程序的一条条指令,在内存里面是连续保存的,也会一条条顺序加载。
而有些特殊指令,好比 J 类指令,也就是跳转指令,会修改 PC 寄存器里面的地址值。这样,下一条要执行的指令就不是从内存里面顺序加载的了。事实上,这些跳转指令的存在,也是咱们能够在写程序的时候,使用 if…else
条件语句和 while/for
循环语句的缘由。
CPU从PC寄存器中取地址,找到地址对应的内存位子,取出其中指令送入指令寄存器执行,而后指令自增,重复操做。因此只要程序在内存中是连续存储的,就会顺序执行这也是冯诺依曼体系的理念。而实际上跳转指令就是当前指令修改了当前PC寄存器中所保存的下一条指令的地址,从而实现了跳转。固然各个寄存器其实是由数电中的一个一个门电路组合出来的。
参考:
计算机中的数在内存中都是以二进制形式进行存储的,用位运算就是直接对整数在内存中的二进制位进行操做,所以其执行效率很是高,在程序中尽可能使用位运算进行操做,这会大大提升程序的性能。固然可读性才是首要保证的目标。
位操做符
&
与运算 两个位都是 1 时,结果才为 1,不然为 01 0 0 1 1
&
1 1 0 0 1
------------------------------
1 0 0 0 1
复制代码
|
或运算 两个位都是 0 时,结果才为 0,不然为 11 0 0 1 1
|
1 1 0 0 1
------------------------------
1 1 0 1 1
复制代码
^
异或运算,两个位相同则为 0,不一样则为 11 0 0 1 1
^
1 1 0 0 1
-----------------------------
0 1 0 1 0
复制代码
~
取反运算,0 则变为 1,1 则变为 0~ 1 0 0 1 1
-----------------------------
0 1 1 0 0
复制代码
<<
左移运算,向左进行移位操做,高位丢弃,低位补 0int a = 8;
a << 3;
移位前:0000 0000 0000 0000 0000 0000 0000 1000
移位后:0000 0000 0000 0000 0000 0000 0100 0000
复制代码
>>
右移运算,向右进行移位操做,对无符号数,高位补 0,对于有符号数,高位补符号位unsigned int a = 8;
a >> 3;
移位前:0000 0000 0000 0000 0000 0000 0000 1000
移位后:0000 0000 0000 0000 0000 0000 0000 0001
int a = -8;
a >> 3;
移位前:1111 1111 1111 1111 1111 1111 1111 1000
移位前:1111 1111 1111 1111 1111 1111 1111 1111
复制代码
参考自:
在真实的程序里,压栈的不仅有函数调用完成后的返回地址。好比函数 A 在调用 B 的时候,须要传输一些参数数据,这些参数数据在寄存器不够用的时候也会被压入栈中。整个函数 A 所占用的全部内存空间,就是函数 A 的栈帧。
实际的程序栈布局,顶和底与咱们的乒乓球桶相比是倒过来的。底在最上面,顶在最下面,这样的布局是由于栈底的内存地址是在一开始就固定的(内存地址偏大的那一边)。而一层层压栈以后,栈顶的内存地址是在逐渐变小而不是变大。(这里理解这句话,须要明白栈是固定大小的,想象一个乒乓球筒,反向往里面填球)
触发的StackOverFlow常见触发方式:函数的递归调用,在栈中声明一个很是占内存的变量(巨大数组)。
程序运行常见的优化方案:
把一个实际调用的函数产生的指令,直接插入到调用该函数的位置,来替换对应的函数调用指令。这种方案在若是被调用的函数中没有调用其余函数的状况下,仍是可行的。这是一个常见的编译器进行自动优化的场景,叫函数内联。
这里编译器优化的具体痛点并不是简单的少了一些指令的执行,而是函数频繁进出栈所花费时间的开销,由于相对于寄存器来讲,内存是十分慢的。因此让CPU反复操做内存的话,开销仍是很大的。因此上述文本着重提示了被调用函数没有调用其余函数的状况下,由于若是有调用的话,一是寄存器内存可能开销不够,二是仍是有操做主存的瓶颈在。
C 语言的文件在编译后会生成以.o
为尾缀的汇编语言文件,如 add_lib.o
以及 link_example.o
并非一个可执行文件而是目标文件。只有经过连接器把多个目标文件以及调用的各类函数库连接起来,咱们才能获得一个可执行文件。
C 语言代码 - 汇编代码 - 机器码 这个过程,在咱们的计算机上进行的时候是由两部分组成的。
由上咱们能够得知程序最终是经过装载器加载程序及数据到内存而后变成指令和数据的,因此其实咱们生成的可执行代码也并不只仅是一条条的指令。
可执行代码和目标代码长得差很少,可是长了不少。由于在Linux下,可执行文件和目标文件所使用的都是一种叫ELF
的文件格式,中文名字叫可执行与可连接文件格式,这里面不只存放了编译成的汇编指令,还保留了不少别的数据。
如函数名称add
、main
等,以及定义的全局能够访问的变量名称,都存放在ELF格式文件里。这些名字和它们对应的地址,在 ELF 文件里面,存储在一个叫做符号表的位置里。符号表至关于一个地址簿,把名字和地址关联了起来。
执行流程: 连接器会扫描全部输入的目标文件,而后把全部符号表里的信息收集起来,构成一个全局的符号表。而后再根据重定位表,把全部不肯定要跳转地址的代码,根据符号表里面存储的地址,进行一次修正。最后,把全部的目标文件的对应段进行一次合并,变成了最终的可执行代码。这也是为何,可执行文件里面的函数调用的地址都是正确的。
main 函数里调用 add 的跳转地址,再也不是下一条指令的地址了,而是 add 函数的入口地址了,这就是 EFL 格式和连接器的功劳。
因此一些文件即使是在同一计算机同一CPU上的不一样操做系统上可能会出现一个可执行而一个不可执行的状况。根本缘由在于不一样OS的装载器所对应的能解析的文件格式也是不一样的。Linux的装载器只能装载EFL的文件格式,而Windows是PE的。
这里Java实现跨平台的机制则是:Java是经过实现不一样平台上的虚拟机,而后即时翻译javac生成的中间代码来作到跨平台的。跨平台的工做被虚拟机开发人员来解决了(如同汇编)。
解决办法:
分段: 在内存中划分一段连续的内存空间,分配给装载的程序,把连续的内存空间和指令指向的内存地址进行映射。
其中指令里用到的内存地址叫做虚拟内存地址,实际内存硬件里面的物理空间叫作物理内存地址。程序员只须要关心虚拟内存地址就好了。因此咱们只须要维护虚拟内存到物理内存的映射关系的起始地址和对应的空间大小就能够了。
问题:内存碎片
解决办法:
内存交换。即先把内存中某个程序所占用的内存写到硬盘上,而后再从硬盘上读回内存中,只不过读回来的时候要紧贴上一个应用所占用内存空间的后面,造成连续的内存占用。
问题:性能瓶颈,内存碎片和内存交换的空间太大,硬盘的读写速度太慢
解决办法:
内存分页。原理是少出现一些内存碎片。另外,当须要进行内存交换的时候,让须要交换写入或者从磁盘装载的数据更少一点。和分段这样分配一整段连续的空间给到程序相比,分页是把整个物理内存空间切成一段段固定尺寸的大小。 。而对应的程序所须要占用的虚拟内存空间,也会一样切成一段段固定尺寸的大小。 这样一个连续而且尺寸固定的内存空间,就是页。通常页远小于程序大小只有几KB。
因为内存空间都是预先划分好的,也就没有了不能使用的碎片,而只有被释放出来的不少 4KB 的页。即便内存空间不够,须要让现有的、正在运行的其余程序,经过内存交换释放出一些内存的页出来,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,让整个机器被内存交换的过程给卡住。
分页的方式使得咱们在加载程序的时候,再也不须要一次性都把程序加载到物理内存中。咱们彻底能够在进行虚拟内存和物理内存的页之间的映射以后,并不真的把页加载到物理内存里,而是只在程序运行中,须要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。
虚拟内存是指一段地址,可是没有加载到物理内存里的时候其实就是放在硬盘上。
虚拟内存,内存交换,内存分页三者结合下,其实运行一个应用程序须要用的必要内存是不多的,也是为何咱们优先的内存能够运行比咱们内存大不少的应用的缘由。
JVM也是一个可执行程序,同其余程序同样依赖于操做系统的内存管理和装载程序,它能够按本身的方式去规划它自身的内存空间给就Java程序使用而无需考虑怎么映射到物理内存这些。这是承载他的操做系统须要作的事情,每一个应用程序都有固定使用的内存空间的限度。
上文提到在使用链接器进行代码合并的时候,这里的连接是指静态连接,相应的,也有对应的动态连接。咱们知道程序在进行装载的时候同一份代码若是多个程序都静态链接了一遍那么内存中将会有多分一样的代码占用内存,这对内存耗费也是很是大的。
既然是共享代码,那么内存中只要装载一份便可。在程序连接的时候咱们连接到该共享库的内存地址便可,不一样系统下,共享库的文件尾缀不一样。Windows是.dll
,Linux下是.so
。
共享库文件代码要求:
编译出来的共享库文件的指令代码,是地址无关的。 缘由是不一样程序若是都用同一份共享代码库的话,不一样程序该代码的虚拟地址是不一样的,虽然物理地址上是相同的,可是对于该共享代码库的虚拟地址和物理地址的映射就没法维护了。
其中利用重定位表的代码就是与地址相关的代码。利用重定位表的代码在程序连接的时候,就把函数调用后要跳转访问的地址肯定下来了,这意味着,若是这个函数加载到一个不一样的内存地址,跳转就会失败。
相对地址: 动态代码库中的数据和指令的虚拟地址都是经过相对地址的方式互相访问的。各类指令中使用到的内存地址,给出的不是一个绝对的地址空间,而是一个相对于当前指令偏移量的内存地址。由于整个共享库是放在一段连续的虚拟内存地址中的,不管装载到哪一段地址,不一样指令之间的相对地址都是不变的。
须要注意的是:虽然共享库的代码部分的物理内存是共享的,可是数据部分是各个动态连接它的应用程序里面各加载一份的。
全局偏移表(GOT): GOT表位于共享库的数据段里。因此使用动态连接的各个程序在共享库中生成各自的GOT,每一个程序的GOT都不一样。 而 GOT 表里的数据,则是在加载一个个共享库的时候写进去的。因此若是当前运行程序的共享库指令须要用到外部的变量和函数地址的话,都会查询 GOT,来找到当前运行程序的虚拟内存地址。
不一样的进程,调用一样的共享库,各自 GOT 里面指向最终加载的动态连接库里面的虚拟内存地址是不一样的(由于各应用程序调用该函数的虚拟内存地址是不一样的)。
虽然不一样的程序调用的一样的动态库,而各自的数据部分的内存地址是独立的,调用的又都是同一个动态库,可是不须要去修改动态库里面的代码所使用的地址,而是各个程序各自维护好本身的 GOT,可以找到对应的动态库就行了。
像动态连接这样经过修改“地址数据”来进行间接跳转,去调用一开始不能肯定位置代码的思路,在Java中,相似多态的实现。
以下代码:
public class DynamicCode {// 动态代码库
private HashMap<String, Object> data; // 各种私有的数据部分 - 其中有一项是GOT
public static void main(strs[] args) {// 公用代码部分
}
}
复制代码
二进制 -> 十进制: 把从右到左的第 N 位,乘上一个 2 的 N 次方,而后加起来。N 从 0 开始记位。
示例:
0011
=====
0×2^3 + 0×2^2 + 1×2^1 + 1×2^0
复制代码
十进制 -> 二进制: 短除法。也就是,把十进制数除以 2 的余数,做为最右边的一位。而后用商继续除以 2,把对应的余数紧靠着刚才余数的右侧,这样递归迭代,直到商为 0 就能够了。而后余数序列从下到上组成的序列就是该整数的二进制表示。
首先须要明白:在计算机中,数字都是用补码来存储的,而对于补码的表示方式一个字节(8bit)的数字,规定1000 0000就是-128。并且对于正数而言,反码,补码是其原码自己。
原码: 0001 在原码中就表示为 +1。而 1001 最左侧的第一位是 1,因此它就表示 -1。这个其实就是整数的原码表示法。
原码表示法的问题
反码: 为了解决“正负相加不等于0”的问题,在“原码”的基础上,人们发明了“反码”。“反码”表示方式是用来处理负数的,符号位置不变,其他位置相反。
这样正负两数相加不为0的状况就解决了
反码表示法的问题:
补码:一样是针对"负数"作处理的,从原来"反码"的基础上 +1。在补一位1的时候,要丢掉最高位(好比1111)。
这样就解决了+0和-0同时存在的问题,另外"正负数相加等于0"的问题,一样获得知足。同时还多了一位数 -8。
用原码的话,一个字节能够表示的范围是:-127~127,用补码的话表示的范围是:-128~127.
二进制负数的补码,等于该负数取反码再加1,也等于其正数按位取反再加1。
正数的反码是其自己 负数的反码是在其原码的基础上, 符号位不变,其他各个位取反。
重点:
说了那么多,只是描述一下三者的区别及由来。由于咱们从一开始就说了,计算机中是按补码来存储数据的,因此咱们只要想办法快速搞清楚一个计算机中的二进制数的十进制是多少。
咱们仍然经过最左侧第一位的0和1,来判断这个数的正负。可是,咱们再也不把这一位当成单独的符号位,在剩下几位计算出的十进制前加上正负号,而是在计算整个二进制值的时候,在左侧最高位前面加个负号。
好比,一个 4 位的二进制补码数值 1011,转换成十进制,就是 -1×2^3 + 0×2^2 + 1×2^1 + 1×2^0 = -5。若是最高位是 1,这个数必然是负数;最高位是 0,必然是正数。而且,只有 0000 表示 0,1000 在这样的状况下表示 -8。一个 4 位的二进制数,能够表示从 -8 到 7 这 16 个数,不会浪费一位。
参考:
ASCII(American Standard Code for Interchange,美国信息交换标准代码: 最先计算机只须要使用英文字符,加上数字和一些特殊符号,而后用8位的二进制,就能表示咱们平常须要的全部字符了,这个就是ASCII码。
ASCII 码就比如一个字典,用 8 位二进制中的 128 个不一样的数,映射到 128 个不一样的字符里。好比,小写字母 a 在 ASCII 里面,就是第 97 个,也就是二进制的 0110 0001,对应的十六进制表示就是 61。而大写字母 A,就是第 65 个,也就是二进制的 0100 0001,对应的十六进制表示就是 41。
须要注意的是:
在 ASCII 码里面,数字 9 再也不像整数表示法里同样,用 0000 1001 来表示,而是用 0011 1001 来表示。字符串 “15” 也不是用 0000 1111 这 8 位来表示,而是变成两个字符 1 和 5 连续放在一块儿,也就是 0011 0001 和 0011 0101,须要用两个 8 位来表示。 两个 8 位的缘由是,由于 4 位最高只能表示到(-8 - 7)。
咱们能够看到,最大的 32 位整数,就是 2147483647。若是用整数表示法,只须要 32 位就能表示了。可是若是用字符串来表示,一共有 10 个字符,每一个字符用 8 位的话,须要整整 80 位。比起整数表示法,要多占不少空间。因此这也是为何咱们在存储数据的时候要经过二进制序列化的方式来存储。
Unicode: 其实就是一个字符集,包含了 150 种语言的 14 万个不一样的字符。
字符编码则是对于字符集里的这些字符,怎么一一用二进制表示出来的一个字典。咱们上面说的 Unicode,就能够用 UTF-八、UTF-16,乃至 UTF-32 来进行编码,存储成二进制。因此,有了 Unicode,其实咱们能够用不止 UTF-8 一种编码形式,只要别人知道这套编码规则,就能够正常传输、显示这段代码。
一样的文本,采用不一样的编码存储下来。若是另一个程序,用一种不一样的编码方式来进行解码和展现,就会出现乱码。
须要注意的是,若是咱们程序中使用了一些或者说存储了一些不经常使用的古老字符集,那么可能Unicode字符集中并不存在这样的字符,那么Unicode 会统一把这些字符记录为 U+FFFD 这个编码。若是用 UTF-8 的格式存储下来,就是\xef\xbf\xbf。
参考:
计算机不用十进制而用二进制缘由以下:
电磁关系及继电器的由来可参考继电器。
电信号在传递的时候,因为电线过长会致使电阻过大此时对电压要求会变大或者说用电器会出现无响应的状态。因此在进行远距离信息传递的时候为了不电路过长这种状况,发明了继电器(电驿)。继电器能够方便咱们的电信号进行传导,或者根据须要组成咱们想要的“与”,“或”,“非”等的逻辑电路。
“与”电路的话至关于咱们在电路上串联两个开关,当两个开关都打开,电路才接通。“或”至关于咱们在输入端通两条电路到输出端,任意一条电路是打开状态,那么到输出端的电路都是联通的。“非”至关于从开关默认关掉,只有通电有了磁场以后打开,换成默认是打开通电的,只有通电以后才关闭,咱们就获得了一个计算机中的“非”操做。输出端开和关正好和输入端相反。
这三种基本逻辑电路实现起来都比较简单,若是要作复杂的工做的话则须要更多的逻辑电路经过分层,组合的方式来实现。
结论:咱们经过电路的“开”和“关”,来表示“1”和“0”。就像晶体管在不一样的状况下,表现为导电的“1”和绝缘的“0”的状态。
这些基本的逻辑电路,也叫门电路。 一方面,咱们能够经过继电器或者中继,进行长距离的信号传输。另外一方面,咱们也能够经过设置不一样的线路和开关状态,实现更多不一样的信号表示和处理方式,这些线路的链接方式其实就是咱们在数字电路中所说的门电路。而这些门电路,也是咱们建立 CPU 和内存的基本逻辑单元。咱们的各类对于计算机二进制的“0”和“1”的操做,其实就是来自于门电路,叫做组合逻辑电路。
所谓门电路在数字电路中,所谓“门”就是只能实现基本逻辑关系的电路。最基本的逻辑关系是与,或,非,最基本的逻辑门是与门,或门和非门。以下是最基本的门电路,其余复杂的门电路都是由这些门电路组合而成。他们是构成现代计算机硬件的“积木”。
半加器
能够看到基础门电路,输入都是两个单独的 bit,输出是一个单独的 bit。若是咱们要对 2 个 8 位的数,计算与、或、非这样的简单逻辑运算,其实很容易。只要连续摆放 8 个开关,来表明一个 8 位数。这样的两组开关,从左到右,上下单个的位开关之间,都统一用“与门”或者“或门”连起来,就是两个 8 位数的 AND 或者 OR 的运算了。
要想实现一个加法器,各二进制位的计算逻辑以下:
能够看到每位的输入输出关系对应着基本门电路中的异或门的逻辑。因此,其实异或门就是一个最简单的整数加法,所须要使用的基本门电路。 但须要注意的是,若是当两个输入位都是1的话,咱们还须要考虑进1位的状况。因此这就用到了基础门电路中的与门。
因此,经过一个异或门计算出个位,经过一个与门计算出是否进位,咱们就经过电路算出了一个一位数的加法。因而,后来就把这两个门电路进行打包,叫他为半加器。
全加器
半加器只能解决个位的运算,二,四,八位的输入状况与个位的并不同。由于二位除了一个加数和被加数以外,还须要加上来自个位的进位信号,一共须要三个数进行相加,才能获得结果。可是基本的门电路以及组合而成的半加器输入内容都是两位的。其实解决办法很简单,即经过两个半加器和一个或门就能组合成一个全加器。
如图W的输出即为二位的值。有了全加器理论上两个8位数的加法运算就能够实现了:
能够看到的是,个位和其余高位不一样,个位只须要一个半加器便可。而最高位即最左侧的一位表示的是咱们的加法是否溢出了。整个电路中有这样一个信号来表示咱们所作的加法运算是否溢出了,能够给到硬件层面的其它标志位中,来让计算机知晓这样算溢出了,以便获得计算机硬件层面的支持。
算术逻辑单元(ALU):是中央处理器的执行单元,是全部中央处理器的核心组成部分,由与门和或门构成的算术逻辑单元,主要功能是进行二进制的算术运算,如加减乘数(不包括整数除法)。
13 * 9 = 117
的二进制转化表:
实际二进制数据在进行乘法运算的时候,退化成了位移和加法。由于是二进制乘法,因此乘数的各位和被乘数的乘积不是所有为0就是把被乘数复制一份下来。须要注意的是乘数的每位进行一次乘积运算以后,下一次的运算结果就须要向高位移动一位。最后这些结果相加起来便可
二进制的乘法运算具体放到电路中的话,也并不须要引入任何新的、更复杂的电路,仍然用最基础的电路便可,只要用不一样的接线方式,就可以实现一个基本的乘法。最简单的实现思路就是,咱们只要根据乘数从个位一直到高位经过一个门电路来控制每位的输出信号,来判断和被乘数的结果是所有为0输出仍是把被乘数复制一份输出,并将结果存储并累加到某个寄存器上便可。
先拿乘数最右侧的个位乘以被乘数,而后把结果存入到寄存器中,而后,把被乘数左移一位,把乘数右移一位,仍然用乘数的个位去乘以被乘数,而后把结果加到刚才的寄存器上。反复重复这一步骤,直到二者分别不能再左移和右移位置。这样,乘数和被乘数其实仅仅须要简单的加法器(结果的累加),一个能够支持其左移一位的电路和一个右移一位的电路,以及一个开关(判断乘数的每位和被乘数乘积的结果是复制仍是0)就能完成整个乘法。 如图所示
这里的控制测试,其实就是经过一个时钟信号,来控制左移、右移以及从新计算乘法和加法的时机。
13 * 9 的具体竖列图
由上图的分解示意图,能够发现其实所谓的位移+加法
。并非彻底独立的,乘数的最高位在进行乘法运算以前任然须要低位的运算完才能够。因此咱们用的是 4 位数,因此要进行 4 组“位移 + 加法”的操做。并且这 4 组操做还不能同时进行。由于 下一组的加法要依赖上一组的加法后的计算结果,下一组的位移也要依赖上一组的位移的结果。这样,整个算法是“顺序”的,每一组加法或者位移的运算都须要必定的时间,及必定的等待时间。
若是要优化整个乘法器的运算,能够看到影响执行速度的缘由有以下几点:
位移+加法
运算都具备强关联及前后关系。解决办法就是把咱们的电路进行展开,首先针对第一点,咱们上面所看到的竖列图分析出所谓的每组位移+加法
的强关联关系及前后关系是由于咱们人分析,但其实对于计算机的电路而言,当相加的两个数是肯定的,那高位是否会进位其实也是肯定的。也就是说,对于计算机的电路而言,高位和地位能够同时出结果,电路是自然并行的,也就不存在所谓的强关联关系。同时对应的第三点的门延时也就只有一组加法进行运算的门延时存在了,即3T的门延时。
能够看到其实乘法器的实现方式共有两种:
定点数的表示方法:用 4 个比特来表示 0~9 的整数,那么 32 个比特就能够表示 8 个这样的整数。而后咱们把最右边的 2 个 0~9 的整数,当成小数部分;把左边 6 个 0~9 的整数,当成整数部分。这样,咱们就能够用 32 个比特,来表示从 0 到 999999.99 这样 1 亿个实数了。
用二进制来表示十进制的编码方式,叫做BCD 编码。
缺点:能表示数值过小,本来32位的数值表示方法,能表示的数值的最大值是42亿。用BCD编码的话最大只能表示到100w。
浮点数弥补了定点数表示方式在表达数值上缺陷。浮点数使用科学计数法的方式来进行数值的表示。 浮点数的科学计数法的表示有一个IEEE 754的标准,它定义了两个基本的格式。一个是用 32 比特表示单精度的浮点数,也就是咱们经常说的 float 或者 float32 类型。另一个是用 64 比特表示双精度的浮点数,也就是咱们平时说的 double 或者 float64 类型。
根据国际标准IEEE 754,任意一个二进制浮点数V能够表示成下面的形式:
例如:
IEEE 754规定,对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。 对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
由于E为8位,因此它的取值范围为0~255。因为科学计数法中的E是能够出现负数的,因此IEEE 754规定,E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。
好比,2^10的E是10,因此保存成32位浮点数时,必须保存成10+127=137,即10001001。
而后,指数E还能够再分红三种状况:
在这样的浮点数表示下,不考虑符号的话,浮点数可以表示的最小的数和最大的数,差很少是 1.17 * 10^-38
和 3.40 * 10^38
,表示的数值范围就大不少了。此时f为23个0,e为-126 和 f为23个1,e为127。
正是由于这个数对应的小数点的位置是“浮动”的,它才被称为浮点数。随着指数位 e 的值的不一样,小数点的位置也在变更。对应的,前面的 BCD 编码的实数,就是小数点固定在某一位的方式,咱们也就把它称为定点数。
0.1~0.9 这 9 个数,其中只有 0.5 可以被精确地表示成二进制的浮点数。而其余的都只是一个近似的表达。
参考: