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. Android 锁屏时的生命周期

    锁定屏幕前,应用被打开:onCreate->onStart->onResume 此时按下电源键,锁定屏幕:onPause->onStop 解锁屏幕,重新回到应用:onRestart- ...

  2. 黑客玩具入门——9、Burp Suite

    Burp Suite是一款集成化的渗透测试工具,包含了很多功能,可以帮助我们快速完成对web应用程序的渗透测试和攻击.Burp Suite是由Java语言编写,因为Java是可以跨平台的,所以Burp ...

  3. SSPRQ码型设计

    serdes速率超过50G之后,在VSR光模块场景下SSPRQ使用较为广泛.SSPRQ用于PAM4光模块眼图压力,闭合率测试. 但是有很多serdes IP不支持此功能.出于测试需要需要新设计SSPR ...

  4. 通过 VS Code 优雅地编辑 Pod 内的代码(非 NodePort)

    目录 1. 概述 2. NodePort 方式 3. Ingress 方式 4. 救命稻草 5. 其他 1. 概述 今天聊点啥呢,话说,你有没有想过怎样用 VS Code 连上 K8s 集群内的某个 ...

  5. 中企网安信息科技:协同办公OA信息化管理系统概述

    由华企网安总公司北京中企网安信息科技有限责任公司开发的<协同办公OA信息化管理系统>,获得国家版权局颁发的计算机软件著作权登记证书. 协同办公OA信息化管理系统是一种综合性的软件系统,用于 ...

  6. 复习:Java基础-泛型方法

    泛型 大家都很熟悉了 泛型方法呢 可能很多小伙伴都有混淆,今天来稍微复习一下 泛型方法(普通方法) public class Test<T> { public T f(T c) { //注 ...

  7. Python——第五章:time模块、datetime模块

    time模块 在平常的代码中,我们常常需要与时间打交道.在Python中,与时间处理有关的模块就包括:time,datetime,calendar(很少用,不讲),下面分别来介绍. 我们写程序时对时间 ...

  8. 马某 说c# 不开源,他是蠢还是坏?

    马某在视频 计算机主流开发语言的现状和未来3-5年的发展前景--Java.Golang.Python.C\C#\C++.JS.前端.AI.大数据.测试.运维.网络安全 点评各种语言,其中说到C# 的时 ...

  9. 在 Walrus 上轻松集成 OpenTofu

    OpenTofu 是什么? OpenTofu 是一个开源的基础设施即代码(IaC)框架,被提出作为 Terraform 的替代方案,并由 Linux 基金会管理.OpenTofu 的问世为应对 Has ...

  10. 通过数字证书对PDF电子文件进行数字签名/盖章

    以下代码详细说明如何使用数字证书对PDF电子文件进行数字签名/盖章.PDF文件签署主要传递PDF文件,数字证书信息,签章图片3个信息.代码中需要的文件.数字证书.签章图片可访问开放签电子签章开源系统详 ...