wmproxy
已用Rust
实现http/https
代理, socks5
代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket
代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子
国内: https://gitee.com/tickbh/wmproxy
github: https://github.com/tickbh/wmproxy
Stack
Stack
可以被认为是一堆书。当我们添加更多的书时,我们将它们添加到栈的顶部。当我们需要一本书时,我们从上面拿一本。
Heap
与栈相反,大多数情况下,我们需要将变量(内存)传递给不同的函数,并使它们保持比单个函数执行更长的时间。这就是我们可以使用heap的时候。
堆的组织性较差:当您将数据放在堆上时,您会请求一个一定的空间。内存分配器在堆中找到一个空位这是足够大的,标志着它正在使用,并返回一个指针,就是那个地方的地址此过程称为在堆,有时缩写为分配(将值推到堆栈不被认为是分配的)。因为指向堆的指针是已知的,固定大小的,你可以把指针存储在堆栈上,但是当你想要的时候,实际数据,您必须遵循指针。想象一下坐在一个餐厅当你进入时,你说明你的小组人数,主人会找到一张适合所有人的空桌子,然后把你带到那里。如果如果你的团队中有人迟到了,他们可以问你坐在哪里,找到你。
statck栈 | heap堆 |
---|---|
在栈中存储数据的速度更快。 | 在堆中存储数据的速度较慢。 |
管理栈中的内存是可预测的,也是微不足道的。 | 管理堆的内存(任意大小)是非常重要的。 |
Rust堆栈默认分配。 | Box用于分配到堆。 |
函数的基元类型和局部变量在栈上分配。 | 大小动态的数据类型,如String 、Vector 、HashMap 、Box 等,在heap上分配。 |
让我们通过一个例子来直观地了解内存是如何在堆栈上分配和释放的。
RUSTfn 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>
类型在堆上分配内存。比如说,
RUSTfn 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
实时查看进程当前占用内存数:RUSTuse 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)
}
}
我们通过调用该类,实现
RUSTuse 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,如果分配过大的栈内存将会导致栈溢出,比如
RUSTfn main() {
let bad = [0;10240000];
}
就会出现如下提示
thread 'main' has overflowed its stack fatal runtime error: stack overflow
在现在的方法中,我并未找到有合适的检查当前进程占用的栈内存数。
alloc
看是否能测出栈内存:RUSTuse 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
实时查看内存RUSTuse 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
进行包裹。RUSTfn 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位的机器)
RUSTpub 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>
的栈大小过大,很容易在递进处理异步的情况下直接栈溢出,而此时完全还未执行到该函数,造成一种很难排查的景象 注意!!!异步的返回值千万栈大小不要过大!不要过大!不要过大!
RUSTfn f(x: i32) {
f(1);
}
fn main() {
f(2);
}
直接会显示
thread 'main' has overflowed its stack fatal runtime error: stack overflow
所以在排查内存泄漏还是排查栈大小时都需要对当前的数据进行分析,需要处理的东西较多,需要有比较好的耐心去处理,一步步的去排查推进。记得异步返回的Output
如果过大,会导致代码还未执行,但已经栈溢出的情况。
点击 [关注],[在看],[点赞] 是对作者最大的支持
本文作者:问蒙服务框架
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!