You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

516 lines
23 KiB
Markdown

2 years ago
# 用户故事|语言不仅是工具,还是思维方式
你好,我是 Pedro一名普普通通打工人平平凡凡小码农。
可能你在课程留言区看到过我,也跟我讨论过问题。今天借着这篇用户故事的机会,正好能跟你再多聊几句。
我简单整理了一下自己入坑编程以来的一些思考,主要会从思维、语言和工具三个方面来聊一聊,最后也给你分享一点自己对 Rust 的看法,当然以下观点都是“主观”的,观点本身不重要,重要的是得到观点的过程。
## 从思维谈起
从接触编程开始,我们就已经开始与编程语言打交道,很多人学习编程的道路往往就是熟悉编程语言的过程。
在这个过程中,很多人会不适应,写出的代码往往都不能运行,更别提设计与抽象。出现这个现象最根本的原因是,代码体现的是计算机思维,**而人脑思维和计算机思维差异巨大,很多人一开始无法接受两种思维差异带来的巨大冲击**。
那么,究竟什么是计算机思维?
计算机思维是全方位的,体现在方方面面,我以个人视角来简单概括一下:
* 自顶向下:自顶向下是计算机思维的精髓,人脑更加适合自底向上。计算机通过自顶向下思维将大而难的问题拆解为小问题,再将小问题逐一解决,从而最终解决大问题。
* 多维度、多任务:人脑是线性的,看问题往往是单维的,我们很难同时处理和思考多个问题,但是计算机不一样,它可以有多个 CPU 核心,在保存上下文的基础上能够并发运行成百上千的任务。
* 全局性:人的精力、脑容量是有限的,而计算机的容量几乎是无限的;人在思考问题时,限于自己的局部性,拿到局部解就开始做了,而计算机可以在海量数据的基础上再做决策,从而逼近全局最优。
* 协作性:计算机本身就是一件极其精细化的工程艺术品,它复杂精巧,每个部分都只会做自己最擅长的事情,比如将计算和存储剥离,计算机高效运作的背后是每个部分协作的结果,而人更擅长单体作战,只有通过大量的训练,才能发挥群体的作用。
* 迭代快:人类进化、成长是缓慢的,直到现在,很多人的思维方式仍旧停留在上个世纪,而计算机则不同,进入信息时代后,计算机就遵循着摩尔定律,每 18 个月翻一番,十年前的手机放在今天可能连微信都无法正常运行。
* 取舍:在长期的社会发展中,人过分喜欢强调对与错,喜欢追求绝对的公平,讽刺的是,由二进制组成的计算机却不会做出非黑即白的决策,无论是计算机本身(硬件),还是里面运行的软件,每一个部分都是性能、成本、易用性多角度权衡的结果。
* So on…
当这些思维直接体现在代码里面,比如,自顶向下体现在编程语言中就是递归、分治;多维度、多任务的体现就是分支、跳转、上下文;迭代、协作和取舍在编程中也处处可见。
而这些恰恰是人脑思维不擅长的点,所以很多人无法短时间内做到编程入门。想要熟练掌握编程,就必须认识到人脑与计算机思维的差异,强化计算机思维的训练,**这个训练的过程是不太可能短暂的,因此编程入门必须要消耗大量的时间和精力**。
## 语言
不过思维的训练和评估是需要有载体的,就好比评估你的英文水平,会考察你用英文听/说/读/写的表达能力。那我们的计算机思维怎么表达呢?
于人而言,我们可以通过肢体动作、神情、声音、文字等来表达思维。在漫长的人类史中,动作、神情、声音这几种载体很难传承和传播,直到近代,音、视频的兴起才开始慢慢解决这个问题。
文字,尤其是语言诞生后的文字,成了人类文明延续、发展的主要途径之一,直至今天,我们仍然可以通过文字来与先贤对话。当然,对话的前提是,这些文字你得看得懂。
而看得懂的前提是,我们使用了同一种或类似的语言。
**回到计算机上来,现代计算机也是有通用语言的**,也就是我们常说的二进制机器语言,专业一点叫指令集。二进制是计算机的灵魂,但是人类却很难理解、记忆和应用,因此为了辅助人类操纵计算机工作,上一代程序员们对机器语言做了第一次抽象,发明了汇编语言。
但伴随着硬件、软件的快速发展程序代码越来越长应用变得愈来愈庞大汇编级别的抽象已经无法满足工程师对快速高效工作的需求了。历史的发展总是如此地相似当发现语言抽象已经无法满足工作时工程师们就会在原有层的基础上再抽象出一层而这一层的著名佼佼者——C语言直接奠定了今天计算机系统的基石。
从此以后,不计其数的编程语言走向计算机的舞台,它们如同满天繁星,吸引了无数的编程爱好者,比如说迈向中年的 Java 和新生代的 Julia。虽然学习计算机最正确的途径不是从语言开始但学习编程最好、最容易获取成就感的路径确实是应该从语言入手。因此编程语言的重要性不言而喻它是我们走向编程世界的大门。
C 语言是一种**命令式编程**语言,命令式是一种编程范式;使用 C 写代码时,我们更多是在思考如何描述程序的运行,通过编程语言来告诉计算机如何执行。
举个例子,使用 C 语言来筛选出一个数组中大于 100 的数字。对应代码如下:
```c++
int main() { 
  int arr[5] = { 100, 105, 110, 99, 0 };
  for (int i = 0; i < 5; ++i) {
    if (arr[i] > 100) {
      // do something
    }
  }
  return 0;
}
```
在这个例子中,代码撰写者需要使用数组、循环、分支判断等逻辑来告诉计算机如何去筛选数字,写代码的过程往往就是计算机的执行过程。
而对于另一种语言而言,比如 JavaScript筛选出大于 100 的数字的代码大概是这样的:
```javascript
let arr = [ 100, 105, 110, 99, 0 ]
let result = arr.filter(n => n > 100)
```
相较于 C 来说JavaScript 做出了更加高级的抽象,代码撰写者无需关心数组容量、数组遍历,只需将数字丢进容器里面,并在合适的地方加上筛选函数即可,这种编程方式被称为**声明式编程**。
可以看到的是,相较于命令式编程,声明式编程更倾向于表达在解决问题时应该做什么,而不是具体怎么做。这种更高级的抽象不仅能够给开发者带来更加良好的体验,也能让更多非专业人士进入编程这个领域。
不过命令式编程和声明式编程其实并没有优劣之分,主要区别体现在**两者的语言特性相较于计算机指令集的抽象程度**。
其中,命令式编程语言的抽象程度更低,这意味着该类语言的语法结构可以直接由相应的机器指令来实现,适合对性能极度敏感的场景。而声明式编程语言的抽象程度更高,这类语言更倾向于以叙事的方式来描述程序逻辑,开发者无需关心语言背后在机器指令层面的实现细节,适合于业务快速迭代的场景。
不过语言不是一成不变的。编程语言一直在进化,它的进化速度绝对超过了自然语言的进化速度。
在抽象层面上,编程语言一直都停留在机器码 -> 汇编 -> 高级语言这三层上。而对于我们广大开发者来说,我们的目光一直聚焦在高级语言这一层上,所以,高级编程语言也慢慢成为了狭隘的编程语言(当然,这是一件好事,每一类人都应该各司其职做好自己的事情,不用过多担心指令架构、指令集差异带来的麻烦)。
谈到这里,不知你是否发现了一个规律:抽象越低的编程语言越接近计算机思维,而抽象越高越接近人脑思维。
是的。**现代层出不穷的编程语言,往往都是在人脑、计算机思维之间的平衡做取舍**。那些设计语言的专家们似乎在这个毫无硝烟的战场上博弈,彼此对立却又彼此借鉴。不过哪怕再博弈,按照人类自然语言的趋势来看,也几乎不可能出现一家独大的可能,就像人类目前也是汉语、英语等多种语言共存,即使世界语于 1887 年就被发明,但我们似乎从未见过谁说世界语。
既然高级编程语言那么多,对于有选择困难症的我们,又该做出何种选择呢?
## 工具
一提到选语言,估计你常听这么一句话,语言是工具。很长一段时间里,我也这么告诫自己,无所谓一门语言的优劣,它仅仅只是一门工具,而我需要做的就是将这门工具用好。语言是表达思想的载体,只要有了思想,无论是何种语言,都能表达。
可当我接触了越来越多的编程语言,对代码、指令、抽象有了更深入的理解之后,我推翻了这个想法,认识到了“语言只是工具”这个说法的狭隘性。
编程语言,显然不仅只是工具,它一定程度上桎梏了我们的思维。
举例来说,使用 Java 或者 C# 的人能够很轻易地想到对象的设计与封装,那是因为 Java 和 C# 就是以类作为基本的组织单位,无论你是否有意识地去做这件事,你都已经做了。而对于 C 和 JavaScript 的使用者来说,大家似乎更倾向于使用函数来进行封装。
**抛开语言本身的优劣,这是一种思维的惯性,恰恰也印证了上面我谈到的,语言一定程度上桎梏了我们的思维**。其实如果从人类语言的角度出发,一个人说中文和说英文的思维方式是大相径庭的,甚至一个人分别说方言和普通话给别人的感觉也像是两个人一样。
## Rust
所以如果说思维是我们创造的出发点那么编程语言在表达思维的同时也在一定程度上桎梏了我们的思维。聊到这里终于到我们今天的主角——Rust这门编程语言出场了。
Rust 是什么?
Rust 是一门高度抽象、性能与安全并重的现代化高级编程语言。我学习、推崇它的主要原因有三点:
* 高度抽象、表达能力强,支持命令式、声明式、元编程、范型等多种编程范式;
* 强大的工程能力,安全与性能并重;
* 良好的底层能力,天然适合内核、数据库、网络。
Rust 很好地迎合了人类思维,对指令集进行了高度抽象,抽象后的表达力能让我们以更接近人类思维的视角去写代码,而 Rust 负责将我们的思维翻译为计算机语言,并且性能和安全得到了极大的保证。简单说就是,完美兼顾了一门语言的思想性和工具性。
仍以前面“选出一个数组中大于 100 的数字”为例,如果使用 Rust那么代码是这样的
```rust
let arr = vec![ 100, 105, 110, 99, 0 ]
let result = arr.iter().filter(n => n > 100).collect();
```
如此简洁的代码会不会带来性能损耗Rust 的答案是不会,甚至可以比 C 做到更快。
我们对应看三个小例子的实现思路/要点,来感受一下 Rust 的语言表达能力、工程能力和底层能力。
### 简单协程
Rust 可以无缝衔接到 C、汇编代码这样我们就可以跟下层的硬件打交道从而实现协程。
实现也很清晰。首先,定义出协程的上下文:
```rust
#[derive(Debug, Default)]
#[repr(C)]
struct Context {
rsp: u64, // rsp 寄存器
r15: u64,
r14: u64,
r13: u64,
r12: u64,
rbx: u64,
rbp: u64,
}
#[naked]
unsafe fn ctx_switch() {
// 注意16 进制
llvm_asm!(
"
mov %rsp, 0x00(%rdi)
mov %r15, 0x08(%rdi)
mov %r14, 0x10(%rdi)
mov %r13, 0x18(%rdi)
mov %r12, 0x20(%rdi)
mov %rbx, 0x28(%rdi)
mov %rbp, 0x30(%rdi)
mov 0x00(%rsi), %rsp
mov 0x08(%rsi), %r15
mov 0x10(%rsi), %r14
mov 0x18(%rsi), %r13
mov 0x20(%rsi), %r12
mov 0x28(%rsi), %rbx
mov 0x30(%rsi), %rbp
"
);
}
```
结构体 Context 保存了协程的运行上下文信息(寄存器数据),通过函数 ctx\_switch当前协程就可以交出 CPU 使用权,下一个协程接管 CPU 并进入执行流。
然后我们给出协程的定义:
```rust
#[derive(Debug)]
struct Routine {
id: usize,
stack: Vec<u8>,
state: State,
ctx: Context,
}
```
协程 Routine 有自己唯一的 id、栈 stack、状态 state以及上下文 ctx。Routine 通过 spawn 函数创建一个就绪协程yield 函数会交出 CPU 执行权:
```rust
pub fn spawn(&mut self, f: fn()) {
// 找到一个可用的
// let avaliable = ....
let sz = avaliable.stack.len();
unsafe {
let stack_bottom = avaliable.stack.as_mut_ptr().offset(sz as isize); // 高地址内存是栈顶
let stack_aligned = (stack_bottom as usize & !15) as *mut u8;
std::ptr::write(stack_aligned.offset(-16) as *mut u64, guard as u64);
std::ptr::write(stack_aligned.offset(-24) as *mut u64, hello as u64);
std::ptr::write(stack_aligned.offset(-32) as *mut u64, f as u64);
avaliable.ctx.rsp = stack_aligned.offset(-32) as u64; // 16 字节对齐
}
avaliable.state = State::Ready;
}
pub fn r#yield(&mut self) -> bool {
// 找到一个 ready 的,然后让其运行
let mut pos = self.current;
//.....
self.routines[pos].state = State::Running;
let old_pos = self.current;
self.current = pos;
unsafe {
let old: *mut Context = &mut self.routines[old_pos].ctx;
let new: *const Context = &self.routines[pos].ctx;
llvm_asm!(
"mov $0, %rdi
mov $1, %rsi"::"r"(old), "r"(new)
);
ctx_switch();
}
self.routines.len() > 0
}
```
运行结果如下:
```rust
1 STARTING
routine: 1 counter: 0
2 STARTING
routine: 2 counter: 0
routine: 1 counter: 1
routine: 2 counter: 1
routine: 1 counter: 2
routine: 2 counter: 2
routine: 1 counter: 3
routine: 2 counter: 3
routine: 1 counter: 4
routine: 2 counter: 4
routine: 1 counter: 5
routine: 2 counter: 5
routine: 1 counter: 6
routine: 2 counter: 6
routine: 1 counter: 7
routine: 2 counter: 7
routine: 1 counter: 8
routine: 2 counter: 8
routine: 1 counter: 9
routine: 2 counter: 9
1 FINISHED
```
具体代码实现参考[协程](https://github.com/PedroGao/rust-examples/blob/main/rsroutine/src/main.rs) 。
### 简单内核
操作系统内核是一个极为庞大的工程,但是如果只是写个简单内核输出 Hello World那么 Rust 就能很快完成这个任务。你可以自己体验一下。
首先,添加依赖工具:
```rust
rustup component add llvm-tools-preview
cargo install bootimage
```
然后编辑 main.rs 文件输出一个 Hello World
```rust
#![no_std]
#![no_main]
use core::panic::PanicInfo;
static HELLO:&[u8] = b"Hello World!";
#[no_mangle]
pub extern "C" fn _start() -> ! {
let vga_buffer = 0xb8000 as *mut u8;
for (i, &byte) in HELLO.iter().enumerate() {
unsafe {
*vga_buffer.offset(i as isize * 2) = byte;
*vga_buffer.offset(i as isize * 2 + 1) = 0xb;
}
}
loop{}
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
```
然后编译、打包运行:
```bash
cargo bootimage
cargo run
```
运行结果如下:
![](https://static001.geekbang.org/resource/image/4e/40/4e200553acbc604a6456f8629dd30340.png?wh=1512x928)
具体代码实现参考[内核](https://github.com/PedroGao/rust-examples/tree/main/rkernel) 。
### 简单网络协议栈
同操作系统一样,网络协议栈也是一个庞大的工程系统。但是借助 Rust 和其完备的生态,我们可以迅速完成一个小巧的 HTTP 协议栈。
首先,在数据链路层,我们定义 Mac 地址结构体:
```rust
#[derive(Debug)]
pub struct MacAddress([u8; 6]);
impl MacAddress {
pub fn new() -> MacAddress {
let mut octets: [u8; 6] = [0; 6];
rand::thread_rng().fill_bytes(&mut octets); // 1. 随机生成
octets[0] |= 0b_0000_0010; // 2
octets[1] &= 0b_1111_1110; // 3
MacAddress { 0: octets }
}
}
```
MacAddress 用来表示网卡的物理地址,此处的 new 函数通过随机数来生成随机的物理地址。
然后实现 DNS 域名解析函数,通过 IP 地址获取 MAC 地址,如下:
```rust
pub fn resolve(
dns_server_address: &str,
domain_name: &str,
) -> Result<Option<std::net::IpAddr>, Box<dyn Error>> {
let domain_name = Name::from_ascii(domain_name).map_err(DnsError::ParseDomainName)?;
let dns_server_address = format!("{}:53", dns_server_address);
let dns_server: SocketAddr = dns_server_address
.parse()
.map_err(DnsError::ParseDnsServerAddress)?;
// ....
let mut encoder = BinEncoder::new(&mut request_buffer);
request.emit(&mut encoder).map_err(DnsError::Encoding)?;
let _n_bytes_sent = localhost
.send_to(&request_buffer, dns_server)
.map_err(DnsError::Sending)?;
loop {
let (_b_bytes_recv, remote_port) = localhost
.recv_from(&mut response_buffer)
.map_err(DnsError::Receiving)?;
if remote_port == dns_server {
break;
}
}
let response = Message::from_vec(&response_buffer).map_err(DnsError::Decoding)?;
for answer in response.answers() {
if answer.record_type() == RecordType::A {
let resource = answer.rdata();
let server_ip = resource.to_ip_addr().expect("invalid IP address received");
return Ok(Some(server_ip));
}
}
Ok(None)
}
```
接着实现 HTTP 协议的 GET 方法:
```rust
pub fn get(
tap: TapInterface,
mac: EthernetAddress,
addr: IpAddr,
url: Url,
) -> Result<(), UpstreamError> {
let domain_name = url.host_str().ok_or(UpstreamError::InvalidUrl)?;
let neighbor_cache = NeighborCache::new(BTreeMap::new());
// TCP 缓冲区
let tcp_rx_buffer = TcpSocketBuffer::new(vec![0; 1024]);
let tcp_tx_buffer = TcpSocketBuffer::new(vec![0; 1024]);
let tcp_socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer);
let ip_addrs = [IpCidr::new(IpAddress::v4(192, 168, 42, 1), 24)];
let fd = tap.as_raw_fd();
let mut routes = Routes::new(BTreeMap::new());
let default_gateway = Ipv4Address::new(192, 168, 42, 100);
routes.add_default_ipv4_route(default_gateway).unwrap();
let mut iface = EthernetInterfaceBuilder::new(tap)
.ethernet_addr(mac)
.neighbor_cache(neighbor_cache)
.ip_addrs(ip_addrs)
.routes(routes)
.finalize();
let mut sockets = SocketSet::new(vec![]);
let tcp_handle = sockets.add(tcp_socket);
// HTTP 请求
let http_header = format!(
"GET {} HTTP/1.0\r\nHost: {}\r\nConnection: close\r\n\r\n",
url.path(),
domain_name,
);
let mut state = HttpState::Connect;
'http: loop {
let timestamp = Instant::now();
match iface.poll(&mut sockets, timestamp) {
Ok(_) => {}
Err(smoltcp::Error::Unrecognized) => {}
Err(e) => {
eprintln!("error: {:?}", e);
}
}
{
let mut socket = sockets.get::<TcpSocket>(tcp_handle);
state = match state {
HttpState::Connect if !socket.is_active() => {
eprintln!("connecting");
socket.connect((addr, 80), random_port())?;
HttpState::Request
}
HttpState::Request if socket.may_send() => {
eprintln!("sending request");
socket.send_slice(http_header.as_ref())?;
HttpState::Response
}
HttpState::Response if socket.can_recv() => {
socket.recv(|raw_data| {
let output = String::from_utf8_lossy(raw_data);
println!("{}", output);
(raw_data.len(), ())
})?;
HttpState::Response
}
HttpState::Response if !socket.may_recv() => {
eprintln!("received complete response");
break 'http;
}
_ => state,
}
}
phy_wait(fd, iface.poll_delay(&sockets, timestamp)).expect("wait error");
}
Ok(())
}
```
最后在 main 函数中使用 HTTP GET 方法:
```rust
fn main() {
// ...
let tap = TapInterface::new(&tap_text).expect(
"error: unable to use <tap-device> as a \
network interface",
);
let domain_name = url.host_str().expect("domain name required");
let _dns_server: std::net::Ipv4Addr = dns_server_text.parse().expect(
"error: unable to parse <dns-server> as an \
IPv4 address",
);
let addr = dns::resolve(dns_server_text, domain_name).unwrap().unwrap();
let mac = ethernet::MacAddress::new().into();
http::get(tap, mac, addr, url).unwrap();
}
```
运行程序,结果如下:
```powershell
$ ./target/debug/rget http://www.baidu.com tap-rust
HTTP/1.0 200 OK
Accept-Ranges: bytes
Cache-Control: no-cache
Content-Length: 9508
Content-Type: text/html
```
具体代码实现参考[协议栈](https://github.com/PedroGao/rust-examples/tree/main/rget) 。
通过这三个简单的小例子,无论是协程、内核还是协议栈,这些听上去都很高大上的技术,在 Rust 强大的表现力、生态和底层能力面前显得如此简单和方便。
思维是出发点,语言是表达体,工具是媒介,而 Rust 完美兼顾了一门语言的思想性和工具性,赋予了我们极强的工程表达能力和完成能力。
## 总结
作为极其现代的语言Rust 集百家之长而成,将性能、安全、语言表达力都做到了极致,但同时也带来了巨大的学习曲线。
初学时,每天都要和编译器做斗争,每次编译都是满屏的错误信息;攻克一个陡坡后,发现后面有更大的陡坡,学习的道路似乎无穷无尽。那我们为什么要学习 Rust
这里引用左耳朵耗子的一句话:
> 如果你对 Rust 的概念认识得不完整,你完全写不出程序,那怕就是很简单的一段代码。这逼着程序员必须了解所有的概念才能编码。
Rust 是一个对开发者极其严格的语言,严格到你学的不扎实,就不能写程序,**但这无疑也是一个巨大的机会,改掉你不好的编码习惯,锻炼你的思维,让你成为真正的大师**。
聊到这里,你是否已经对 Rust 有了更深的认识和更多的激情,那么放手去做吧!期待你与 Rust 擦出更加明亮的火花!
### 参考资料
1. [Writing an OS in Rust](https://os.phil-opp.com/)
2. [green-threads-explained-in-200-lines-of-rust](https://cfsamson.gitbook.io/green-threads-explained-in-200-lines-of-rust)
3. [https://github.com/PedroGao/rust-examples](https://github.com/PedroGao/rust-examples)
4. 《深入理解计算机系统》
5. 《Rust in Action》
6. 《硅谷来信》
7. 《浪潮之巅》