first commit

This commit is contained in:
张乾
2024-10-16 13:06:13 +08:00
parent 2393162ba9
commit c47809d1ff
41 changed files with 9189 additions and 0 deletions

View File

@ -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能同时执行多条指令
欢迎你在留言区跟我交流互动,如果觉得这节课讲得不错,也推荐你分享给身边的朋友。

View File

@ -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语言的运行环境必须要有栈栈是现代计算机运行的基础数据结构。
这节课的导图如下,供你参考回顾。
思考题
你觉得堆、栈空间是虚拟内存空间吗?如果是,请问是在什么时候分配的物理内存呢?
期待你在留言区记录自己的思考或疑问,积极参与是提升学习效果的秘诀。如果觉得这节课不错,别忘了分享给更多朋友。

View File

@ -0,0 +1,171 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
加餐02 学习攻略(一):大数据&云计算,究竟怎么学?
你好我是LMOS。
上节课我带你了解了云计算中IAAS层的技术。结合云计算的分层架构下面一层就是PaaSPaaS与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结构化数据的分布式存储系统。
GFSGoogle文件系统是一种分布式文件系统它为Google的大型数据处理应用提供了数据存储和访问功能MapReduce是一种编程模型它允许开发人员更方便地处理大量数据而BigTable是一种高性能的分布式存储系统它可以处理海量的结构化数据。
如果学过今天内容你还觉得意犹未尽想要更深入地学习这三种技术建议阅读谷歌相关的论文和文档并尝试去做一下mit 6.824分布式系统课程提供的课后练习。
思考题
推荐你在课后能搜索GFS、MapReduce、BigTable这三篇原始论文阅读一下结合今天学到的设计过程的思路进一步思考这么设计的优点和缺点分别是什么还有什么改进空间
欢迎你在评论区和我交流讨论,如果觉得这节课内容还不错,也可以转发给你的朋友,一起学习进步。

View File

@ -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使用递增的事务IDzxid来标识事务。所有提案提交时都会附上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可以帮我们快速搭建一套大数据开发环境课后你有兴趣的话也推荐自己上手试试看。
欢迎你在留言区和我交流讨论,我们下节课见。