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

栈与堆的分配示例

让我们通过一个例子来直观地了解内存是如何在堆栈上分配和释放的。

RUST 复制代码
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>类型在堆上分配内存。比如说,

RUST 复制代码
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实时查看进程当前占用内存数:
RUST 复制代码
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
RUST 复制代码
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)
    }
}

我们通过调用该类,实现

RUST 复制代码
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());
}

我们可以得到以下输出:

yaml 复制代码
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,如果分配过大的栈内存将会导致栈溢出,比如

RUST 复制代码
fn main() {
    let bad = [0;10240000];
}

就会出现如下提示

arduino 复制代码
thread 'main' has overflowed its stack
fatal runtime error: stack overflow

在现在的方法中,我并未找到有合适的检查当前进程占用的栈内存数。

  • 测试用alloc看是否能测出栈内存:
RUST 复制代码
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());
}

运行上述程序,如下输出:

python 复制代码
memory used: 0 bytes
memory used: 0 bytes

程序无法感知到栈内存的变化。

  • 测试用memory-stats实时查看内存
RUST 复制代码
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 :(");
    }
    
}

以上程序会输出:

yaml 复制代码
初始内存     usage: 1024000
申请堆内存后 usage: 1478656
申请栈内存后 usage: 1478656

我们可以感知到堆内存的变化,无法感知到栈内存的变化。

  • 目前找到的可以测量类对象的栈内存值。可以用std::mem::size_of_val来测量类对象占用的栈内存大小,我们可以通过该方法进行栈大小的排查,看是否存在超级大的占用栈的对象,如果存在,需将其移动到堆,也就是用Box进行包裹。
RUST 复制代码
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位的机器)

RUST 复制代码
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>的栈大小过大,很容易在递进处理异步的情况下直接栈溢出,而此时完全还未执行到该函数,造成一种很难排查的景象 注意!!!异步的返回值千万栈大小不要过大!不要过大!不要过大!

  • 另外还有一种是递归的函数调用,也会造成栈溢出,这类问题相对好定位:
RUST 复制代码
fn f(x: i32) {
    f(1);
}
fn main() {
    f(2);
}

直接会显示

arduino 复制代码
thread 'main' has overflowed its stack
fatal runtime error: stack overflow

小结

所以在排查内存泄漏还是排查栈大小时都需要对当前的数据进行分析,需要处理的东西较多,需要有比较好的耐心去处理,一步步的去排查推进。记得异步返回的Output如果过大,会导致代码还未执行,但已经栈溢出的情况。

点击 [关注][在看][点赞] 是对作者最大的支持

相关推荐
zopple3 小时前
常见的 Spring 项目目录结构
java·后端·spring
cjy0001115 小时前
springboot的 nacos 配置获取不到导致启动失败及日志不输出问题
java·spring boot·后端
小江的记录本5 小时前
【事务】Spring Framework核心——事务管理:ACID特性、隔离级别、传播行为、@Transactional底层原理、失效场景
java·数据库·分布式·后端·sql·spring·面试
sheji34165 小时前
【开题答辩全过程】以 基于springboot的校园失物招领系统为例,包含答辩的问题和答案
java·spring boot·后端
程序员cxuan6 小时前
人麻了,谁把我 ssh 干没了
人工智能·后端·程序员
wuyikeer7 小时前
Spring Framework 中文官方文档
java·后端·spring
Victor3567 小时前
MongoDB(61)如何避免大文档带来的性能问题?
后端
Victor3567 小时前
MongoDB(62)如何避免锁定问题?
后端
wuyikeer8 小时前
Spring BOOT 启动参数
java·spring boot·后端
子木HAPPY阳VIP9 小时前
Ubuntu 22.04 VMware 设置固定IP配置
人工智能·后端·目标检测·机器学习·目标跟踪