计算机相关技术脉络整理

隐藏

前言

试图用通俗的语言聊一聊计算机的相关知识体系,尽可能把重点脉络和原理讲清楚, 当然不可能面面俱到,也可能会用到不太准确但更通俗的说法。 主要谈原理,尽可能不涉及细节,因为细节很多时候不一定有用,且容易忘记,原理则可以知道来龙去脉,甚至借鉴一些其中的思想。

计算机程序如何执行

高低电平映射到01序列,01序列映射到其他

计算机从硬件的角度来看只懂得0和1(高电平、低电平), 然后通过与非门等逻辑电路组成更复杂的支持加法或者乘法的运算单元,进行运算。

例如打个比方八根排线,假设0为低电平,1为高电平,0000 0001这个最右边的排线假设赋予其一个数字1的含义, 0000 0010这个右二为高电平的排序赋予数字2的含义(排线的高低电平 映射到 01序列 映射到 我们认识的整数), 那么0000 0001这个电信号与0000 0010这个电信号输入到 由 与非门电路 组合而成的加法运算器中后,输出排线的电平为0000 0011于是得到数字3, 这里将0000 0010与2对应,以及其他对应整型数的方式比较流行的是补码, 补码流行的原因是对正数、负数的累加都一样,就不用为了正数、负数单独设计累加器。

除了与非门构成的运算结构,还需要保存一些中间运算结果,硬件名字不记得了,总之是能贮存电平的单元, 运算和贮存,这几乎就是组成计算机的硬件的最基本的原理构架了。

01序列映射的其他例子

因为计算机只懂得高电平,低电平,或者说01序列,实际上,我们需要用01序列去表示所有需要的东西,

首先是整型数字,也就是补码,也就是一种01序列映射到整型的一个函数。

然后是字符串,最著名的是ASCII编码, 例如将65(实际上对计算机而言是 0100 0001,65只是人比较好理解的十进制)映射为字母A, 所以当计算机看到0100 0001时,并且告诉计算机将其理解为ASCII字符时,计算机将其解读为字符A并执行接下来的操作,例如打印到屏幕上 (实际上是存储到某个特定的用于显示的内存区间)。 ASCII缺点在于位数太低,不能涵盖中日韩等字符,于是出现了unicode、UTF-8编码等,实际上就是对01序列不同的映射到字符的方式。

然后是浮点数来表示小数,通常采用IEEE-754标准。

而计算机的执行指令,也就是控制计算机对某个数字进行加法、乘法等运算的机器命令,也是01序列, 其映射规则称为指令集, 例如x86指令集,也比较流行的是arm构架指令集。 这些指令集都有对应的,最原始的人可以读懂的方式,称为汇编语言。 注意到不同的机器,如果其指令集不一样,其汇编语言也不一样。而且是机器直接执行的指令, 所以其规则是直接在硬件上写死的。

计算机如何运算

  1. 取指: 从指令的排线中,读取某一段指令序列。
  2. 译码: 判断要执行的操作,如果需要对数据进行运算还要从数据排线中读取数据(称为操作数)
  3. 运算: 执行运算,将输入数据输入到运算单元
  4. 回写: 保存运算后的数据,将其存到例如内存或者寄存器等地方

然后读取下一段指令序列,不断循环以上四个步骤。 至于下一段指令在哪里,是将指令的位置(地址,对应于C语言的指针)存放到曾为PC寄存器的位置, 每次通过读取PC寄存器,来找到下一段指令的门牌号(地址),执行取指、译码、运算、回写这四个步骤。 通常情况下PC寄存器是按01指令序列的顺序进行读写,但是也有跳转,例如对于C语言的循环或者函数调用就是跳转, 也就是设定PC寄存器内的值,使得下一个执行序列跳转到预期的地方。

这些操作需要保证一定的时序性,因为都共用了同一个数据线,否则后面的信号可能跟前面的信号弄混了, 所以为了保证其时序性,加入一个 …高低高低高低… 这样脉动变化的信号作为同步的时钟, 通常说的CPU I5有3.5 GHz 也即是说的这个时钟的频率。

计算机语言

汇编

计算机只懂得0和1,控制计算机,也就是构成计算机能够执行的正确的01序列,如上所说的x86、ARM等指令集, 这些指令集的文本表示称为汇编语言,也就是最接近计算机指令,同时又映射成人可以读懂的语言。 汇编语言即使类似与x86这样的机器码的文本表示,对应人可以读懂的,机器的执行命令, 这些命令绝大多数都是赋值、运算、根据条件跳转这样的机器可以做的事情。

但不同机器设计的汇编语言不一样,这样在一台机器上编写的代码,就不能在另外一台机器上跑, 尤其对于早期各类机器而言。

C

这时出现了一个对汇编语言的执行的某种通用的归纳(抽象),称为C语言, 这样机器虽然不同,但通过C语言的编译器编译成针对这台机器的汇编代码,再编译成机器执行指令的01序列就可以控制机器了。 C语言的最大贡献是对汇编语言执行结构的合理的抽象,同时又方便更高层次地思考问题,而不用太细化到机器指令这一层面, 例如循环结构,对应机器指令而言就是条件跳转,符合某个条件(例如某个数大于0), 就通过改变PC寄存器的值跳到当前执行序列前的某个地方,重复执行这一段01序列。

这样不同的机器都可以共用同一份代码,只要该机器上有C语言编译器就能执行, 通过C语言对汇编语言的抽象,这样就从需要考虑机器编码这一硬件执行过程的苦力中解脱出来, 只需要考虑逻辑问题。 所以总的来说C语言是对汇编的一层抽象,最接近汇编语言,所以执行效率很高且常用于系统编程。

稍微总结一下,所谓编程语言,就是人与机器之间交流的中间语言,我们看到的各种语言都是文本表示, 机器看得懂的是01序列,编译器(例如vc)就是将我们看得懂的文本语言翻译成机器看得懂的01序列。

编译器就是一个翻译官,读某种语言的源代码,理解其内容,映射到目标语言中。 目标语言不仅仅是01序列,也可以是其他,例如将C语言翻译成Fortran语言也可以。 例如本文的撰写格式采用的是markdown语法,通过某个翻译器(不专业的说法), 最终会转化成html格式,然后浏览器理解这段html格式,将其按照内容中的要求将内容显示在显示器上。 这里浏览器实际上是一个解释器,不同于编译器这样将一段程序翻译成01执行序列或者另一种程序的方式。 浏览器这样的解释器是理解html格式,然后按照其要求执行浏览器程序自己设定的01序列, html并没有被翻译成01序列,而是通过浏览器代理执行,或者说浏览器成了一个可以理解更高级内容的虚拟机

稍微跑一下题,进一步说,很多程序就是一个理解某个规则的片段并执行的过程,例如excel可以理解的csv文本格式如下:

Time,H2,CO,CH4,case_example,stages
0.05556,23.35235,3.10185,8.68601,case1,C1S1
0.11111,23.62906,3.55692,7.60854,case1,C1S1
0.16667,24.64287,3.62913,7.69976,case1,C1S1
0.22222,29.64828,5.03771,7.87253,case1,C1S1
0.27778,31.87434,4.77112,7.90599,case1,C1S1
0.33333,32.39345,4.02943,7.54947,case1,C1S1
0.38889,31.42271,3.50293,6.47658,case1,C1S1
0.44444,30.96054,4.52849,6.69443,case1,C1S1

就是解析表格内容,由逗号分割不同列,由回车符分割不同行。 这就是csv定义的读取数据的规则或者说语法。 假设Fluent生成的文件不符合tecplot的语法规则,那么tecplot的程序就读不懂Fluent生成的内容。

回到正题,不同的语言有的更接近人,有的更接近机器,例如汇编语言最接近机器,人看汇编语言就会很繁琐, ,例如一个函数调用,在C语言里看很容易一眼看懂,对应到机器码就会看着有些累,编写一个函数调用的机器码更累。 这就带来了编写效率的问题, 早期的语言例如Fortran语言是为了解决机器的执行效率问题,其语法特点方便优化,方便数值计算, 但从现在的角度来说,对人不太友好,例如其函数通常有非常多个参数,且命名很难读懂, 变量作用范围没有加以限制,某个地方出错了可能要找很远才发现问题,很容易出错。 现在的硬件条件已经很丰富了,绝大多数现代语言都是解决人的编写效率问题,

这里有一个问题,有没有执行效率高,同时人编写代码效率高的编程语言呢? 我个人认为,由于机器执行逻辑与人遇到的真实现实问题的差异导致很难实现。 执行效率高意味着对机器的精确控制,而代码编写效率高的一个方面就是尽可能减少考虑细节问题的负担, 而将精力放在现实问题上。

C++

C++想要成为a better C,提供C没有的一些能力,例如对象、模板等,同时尽可能考虑不降低性能损失, (当然,面向对象的设计风格,C也可以通过函数指针变相地实现,只是会麻烦一点) 可以简单的理解C++是C的超集,目的是让人更有效率地的进行机器底层的开发。 因此现代操作系统、游戏引擎、机器学习后端引擎、数值模拟软件等需要与具体机器打交道、需要考虑性能调优的场景 多采用C++开发。

需要说明的是C++注重于开发效率与优化,添加了模板元编程、面向对象等多个范式,因此不可避免地其语法复杂度较高, 也有各种语法的坑,容易出现一些其他高级语言不会出现的一些意想不到的错误, 所以需要了解其各种坑和细节,我差不多死磕C++一年半,也只能说稍微理解一点其面向对象的内容, 很多编程十年的也不敢说精通C++,其创始人也不敢说完全懂C++, 这些细节和坑对于初步学习编程并没有什么用,所以并不是一个初学编程的好选择。

另外,C++注重效率意味着其编译器设计也很复杂,对于较小的单片机之类的程序而言,其生成的代码可能会有些臃肿, 因此类似单片机之类的小型机器多采用C语言。

c为了适用于不同的硬件条件,对于有些地方,例如int类型是32为还是16为并没有严格的限制,这样适配了高中低端不同机器, 但是这可能依赖于某种条件的编程带来一定的问题,于是很多跨机器的C/C++程序都是首先用某种方式(例如宏)检测编译器和硬件平台是什么, 以此实现一份代码适用于所有机型。

Java

接着说一下Java,总体而言,Java的设计初衷很可能是为了让程序员更好地写应用级程序, 而不是写跟机器直接打交道的系统级程序,因此相对于C/C++做了诸多改进或者限制。

例如Java选择将Integer类型采用固定的32位长度, 同时它不直接生成具体针对某个机器的机器码01序列,而是设计了一个称为JVM的虚拟机,并设计一个通用的汇编规则, 运行的程序先生成符合这个通用的类似汇编语言的01序列(称为字节码), 然后JVM读取这一段字节码,代理机器执行任务。 这样只要某个机器上有针对该机器的JVM就能执行相应程序, 而优化之类的事情就交给JVM处理,甚至可以边运行边优化。

另外Java是纯粹的面向对象语言,而且不让程序员直接使用指针这一危险的操作,并用GC代替程序员free(pointer)手动释放不需要的内存。 (因为指针可以理解为地址,指向函数的地址、指向内存的地址等,就好比收拾东西, 由Java自动帮你收拾整理,而不用自己收拾整理,因为人工的就容易出错)

总之,Java为了让程序员更好地写应用级的程序,进一步代替程序员与具体的机器打交道,并限制了一些程序员容易出错的地方,同时也进一步贯彻面向对象的思想。 相同的语言还包括微软的C#。

Java的优势在于写应用程序,并提供了良好的生态,很多都有现成的框架或者库,所以程序员开发Java的应用程序快很多。

再跑个题,所谓框架,就好比建一栋房子,把架子已经搭好了,也预设了一些门窗,你不喜欢可以换掉,房间可以自己按需求布置, 但基本结构已经帮你处理好了,只用填对应的东西。优点是开发速度快,缺点是可定制性被限制了。 就好比造火箭,已经把各个组件设计好了,只需要组装一下,或者少量地方更改一下,这样普通人也可以”造火箭”,但不可能用这个造船。 可以把Fluent视为一个流体力学的数值计算框架。 而库,或者说函数库,通常是提供工具,例如找房子提供水泥,砖头,打桩机等等,但不会预设一些房子的骨架给你,因为库不知道你用这些工具干什么, 所以也可以用来造桥、修路。例如Lapack,或者C语言中的#include <stdio>,stdio这些应该都算作库。 总之纯粹的只有语言自身是没有用的,必须有好用的框架、库,有很多同行经验才行。

回到正题,Java优势在于代理程序员与机器打交道,让程序员专注于应用开发,并有良好的生态,包括框架、库和诸多从业者,使得开发程序更为方便、快速。 但Java语言本身设计上实际上不如C#,只不过Java生态比C#发展地好,所以流行度比C#更高。

Python

Java的一个问题是语言设计上的不灵活,导致有些功能需要绕一大圈实现,为了代码良好的复用性,也衍生出各种设计模式,所以Java代码通常冗长啰嗦。 而后起之秀Python则以其语法简洁著称,Life is short, so I use Python,开发更为简洁方便。 我对Python了解不够深入,故不能深入分析其优劣, Python的生态非常好,无论Web开发、网络爬虫、大数据、人工智能、科学计算(有取代matlab的趋势)等各个领域都有良好的生态。

学习编程语言的选择

如果需要跟硬件打交道,需要学习C,并了解汇编,可以看王爽的《汇编语言》,如果想进一步了解一段程序是如何运行的可以看《程序员的自我修养》

如果想学习面向对象,从更高层次上思考软件构架,可以学习Java或C#,建议学C#,语言是想通的,学一门再学另一门很快,且能更好地理解该语言设计的优劣。

不推荐学习C++,除非要开发计算图形学引擎,数值算法库,深度学习算法库等高深内容时, 而且应该是在通过C#或Java掌握了面向对象思想,以及通过汇编了解了底层后再学习C++, 因为C++的书籍绝大多数都涉及的太深入的细节上。 如果把编程语言比作棋,C++的书都是在讲下棋的规则,而且很厚很复杂,而少涉及初涉棋坛更实用的下棋的策略。

如果做科研,Python应该是非常值得一学的语言,因为可以做的事情非常多,例如可以操作文件,自动处理excel表格, 科学绘图等等,当某个过程似乎可以自动化实现时,不妨考虑用Python,随便搜一些教程,用中学,而且Python是一门高级语言,不需要涉及底层细节,Python在Windows下的安装,推荐Anaconda或者著名的JetBrain的Pycharm,都是免费的软件, 书籍方面因为我涉猎不多,就不推荐了。

如果对Web计算机网络感兴趣,需要学习HTML/css/Javascript,而网络的基础需要看《计算机网络:自顶向下》这本书。

如果想进一步深入到函数式编程领域,顺便熟悉下递归,可以学习Scheme,推荐看《SICP》

当然,编程的基础,算法与数据结构,作为基础还是要掌握的。 如果把编程比作下棋,学习语言是学习的是规则,算法与数据结构学习的是局部策略,而面向对象,设计模式,测试驱动开发等是通盘考虑的大局观。

工作中编程语言的选择

应该视情况而定,例如不能因为Fortran已经落伍了就全盘否定, 假设开发一个程序源码就是Fortran,改成C或者C++固然好,但工作量太大,就没必要。

原则应该依据情况怎么方便,怎么好维护怎么来。 例如该领域最常用的语言是什么,因为这通常意味着好的生态,无论文档资料、相关库与框架以及从业人员都比较不错, 生态的重要性往往大于技术本身,例如Javascript语言从语言设计的角度上来说,有很多的问题,但许多更先进的语言依然撼动不了它在Web编程中的地位,

而有些程序有自己设计的领域专用语言(Domain Special Language),例如matlab的语言,Aspen的语言,这些都是模仿一下现学现用。 总之,语言是工具,只是当下那个用着趁手用哪个,只要熟悉了常规的编程思路(基本功是算法与数据结构)、几种编程范式(例如面向对象),学其他相似的语言就非常快。

如何学习编程语言

面对一门还没有学过的新语言,我的学习步骤通常是首先根据文档或者资料写一些简单的例子,将这门语言大体轮廓熟悉一遍, 然后在我需要用编程实现某些自动化脚本或功能时可能想到了用这门语言,脑海里形成一定的思路,然后上网查每一个步骤怎么写。

在我已经掌握了一些相似语言的情况下会考虑为什么这门语言会这样设计,但在我刚开始进入编程领域,其实很多是不懂的,也没关系, 补充完整相关知识后,无论学习速度还是深度都比较好。关键在实践中学习,也就是得写代码,用于实际。

另外每个人学习习惯不一样,这只是我个人认为较高效率的方式。

操作系统

从前述内容中了解到,让机器执行任务只需要通过编程,让编译器翻译生成机器能够读得懂的01序列,由机器执行即可, 有些认为极为简单,几乎是单一的流程化的任务,例如单片机,这就可以直接写好程序后烧录进单片机就能按照要求运行。

但更复杂的机器,例如个人电脑,需要执行通用的任务,而且有时候是多个任务,需要调度任务。 另一方面,多个任务都用的同一块内容,如果一个任务用了某个内存地址用来做某件事情,另一个任务在不知道的情况下也用了同一块地址,改写了其中的内容,那么就会造成故障, 所以需要一个代理来调度任务分配存储空间(包括内存和硬盘上存储的文件),以及跟各种外部硬件打交道,如鼠标、键盘打印机,驱动这些硬件,

这个代理就是操作系统,其他的程序不直接与硬件打交道,而是跟操作系统打交道,操作系统提供与这些硬件打交道的接口,实际与硬件打交道的是操作系统,由操作系统代理完成。

这样做的一个好处是,硬件只需要跟操作系统之间有一个通信规则,或者说标准、契约、规范,那么无论是A厂家生产的键盘还是B厂家生产的键盘,程序不需要管, 只需要调用操作系统提供的接口即可,这样就可以适配不同的硬件。

另一个好处是内存交由操作系统管理避免了一些危险的操作,例如早期的程序对内存访问没有权限控制,那么就很容易通过改写某个内存,将其中的内容改成自己的01序列, 就实现的病毒攻击之类, 现代的操作系统对于内存管理有严格的访问权限控制,除了操作系统自身的疏漏之外,病毒很难入侵。

编程思想

就跟传统工程领域一样,软件工程领域也是在断的试错中发展起来的, 编程发展至今有诸多实践,也总结了诸多实践经验,这些经验形成了一些有用的思想。

总体而言,我认为目标就是更好的管理软件工程, 那么怎样才算好呢? 我认为主要是:

  1. 开发更高效
  2. 代码人更容易看懂,更方便理解
  3. 凡是人容易出错的地方,可以由代码自动化完成的地方都自动化
  4. 容易尽早暴露错误,因为错误越晚发现,越难修改,损失也越大
  5. 出错了易于诊断错误来源
  6. 易于修改,而不是为了改动某个地方需要附带改动很多地方
  7. 用户使用后,易于更新

以下探讨一些常见的思想

面向对象设计思想

面向对象与面向过程是两种不同的编程组织风格, 面向对象软件设计方式可以说是当前设计复杂软件最为流行的范式, 相信很多人都听说过面向对象,但深入了解面向对象却并不容易, 我也是在编程一年后才逐渐理解其思想,在这里我试图简短说明其思想。

面向过程

理解面向对象首先理解面向过程, 不严格的说面向过程是以函数为中心进行分析,数据作为函数的输入,被函数“加工”后输出, 例如sin(x),输入x,输出得到sin(x),就像一个工厂的流水线一样, 数据进入一道函数,输出成一个半成品,再输入到下一道函数中“加工”。 早期的软件设计大多是按照面向过程设计,

正因为将数据的处理过程看作是流水线,往往带有强烈的时序性在里面, 例如数值模型软件,首先将所有变量初始化,然后将网格相关的初始化,生成离散的网格点,然后根据数学模型构建偏微分方程组,然后迭代求解计算,收敛时输出结果。

面向对象也会带有时序性的思考,而且也离不开时序性的分析, 但是不会像面向过程这样将整个数据按照流水线式那么强烈的对整个数据进行流程化的设计。 而是在一个对象内才涉及更细分的时序分析, 在生成一个对象时,初始化该对象直接相关的一些特征量,例如初始化一个对象的写法通常类似于如下方式:

Person mike = new Person(Name: "mike", Gender: Male, Age: 21);

那么,这样做的好处是什么呢?如果是面向过程,按照工厂流水线式的分析,下游往往受上游的状态的影响, 也就是函数与函数之间很可能存在一些相互依赖的关系,而这种依赖关系只有设计时头脑才是清晰的, 知道一个函数正确的调用可能会受到某个因素的影响,但过一段时间就忘了,或者其他人想要看懂就得不断追根溯源, 这使得问题变得复杂,增加思考的负担,不便于维护。

问题出在哪里呢?在面向过程中,是通过将某一整块相关的过程封装成函数的方式进行结构优化, 也就是以过程为组织单元, 而面向对象以对象为组织单元,对象就是一组内聚性很强的函数与数据。 一种理解的方法可以将面向过程理解为以函数为中心, 而面向对象以相互关联的一组函数与一组数据为中心思考。 而数据与函数之间,函数与函数之间往往存在某种划分关系,相同域内,函数与数据紧密相关。

例如一个空调的控制程序,打开空调AirCondition.Open(), 调节温度AirCondition.SetTemperature(int temp)等操作可以看作是函数, 这些函数需要空调的状态量AitConditionStatus, 例如调节空调温度首先得确保空调的状态是开启的状态, 所有这两个函数与这个状态量之间是紧密联系而不是独立存在的,

而面向对象的思考组织方式更符合现实, 更有利于讲问题划分到一个有限的范围内进行分析、调试。

层次结构的思想

一些设计原则

纯软件开发与传统开发的区别与启示

测试驱动开发

待续

-----EOF-----

Categories: programming Tags: computer