《Rust 编程第一课》 学习笔记 Day 1

423 阅读6分钟

大家好,我是砸锅。一个摸鱼八年的后端开发。熟悉 Go、Lua

Rust 的优点

  • 开发体验好,编译器不仅在编译期间可以找出异常,并且还会提示错误原因和正确的处理方式
  • Rust 适用的场景丰富:高性能网络及 I/O 开发、操作系统、设备驱动、嵌入式、数据库、游戏引擎、数据处理、图形界面、区块链等等,总体看来都是一些注重性能的方向
  • 提供了非常多并发原语的支持,且能保证并发安全。拥有目前最优秀的异步处理模型,采用状态机巧妙实现零成本抽象的异步处理机制

Rust 的缺点

  • 学习路线曲折,特别是概念繁多以及一些所有编程语言都没有的概念
  • 语言生态较新,没有足够号召力的公司背书

如何学习 Rust

  • 透过现象看本质。也就是常说的第一性原理,回归事物最基础的条件,拆分成基本元素解构分析,来探索要解决的问题。新的技术往往会带来新的名词,回归底层本质,通过类比、联想等方法,把知识点与基础知识关联起来,建立知识体系
  • 刻意练习,通过一些精妙设计的例子练习,进一步巩固学到的知识。在这个过程里发现自身知识的漏洞,在循环中弥补。学习概念 → 实操练习 → 反思理解

变量和内存

Section

在 ELF 格式的可执行文件里,全局内存包括三种:bssdatarodata 临时变量(局部非静态变量)既不会出现在 .data 中,也不会出现在 .bss 中,它由运行期栈维护

Section name区别
.rodataread only data(只读数据) 例如常量字符串、带 const 修饰的全局变量和静态变量等 在目标文件中占用空间
.data指那些初始化过(非零)非 const 的全局变量和静态变量(不带 const 修饰) 在目标文件中占用空间
.bss指那些未初始化和初始化为 0 的全局变量和静态变量(不带 const 修饰) 不占用目标文件的空间

栈(stack)是程序运行的基础,是一种特殊的(先进后出)线性表。只允许在表的一端进行插入或者删除操作 进行插入、删除操作的这一端成为栈顶(stack top),另一端为栈底(stack buttom) 每当一个函数被调用时,一块连续的内存就会在栈顶被分配出来,这块内存就称为帧(frame)

在编译并优化代码的时候,一个函数就是一个最小的编译单元。编译器需要明确的知道每个局部变量的大小,便于预留空间

在编译时,一切无法确定大小或者大小可以改变的数据,都无法安全地放在栈上,最好放在堆上

栈上存储的变量生命周期在当前调用栈的作用域内,无法跨调用域引用

栈上内存分配非常高效,改动栈指针 (stack pointer) 就可以预留相应的空间。把栈指针改动回来,就可以释放预留的空间,不涉及额外计算和系统调用,所以效率很高 栈上的内存在函数结束之后就会回收所使用的帧,相关变量对应的内存也会回收,所以栈上内存的生命周期是不受开发者控制的,并且局限在当前调用栈

但是也要避免把大量数据分配到栈上,因为需要考虑调用栈的大小,避免发生栈溢出 (stack overflow),如果程序的调用栈超出了系统允许的最大栈空间,就会出现无法创建新的栈来执行下一个要执行的函数,然后发生栈溢出,程序就会被终止,产生崩溃信息。常见的原因是过大的栈内存分配或者是递归函数没有妥善终止,导致不断生成新的帧

获取线程默认栈大小

ulimit -s # 默认 8192k 也就是 8M

如果有一个数据结构需要在多个线程中访问,可以放在栈上吗?

不能,因为在多线程的场景下,每个线程的生命周期都是不确定的,无法在编译期知道结束的先后,所以只能使用堆内存。除非结束的顺序确定,就可以共享,例如 scoped thread

可以使用指针引用栈上的某个变量吗?如果可以,在什么情况下可以?

可以,在当前函数调用栈里,可以新建变量在栈里分配内存,然后用一个指针指向它,这个指针的生命周期只能在当前栈帧,不能当做返回值

当要动态大小的内存或者动态生命周期的内存时,只能使用堆 (heap)。例如可变长度的数组、列表、哈希表、字典

堆上分配内存时,通常会预留一些空间。因为堆上分配内存会使用 libc 提供的 malloc() 函数,内部会请求操作系统的调用来分配内存。由于系统调用的成本是昂贵的,所以要避免频繁 malloc()

如果按需分配的话,每次新增加值都会重新分配一块内存,然后拷贝旧的数据,再把新的数据添加进去,释放旧的内存,这样效率很低

堆上分配的内存都需要显式释放,这样可以使堆上内存拥有更灵活的生命周期,以便在不同调用栈之间共享数据。如果堆内存分配之后忘记释放,就会造成内存泄露,导致程序运行时间长了就会占满内存而被操作系统终止

内存安全问题

  • 越界访问漏洞(Out-of-bounds) 堆上的数据如果被多个线程的调用栈引用,该内存改动必须要加锁。否则一个线程在遍历列表,另一个线程在释放列表其中一项,就可能会访问到野指针(指针指向的位置不可知,指向非法内存的指针),导致堆越界访问
  • 释放后重引用漏洞(Use-After-Free) 数据已经被删除或者移动之后原来的指针仍然被保留,指针就是内存地址和数据的一种对应关系,如果只处理了数据没有处理对应关系,就像家里被盗是因为上一个主人走后没有换锁,别人还可以拿旧的钥匙开你家的门

Tracing GC VS Automatic Reference Counting (ARC)

gcdiff
Tracing GC在内存分配和释放上无需额外操作,效率高 释放内存的时机不确定,释放时引发的 STW(stop the world)会导致代码执行的延迟不确定
Automatic Reference Counting添加大量的额外代码处理引用计数,效率低 当引用计数为零时,则释放对象