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用于分配到堆。
函数的基元类型和局部变量在栈上分配。 大小动态的数据类型,如StringVectorHashMapBox等,在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()函数有两个变量绑定,yz

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的更多相关文章

  1. (转)Spring Boot干货系列:(七)默认日志logback配置解析

    转:http://tengj.top/2017/04/05/springboot7/ 前言 今天来介绍下Spring Boot如何配置日志logback,我刚学习的时候,是带着下面几个问题来查资料的, ...

  2. 【转】Spring Boot干货系列:(一)优雅的入门篇

    转自Spring Boot干货系列:(一)优雅的入门篇 前言 Spring一直是很火的一个开源框架,在过去的一段时间里,Spring Boot在社区中热度一直很高,所以决定花时间来了解和学习,为自己做 ...

  3. Spring Boot干货系列:(八)数据存储篇-SQL关系型数据库之JdbcTemplate的使用

    Spring Boot干货系列:(八)数据存储篇-SQL关系型数据库之JdbcTemplate的使用 原创 2017-04-13 嘟嘟MD 嘟爷java超神学堂 前言 前面几章介绍了一些基础,但都是静 ...

  4. Spring Boot干货系列:(七)默认日志框架配置

    Spring Boot干货系列:(七)默认日志框架配置 原创 2017-04-05 嘟嘟MD 嘟爷java超神学堂 前言 今天来介绍下Spring Boot如何配置日志logback,我刚学习的时候, ...

  5. Spring Boot干货系列:(五)开发Web应用JSP篇

    Spring Boot干货系列:(五)开发Web应用JSP篇 原创 2017-04-05 嘟嘟MD 嘟爷java超神学堂 前言 上一篇介绍了Spring Boot中使用Thymeleaf模板引擎,今天 ...

  6. Spring Boot干货系列:(四)Thymeleaf篇

    Spring Boot干货系列:(四)Thymeleaf篇 原创 2017-04-05 嘟嘟MD 嘟爷java超神学堂 前言 Web开发是我们平时开发中至关重要的,这里就来介绍一下Spring Boo ...

  7. Spring Boot干货系列:(一)优雅的入门篇

    Spring Boot干货系列:(一)优雅的入门篇 2017-02-26 嘟嘟MD 嘟爷java超神学堂   前言 Spring一直是很火的一个开源框架,在过去的一段时间里,Spring Boot在社 ...

  8. Java多线程干货系列—(四)volatile关键字

    原文地址:http://tengj.top/2016/05/06/threadvolatile4/ <h1 id="前言"><a href="#前言&q ...

  9. Spring Boot干货系列:(十二)Spring Boot使用单元测试(转)

    前言这次来介绍下Spring Boot中对单元测试的整合使用,本篇会通过以下4点来介绍,基本满足日常需求 Service层单元测试 Controller层单元测试 新断言assertThat使用 单元 ...

  10. (转)Spring Boot干货系列:(四)开发Web应用之Thymeleaf篇

    转:http://tengj.top/2017/03/13/springboot4/ 前言 Web开发是我们平时开发中至关重要的,这里就来介绍一下Spring Boot对Web开发的支持. 正文 Sp ...

随机推荐

  1. Ansible自动化部署工具-组件及语法介绍

    大家好,我是蓝胖子,我认为自动化运维要做的事情就是把运维过程中的某些步骤流程化,代码化,这样在以后执行类似的操作的时候就可以解放双手了,让程序自动完成.避免出错,Ansible就是这方面非常好用的工具 ...

  2. Mysql数据库查询之模糊查询

    一.什么是模糊查询模糊查询是根据一定的模式匹配规则,查找与指定条件相似或相符的数据.二.模糊查询实操通配符查询1.% 表示任意0个或多个字符形式一: select 查询字段 from 表名 where ...

  3. Android 输入系统介绍

    目录 一.目的 二.环境 三.相关概念 3.1 输入设备 3.2 UEVENT机制 3.3 JNI 3.4 EPOLL机制 3.5 INotify 四.详细设计 4.1 结构图 4.2 代码结构 4. ...

  4. 【生活技巧记录】歌词Lyric生成及音乐标签嵌入

    前置工具准备: BesLyric:一款专门制作 网易云音乐 LRC 滚动歌词的软件! 搜索.下载.制作 歌词更方便! Foobar 2000:一款适用于 Windows 平台的高级免费软件音频播放器 ...

  5. nordic的nrf52系列32M速率的SPI-SPIM3

    简介:在nordic的nrf52系列中的nrf52833和nrf52840的SPIM3都是支持最大32M的spi速率,其余的只有8M,当在需要刷屏,或者一些需要高速32M-SPI时,这是一个很好的使用 ...

  6. SQL Server 自动增长清零的方法

    方法一: truncate table TableName 删除表中的所有的数据的同时,将自动增长清零.如果有外键参考这个表,这个方法会报错(即便主键表和外键表都已经没有数据),请参考方法2. 方法二 ...

  7. vertx的学习总结三

    一.event bus是什么 各个verticle的通信 二.point-to-point, request-reply, publish/subscribe 通过 the event bus 例题一 ...

  8. 将mysql的输出文本写回mysql

    1 准备工作 1.1 环境准备 操作系统:Microsoft Windows 10 专业工作站版 软件版本:Python 3.9.6 第三方包: pip install pandas2.1.0 pip ...

  9. flchart库判断当前点击的底部title的index

    使用flchart库,版本0.35,在点击柱状图时,当柱状图的高度为0,默认选中热区很小,很难点击选中对应区域,如图一 9和10的柱状图高度为0. 查了源码,貌似没有单独针对底部title设置点击事件 ...

  10. MD5在文件安全中的应用与重要性

    一.MD5简介 MD5(Message-Digest Algorithm 5)是一种广泛应用的密码散列函数,由美国密码学家罗纳德·李维斯特(Ronald Linn Rivest)于1992年提出.它主 ...