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 ...
随机推荐
- AcWing 456. 车站分级
原题链接AcWing 456. 车站分级 抽象出题意,停靠过的车站的等级一定严格大于为停靠过的车站的等级,且不存在环,例如车站\(A\)等级大于车站\(B\),则\(A >= B + 1\),不 ...
- 发现AI自我意识:从理解到思维
广义"理解"已经实现 在最新的人工智能系统中,我们经常可以观察到一种类似"理解"的能力.这种广义的"理解"能力,主要建立在两个基础之上:海量 ...
- C语言已知四位数3025具有一个特殊性质:它的前两位数字30与后两位数字25之和是55,而55的平方正好等于3025。编程找出所有具有这种性质的四位数。
#include<stdio.h> void main() { int n, i, j; for (n = 1000; n < 10000; n++) { i = n / 100; ...
- 手撕Vue-Router-实现router-link
前言 在上一篇 [手撕Vue-Router-添加全局$router属性] 中,实现了将每一个 Vue 实例上挂载一个 $router 属性,这个属性就是我们在上一篇文章中创建的 VueRouter 实 ...
- 离散傅里叶变换DFT的应用
目录 一维DFT 1 DFT的相关内容 2 DFT计算结果验证 3 DFT的时频曲线分析 4 DFT的应用 二维DFT 1 DFT在图像处理时的相关内容 2 DFT滤波应用 一维DFT 1 DFT的相 ...
- 【scikit-learn基础】--『数据加载』之真实数据集
上一篇介绍了scikit-learn中的几个玩具数据集,本篇介绍scikit-learn提供的一些真实的数据集.玩具数据集:scikit-learn 基础(01)--『数据加载』之玩具数据集 1. 获 ...
- 数据仓库——Hive
数据仓库:是一个用于储存,分析,报告的数据系统 数据仓库的目的是构建面向分析的集成化数据环境,分析结果为企业提供决策支持 数仓专注分析 数据仓库仓库为何而来,解决什么问题的? 为了分析数据而来,分析结 ...
- 聊一聊 .NET高级调试 内核模式堆泄露
一:背景 1. 讲故事 前几天有位朋友找到我,说他的机器内存在不断的上涨,但在任务管理器中查不出是哪个进程吃的内存,特别奇怪,截图如下: 在我的分析旅程中都是用户态模式的内存泄漏,像上图中的异常征兆已 ...
- [.NET开发者的福音]一个方便易用的在线.NET代码编辑工具.NET Fiddle
前言 今天给大家分享一个方便易用的.NET在线代码编辑工具,能够帮助.NET开发人员快速完成代码编写.测试和分享的需求(.NET开发者的福音):.NET Fiddle. .NET Fiddle介绍 我 ...
- 数字孪生系统融合GIS系统能够在洪涝灾害防治上带来什么帮助?
数字孪生技术与GIS系统的融合,为防治洪涝灾害方式带来了巨大的改变.这种整合的力量超越了过去单一技术的局限,为防洪抗灾工作提供了更全面.更准确的决策支持和应急响应能力. 在过去,防洪抗灾工作主要依赖于 ...