first commit
This commit is contained in:
@ -0,0 +1,166 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
01 CISC & RISC:从何而来,何至于此
|
||||
你好,我是LMOS。
|
||||
|
||||
这个专栏我会带你学习计算机基础。什么是基础?
|
||||
|
||||
基础就是根,从哪里来,到哪里去。而学习计算机基础,首先就要把握它的历史,这样才能了解计算机是怎么一步步发展到今天这个样子的,再根据今天的状况推导出未来的发展方向。
|
||||
|
||||
正所谓读历史方知进退,明兴衰。人类比其它动物高级的原因,就是人类能使用和发现工具。从石器时代到青铜器时代,再到铁器时代,都是工具种类和材料的发展,推动了文明升级。
|
||||
|
||||
让我们先从最古老的算盘开始聊起,接着了解一下机械计算机、图灵机和电子计算机。最后我会带你一起看看芯片的发展,尤其是它的两种设计结构——CISC与RISC。
|
||||
|
||||
从算盘到机械计算机
|
||||
|
||||
算盘就是一种辅助计算的工具,由中国古代劳动人民发明,迄今已有两千多年的历史,一直沿用至今。我准备了算盘的平面草图,你可以感受一下:
|
||||
|
||||
|
||||
|
||||
上图中周围一圈蓝色的是框架,一串一串的是算椽和算珠,一根算椽上有七颗算珠,可以上下拨动,从右至左有个、十、百……亿等计数位。有了算盘,计算的准确性和速度得到提高,我们从中可以感受到先辈的智慧。
|
||||
|
||||
与其说算盘是计算机,还不如说它是个数据寄存器。“程序”的执行需要人工实现,按口诀拨动算珠。过了两千多年,人们开始思考,能不能有一种机器,不需要人实时操作就能自动完成一些计算呢?
|
||||
|
||||
16世纪,苏格兰人John Napier发表了论文,提到他发明了一种精巧设备,可以进行四则运算和解决方根运算。之后到了18世纪,英国人Babbage设计了一台通用分析机。这期间还出现了计算尺等机械计算设备,主要是利用轴、杠杆、齿轮等机械部件来做计算。
|
||||
|
||||
尤其是Babbage设计的分析机,设计理论非常超前,既有保存1000个50位数的“齿轮式储存室”,用于运算的“运算室”,还有发送和读取数据的部件以及负责在“存储室”、“运算室”运算运输数据的部件。具体的构思细节,你有兴趣可以自行搜索资料探索。
|
||||
|
||||
一个多世纪之后,现代电脑的结构几乎是Babbage分析机的翻版,无非是主要部件替换成了大规模集成电路。仅此一点,Babbage作为计算机系统设计的“开山鼻祖”,就当之无愧。
|
||||
|
||||
值得一提的是,Babbage设计分析机的过程里,遇到了一位得力女助手——Ada。虽说两人的故事无从考证,但Ada的功劳值得铭记,她是为分析机编写程序(计算三角函数的程序、伯努利函数程序等)的第一人,也是公认的世界上第一位软件工程师。
|
||||
|
||||
又过了一个世纪,据说美国国防部花了十年光阴,才把开发军事产品所需的全部软件功能,都归纳整理到了一种计算机语言上,期待它成为军方千种计算机的标准。1981年,这种语言被正式命名为ADA语言。
|
||||
|
||||
可惜的是,这种分析机需要非常高的机械工程制造技术,后来政府停止了对他们的支持。尽管二人后来贫困潦倒,Ada也在36岁就英年早逝,但这两个人的思想和为计算机发展作出的贡献,足以彪炳史册,流芳百世。
|
||||
|
||||
图灵机
|
||||
|
||||
机械计算机有很多缺点,比如难于制造,难于维护,计算速度太慢,理论不成熟等。这些难题导致用机械计算机做通用计算的话,并不可取。
|
||||
|
||||
而真正奠定现代通用计算机理论的人,在20世纪初横空出世,他就是图灵,图灵奖就是用他名字命名的。
|
||||
|
||||
图灵在计算可行性和人工智能领域贡献卓越,最重要的就是提出了图灵机。
|
||||
|
||||
图灵机的概念是怎么来的呢?图灵在他的《论可计算数及其在判定问题中的应用》一文中,全面分析了人的计算过程。他把计算提炼成最简单、基本、确定的动作,然后提出了一种简单的方法,用来描述机械性的计算程序,让任何程序都能对应上这些动作。
|
||||
|
||||
该方法以一个抽象自动机概念为基础,不但定义了什么“计算”,还首次将计算和自动机联系起来。这对后世影响巨大,而这种“自动机”后来就被我们称为“图灵机”。
|
||||
|
||||
图灵机是一个抽象的自动机数学模型,它是这样运转的:有一条无限长的纸带,纸带上有无限个小格子,小格子中写有相关的信息。纸带上有一个读头,读头能根据纸带小格子里的信息做相关的操作,并且能来回移动。
|
||||
|
||||
如果你感觉文字叙述还不够形象,我再来画一幅示意图:
|
||||
|
||||
|
||||
|
||||
我们不妨想象一下,把自己写的一条条代码,放入上图纸带的格子中,随着读头的读取代码做相应的动作。读头移动到哪一个,就会读取哪一格的代码,然后执行相应的顺序、跳转、循环动作,完成相应计算工作。
|
||||
|
||||
如果我们把读头及读头的运行规则理解为CPU,把纸带解释为内存,把纸带上信息理解为程序和数据,那这个模型就非常接近现代计算机了。在我看来,以最简单的方法抽象出统一的计算模型,这就是图灵的伟大之处。
|
||||
|
||||
电子计算机
|
||||
|
||||
图灵机这种美好的抽象模型,如果没有好的实施方案,是做不出实际产品的,这将是一个巨大的遗憾。为此,人类进行了多次探索,可惜都没有结果。最后还是要感谢弗莱明和福雷斯特,尽管他们一个是英国人,一个是美国人。
|
||||
|
||||
这两个人本来没什么交集,不过后来福雷斯特在弗莱明的真空二极管里,加上了一个电极(一种栅栏式的金属网,形成电子管的第三个极),就获得了可以放大电流的新器件,他把这个新器件命名为三极管,也叫真空三极管。这个三极管提高了弗莱明的真空二极管的检波灵敏度。
|
||||
|
||||
不过,一个三极管虽然做不了计算机,但是许多个三极管组合起来形成的数字电路,就能够实现布尔代数中的逻辑运算,电子计算机的大门自此打开。
|
||||
|
||||
1946年,ENIAC成功研制,它诞生于美国宾夕法尼亚大学,是世界上第一台真正意义上的电子计算机。
|
||||
|
||||
ENIAC占地面积约170平方米,估计你在城里的房子也放不下这台机器。它有多达30个操作台,重达30吨,耗电量150千瓦。
|
||||
|
||||
别说屋子里放不下,电费咱们也花不起。这台机器包含了17468根电子管和7200根晶体二极管,1500个继电器,6000多个开关等许多其它电子元件,计算速度是每秒5000次加法或者400次乘法,大约是人工计算速度的20万倍。
|
||||
|
||||
但是三极管也不是完美的,因为三极管的内部封装在一个抽成真空的玻璃管中,这种方案在当时是非常高级的,但是仍然不可靠,用不了多久就会坏掉了。电子计算机一般用一万多根三极管,坏了其中一根,查找和维护都极为困难。
|
||||
|
||||
直到1947年12月,美国贝尔实验室的肖克利、巴丁和布拉顿组成的研究小组,研制出了晶体管,问题才得以解决。现在我们常说的晶体管通常指的是晶体三极管。
|
||||
|
||||
晶体三极管跟真空三极管功能一样,不过制造材料是半导体。它的特点在于响应速度快,准确性高,稳定性好,不易损坏。关键它可以做得非常小,一块集成电路即可容纳十几亿到几十亿个晶体管。
|
||||
|
||||
这样的器件用来做计算机就是天生的好材料。可以说,晶体管是后来几十年电子计算机飞速发展的基础。没有晶体管,我们简直不敢想像,计算机能做成今天这个样子。具体是如何做的呢?我们接着往下看。
|
||||
|
||||
芯片
|
||||
|
||||
让我们加点速,迈入芯片时代。我们不要一提到芯片,就只想到CPU。
|
||||
|
||||
CPU确实也是芯片中的一种,但芯片是所有半导体元器件的统称,它是把一定数量的常用电子元件(如电阻、电容、晶体管等),以及这些元件之间的连线,通过半导体工艺集成在一起的、具有特定功能的电路。你也可以把芯片想成集成电路。
|
||||
|
||||
那芯片是如何实现集成功能的呢?
|
||||
|
||||
20世纪60年代,人们把硅提纯,切成硅片。想实现具备一定功能的电路,离不开晶体管、电阻、电容等元件及它们之间的连接导线,把这些集成到硅片上,再经过测试、封装,就成了最终的产品——芯片。相关的制造工艺(氧化、光刻、粒子注入等)极其复杂,是人类的制造极限。
|
||||
|
||||
正因为出现了集成电路,原先占地广、重量大的庞然大物才能集成于“方寸之间”。而且性能高出数万倍,功耗缩小数千倍。随着制造工艺的升级,现在指甲大小的晶片上集成数十亿个晶体管,甚至在一块晶片上集成了CPU、GPU、NPU和内部总线等,每秒钟可进行上10万亿次操作。在集成电路发展初期,这样的这样的性能是不可想像的。
|
||||
|
||||
下面我们看看芯片中的特例——CPU,它里面包括了控制部件和运算部件,即中央处理器。1971年,Intel将运算器和控制器集成在一个芯片上,称为4004微处理器,这标志着CPU的诞生。到了1978年,开发的8086处理器奠定了X86指令集架构。此后,8086系列处理器被广泛应用于个人计算机以及高性能服务器中。
|
||||
|
||||
那CPU是怎样运行的呢?CPU的工作流程分为以下 5 个阶段:取指令、指令译码、执行指令、访存读取数据和结果写回。指令和数据统一存储在内存中,数据与指令需要从统一的存储空间中存取,经由共同的总线传输,无法并行读取数据和指令。这就是大名鼎鼎的冯诺依曼体系结构。
|
||||
|
||||
CPU运行程序会循环执行上述五个阶段,它既是程序指令的执行者,又被程序中相关的指令所驱动,最后实现了相关的计算功能。这些功能再组合成相应算法,然后由多种算法共同实现功能强大的软件。
|
||||
|
||||
既然CPU的工作离不开指令,指令集架构就显得尤其重要了。
|
||||
|
||||
CISC
|
||||
|
||||
从前面的内容中,我们已经得知CPU就是不断地执行指令,来实现程序的执行,最后实现相应的功能。但是一颗CPU能实现多少条指令,每条指令完成多少功能,却是值得细细考量的问题。
|
||||
|
||||
显然,CPU的指令集越丰富、每个指令完成的功能越多,为该CPU编写程序就越容易,因为每一项简单或复杂的任务都有一条对应的指令,不需要软件开发人员写大量的指令。这就是复杂指令集计算机体系结构——CISC。
|
||||
|
||||
CISC的典型代表就是x86体系架构,x86 CPU中包含大量复杂指令集,比如串操作指令、循环控制指令、进程任务切换指令等,还有一些数据传输指令和数据运算指令,它们包含了丰富的内存寻址操作。
|
||||
|
||||
有了这些指令,工程师们编写汇编程序的工作量大大降低。CISC的优势在于,用少量的指令就能实现非常多的功能,程序自身大小也会下降,减少内存空间的占用。但凡事有利就有弊,这些复杂指令集,包含的指令数量多而且功能复杂。
|
||||
|
||||
而想实现这些复杂指令,离不开CPU运算单元和控制单元的电路,硬件工程师要想设计制造这样的电路,难度非常高。
|
||||
|
||||
到了20世纪80年代,各种高级编程语言的出现,大大简化了程序的开发难度。
|
||||
|
||||
高级语言编写的代码所对应的语言编译器,很容易就能编译生成对应的CPU指令,而且它们生成的多条简单指令,跟原先CISC里复杂指令完成的功能等价。因此,那些功能多样的复杂指令光环逐渐黯淡。
|
||||
|
||||
说到这里,你应该也发现了,在CPU发展初期,CISC体系设计是合理的,设计大量功能复杂的指令是为了降低程序员的开发难度。因为那个时代,开发软件只能用汇编或者机器语言,这等同于用硬件电路设计帮了软件工程师的忙。
|
||||
|
||||
随着软硬件技术的进步,CISC的局限越来越突出,因此开始出现了与CISC相反的设计。是什么设计呢?我们继续往下看。
|
||||
|
||||
RISC
|
||||
|
||||
每个时代都有每个时代的产物。
|
||||
|
||||
20世纪80年代,编译器技术的发展,导致各种高级编程语言盛行。这些高级语言编译器生成的低级代码,比程序员手写的低级代码高效得多,使用的也是常用的几十条指令。
|
||||
|
||||
前面我说过,文明的发展离不开工具的种类与材料升级。指令集的发展,我们也可以照这个思路推演。芯片生产工艺升级之后,人们在CPU上可以实现高速缓存、指令预取、分支预测、指令流水线等部件。
|
||||
|
||||
不过,这些部件的加入引发了新问题,那些一次完成多个功能的复杂指令,执行的时候就变得捉襟见肘,困难重重。
|
||||
|
||||
比如,一些串操作指令同时依赖多个寄存器和内存寻址,这导致分支预测和指令流水线无法工作。另外,当时在IBM工作的John Cocke也发现,计算机80%的工作由大约20%的CPU指令来完成,这代表CISC里剩下的80%的指令都没有发挥应有的作用。
|
||||
|
||||
这些最终导致人们开始向CISC的反方向思考,由此产生了RISC——精简指令集计算机体系结构。
|
||||
|
||||
正如它的名字一样,RISC设计方案非常简约,通常有20多条指令的简化指令集。每条指令长度固定,由专用的加载和储存指令用于访问内存,减少了内存寻址方式,大多数运算指令只能访问操作寄存器。
|
||||
|
||||
而CPU中配有大量的寄存器,这些指令选取的都是工程中使用频率最高的指令。由于指令长度一致,功能单一,操作依赖于寄存器,这些特性使得CPU指令预取、分支预测、指令流水线等部件的效能大大发挥,几乎一个时钟周期能执行多条指令。
|
||||
|
||||
这对CPU架构的设计和功能部件的实现也很友好。虽然完成某个功能要编写更多的指令,程序的大小也会适当增加,更占用内存。但是有了高级编程语言,再加上内存容量的扩充,这些已经不是问题。
|
||||
|
||||
RISC的代表产品是ARM和RISC-V。其实到了现在,RISC与CISC早已没有明显界限,开始互相融合了,比如ARM中加入越来越多的指令,x86 CPU通过译码器把一条指令翻译成多条内部微码,相当于精简指令。x86这种外CISC内RISC的选择,正好说明了这一点。
|
||||
|
||||
历史的车轮滚滚向前,留下的都是经典,历史也因此多彩而厚重,今天的课程就到这里了,我们要相信,即便自己不能改写历史,也能在历史上留下点什么。我们下一节课见,下次,我想继续跟你聊聊芯片行业的新贵RISC-V。
|
||||
|
||||
重点回顾
|
||||
|
||||
今天我们一起完成了一次“穿越之旅”,从最早的算盘、机械计算机,现代计算机雏形的图灵机,一路讲到芯片和CPU的两种指令架构集。
|
||||
|
||||
其实仅仅一节课的时间,很难把计算机的历史一一道来,所以我选择了那些对计算机产生和演进最关键的事件或者技术,讲给你听。我把今天的重点内容为你梳理了一张思维导图。
|
||||
|
||||
-
|
||||
有了这些线索,你就能在脑海里大致勾勒出,计算机是如何一步步变成今天的样子。技术发展的“接力棒”现在传到了我们这代人手里,我对未来的发展充满了期待。
|
||||
|
||||
就拿CPU的发展来说,我觉得未来的CPU可能是多种不同指令集的整合,一个CPU指令能执行多类型的指令,分别完成不同的功能。不同类型的指令由不同的CPU功能组件来执行,有的功能组件执行数字信号分析指令,有的功能组件执行图形加速指令,有的功能组件执行神经网络推算指令……
|
||||
|
||||
思考题
|
||||
|
||||
为什么RISC的CPU能同时执行多条指令?
|
||||
|
||||
欢迎你在留言区跟我交流互动,如果觉得这节课讲得不错,也推荐你分享给身边的朋友。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,194 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
25 堆&栈:堆与栈的区别和应用
|
||||
你好,我是LMOS。
|
||||
|
||||
在上一课中,我们讲了虚拟内存和物理内存,明白了虚拟内存是一个假想的地址空间,想要真正工作运行起来,就必须要经过MMU把虚拟地址转换成物理地址,寻址索引到真正的DRAM。
|
||||
|
||||
今天,我们继续深入到应用程序的虚拟内存地址空间中,弄清楚一个常规应用程序的虚拟内存地址空间中都有哪些东西。首先,我们看看里面的整体布局,然后看看里面的堆与栈,最后我还会重点带你了解一下堆与栈的区别和应用场景。
|
||||
|
||||
课程的配套代码你可以从这里下载。
|
||||
|
||||
应用程序的虚拟内存布局
|
||||
|
||||
你可以把应用程序的虚拟内存,想成一个房子。房子自然要有个合理的布局,有卧室、客厅、厨房这些不同的房间。同样地,应用程序的虚拟内存,承载着应用程序的指令、数据、资源等各种信息。
|
||||
|
||||
既然我们想要观察应用程序的虚拟内存布局,首先得有一个应用程序。当然,你也可以观察系统正在运行的应用程序,但是这些应用往往是很复杂的。
|
||||
|
||||
为了找到一个足够简单、又能说明问题的观察对象,我们还是自己动手写一个应用,代码如下所示:
|
||||
|
||||
#include "stdio.h"
|
||||
#include "stdlib.h"
|
||||
#include "unistd.h"
|
||||
//下面变量来自于链接器
|
||||
extern int __executable_start,etext, edata, __bss_start, end;
|
||||
int main()
|
||||
{
|
||||
char c;
|
||||
printf("Text段,程序运行时指令数据开始:%p,结束:%p\n", &__executable_start, &etext);
|
||||
printf("Data段,程序运行时初始化全局变量和静态变量的数据开始:%p,结束:%p\n", &etext, &edata);
|
||||
printf("Bss段,程序运行时未初始化全局变量和静态变量的数据开始:%p,结束:%p\n", &__bss_start, &end);
|
||||
while(1)
|
||||
{
|
||||
printf("(pid:%d)应用程序正在运行,请输入:c,退出\n", getpid());
|
||||
printf("请输入:");
|
||||
c = getchar();
|
||||
if(c == 'c')
|
||||
{
|
||||
printf("应用程序退出\n");
|
||||
return 0;
|
||||
}
|
||||
printf("%c\n", c);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
我来给你解释一下这个简单的应用程序,开始的三个printf函数会输出该应用程序自身的三大段,即Text段、Data段、Bss段的开始、结束地址,这些地址由链接器产生,都在应用程序的虚拟内存空间中。Text段、Data段、Bss段中包含了什么,在代码里我已经做了说明,只是Bss段并不在应用程序文件中占有空间,而是操作系统加载应用程序时,动态分配空间并将其初始化为0。
|
||||
|
||||
由于我们要观察应用程序在运行中的虚拟内存布局,这就需要人为地控制应用程序退出,而不是不直接运行完就退出,导致我们没办法观察。所以,我们要在一个死循环中,输出应用程序对应进程的id和提示信息,然后等待我们下一步的输入。如果输入c则退出,否则输出信息,继续循环。
|
||||
|
||||
你现在需要把这个应用程序编译并运行起来。其实这个工作并不复杂,只需要进入对应的工程目录,make一下,再make run就可以把程序运行起来了。
|
||||
|
||||
要如何才能观察到应用程序的虚拟内存布局呢?这在Windows下非常困难,但是Linux对开发人员很友好,它提供了一个proc文件系统,这个目录下有所有应用程序进程的相关信息,每个进程一个文件夹,文件夹的名称就是进程的id,这就是上述代码中要打印进程的pid的原因。
|
||||
|
||||
每个进程目录下,包括一个maps和smaps文件,后者更为详细,我们只要用后面的命令读取它们就行了。
|
||||
|
||||
sudo cat /proc/59916/maps > main.map
|
||||
#或者
|
||||
sudo cat /proc/59916/smaps > main.map
|
||||
|
||||
|
||||
上述命令是我机器上的情况,59916是我运行程序后给出的pid,上述命令就是把/proc/59916/maps 或者 smaps 读取输出到main.map文件中,我们打开main.map文件,看到的情况如下图所示:
|
||||
|
||||
|
||||
|
||||
对照截图我们可以看到,每一行都表示一个应用进程虚拟内存中的一个区段。第一列表示该区间的起始、结束虚拟地址。第二列是该区段的属性,r代表读、w代表写、x代表执行、p代表私有。最后一列是该区段的内容属于哪个文件。
|
||||
|
||||
我们发现,一个应用程序运行之后,它的虚拟内存中不仅仅有它自身的指令和数据,main.elf一共有5个区段,包含了text、data、bss,还有其它的文件内容,比如共享动态链接库。共享动态链接库也是一种程序,可以通过应用调用其功能接口。
|
||||
|
||||
同时,我们也注意到了后面要详细探索的堆、栈,我为你画幅图总结一下,如下所示:
|
||||
|
||||
|
||||
|
||||
应用程序自身的段,取决于编译器和链接器的操作,堆段、内存映射段、栈段、环境变量和命令行参数段,这取决于操作系统的定义。需要注意的是,堆段和栈段的大小都是动态增加和减少的、且增长方向相反。堆是向高地址方向增长,栈是向低地址方向增长。这就是一个应用程序被操作系统加载运行后的虚拟内存布局。
|
||||
|
||||
堆
|
||||
|
||||
下面我们将重点关注堆和栈。我们经常把堆栈作为一个名词,连在一起说,但这其实并不准确。因为堆是堆而栈是栈,这是两个不同的概念,不可以混为一谈。
|
||||
|
||||
在计算机学科里,堆(heap)是一类特殊的数据结构的统称,我们通常把堆看作一棵树的数组对象。堆具备这样两个性质:一是堆中某个结点的值总是不大于或不小于其父结点的值;二是堆总是一棵完全二叉树。
|
||||
|
||||
不过,我们今天要关注的重点,是操作系统为应用程序建立的堆。所以这节课要探讨的“堆”,不具有数据结构中对堆定义的完整特性,你可以只把它看作一个可以动态增加和减少大小的数组对象。
|
||||
|
||||
简单点说,堆就是应用程序在运行时刻调用malloc函数时,动态分配的一块儿内存区域,有了它,就能满足应用程序在运行时动态分配内存空间,从而存放数据的需求了。
|
||||
|
||||
你可以结合后面的示意图来理解。
|
||||
|
||||
|
||||
|
||||
由上图可以看出,堆其实是虚拟内存空间中一个段,由堆的开始地址和结束地址控制其大小,有一个堆指针指向未分配的第一个字节。所以,堆在本质上是指应用程序在运行过程中进行动态分配的内存区域,堆的管理通常在库函数中完成。
|
||||
|
||||
之所以叫做堆,是因为通常会使用堆这种数据结构来管理分配出来的这块内存,但也可以使用更简单的方法来管理,下面让我们看看Linux是如何对堆区进行操作的。
|
||||
|
||||
|
||||
|
||||
关于如何得到上图右边的map文件,可以参考前面应用程序虚拟内存布局的那部分内容。
|
||||
|
||||
上图代码中的sbrk函数是库函数,它会调用Linux内核中的brk系统调用。这个brk系统调用,用于增加或者减少进程的mm_struct中的堆区指针brk。
|
||||
|
||||
由于堆区指针始终指向未分配的堆区空间,brk系统调用会首先保存当前的brk到临时的tmpbrk,然后让当前brk加上传进来的大小,赋给brk,最后返回tmpbrk,这样就实现了堆区内存的分配。你可以看到图中三次调用sbrk函数返回的地址,确实落在应用程序的堆区内。
|
||||
|
||||
分配的地址也是从低到高,这也验证了我们之前所说的堆的增长方向。你也可以自行阅读Linux内核中,brk系统调用函数的代码进行考证,尽管内核代码中的细节很多,但核心逻辑和我们这里描述的相差无几。
|
||||
|
||||
堆也有界限,虽然可以调整,但却不能无限增加其大小。堆到底可以“占多大面积”,这取决于虚拟地址空间的大小和操作系统的定义。
|
||||
|
||||
在堆区分配内存速度很快,为什么呢?根据前面的信息可知,在堆区分配内存,只需要增加堆指针就行了,因此分配速度很快。由于实现分配的大小与请求分配大小是相同的、且地址也是连续的,所以它不会有内存碎片的情况。
|
||||
|
||||
但这个分配方式有一个致命的缺点,释放堆区中的内存不会立即见效。比如上述代码中,分配了alloc2之后,释放alloc,虽然这时currheap与alloc2之间有空闲内存,这时也是不能分配的,由此产生了内存空洞,只有等alloc2也释放了,内存空洞才会消失。
|
||||
|
||||
现在我们已经知道了操作系统为应用程序建立的堆,不同于数据结构中的堆。应用程序的堆区,不过是一个动态增加或减少的内存空间,用于应用程序动态分配内存,它的分配性能很好,但会产生内存空洞。
|
||||
|
||||
好,堆就说到这里,我们接下来去研究栈。
|
||||
|
||||
栈
|
||||
|
||||
说到栈,你应该想到存储货物的仓库或者供旅客歇脚住宿的客栈,那么引入到计算机领域里,就是指数据暂时存储的地方,所以才有了后面的压栈、出栈的说法。
|
||||
|
||||
虽然应用程序的堆区和数据结构中的堆不是一回事儿,但应用程序的栈区确实就是数据结构的那个栈。栈是支持程序运行的基本数据结构,它跟计算机硬件,比如CPU的栈指针寄存器、操作系统息息相关,还跟编译器关系密切。
|
||||
|
||||
我们先来看看栈的本质是什么,再分析它怎么用。
|
||||
|
||||
栈作为一种数据结构,相当于只能在一端进行插入和删除操作的特殊线性表。它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后进入的数据在栈顶,需要读数据的时候从栈顶开始弹出数据,最后进入的数据会被首先读出来。
|
||||
|
||||
你可以把栈想象成一个桶,你往桶里压入东西就是压栈,你从桶里拿出东西就是出栈。但是要记住,你只能从桶的最上面开始拿,这就是栈。如下图所示:
|
||||
|
||||
|
||||
|
||||
由于CPU的硬件特性,导致栈是从内存高地址向内存低地址增长的,所以实际应用程序中的栈更像是一个倒立的桶,栈其实也像一个反过来的堆。
|
||||
|
||||
栈有两个基本的操作:压栈和出栈,有时也称为压入和弹出。压入操作就是栈指针减去一个栈中对象的大小,然后将对象写入栈指针对应的内存空间中;而弹出是将栈指针指向的对象读出,然后将栈指针加上一个栈中对象的大小,从而指向栈中的前一个对象。
|
||||
|
||||
前面我们说过栈是和计算机硬件相关的,那是因为CPU很多指令都依赖于栈,例如x86 CPU的 call、ret、push、pop等指令,push和pop是栈的压入和弹出指令,call是函数调用指令,它把下一条指令的地址压入栈中,而ret指令则将call指令压入栈中的地址弹出,实现函数返回。
|
||||
|
||||
栈还和编译器,特别跟C语言编译器有关,这是因为我们在函数中定义的局部变量,就是放在栈中的。C语言编译器会生成额外的代码,来为局部变量在栈中分配和释放空间,自动处理各个变量的生命周期,不需要程序员手动维护,更不用担心局部变量导致内存泄漏,因为C函数返回时会自己从栈中弹出变量。栈的先进后出的特性,能保证被调用函数可以使用调用者函数的数据,反过来就不行了。
|
||||
|
||||
另一个重点是函数的调用和返回,也是依赖于栈,所以C语言想要正常工作,必须要有栈才行。下面我们写代码验证一下。
|
||||
|
||||
我们来写两个函数,主要就是打印自身的三个局部变量的地址,stacktest2函数被stacktest1函数调用,而stacktest1函数最终会被main函数所调用。打印这些局部变量的地址,是为了方便我们查看这些变量放在了内存的什么地方。代码如下:
|
||||
|
||||
void stacktest2()
|
||||
{
|
||||
long val1 = 1;
|
||||
long val2 = 2;
|
||||
long val3 = 3;
|
||||
printf("stacktest2运行时val1地址:%p val2地址:%p val3地址:%p\n", &val1, &val2, &val3);
|
||||
return;
|
||||
}
|
||||
void stacktest1()
|
||||
{
|
||||
long val1 = 1;
|
||||
long val2 = 2;
|
||||
long val3 = 3;
|
||||
printf("stacktest1运行时val1地址:%p val2地址:%p val3地址:%p\n", &val1, &val2, &val3);
|
||||
stacktest2();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
按照前面的描述,C函数的局部变量是放在栈中的,现在我们运行这个程序看一看,运行截图如下所示:
|
||||
|
||||
|
||||
|
||||
由上图可以看出,两个函数的三个变量都落在了应用程序的栈区,我们可以用课程开头的命令得到图中的map文件,就可以看到应用程序栈区的地址区间的范围了。
|
||||
|
||||
再结合前面说的栈区空间是从高地址向低地址方向增长继续分析。我们首先看到的是stacktest1函数的三个变量,其地址从高到低每次会下降8个字节,这就是因为long类型在64位系统上占用8字节的空间。然后是stacktest2函数的三个变量,它们的地址要远低于stacktest1函数的三个变量的地址,这是因为stacktest2函数是被stacktest1函数调用的。
|
||||
|
||||
现在我们已经知道了,栈是现代计算机运行不可缺少的基础数据结构。本质上,栈就是动态增长的内存空间,它遵守先进后出的原则,在此基础上就定义了两个操作:压入和弹出。
|
||||
|
||||
重点回顾
|
||||
|
||||
今天我们学习了应用虚拟内存布局。需要区分清楚的是,堆是堆、栈是栈,它们之间区别很大。理解了这节课,相信你也能清晰地把堆和栈的本质讲给身边的同学了。
|
||||
|
||||
现在我们来回顾一下这节课的重点内容。首先,我们从应用程序的虚拟内存空间布局出发,了解了应用程序虚拟内存空间中都有什么。除了程序自身的指令和数据,虚拟内存空间里包括有堆区、内存映射区、栈区、环境变量与命令行参数区。
|
||||
|
||||
然后,我们重点研究了堆,发现应用程序虚拟内存空间的堆区,跟数据结构里的堆并不是一回事儿,它只是一个可以从低地址向高地址动态增长的内存空间,用于应用程序动态分配内存。
|
||||
|
||||
最后,我们探讨了栈。硬件、应用程序、高级语言编译器,都需要栈。它是一种地址由高向低动态增长的内存空间,并且定义了压栈、出栈两个操作,遵守先进后出的原则。C语言的运行环境必须要有栈,栈是现代计算机运行的基础数据结构。
|
||||
|
||||
这节课的导图如下,供你参考回顾。
|
||||
|
||||
|
||||
|
||||
思考题
|
||||
|
||||
你觉得堆、栈空间是虚拟内存空间吗?如果是,请问是在什么时候分配的物理内存呢?
|
||||
|
||||
期待你在留言区记录自己的思考或疑问,积极参与是提升学习效果的秘诀。如果觉得这节课不错,别忘了分享给更多朋友。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,171 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
加餐02 学习攻略(一):大数据&云计算,究竟怎么学?
|
||||
你好,我是LMOS。
|
||||
|
||||
上节课,我带你了解了云计算中IAAS层的技术。结合云计算的分层架构,下面一层就是PaaS,PaaS与IaaS相似,区别在于云服务提供商还提供了操作系统和数据库。
|
||||
|
||||
这节课,我们就一起了解一下云计算PaaS层的大数据体系吧。什么是大数据呢?其实这是早在1980年出版的图书《第三次浪潮》里就预见到的一种场景,而具体到工程落地层面,就不得不提到Google的“三驾马车”。
|
||||
|
||||
今天这节课,我想从需求角度,和你讨论一下在工程上为什么要这样设计。
|
||||
|
||||
GFS的核心问题
|
||||
|
||||
我们先从谷歌文件系统GFS开始说起。
|
||||
|
||||
顾名思义,这个系统是用来储存文件的。你可能觉得,存文件听起来好像不难呀?
|
||||
|
||||
我们可以仔细思考一下,存文件会有什么难度呢?先让我们停下手头的工作,看看自己电脑上的硬盘空间还有多大,500G还是1TB、5TB?
|
||||
|
||||
没错,空间容量就是我们遇到的第一个门槛,单台电脑的存储空间确实不是无限大的。
|
||||
|
||||
接下来,我们找出一份大一点的文件,把它复制到另一个目录,看看复制速度如何?这里就碰到了第二个问题——文件写入速度。一般来说,机械盘硬盘的最高写入速度是200MB/s左右,而固态硬盘的写入速度是3000MB/s左右。
|
||||
|
||||
试想一下,如果我们有1TB的数据写入硬盘(就算真的有一块1TB空间的固态硬盘可以使用)那我们也至少需要4天时间,数据才能完全写入完毕。
|
||||
|
||||
还有一个生活中常见的问题,你遇到过电脑故障、死机或者硬盘坏掉的情况么?是的,在普通PC机器运行的过程中,故障其实是常态。你平常用家里的网络打网游时遇到过丢包、掉线、卡顿之类的情况么?没错,网络故障确实也是我们要考虑的问题。
|
||||
|
||||
那么到底怎样才能设计一套文件系统,同时满足以下条件呢:
|
||||
|
||||
|
||||
容量“无限”大;
|
||||
对大容量的数据读写性能高;
|
||||
遇到软硬件问题时,系统可靠性也很高。
|
||||
|
||||
|
||||
这里就要用到问题切分和并行化的思想了,这些我们在[第四十节课]也讲过。
|
||||
|
||||
比如想要解决文件比较大的问题,就我们可以考虑把它切分成很多份。切分完了之后,我们还得想到鸡蛋(文件)放在一个篮子里,遇到故障“全军覆没”的风险。为此,咱们就得多搞几台机器,多存几份呗。
|
||||
|
||||
还担心存的比较慢?那我们就把多个文件并行存储到不同的硬盘上,这样就不会受到磁盘写入速度的限制了。
|
||||
|
||||
说到这里,你现在是不是已经跃跃欲试,想要开始实现一套分布式文件系统啦?别着急,让我们先把刚刚讨论到的设计思路梳理一下:
|
||||
|
||||
首先,为了不给使用者应用程序增加太多负担,我们还是希望用户能像以前单机读写文件一样通过简单的API就能完成文件读写。这时候,我们就需要抽象出一套统一的客户端client,提供给用户使用。
|
||||
|
||||
其次,是切分成很多份文件。GFS会把每一份文件叫做一个chunk,这个chunk大小的默认值是64MB,这比操作系统上的文件系统要大一些,这么做为了减少GFS client和GFS master的交互次数、提升文件读取性能。同时,为了保障可靠性,GFS还会为每个chunk保留三个副本。
|
||||
|
||||
但是这里还有个问题没解决,文件都切成很多份存到很多机器上了,我们怎么知道哪一个chunk存到哪里去了呢?这时候,我们就需要把这种chunk分片文件映射到存储位置、原始文件名、权限之类的关联关系抽象出来,我们把这类用来找数据的数据叫做元数据信息。
|
||||
|
||||
那么元数据存在哪里好一点?
|
||||
|
||||
聪明的你可能已经想到了,我们可以给这些服务器分一下类,让老大master带着小弟chunkserver来干活儿,元数据比较重要,所以咱们就交给老大来保管。有了这些思路,相信你再看 GFS论文中的架构图时,就会感觉清晰很多。
|
||||
|
||||
|
||||
|
||||
MapReduce的分分合合
|
||||
|
||||
接下来,我们再说说MapReduce。
|
||||
|
||||
我们首先要搞清楚MapReduce是什么,当看到MapReduce时,你可能感觉它是一个概念,但其实不然,MapReduce应该是Map、Reduce,是两个概念,即映射和归约。
|
||||
|
||||
用软件实现这两个概念,就会形成Map、Reduce两个操作,落实到代码中可能是两个接口函数、或者库,又或者是进程。我们可以把这些东西,理解成一套编程模型。
|
||||
|
||||
那么什么是Map呢?Map字面意思为映射,但本质是拆分。
|
||||
|
||||
接下来,我们以汽车为例,看一下我们把一辆完好的汽车执行Map操作之后的状态,如下图所示:
|
||||
|
||||
|
||||
|
||||
从上图可以看出,执行map操作时,汽车首先作为输入,然后标记出汽车的各种零部件,最后汽车被拆分成各种零件。
|
||||
|
||||
现在。让我们切换一下视角,把这辆汽车转换成用户的大规模数据,于是就变成了对一个大数据进行标记,然后拆分成许多小数据的过程,这就是MapReduce中的Map操作。
|
||||
|
||||
什么又是Reduce呢?Reduce的字面意思为归约,是Map操作是逆向操作,其本质是合并。同样地,我们以汽车为例,看看一辆被Map操作的汽车,在Reduce的操作下,会变成什么样子。如下图所示:
|
||||
|
||||
|
||||
|
||||
我们可以看到,执行Reduce操作时,是之前把Map汽车产生的各个零件作为输入,然后进行各种零部件的组装,最后合并生成汽车,或者是更高级的类汽车产品。
|
||||
|
||||
同样地,把这辆汽车各种零部件换成用户Map后的各种小数据,就相当于合并许多个小数据,然后生成原来的大数据或者对数据进行更高级的处理,这就是MapReduce中Reduce操作的作用。
|
||||
|
||||
我们刚刚把一台车子进行了一大波MapReduce操作,这台车子就变成了变形金刚了,哈哈。 举个例子,理解了MapReduce的原理之后,我们再来看一下它的六大步骤。
|
||||
|
||||
如果你是家大型汽车生产厂家, 你拥有许多不同类型的汽车设计方案(Input),还拥有许多汽车零件供应商,不同的汽车零件供应商会主动挑选不同的汽车零件(Split),挑选好之后你就把汽车生产方案进行拆解(Map)。
|
||||
|
||||
之后,再把不同的零件下发到不同供应商的生产车间生产(Shuffle),最后要能根据不同的顾客需求,取用不同的零件拼装成最终的汽车,这就是Reduce。拼装好汽车之后,会放到售卖部那边等待客户取货(Ticket),这个过程是Finalize。
|
||||
|
||||
所以MapReduce是六大过程,简单来说,就是 Input、Split、Map、Shuffle、Reduce和 Finalize。那么这六大步骤又是怎样被一套框架管理起来的呢?答案其实还是老大(Master)带着小弟(Worker)干活。
|
||||
|
||||
下面,我们结合MapReduce的架构图,分析一下它的工作原理。
|
||||
|
||||
|
||||
|
||||
我们的用户程序要想使用MapReduce,必须要链接MapReduce库。有了MapReduce库就可以进行Map、Reduce操作了。
|
||||
|
||||
用户程序运行后先声明数据有多少,然后需要将它们拆分成一些Mapper和Reducer来执行。假如把数据分成n份,那就要找n个Mapper来处理。这时会产生许多Worker,这些Worker有的是执行Map操作的,有的 Worker是执行Reduce操作的。
|
||||
|
||||
最重要的是还会产生一个 Master Worker,它与其他Worker的等级是相同的,它会调度其它Worker运行,并作为用户的代理来协调整个过程,让用户可以做其他事情。
|
||||
|
||||
Master Worker会让一个Worker去处理0号数据,另一个Worker负责处理1号数据等等,这就是分配数据的过程。每个Worker都会在本地处理数据,并把结果写入缓存或硬盘。当执行Map操作的 Worker完成任务后,Master Worker会让执行Reduce操作的Worker去获取数据。
|
||||
|
||||
他们会从各个Worker那里获取需要的数据,并在本地完成Reduce操作,最后将结果写入最终的文件中,这就是Finalize。这个过程其实就是前面说过的六个步骤。
|
||||
|
||||
BigTable
|
||||
|
||||
最后一驾马车就是BigTable。在说它之前,我们先聊聊表。
|
||||
|
||||
请和我一起思考一下,什么是表呢?为了更好理解,我们可以抄出Excel这个神器,来仔细认识一下表的基本构成:
|
||||
|
||||
|
||||
|
||||
不难发现,表是由一个又一个的格子构成的,而每一个格子里的内容,又能通过行和列的坐标定位到。
|
||||
|
||||
这时候我们不妨联想一下,是不是我们只需要存储足够多的格子,就可以存储各种各样的表啦。那么光有行、列和格子里内容就足够了么?
|
||||
|
||||
并非如此,别忘了格子里的内容还有可能会修改。比如上图中的B1单元格里的Linux版本需要从1.0.5更新到1.0.6,因此还需要记录格子的时间。
|
||||
|
||||
没错,BigTable其实也是这样的思路,BigTable把每个格子的数据都抽象成了Key Value的键值对的格式。其中,key是由行(row:string)、列(column:string)、时间戳(time:int64)这三部分构成的,而Value则是用string来存储的。
|
||||
|
||||
这样的Key Value数据结构有没有让你联想到什么?其实它就类似于我们数据结构中常用的HashMap。但这个HashMap有点特殊,因为它还要支持后面这几种功能:
|
||||
|
||||
|
||||
给定几个key,能够快速返回小于或者等于某个key的那个数据。
|
||||
给定key1和key2,可以返回key3值中最高的数据。
|
||||
key也可以只给前缀格式prefix,返回所有符合前缀的值。
|
||||
这个“HashMap”在读、写性能上,都要相对比较好。
|
||||
这个“HashMap”要能持久化,因为数据不能丢。
|
||||
|
||||
|
||||
有了上述功能的约束,你是不是感觉一时半会儿还真没想出来,要怎么设计这个数据结构?
|
||||
|
||||
其实Google已经把这个数据结构设计好了,这个数据结构叫做SSTable,具体实现确实有些复杂,但好在有官方开源的单机实现——LevelDB。后面还有基于LevelDB演进升级的RocksDB,也是一个不错的项目,感兴趣的话可以自行了解。
|
||||
|
||||
现在,我们有了把表化简成小格子,再把每个格子使用Key Value结构存储到了单机的“HashMap”数据结构上。接下来,我们还得想清楚,如何让单机的“HashMap”数据结构变成可以分布式运算的。
|
||||
|
||||
这时候,我们就可以把前面这个思路做进一步抽象,你可以结合后面的示意图看一下,具体是抽象成了三层:
|
||||
|
||||
|
||||
|
||||
首先,对于每个表,我们都需要保存这个表的元数据。
|
||||
|
||||
其次如果随着数据增长,表变得比较大了,我们需要具备自动切分这张表的能力。切分表的最小单位我们叫做Tablet,也就是说,一张表会对应一个或多个Tablet。
|
||||
|
||||
具体到每一个Tablet,我们是基于一个或多个单机的“HashMap”数据结构,也就是SSTable来实现的;而每一个SSTable中存储的,又是一堆用Key Value格式表示的单元格。
|
||||
|
||||
对应到服务上,我们又可以套用前面讲的老大带小弟干活(主从架构)的思路,把一个或者多个Tablet交给Tablet Server这一类小弟(服务)来干活儿。而老大(Master)主要负责为Tablet服务器分配Tablets、检测新加入的或者过期失效的Tablet服务器、对Tablet服务器进行负载均衡、对保存在GFS上的文件做垃圾收集、处理和模式相关的修改操作(比如建立表和列族)。
|
||||
|
||||
理清了思路,你再来看看后面这张架构图,是不是就很容易理解了呢?
|
||||
|
||||
|
||||
|
||||
小结
|
||||
|
||||
今天,我们主要了解了现代云计算PAAS层中,大数据体系的由来。其中最核心的就是谷歌的三驾马车,即谷歌文件系统GFS、面向大型集群的简化数据处理MapReduce、BigtTable结构化数据的分布式存储系统。
|
||||
|
||||
GFS(Google文件系统)是一种分布式文件系统,它为Google的大型数据处理应用提供了数据存储和访问功能;MapReduce是一种编程模型,它允许开发人员更方便地处理大量数据;而BigTable是一种高性能的分布式存储系统,它可以处理海量的结构化数据。
|
||||
|
||||
如果学过今天内容,你还觉得意犹未尽,想要更深入地学习这三种技术,建议阅读谷歌相关的论文和文档,并尝试去做一下mit 6.824分布式系统课程提供的课后练习。
|
||||
|
||||
思考题
|
||||
|
||||
推荐你在课后能搜索GFS、MapReduce、BigTable这三篇原始论文阅读一下,结合今天学到的设计过程的思路,进一步思考这么设计的优点和缺点分别是什么,还有什么改进空间?
|
||||
|
||||
欢迎你在评论区和我交流讨论,如果觉得这节课内容还不错,也可以转发给你的朋友,一起学习进步。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,192 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
加餐03 学习攻略(二):大数据&云计算,究竟怎么学?
|
||||
你好,我是LMOS。
|
||||
|
||||
上节课我们从谷歌的三驾马车开始,学习了大数据三件套的设计思路,可惜谷歌三驾马车作为商用软件,只开放了论文来讲解原理,却并没有开放出对应的源代码。
|
||||
|
||||
为了帮你更好地理解这些核心技术是怎么落地的,这节课我会简单介绍一下另外三个基础组件的设计原理,它们也是开源大数据生态中的重要角色。
|
||||
|
||||
HDFS设计原理
|
||||
|
||||
首先我们来说说HDFS,它的全称是Hadoop Distributed File System,你可以理解为一个可以由低成本的普通PC机组成的大规模分布式文件系统。
|
||||
|
||||
HDFS的架构图如下所示:
|
||||
|
||||
|
||||
|
||||
其实,HDFS的核心架构和[上节课]讲过的GFS,架构思路是一脉相承的。
|
||||
|
||||
HDFS基于主/从架构设计,其集群的核心是由NameNode(充当主服务器)、DataNode(充当从服务器)、Client这三部分构成的。各部分的含义和功能,你可以参考后面这张表:-
|
||||
|
||||
|
||||
通过这几个组件的配合,我们就拥有了一个可靠的分布式文件系统。
|
||||
|
||||
那么HDFS有哪些优势呢?主要是后面这四点:
|
||||
|
||||
|
||||
容错性:可以在集群中的任意节点发生故障时继续运行,这能保证数据的安全性。
|
||||
大数据处理能力:HDFS可以存储海量的数据,并支持大规模并行计算。
|
||||
高可靠性:HDFS将文件分割成多个块存储,并在集群中多次复制,可以保证数据的高可靠性。
|
||||
简单易用:HDFS提供了简单易用的文件存储和访问接口,与其他系统集成很方便。
|
||||
|
||||
|
||||
但是,HDFS也有一些不足,具体包括:
|
||||
|
||||
|
||||
性能相对较低:不适合低延迟的数据访问。
|
||||
不支持随机写入:不支持随机写入,只能进行顺序写入。
|
||||
对小文件不友好:不能很好地存储小文件,因为它需要将小文件分割成大块存储,而这会导致存储和计算效率低下。
|
||||
|
||||
|
||||
总之,HDFS能够高效地存储海量数据,并支持大规模并行计算。但是,HDFS 不适合用于低延迟的数据访问,也不适合用于存储小文件。
|
||||
|
||||
说到这,我们就不难推测HDFS的适用场景了——它适合用在海量数据存储和大规模数据分析的场景中,例如搜索引擎、广告系统、推荐系统等。
|
||||
|
||||
YARN设计原理
|
||||
|
||||
其实早期Hadoop也按照Google Mapreduce的架构,实现了一套Mapreduce的资源管理器,用于管理和调度MapReduce任务所需要的资源。但是JobTracker存在单点故障,它承受的访问压力也比较大,这影响了系统的可扩展性。另外,早期设计还不支持MapReduce之外的计算框架(比如Spark、Flink)。
|
||||
|
||||
正是因为上述问题,Hadoop才做出了YARN这个新的Hadoop资源管理器。YARN的全称是Yet Another Resource Negotiator,让我们结合架构图了解一下它的工作原理。-
|
||||
|
||||
|
||||
根据架构图可见,YARN由ResourceManager、NodeManager、JobHistoryServer、Containers、Application Master、job、Task、Client组成。
|
||||
|
||||
YARN的架构图中的各个模块的功能,你可以参考后面这张表格:-
|
||||
|
||||
|
||||
了解了每个模块大致的功能之后,我们再看看YARN运行的基本流程吧!-
|
||||
|
||||
|
||||
到YARN运行主要是包括后面表格里的八个步骤。-
|
||||
|
||||
|
||||
其实我们计算的每一个MapReduce的作业,也都是通过这几步,被YARN资源管理器调度到不同的机器上运行的。弄懂了YARN的工作原理,对“Hadoop大数据生态下如何调度计算作业到不同容器做计算”这个问题,你会更容易理解。
|
||||
|
||||
然而,解决了存储和计算问题还不够。因为大数据生态下需要的组件非常多,各种组件里还有很多需要同步、订阅或通知的状态信息。如果这些信息没有一个统一组件处理,那整个分布式系统的运行都会失控,这就不得不提到一个重要的协调组件——ZooKeeper了。
|
||||
|
||||
ZooKeeper设计原理
|
||||
|
||||
ZooKeeper集群中包含Leader、Follower以及Observer三个角色。
|
||||
|
||||
Leader负责进行投票的发起和决议,更新系统状态,Leader是由选举产生的。Follower用于接受客户端请求并向客户端返回结果,在选主过程中会参与投票。
|
||||
|
||||
Observer的目的是扩展系统,提高读取速度。Observer会从客户端接收请求,并将结果返回给客户端。Observer可以接受客户端连接,也可以接收读写请求,并将写请求转发给Leader。但是,Observer不参与投票过程,只同步Leader的状态。
|
||||
|
||||
后面是ZooKeeper的架构图:
|
||||
|
||||
-
|
||||
在其核心,Zookeeper使用原子广播来保持服务器同步。实现这种机制的协议称为Zab协议,它包括恢复模式(用于主选择)和广播模式(用于同步)。
|
||||
|
||||
当服务启动或leader崩溃后,Zab协议进入恢复模式。恢复模式结束时,leader已经当选,大多数服务器已经同步完成leader的状态。这种状态同步可以确保leader和Server的系统状态相同。
|
||||
|
||||
为了保证事务序列的一致性,ZooKeeper使用递增的事务ID(zxid)来标识事务。所有提案提交时都会附上zxid。Zxid为64位整数,高32位表示领导人关系是否发生变化(每选出一个领导者,就会创建一个新的epoch,表示当前领导人所属的统治时期),低32位用于增量计数。
|
||||
|
||||
在工作期间,每个服务器都有三种状态:
|
||||
|
||||
|
||||
LOOKING:表示当前服务器不知道该领导者,正在寻找他。
|
||||
LEADING:表示当前Server为已当选的leader。
|
||||
FOLLOWING:表示该leader已经当选,当前Server正在与该leader同步。
|
||||
|
||||
|
||||
通过这样一套可靠的一致性协议和架构设计,Zookeeper把用户改变数据状态的操作,抽象成了类似于对文件目录树的操作。这样就简化了分布式系统中数据状态协调的难度,提高了分布式系统运行的稳定性和可靠性。
|
||||
|
||||
综合应用与环境搭建
|
||||
|
||||
学了这么多基础概念,我们来挑战一个综合性问题。假设在一个大型Hadoop集群中,你作为系统管理员需要解决这样一个问题——如何保证数据的安全性?
|
||||
|
||||
你会如何解决呢,使用哪些HDFS、YARN、ZooKeeper中的哪些功能,为什么这样选择呢?你可以自己先思考一下,再听听我的想法。
|
||||
|
||||
为了保证数据的安全性,我们可以使用HDFS的多副本机制来保存数据。在HDFS中,我们可以将文件分成若干块存储在集群中的多个节点上,并设置每个块的副本数量。这样,即使某个节点出现故障,也可以通过其他节点上的副本来恢复数据。
|
||||
|
||||
此外,还可以利用YARN的资源管理功能来控制集群中节点的使用情况,以避免资源过度使用导致的数据丢失。
|
||||
|
||||
最后,我们还可以利用ZooKeeper的分布式锁功能,来保证集群中只有一个节点可以访问某个文件。这样多个节点同时写入同一个文件造成的数据冲突,也能够避免。
|
||||
|
||||
总的来说,综合使用HDFS的多副本机制、YARN的资源管理功能以及Zookeeper的分布式锁功能,可以帮我们有效保证数据的安全性。
|
||||
|
||||
接下来就让我们动手搭建一套大数据开发环境吧。大数据开发环境搭建一般环节比较多,所以比较费时。为了节约部署时间,提高开发效率,我比较推荐使用Docker部署。
|
||||
|
||||
首先,我们先安装好Docker和docker-compose。
|
||||
|
||||
要安装Docker,一共要执行六步操作。第一步,在终端中更新软件包列表:
|
||||
|
||||
sudo apt update
|
||||
|
||||
|
||||
第二步,安装依赖包:
|
||||
|
||||
sudo apt install apt-transport-https ca-certificates curl software-properties-common
|
||||
|
||||
|
||||
第三步,添加Docker的官方GPG密钥:
|
||||
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
|
||||
|
||||
|
||||
第四步,在系统中添加Docker的存储库:
|
||||
|
||||
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
|
||||
|
||||
|
||||
第五步,更新软件包列表并安装Docker:
|
||||
|
||||
sudo apt update
|
||||
sudo apt install docker-ce
|
||||
|
||||
|
||||
第六步,启动Docker服务并将其设置为开机启动:
|
||||
|
||||
sudo systemctl start docker
|
||||
sudo systemctl enable docker
|
||||
|
||||
|
||||
安装完Docker,接下来我们来还需要执行两个步骤,来安装 Docker Compose。首先我们要下载Docker Compose可执行文件,代码如下:
|
||||
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
|
||||
|
||||
第二步,为Docker Compose可执行文件设置执行权限:
|
||||
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
|
||||
|
||||
现在,Docker和Docker Compose都安好了。为了确认安装是否成功,可以使用后面的命令验证:
|
||||
|
||||
docker --version
|
||||
docker-compose --version
|
||||
|
||||
|
||||
接下来,我们就可以启动大数据项目了。首先需要使用命令克隆仓库:
|
||||
|
||||
git clone https://github.com/spancer/bigdata-docker-compose.git
|
||||
|
||||
|
||||
然后,我们打开项目目录运行下面的命令:
|
||||
|
||||
docker-compose up -d
|
||||
|
||||
|
||||
等待项目启动成功,我们就可以使用Hadoop生态的各个组件,做更多的探索实验啦。
|
||||
|
||||
总结
|
||||
|
||||
这节课我们学到了开源大数据生态中的三个重要角色,它们是Hadoop大数据平台的基础,负责了文件存储、资源管理和分布式协调。
|
||||
|
||||
HDFS是Hadoop的分布式文件系统,它可以将海量数据分布在集群中的多个节点上进行存储,采用多副本机制保证数据安全。
|
||||
|
||||
YARN是Hadoop的资源管理系统,负责调度任务并管理资源。
|
||||
|
||||
ZooKeeper是分布式协调服务,提供分布式锁、队列、通知等功能,常用于分布式系统的配置管理、分布式协调和集群管理。
|
||||
|
||||
了解了这些组件的原理之后,我们还一起分析了一道综合应用题帮你加深理解。最后,动手环节也必不可少,利用Docker,可以帮我们快速搭建一套大数据开发环境,课后你有兴趣的话也推荐自己上手试试看。
|
||||
|
||||
欢迎你在留言区和我交流讨论,我们下节课见。
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user