diff --git a/basic/09-drive-your-computer-5.md b/basic/09-drive-your-computer-5.md index c28f3a5..aa9f832 100644 --- a/basic/09-drive-your-computer-5.md +++ b/basic/09-drive-your-computer-5.md @@ -12,20 +12,20 @@ 用最简单的话来说,操作系统负责管理计算机的软硬件资源,并给用户和其他软件提供接口和环境,是计算机中**最基础的软件**。 -很显然,对于一个程序开发者而言,直接面向硬件编写程序是费时费力的。这样的话,大量的程序可能都会涉及到一些相似的功能,比如如何处理来自键盘等设备的输入,再比如如何管理文件与内存。从“合并同类项”的角度来看,建立一个新的抽象层次来负责管理系统资源相关的事项是自然且合理的。顺带一提,这种**建立抽象层次**的设计模式在计算机中是相当常见的。 -抽象意味着上层次只需要知道下层次可以满足他们的某项需求,而无需了解这项需求是如何被实现的。这种设计理念可以让每一个层次专注于它们自己应该实现的功能,而避免被来自其他层次的细节所困扰。如果一台电脑更换了 cpu,我们显然不会重写运行在这台电脑上的所有程序,而是仅仅将 cpu 调用模块相关的代码“更换”掉。将不同的功能**封装**在不同的模块里所带来的好处是显而易见的,而这种封装就是抽象的一种体现。 +显然,对于程序开发者而言,直接面向硬件编写程序费时费力。大量程序可能涉及相似功能,比如如何处理来自键盘等设备的输入,再比如如何管理文件与内存。从“合并同类项”的角度来看,建立一个新的抽象层次来负责管理系统资源相关的事项是自然且合理的。顺带一提,这种**建立抽象层次**的设计模式在计算机中是相当常见的。 +抽象意味着上层次只需要知道下层次可以满足他们的某项需求,而无需了解这项需求是如何被实现的。这种设计让每个层次专注于自身功能,避免被其他层次细节困扰。如果一台电脑更换了 CPU,我们显然不会重写运行在这台电脑上的所有程序,而是仅仅将 CPU 调用模块相关的代码“更换”掉。将不同的功能**封装**在不同的模块里所带来的好处是显而易见的,而这种封装就是抽象的一种体现。 操作系统可以被看做一个功能很强大的封装模块。在有了操作系统之后,我们可以实现更方便地实现一些功能,同时实现一些没有操作系统就实现不了的功能。以下有一些简单的例子: -- 哪怕在只有一颗 cpu 的电脑上,也可以做到同时写代码和听音乐。但很明显,音乐播放器和代码编辑器的代码中不会包含自己如何和别的程序同时运行的功能,cpu 也不是生来就会同时运行多个程序。操作系统通过微观上协调多个程序**交替**执行,而在宏观上表现为多个程序**同时**执行。这种**并发**技术是操作系统的一个重要特征。 -- 在实际上的电脑使用中,我们会遇到各种各样的错误,一个软件的小错误就导致整台电脑蓝屏甚至硬件损坏显然是不可接受的。虽然程序内部也应该包含错误处理机制,但遇到一些不负责的程序或者程序遇到了一些它们自己处理不了的情况时,操作系统的错误处理机制是必要的。比如在信科相关的课上,会有同学尝试编写生成上万个子进程却不负责销毁的代码,再比如有些同学会编写占用远大于内存大小的空间的代码。此时则需要操作系统进行“兜底”,在软件层面即时地阻断错误与异常继续向下层传播。关于操作系统与错误处理,这里有一个(可能)有趣的知乎问题:[如果抛开操作系统,直接在裸机上进行除零操作会发生什么情况?](https://www.zhihu.com/question/552173126) +- 哪怕在只有一颗 CPU 的电脑上,也可以做到同时写代码和听音乐。但音乐播放器和代码编辑器的代码本身不包含协同运行功能,CPU也非天生支持多程序并行。操作系统通过微观上协调多个程序**交替**执行,而在宏观上表现为多个程序**同时**执行。这种**并发**技术是操作系统的一个重要特征。 +- 在实际上的电脑使用中,我们会遇到各种各样的错误,一个软件的小错误就导致整台电脑蓝屏甚至硬件损坏显然是不可接受的。虽然程序内部也应该包含错误处理机制,但遇到一些不负责的程序或者程序遇到了一些它们自己处理不了的情况时,操作系统的错误处理机制是必要的。比如在信科相关的课上,某些代码尝试生成上万个不销毁的子进程(我们有时称其为 "fork炸弹" ),再比如占用远大于内存大小的空间的代码。此时则需要操作系统进行“兜底”,在软件层面即时地阻断错误与异常继续向下层传播。关于操作系统与错误处理,这里有一个(可能)有趣的知乎问题:[如果抛开操作系统,直接在裸机上进行除零操作会发生什么情况?](https://www.zhihu.com/question/552173126) ### 用户界面——CLI,TUI 与 GUI 目前常见的计算机操作系统有 Windows, Linux 和 macOS,移动操作系统则包括 Android 与 iOS,当然华为在近些年研发的鸿蒙系统也包括在内。 -前文提到,操作系统负责给用户和其他软件提供接口,给其他软件提供的接口的使用方法往往藏身与各种繁杂的文档中,我们对他们并没有太大的兴趣。相比之下,我们则每天都在使用操作系统为用户提供的接口。从关机到新建文件夹再到打开一大堆程序,这都是我们直接与操作系统交互进行的例子。**UI (User Interface),用户界面**,则是直接涉及到用户应该如何与操作系统(或者是其他的软件)进行交互的核心模块。 +前文提到,操作系统负责给用户和其他软件提供接口,给其他软件的接口常藏身于繁杂文档中,我们兴趣不大。相比之下,我们则每天都在使用操作系统为用户提供的接口。从关机到新建文件夹再到打开一大堆程序,这都是我们直接与操作系统交互进行的例子。**UI (User Interface),用户界面**,则是直接涉及到用户应该如何与操作系统(或者是其他的软件)进行交互的核心模块。 -目前大部分常见的 UI 都是** GUI (Graphics User Interface),图形用户界面**,显著特征为通过鼠标(以及触摸屏)等输入设备与图标或菜单选项进行交互,启动对应的程序或执行相应的命令。这种交互方式最大的优点在于直观且易于上手,学习曲线平和,鼠标交互的方式可以省去大量指令的记忆成本,同时也有不错的效率。相对应的,**CLI (Command Line Interface,命令行界面)、TUI (Terminal User Interface/Text-based User Interface,终端用户界面/基于文本的用户界面)** 则不依赖图形而是主要依赖键盘输入大量指令,对指令的记忆成本也造成了较为陡峭的学习曲线。CLI 是早期大部分计算机的交互方式,而 TUI 可以部分视作在 CLI 的基础上进行了丰富。 +目前大部分常见的 UI 都是** GUI (Graphics User Interface),图形用户界面**,显著特征为通过鼠标(以及触摸屏)等输入设备与图标或菜单选项进行交互,启动对应的程序或执行相应的命令。这种交互方式最大的优点在于直观且易于上手,学习曲线平和,鼠标交互省去大量指令记忆成本,同时也有不错的效率。相对应的,**CLI (Command Line Interface,命令行界面)、TUI (Terminal User Interface/Text-based User Interface,终端用户界面/基于文本的用户界面)** 则不依赖图形而是主要依赖键盘输入大量指令,对指令的记忆成本也造成了较为陡峭的学习曲线。CLI 是早期大部分计算机的交互方式,而 TUI 可以部分视作在 CLI 的基础上进行了丰富。 在 CLI 中,所有操作都通过在命令行中输入指令进行。相应地,系统会通过文本形式输出相应内容。CLI 与现代常见交互方式的一个主要不同是它并没有一个用来交互的“菜单”之类的东西。下面是 Wiki 上关于 CLI 条目里的一张图,可以看到用户在终端中输入了 `ping`, `pwd`, `cd`, `ls`, `yum` 这些常见的指令,之后计算机将这些指令的执行结果输出到了终端里。虽然输出结果中存在简单的排版与动态进度条之类的要素,但这些结果本身并不能做出“光标选中”之类的交互动作,而是仅仅作为“展示”之用。 @@ -46,19 +46,19 @@ ### 什么是命令式语言 -不同的计算概论课程会分别介绍 Python 和 C/C++这两种语言(计概范围内的 C 与 C++ 其实可大致看成一种语言)。虽然上到设计理念下到编写细节这两种语言都有很多不同点,但其都属于命令式语言(虽然现代 C++ 以及 Python 都支持面向对象和函数式设计,但其主体还是属于命令式语言)。命令式语言的计算理论来自于**图灵机**,通过对**状态的改变**描述计算的过程。它们的语句主要为对状态(也就是变量内部存储的值)的改变以及对控制流的改变(也就是条件或循环的跳转语句)。因此将简单的 C++ 和 Python 代码互相转换并不是什么难事,因为它们描述计算所用的方式所用的方式一样。另一种常见的编程范式是函数式语言,主要例子为 Haskell。其计算理论来自于 **λ演算**,通过创建匿名函数和应用函数描述计算。有和图灵机一样的描述能力。函数式语言相对来说更难理解,这里不再深入。 +不同的计算概论课程会分别介绍 Python 和 C/C++这两种语言(计概范围内的 C 与 C++ 其实可大致看成一种语言)。虽然上到设计理念下到编写细节这两种语言都有很多不同点,但其都属于命令式语言(虽然现代 C++ 以及 Python 都支持面向对象和函数式设计,但其主体还是属于命令式语言)。命令式语言的计算理论来自于**图灵机**,通过对**状态的改变**描述计算的过程。它们的语句主要为对状态(也就是变量内部存储的值)的改变以及对控制流的改变(也就是条件或循环的跳转语句)。因此将简单的 C++ 和 Python 代码互相转换并不是什么难事,因为它们描述计算所用的方式所用的方式一样。另一种常见的编程范式是函数式语言,主要例子为 Haskell 和 Nix。 Haskell的计算理论来自于 **λ演算**,通过创建匿名函数和应用函数描述计算。有和图灵机一样的描述能力。而 Nix 语言的核心价值在于通过声明式 + 纯函数式的设计,让软件依赖管理和系统配置变得可复现,可维护,可扩展,无论何时何地,只要声明相同,环境必一致(如果输入是一成不变的话)。函数式语言相对来说更难理解,这里不再深入。 ### 编译型语言与解释型语言 -C++ 与 Python 的一个重要区别就是代码实际运行的方式。~~另一个重要的区别是类型系统,不过这一项相对直观我们不做深入。~~ 计算机只能够运行字节形式的可执行文件,而可执行文件(中的代码部分)与编程语言最底层的汇编语言有着一一对应的关系,因此可以说计算机只能识别汇编模式的代码。 +C++ 与 Python 的重要区别之一是代码运行方式。~~另一个重要的区别是类型系统,不过这一项相对直观我们不做深入。~~ 计算机只能够执行字节形式的可执行文件,而可执行文件(中的代码部分)与编程语言最底层的汇编语言有着一一对应的关系,因此可以说计算机只能识别汇编模式的代码。 -C/C++ 的**编译器**做的事情实际上就是把源代码翻译成汇编代码,再经由**链接器**进行与头文件的整合相关工作,最终得到一份可以被直接运行的可执行文件。因此,C++代码的运行分为两步:先是编译再是执行。这种方式的一个主要好处就是一份需要被反复运行的代码只需要被编译一次,而节省了编译部分的耗时。编译器与链接器报的错误大部分情况下很好解决,~~小部分情况下会因为涉及到环境问题而变得棘手。~~ 编译器可以识别语法错误以及部分的语义错误,因此一份运行起来的 C++ 代码天然地有更小几率出现 bug。在 “通过编译器减少 bug” 方向上一门叫 **Rust** 的语言做得更加极端,其在编译器内建立了大量的语义约束,保证能被运行的 Rust 代码**一定**不会出现内存安全问题与线程安全问题,不过相应地也显著提高了 Rust 的代码编写难度。 +C/C++ 的**编译器**做的事情实际上就是把源代码翻译成汇编代码,再经由**链接器**进行与头文件的整合相关工作,最终得到一份可以被直接运行的可执行文件。因此,C++代码的运行分为两步:先是编译再是执行。这种方式的一个主要好处就是一份需要被反复运行的代码只需要被编译一次,而节省了编译部分的耗时。编译器与链接器报的错误大部分情况下很好解决,~~小部分情况下会因为涉及到环境问题而变得棘手。~~ 编译器可以识别语法错误以及部分的语义错误,因此一份运行起来的 C++ 代码天然地有更小几率出现 bug。在 “通过编译器减少 bug” 方向上一门叫 **Rust** 的语言做得更加激进,其在编译器内建立了大量的语义约束,保证能被运行的 Rust 代码**一定**不会出现内存安全问题与线程安全问题,不过相应地也显著提高了 Rust 的代码编写难度。 -而 Python 则走向了另一个极端。Python 代码运行使用的是另一套称为**解释器**的代码翻译逻辑。直观来讲,解释器允许代码一边被翻译为汇编语言一边被执行。也因此,终端里可以发现 `.py` 后缀名的 Python 代码文件可以被直接视为可执行文件,而 `.cpp` 后缀名的 C++ 代码文件只能被视为文本文件。解释器可以省去每次更新代码时都要将项目重新编译的时间花销,但代码的实际运行效率相比编译型语言来说更差。体感表现为 Python 代码比 C++ 代码慢很多,~~真的非常多!~~ 一些完全不符合语法要求的代码也可以在解释器中跑起来——解释器只会在按顺序运行代码,直到在出现问题的的地方停止。Python 自身的动态类型系统与缺少编译器带来的静态查错系统使得实际写出来的 Python 代码中经常包含大量的 bug,并除实际运行之外缺少 debug 的手段。~~但因为语言特性而引发的 bug 必然不是过于复杂的 bug,不需要在这点上过于担心。~~ Python 并不适合大型项目的开发,不过动态类型与解释器带来的灵活性使得 Python 在小型项目上拥有无可匹敌的竞争力。与 C++ 相比,Python 作为现代语言,拥有更为成熟的库文件机制,`import` 比起 `include` 实在是好用了太多。大量的第三方库以及 `pip`, `anaconda` 等 Python 环境管理工具也是 Python 竞争力的重要来源。 +而 Python 则走向了另一个极端。Python 代码运行使用的是另一套称为**解释器**的代码翻译逻辑。直观来讲,解释器允许代码一边被翻译为汇编语言一边被执行。也因此,终端里可以发现 `.py` 后缀名的 Python 代码文件可以被直接视为可执行文件,而 `.cpp` 后缀名的 C++ 代码文件只能被视为文本文件。解释器可以省去每次更新代码时都要将项目重新编译的时间花销,但代码的实际运行效率相比编译型语言来说更差。体感表现为 Python 代码比 C++ 代码慢很多,~~真的非常多!~~ 一些完全不符合语法要求的代码也可以在解释器中跑起来——解释器只会在按顺序运行代码,直到在出现问题的的地方停止。Python 自身的动态类型系统与缺少编译器带来的静态查错系统使得实际写出来的 Python 代码中经常包含大量的 bug,并除实际运行之外缺少 debug 的手段。~~但因为语言特性而引发的 bug 必然不是过于复杂的 bug,不需要在这点上过于担心。~~ Python 并不适合大型项目的开发,不过动态类型与解释器带来的灵活性使得 Python 在小型项目上拥有无可匹敌的竞争力。与 C++ 相比,Python 作为现代语言,拥有更为成熟的库文件机制,`import` 比起 `include` 实在是好用了太多。大量的第三方库以及 `pip`, `Anaconda` 等 Python 环境管理工具也是 Python 竞争力的重要来源。 ### 如何阅读报错与调试代码 -调试代码中的错误是每位写代码人都不可避免的一件事。错误可以简单地分为语法错误、语义错误与逻辑错误。其中语法错误和大部分语义错误会被编译器(为方便,以下“编译器”包含 Python 等语言中的“解释器)直接发现。而逻辑错误,也就是编写出的代码与预期不符的情况,由于代码逻辑本身所带有的复杂性,而不可避免地拥有复杂性,这种 debug(为方便,以下可能会混用 `debug` 与 `调试`)往往需要优秀的经验、直觉、心态以及运气。 +调试代码中的错误是每位写代码人都不可避免的一件事。错误可以简单地分为语法错误、语义错误与逻辑错误。其中语法错误和大部分语义错误会被编译器(为方便,以下“编译器”包含 Python 等语言中的“解释器)直接发现。而逻辑错误,也就是编写出的代码与预期不符的情况,则因逻辑本身的复杂性,其调试往往依赖经验、直觉、心态和运气,这种 debug(为方便,以下可能会混用 `debug` 与 `调试`)往往需要优秀的经验、直觉、心态以及运气。 之所以在节标题中单列了如何阅读报错,就是因为语法语义错误是初学者最经常遇到的问题,而编译器会以报错信息的形式友情提示这些错误是什么、在哪里。而高级一些的代码编辑工具,还会把能被编译器检测出来的错误实时地标注在代码上面。~~这也是为什么写出来的 Python 代码往往会带有很多 bug 的原因,因为动态类型导致语法语义层面的错误无法被编译器静态检测出来,从而无法在编写代码时就获得提示。~~ 以下是一个简单的编译器报错信息例子。 @@ -82,7 +82,7 @@ C:/M/B/src/mingw-w64/mingw-w64-crt/crt/crtexewin.c:70: undefined reference to `W collect2.exe: error: ld returned 1 exit status ``` -虽然信息略显抽象,但我们还是可以看到很多有用的信息。 `ld` 是 c++中的链接器,再往上看可以发现对 `WinMain` 的引用是未定义的。这提示我们去看 main 函数,从而发现这里 `main()` 被输入成了 `mian()`,因此链接器无法找到 main 函数,从而引发错误 +虽然信息略显抽象,但我们还是可以看到很多有用的信息。 `ld` 是 c++中的链接器,报错指出对 `WinMain` 的引用未定义(Windows程序入口应为 WinMain 或 main)。这提示我们去看 main 函数,从而发现这里 `main()` 被输入成了 `mian()`,因此链接器无法找到 main 函数,从而引发错误 下面是另一个例子;