32. 干货系列从零用Rust编写正反向代理,关于堆和栈以及如何解决stack overflow
wmproxy
wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子
项目地址
国内: https://gitee.com/tickbh/wmproxy
github: https://github.com/tickbh/wmproxy
关于栈Stack
Stack可以被认为是一堆书。当我们添加更多的书时,我们将它们添加到栈的顶部。当我们需要一本书时,我们从上面拿一本。
- 添加数据称为压入栈
- 移除数据称为弹出栈
这种现象在编程中被称为后进先出(LIFO)。
存储在栈上的数据在编译时必须具有固定的大小。默认情况下,Rust在栈上为原始类型分配内存。所有存储在堆栈上的数据必须具有已知的固定大小。未知数据编译时的大小或可能更改的大小必须存储在堆中而不是栈中。
关于堆Heap
与栈相反,大多数情况下,我们需要将变量(内存)传递给不同的函数,并使它们保持比单个函数执行更长的时间。这就是我们可以使用heap的时候。
堆的组织性较差:当您将数据放在堆上时,您会请求一个一定的空间。内存分配器在堆中找到一个空位这是足够大的,标志着它正在使用,并返回一个指针,就是那个地方的地址此过程称为在堆,有时缩写为分配(将值推到堆栈不被认为是分配的)。因为指向堆的指针是已知的,固定大小的,你可以把指针存储在堆栈上,但是当你想要的时候,实际数据,您必须遵循指针。想象一下坐在一个餐厅当你进入时,你说明你的小组人数,主人会找到一张适合所有人的空桌子,然后把你带到那里。如果如果你的团队中有人迟到了,他们可以问你坐在哪里,找到你。
栈与堆对比
- 分配到栈比在堆上分配更快,因为分配器永远不必搜索存储新数据的位置;该位置总是在栈的顶部。相比之下,在堆上分配空间需要更多的工作,因为分配器必须首先找到足够大的空间,保存数据,然后进行簿记,为下一次配置。
- 在堆中访问数据比访问栈上的数据慢,因为你得跟着指示牌走。因为访问堆需要得到相应的指示牌,然后再根据相应的指示牌去寻找相应的位置,然后还要确定位置所占的大小。
| statck栈 | heap堆 |
|---|---|
| 在栈中存储数据的速度更快。 | 在堆中存储数据的速度较慢。 |
| 管理栈中的内存是可预测的,也是微不足道的。 | 管理堆的内存(任意大小)是非常重要的。 |
| Rust堆栈默认分配。 | Box用于分配到堆。 |
| 函数的基元类型和局部变量在栈上分配。 | 大小动态的数据类型,如String、Vector、HashMap、Box等,在heap上分配。 |
栈与堆的分配示例
让我们通过一个例子来直观地了解内存是如何在堆栈上分配和释放的。
fn foo() {
let y = 999;
let z = 333;
}
fn main() {
let x = 111;
foo();
}
在上面的例子中,我们首先调用函数main()。main()函数有一个变量绑定x。
| Address地址 | Name名称 | Value值 |
|---|---|---|
| 0 | x | 111 |
在表中,“地址”列指的是RAM的内存地址。它从0开始,并转到您的计算机有多少RAM(字节数)。“名称”列是指变量,“值”列是指变量的值。
当foo()被调用时,一个新的栈帧被分配。foo()函数有两个变量绑定,y和z。
| Address地址 | Name名称 | Value值 |
|---|---|---|
| 2 | z | 333 |
| 1 | y | 999 |
| 0 | x | 111 |
数字0、1和2不使用计算机实际使用的地址值。实际上,地址根据值由一定数量的字节分隔。
foo()完成后,其栈帧被释放。
| Address地址 | Name名称 | Value值 |
|---|---|---|
| 0 | x | 111 |
main()完成后,其栈帧被释放。Rust自动在堆栈中分配和释放内存。
与堆栈相反,大多数情况下,我们需要将变量(内存)传递给不同的函数,并使它们保持比单个函数执行更长的时间。这就是我们可以使用heap的时候。
我们可以使用Box<T>类型在堆上分配内存。比如说,
fn main() {
let x = Box::new(100);
let y = 222;
println!("x = {}, y = {}", x, y);
}
让我们可视化在上面的例子中调用main()时的内存。
| Address地址 | Name名称 | Value值 |
|---|---|---|
| 0 | x | ??? addr |
| 1 | y | 222 |
和前面一样,我们在堆栈上分配两个变量x和y。
然而,当调用x时,Box::new()的值被分配在堆上。因此,x的实际值是指向堆的指针。
| Address地址 | Name名称 | Value值 |
|---|---|---|
| 578 | 100 | |
| ... | ... | ... |
| 0 | x | -> 578 |
| 1 | y | 222 |
这里,变量x保存指向地址→578,这是用于演示的任意地址。堆可以以任何顺序分配和释放。因此,它可能会以不同的地址结束,并在地址之间产生漏洞。
因此,当x消失时,它首先释放堆上分配的内存。
| Address地址 | Name名称 | Value值 |
|---|---|---|
| ... | ... | ... |
| 1 | y | 222 |
一旦main()完成,我们释放堆栈帧,所有东西都消失了,释放了所有内存。
如何排查问题
堆内存的排查
关于堆内存的排查,堆内存的内存量比较大,因此数值相对会大很多,堆内存的大小通常小到几M,大到几个G,所以在堆内存排查的时候可以用宏观的内存管理器,有以下几种方法
- 如
TOP查看内存,也可以通过调用系统的api, - 如
memory-stats实时查看进程当前占用内存数:
use memory_stats::memory_stats;
fn main() {
if let Some(usage) = memory_stats() {
println!("Current physical memory usage: {}", usage.physical_mem);
println!("Current virtual memory usage: {}", usage.virtual_mem);
} else {
println!("Couldn't get the current memory usage :(");
}
}
- 可以自定义
Alloc,因为Rust提供的全局global_alloc,我们可以通过自定义Alloc计算当前申请的内存数,以及可以用这种方式检查内存泄漏,典型的jemalloc就是通过这种方式来的,我们用这种方式实现简单的内存统计,我们定义了一个Trallocator:
use std::alloc::{GlobalAlloc, Layout};
use std::sync::atomic::{AtomicU64, Ordering};
pub struct Trallocator<A: GlobalAlloc>(pub A, AtomicU64);
unsafe impl<A: GlobalAlloc> GlobalAlloc for Trallocator<A> {
unsafe fn alloc(&self, l: Layout) -> *mut u8 {
self.1.fetch_add(l.size() as u64, Ordering::SeqCst);
self.0.alloc(l)
}
unsafe fn dealloc(&self, ptr: *mut u8, l: Layout) {
self.0.dealloc(ptr, l);
self.1.fetch_sub(l.size() as u64, Ordering::SeqCst);
}
}
impl<A: GlobalAlloc> Trallocator<A> {
pub const fn new(a: A) -> Self {
Trallocator(a, AtomicU64::new(0))
}
pub fn reset(&self) {
self.1.store(0, Ordering::SeqCst);
}
pub fn get(&self) -> u64 {
self.1.load(Ordering::SeqCst)
}
}
我们通过调用该类,实现
use std::alloc::System;
// 这句使全局的的分配器变成我们自己的分配器
#[global_allocator]
static GLOBAL: Trallocator<System> = Trallocator::new(System);
fn main() {
GLOBAL.reset();
println!("memory used: {} bytes", GLOBAL.get());
GLOBAL.reset();
{
let mut vec = vec![1, 2, 3, 4];
for i in 5..20 {
vec.push(i);
println!("memory used: {} bytes", GLOBAL.get());
}
println!("{:?}", v);
}
println!("memory used: {} bytes", GLOBAL.get());
}
我们可以得到以下输出:
memory used: 0 bytes
memory used: 32 bytes
memory used: 32 bytes
memory used: 32 bytes
memory used: 32 bytes
memory used: 64 bytes
memory used: 64 bytes
memory used: 64 bytes
memory used: 64 bytes
memory used: 64 bytes
memory used: 64 bytes
memory used: 64 bytes
memory used: 64 bytes
memory used: 128 bytes
memory used: 128 bytes
memory used: 128 bytes
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
memory used: 0 bytes
可以看到分配完之后已经及时释放
栈内存的排查
因为系统提供的栈内存通常只有8m左右,且Rust中的线程的默认栈内存只有2M,如果分配过大的栈内存将会导致栈溢出,比如
fn main() {
let bad = [0;10240000];
}
就会出现如下提示
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
在现在的方法中,我并未找到有合适的检查当前进程占用的栈内存数。
- 测试用
alloc看是否能测出栈内存:
use std::alloc::System;
#[global_allocator]
static GLOBAL: Trallocator<System> = Trallocator::new(System);
fn main() {
GLOBAL.reset();
println!("memory used: {} bytes", GLOBAL.get());
GLOBAL.reset();
let x = 0;
let bad = [0;10240];
println!("memory used: {} bytes", GLOBAL.get());
}
运行上述程序,如下输出:
memory used: 0 bytes
memory used: 0 bytes
程序无法感知到栈内存的变化。
- 测试用
memory-stats实时查看内存
use memory_stats::memory_stats;
fn main() {
if let Some(usage) = memory_stats() {
println!("初始内存 usage: {}", usage.physical_mem);
} else {
println!("Couldn't get the current memory usage :(");
}
let value1 = vec![10;102400];
std::thread::sleep(std::time::Duration::from_secs(1));
if let Some(usage) = memory_stats() {
println!("申请堆内存后 usage: {}", usage.physical_mem);
} else {
println!("Couldn't get the current memory usage :(");
}
let value = [10;102400];
std::thread::sleep(std::time::Duration::from_secs(1));
if let Some(usage) = memory_stats() {
println!("申请栈内存后 usage: {}", usage.physical_mem);
} else {
println!("Couldn't get the current memory usage :(");
}
}
以上程序会输出:
初始内存 usage: 1024000
申请堆内存后 usage: 1478656
申请栈内存后 usage: 1478656
我们可以感知到堆内存的变化,无法感知到栈内存的变化。
- 目前找到的可以测量类对象的栈内存值。可以用
std::mem::size_of_val来测量类对象占用的栈内存大小,我们可以通过该方法进行栈大小的排查,看是否存在超级大的占用栈的对象,如果存在,需将其移动到堆,也就是用Box进行包裹。
fn main() {
let x = 0u32;
assert_eq!(4, std::mem::size_of_val(&x));
let val = vec![0u64;9999];
assert_eq!(24, std::mem::size_of_val(&val));
let mut hash = HashMap::new();
hash.insert(1, 2);
assert_eq!(48, std::mem::size_of_val(&hash));
hash.insert(2, 4);
assert_eq!(48, std::mem::size_of_val(&hash));
}
我们来分析下Vec的内存,为什么其占用大小为24个字节(64位的机器)
pub struct Vec<T, A: Allocator = Global> {
buf: RawVec<T, A>, /// 需要再进行类的分析
len: usize, /// 占用64位,也就是8个字节
}
pub(crate) struct RawVec<T, A: Allocator = Global> {
ptr: Unique<T>, /// 指针大小,占用64位,8字节
cap: usize, /// 容量大小,占用64位,8字节
alloc: A, /// 分配器,不占用栈内存
}
综上分析,每个Vec的栈大小占用内存均为24字节。程序测试一致。同样HashMap占用的栈大小均为48个字节,不受其Map大小的影响。
注意:如果用异步的Future的包围,如果返回的对象也就是
Furture<Output=xxx>的栈大小过大,很容易在递进处理异步的情况下直接栈溢出,而此时完全还未执行到该函数,造成一种很难排查的景象
注意!!!异步的返回值千万栈大小不要过大!不要过大!不要过大!
- 另外还有一种是递归的函数调用,也会造成栈溢出,这类问题相对好定位:
fn f(x: i32) {
f(1);
}
fn main() {
f(2);
}
直接会显示
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
小结
所以在排查内存泄漏还是排查栈大小时都需要对当前的数据进行分析,需要处理的东西较多,需要有比较好的耐心去处理,一步步的去排查推进。记得异步返回的Output如果过大,会导致代码还未执行,但已经栈溢出的情况。
点击 [关注],[在看],[点赞] 是对作者最大的支持
32. 干货系列从零用Rust编写正反向代理,关于堆和栈以及如何解决stack overflow的更多相关文章
- (转)Spring Boot干货系列:(七)默认日志logback配置解析
转:http://tengj.top/2017/04/05/springboot7/ 前言 今天来介绍下Spring Boot如何配置日志logback,我刚学习的时候,是带着下面几个问题来查资料的, ...
- 【转】Spring Boot干货系列:(一)优雅的入门篇
转自Spring Boot干货系列:(一)优雅的入门篇 前言 Spring一直是很火的一个开源框架,在过去的一段时间里,Spring Boot在社区中热度一直很高,所以决定花时间来了解和学习,为自己做 ...
- Spring Boot干货系列:(八)数据存储篇-SQL关系型数据库之JdbcTemplate的使用
Spring Boot干货系列:(八)数据存储篇-SQL关系型数据库之JdbcTemplate的使用 原创 2017-04-13 嘟嘟MD 嘟爷java超神学堂 前言 前面几章介绍了一些基础,但都是静 ...
- Spring Boot干货系列:(七)默认日志框架配置
Spring Boot干货系列:(七)默认日志框架配置 原创 2017-04-05 嘟嘟MD 嘟爷java超神学堂 前言 今天来介绍下Spring Boot如何配置日志logback,我刚学习的时候, ...
- Spring Boot干货系列:(五)开发Web应用JSP篇
Spring Boot干货系列:(五)开发Web应用JSP篇 原创 2017-04-05 嘟嘟MD 嘟爷java超神学堂 前言 上一篇介绍了Spring Boot中使用Thymeleaf模板引擎,今天 ...
- Spring Boot干货系列:(四)Thymeleaf篇
Spring Boot干货系列:(四)Thymeleaf篇 原创 2017-04-05 嘟嘟MD 嘟爷java超神学堂 前言 Web开发是我们平时开发中至关重要的,这里就来介绍一下Spring Boo ...
- Spring Boot干货系列:(一)优雅的入门篇
Spring Boot干货系列:(一)优雅的入门篇 2017-02-26 嘟嘟MD 嘟爷java超神学堂 前言 Spring一直是很火的一个开源框架,在过去的一段时间里,Spring Boot在社 ...
- Java多线程干货系列—(四)volatile关键字
原文地址:http://tengj.top/2016/05/06/threadvolatile4/ <h1 id="前言"><a href="#前言&q ...
- Spring Boot干货系列:(十二)Spring Boot使用单元测试(转)
前言这次来介绍下Spring Boot中对单元测试的整合使用,本篇会通过以下4点来介绍,基本满足日常需求 Service层单元测试 Controller层单元测试 新断言assertThat使用 单元 ...
- (转)Spring Boot干货系列:(四)开发Web应用之Thymeleaf篇
转:http://tengj.top/2017/03/13/springboot4/ 前言 Web开发是我们平时开发中至关重要的,这里就来介绍一下Spring Boot对Web开发的支持. 正文 Sp ...
随机推荐
- 【re】[NISACTF 2022]string --linux下的随机数
附件下载,查壳 发现是ELF程序,64位,ida打开分析 flag函数点进去 前面一堆代码其实都不重要,直接看主要代码: puts("The length of flag is 13&qu ...
- raspberry pi Pico使用MicroPython变砖后的解决方法
使用raspberry pi Pico的原因 在硬件产品(单片机)的开发中我们往往需要借助一些额外的仪器/设备进行产品的辅助测试, 假设我们需要一个IO+ADC类型辅助设备, 以往的做法是 原理图-& ...
- 解密Prompt系列19. LLM Agent之数据分析领域的应用:Data-Copilot & InsightPilot
在之前的 LLM Agent+DB 的章节我们已经谈论过如何使用大模型接入数据库并获取数据,这一章我们聊聊大模型代理在数据分析领域的应用.数据分析主要是指在获取数据之后的数据清洗,数据处理,数据建模, ...
- 五分钟 k8s 实战-应用探针
今天进入 kubernetes 的运维部分(并不是运维 kubernetes,而是运维应用),其实日常我们大部分使用 kubernetes 的功能就是以往运维的工作,现在云原生将运维和研发关系变得更紧 ...
- 公司要做大数据可视化看板,除了EXCEL以外有没有好用的软件可以用
当企业需要进行大数据可视化看板的设计和开发时,除了Excel,还有许多其他强大且适合大数据可视化的软件工具.以下是几种常用的好用软件,以及它们的特点和优势,供您参考. 一.Datainside 特点和 ...
- [ABC262G] LIS with Stack
Problem Statement There is an empty sequence $X$ and an empty stack $S$. Also, you are given an inte ...
- 一个Servlet如何实现增-删-改-查的业务逻辑
一.业务场景 最近在教学生学习JavaWeb中的Servlet,它就是一个Java服务端的小程序,用来提供各种服务. 在讲解得时候,自己突然遇到一个问题,那就是现在没有使用什么SpringMvc框架, ...
- 为什么要重写equals()?
为什么要重写equals()? Equals和 == 的区别: ==:是个运算符, 判断是否相等,基本数据类型进行判断 也可判断两个对象相等,比较两个对象的哈希码值 Equals:是个Object类的 ...
- Python——第二章:字符串操作——索引和切片
索引: 按照位置提取元素 可以采用索引的方式来提取某一个字符(文字) s = "我叫周杰伦" print(s[3]) #程序员都是从0开始数,这里的3代表第4位,也就是" ...
- Dart 3.2 更新盘点
作者 / Kevin Moore 和 Michael Thomsen 我们隆重宣布推出 Dart 3.2,这一版本针对以下方面做出了改进: 新增了一项语言功能,可对私有 final 字段进行非空升级: ...