第3章:计算机基础与Rust定位
对于一些小白来说,第一章我们操作的安装Rust的操作,其实是一知半解的。Rust是一个编程语言 or 软件 or 什么别的? cargo
又是什么东西。其实在学习编程的时候,很多人都是被这些底层的原理给吓跑了,因为在学校的时候,我们只是按照书本上的内容进行操作,进行配置环境,但是问到这是怎么运行的,就会说:"我不道啊,都是老师教的。"这一章就是用来补齐一些计算机基础,从第一步开始就稳扎稳打、注重内力,这不仅对学习Rust有帮助,这是学习任何计算机相关的知识的基础。本章会有非常多的反复的类比,旨在形象地强化一些概念。
3.1 计算机系统的基本组成
计算机系统主要由硬件和软件两大部分组成,二者密不可分:
- 硬件:计算机的"躯壳",包括CPU(中央处理器)、内存(RAM)、硬盘(存储)、输入输出设备(如键盘、鼠标、显示器等)。
- 软件:计算机的"灵魂",包括操作系统(如Windows、Linux、macOS)和各种应用程序(如微信、浏览器、游戏等)。软件通过一系列指令控制硬件完成各种任务。
也可以用厨房来比喻:
- CPU 就像厨师,负责思考和做菜(处理数据和指令)。
- 内存 就像操作台,厨师把正在做的菜和用到的食材都放在这里,方便随时拿取。
- 硬盘 就像储藏室,存放大量还没用到的食材和工具,需要时再拿出来。
- 输入设备(如键盘、鼠标)就像点菜的顾客或送菜的服务员,把需求和信息传递给厨师。
- 输出设备(如显示器、打印机)就像上菜窗口,把做好的菜端给顾客。
- 软件 就像菜谱和餐厅的规则,指导厨师和服务员如何协作。
整个厨房高效运作,离不开每个环节的协作,这就像计算机硬件和软件的分工一样。
思考与解释:
- 为什么内存比硬盘小很多,但速度快很多?
- 类比:
- 操作台(内存)空间有限,但厨师可以非常快地拿取和处理。
- 储藏室(硬盘)空间大,但每次取用都要走一趟,速度慢得多。
- 内存技术快但贵,硬盘容量大但慢。
- 类比:
- 为什么不能把所有东西都放在内存?
- 操作台太大既贵又占地方,很多食材暂时用不到,放在储藏室更合适。
- 为什么程序运行时要"加载"到内存?
- 就像厨师做菜前要把食材从储藏室拿到操作台,程序运行前也要把数据和指令从硬盘搬到内存,CPU才能高效处理。
3.2 软件是如何运行的
3.2.1 编程语言和编译器
计算机只能识别0和1(二进制),所有数据和指令最终都要转成0/1(二进制文件)。十六进制常用于表示内存地址和数据,便于阅读和调试。 而人类能理解的语言是自然语言,例如英语。所以如何让计算机理解人类的指令是很关键的。
**而自然语言是有歧义的。**例如:这个人的头发长得奇怪。究竟是头发长度长的奇怪,还是头发的形状看起来奇怪? 那么,我们就需要一种人类能够理解的,能够翻译成0和1二进制代码的,无二义性的东西,作为人和计算机之间的桥梁。
高级编程语言应运而生。而从人类编写的高级编程语言到机器能懂的二进制语言之间,需要一个翻译器 —— 编译器(解释器)
- 编译器:一种特殊的软件工具("翻译器"),能把我们写的"高级语言"代码(如C、Rust)一次性翻译成计算机能直接执行的"机器码",生成可执行文件,运行速度快。
- 解释器:像"翻译员",每次运行时逐行翻译代码,边翻译边执行,灵活但速度慢(如Python、JavaScript)。
为什么会有两种不同的翻译器呢?其实,这和不同的需求和历史发展有关:
-
编译器(如C、Rust):
- 优点:一次性把全部代码翻译成机器码,生成可执行文件,运行速度快,适合对性能要求高的场景。
- 缺点:每次修改代码都要重新编译,开发调试周期稍长。
- 适用场景:操作系统、游戏、服务器等需要高性能的程序。
- 类比:像把一本英文小说全部翻译成中文再出版,读者可以直接看中文版,阅读体验流畅。
-
解释器(如Python、JavaScript):
- 优点:可以边写边运行,修改一行代码马上看到效果,开发效率高,适合快速试验和学习。
- 缺点:每次运行都要"翻译"一遍,速度慢一些。
- 适用场景:脚本、自动化、数据分析、网页前端等对性能要求不高、但开发灵活性要求高的场合。
- 类比:像有个翻译员在你身边,每读一句英文就翻译一句中文,虽然灵活但速度慢。
历史背景:早期计算机资源有限,大家更关注运行效率,所以C、C++等编译型语言流行。随着计算机变快,开发效率变得更重要,解释型语言(如Python)开始流行,适合快速开发和原型设计。
我们在第一章安装的软件里面,其中一个很关键的就是编译器。Rust的编译器叫做rustc,而c、c++的编译器主流是gcc、g++、clang、clang++等。
3.2.2 程序的运行流程
程序的运行大致分为以下几个步骤:
- 编写代码(高级语言)。开发者使用如Rust、C、Python等高级编程语言编写源代码。
- 编译/解释生成二进制文件。源代码通过编译器(如rustc、gcc)或解释器(如python)被翻译成计算机能直接执行的机器码(二进制指令)。编译型语言会生成可执行文件,解释型语言则在运行时逐行翻译执行。
- 存入硬盘。编译生成的可执行二进制文件或脚本文件被保存到硬盘等存储介质中。
- 操作系统加载到内存。当用户运行程序时,操作系统会将可执行文件从硬盘加载到内存。只有在内存中的程序才能被CPU访问和执行。
- CPU执行指令。CPU从内存中读取指令,逐条执行,实现程序的功能。
简而言之:代码经过编译/解释后生成二进制文件,存储在硬盘,运行时由操作系统加载到内存,最终由CPU执行。
3.3 操作系统基础
操作系统(Operating System, OS)是管理和协调计算机硬件与软件资源的核心系统软件。
例子:Windows、Linux、macOS、Android
直观来看,操作系统就是给我们提供用户界面的一个东西,方便我们操作。 但实际上,并不是这样,图形用户界面(GUI)只是操作系统很小的一部分!
操作系统到底是什么?
这里是一个主板,有非常复杂的硬件:
如果没有操作系统,每个我们编写的程序都必须自己负责: 如何让CPU执行自己的指令; 如何分配和管理内存; 如何读写硬盘、显示内容、响应键盘鼠标等外设; 如何和其他程序"和平共处",避免互相干扰。 这会让每个程序都变得极其复杂,而且容易出错,甚至会导致系统崩溃或数据丢失。
操作系统的作用就是:
- 统一管理和调度硬件资源,简化程序开发;
- 让多个程序可以安全、稳定地同时运行,互不干扰;
- 提供标准的接口和服务(如文件系统、网络、图形界面等),让开发者专注于自己的业务逻辑。
有了操作系统,开发者只需要关心"做什么",而不必关心"怎么和硬件打交道",大大提升了开发效率和系统安全性。
在程序运行过程中,操作系统负责下面的工作:
- 把程序从硬盘加载到内存;
- 分配CPU资源让程序执行;
- 管理程序运行时产生的数据和文件;
- 处理输入输出(如键盘、鼠标、网络等)。
这样我们编写的程序只需要调用下面这样的接口就好了。
对于c语言:
#include <stdio.h>
int main() {
FILE *fp = fopen("test.txt", "w"); // 打开文件,写入模式
if (fp == NULL) {
printf("无法打开文件\n");
return 1;
}
fprintf(fp, "Hello, world!\n"); // 写入内容
fclose(fp); // 关闭文件
return 0;
}
对于Rust:
use std::fs::File; use std::io::Write; fn main() { let mut file = File::create("test.txt").expect("无法创建文件"); file.write_all(b"Hello, world!\n").expect("写入失败"); }
否则,我们就要手动用代码操纵磁盘,来读取,这是非常痛苦且低效的!!! 简而言之,操作系统是所有程序运行的"总管",没有操作系统,绝大多数应用程序都无法独立运行。 操作系统为我们提供了很多接口,用于管理和控制硬件,下面我会以C和Rust的例子来阐释。
进程与线程、并发与并行
在现代操作系统中,进程和线程是实现多任务的两种基本单位。
-
进程:可以理解为正在运行的一个"程序实例"。比如你同时开着微信和浏览器,这就是两个进程。每个进程有自己独立的内存空间,互不干扰。
- 类比:进程就像一栋大楼里的不同公司,各自有自己的办公室和员工,互不打扰。
-
线程:是进程内部的"执行小分队",一个进程可以有多个线程同时工作。比如浏览器可以一边加载网页一边播放音乐,这通常是不同线程在协作。
- 类比:线程就像公司里的不同部门,大家在同一个办公室里协作完成不同任务。
并发与并行
- 并发:指的是多个任务"轮流"执行,看起来像同时进行(比如单核CPU下的多任务切换)。
- 类比:一个厨师轮流炒几道菜,虽然只有一口锅,但切换得快,顾客感觉每道菜都在做。
- 并行:指的是多个任务"真正"同时进行(比如多核CPU下,每个核各干一件事)。
- 类比:几位厨师各自炒一道菜,真正同时出锅,效率更高。
为什么要有进程和线程?
有了操作系统,我们只需要用简单的接口就能让程序"多线程"运行,无需自己管理底层的CPU调度和资源分配。
代码示例
C语言:
#include <pthread.h>
void* thread_func(void* arg) {
// 线程要做的事情
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL); // 创建线程
pthread_join(tid, NULL); // 等待线程结束
return 0;
}
Rust:
use std::thread; fn main() { let handle = thread::spawn(|| { // 线程要做的事情 }); handle.join().unwrap(); // 等待线程结束 }
只需几行代码,就能让程序同时做多件事。底层的线程调度、资源分配都由操作系统负责,开发者不用操心。
变量、内存与地址
我们只需声明变量,系统自动分配内存。需要获取地址时,也有标准写法。
C语言:
int a = 10; //底层是编程语言操作操作系统在内存里开辟一块新区域
int* p = &a; // 获取变量a在内存里的地址
Rust:
#![allow(unused)] fn main() { let a = 10; let p = &a; // 获取变量a的引用 }
不用关心内存的具体分配细节,操作系统和语言帮我们管理好。
文件与文件系统
如前所述,读写文件只需调用标准接口。
C语言:
fopen("test.txt", "w"); // 打开文件
Rust:
#![allow(unused)] fn main() { File::create("test.txt"); // 创建文件 }
不用自己操作磁盘硬件,操作系统帮我们完成所有底层工作。