目录
- 内存分配
- 工具
- free
- vmstat
- top/htop
- ps
- pmap
- bcc工具
- Valgrind
- AddressSanitizer(ASAN)
内存分配
malloc是C标准库提供的内存分配函数,对应到系统调用上,有两种实现方式:brk()和mmap()。 对于小块内存(小于128k),C标准库使用brk来分配,也就是通过移动堆顶的位置来分配内存,这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。这种分配方式可以减少缺页异常的发生,提高内存访问效率,不过由于这些内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片; 对大块内存(大于128k),则直接使用mmap来分配,也就是在文件映射段找一块空闲内存分配出去。mmap分配的内存会在释放时直接归还系统,所以每次mmap都会发生缺页异常,在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大,这也是malloc只对大块内存使用mmap的原因。 这两种内存分配调用发生后,并没有真正分配内存,这些内存只在首次访问时才分配,也就是通过缺页异常进入内核中,再由内核来分配内存。
系统会限制进程避免耗尽内存,如果某个进程申请或占用大量内存,系统会杀死进程。
工具
free
首先可以使用free查看系统的内存大小
free的用法:
- -h:显示较详细的信息,便于读取
- -s:指定间隔秒数不断获取
- -k/-m/-g:显示存储
$free -h
total used free shared buff/cache available
内存: 30Gi 2.0Gi 799Mi 3.0Mi 28Gi 28Gi
交换: 2.0Gi 1.0Mi 2.0Gi
输出详解: 第一列,total 是总内存大小; 第二列,used 是已使用内存的大小,包含了共享内存; 第三列,free 是未使用内存的大小; 第四列,shared 是共享内存的大小; 第五列,buff/cache 是缓存和缓冲区的大小; 最后一列,available 是新进程可用内存的大小。
可以通过free -h -s 3 | tee free.txt输出free的数据并重定向到free.txt文本中,可以通过以下py代码统计这些数据:
import re
from collections import defaultdict
class FreeData:
"""
用于分析free命令输出的数据
"""
def __init__(self, free_text):
self.free_text = free_text
self.mem = defaultdict(list)
self.mem_average = dict()
self.swap = defaultdict(list)
self.swap_average = dict()
self.mem_unit = str()
self.regex_str = r'\s*([\w\.]+)'
self.regex_pattern = re.compile(self.regex_str)
def read_free_text(self):
with open(self.free_text, 'rt', encoding='utf-8') as f:
for line_data in f:
self.parse_date(line_data)
def parse_date(self, data_str):
line_list = self.regex_pattern.findall(data_str)
if len(line_list) == 0:
return
if line_list[0] == 'Mem' or line_list[0] == '内存':
self.mem['total'].append(line_list[1])
self.mem['used'].append(line_list[2])
self.mem['free'].append(line_list[3])
self.mem['shared'].append(line_list[4])
self.mem['buff'].append(line_list[5])
self.mem['available'].append(line_list[6])
elif line_list[0] == 'Swap' or line_list[0] == '交换':
self.swap['total'].append(line_list[1])
self.swap['used'].append(line_list[2])
self.swap['free'].append(line_list[3])
else:
if line_list[0] != 'total' or line_list[1] != 'used' or line_list[2] != 'free' or line_list[3] != 'shared' or line_list[5] != 'available':
pass
def calc_mem(self, key_str):
val = 0
num = 0
for item in self.mem[key_str]:
val = val + float(item[:-2])
num = num + 1
self.mem_average[key_str] = val / num
def calc_swap(self, key_str):
val = 0
num = 0
for item in self.swap[key_str]:
val = val + float(item[:-2])
num = num + 1
self.swap_average[key_str] = val / num
def calculate_average(self):
for key_str in self.mem:
self.calc_mem(key_str)
for key_str in self.swap:
self.calc_swap(key_str)
def run(self):
self.read_free_text()
self.calculate_average()
for key, val in self.mem.items():
print('key is: ', key, ', value is: ', val)
for key, val in self.swap.items():
print('key is: ', key, ', value is: ', val)
if __name__ == '__main__':
free_data = FreeData('./free.txt')
free_data.run()
free只是系统整体使用情况,要查看某个进程的内存使用需要继续使用以下工具。
vmstat
查看内存变化情况
通过vmstat,可以看到空闲列是否一直是减少的趋势,而缓冲和缓存一直不变,说明存在内存泄漏
top/htop
top默认使用3s的时间间隔,取这段间隔时间的cpu/mem 的平均值 top动态观察进程的变化情况,有以下选项:
- -d:后接秒数,进程数据更新的秒数,如果不指定默认为3秒
- -b:以批次的方式执行top,搭配重定向使用,如果不使用这个选项,重定向后的文件乱码
- -n:指定top命令执行的次数
- -p:仅查看指定ID的进程
- -u:只监听某个用户的进程
- -H:显示线程而非进程
用法举例:
- 如需要查看某个进程的各线程:top -H -p xx top命令的主要输出选项:
top输出内容如下:
top - 15:19:07 up 35 days, 55 min, 1 user, load average: 0.05, 0.01, 0.00
任务: 282 total, 1 running, 281 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.7 us, 1.5 sy, 0.0 ni, 97.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 31652.3 total, 1872.2 free, 1305.8 used, 28474.3 buff/cache
MiB Swap: 2048.0 total, 2046.5 free, 1.5 used. 29886.6 avail Mem
进程号 USER PR NI VIRT RES SHR %CPU %MEM TIME+ COMMAND
1960352 user 20 0 1009564 144228 34744 S 6.2 0.4 0:15.64 user_test
1 root 20 0 168596 14144 8352 S 0.0 0.0 1:29.64 systemd
...
输出解释:
- top - 01:23:45 up 1 day, 5:00, 1 user, load average: 0.00, 0.01, 0.05
- 01:23:45:当前系统时间
- up 1 day, 5:00:系统已经运行了1天5小时
- 1 user:当前有1个用户登录系统
- load average: 0.00, 0.01, 0.05:系统负载,即任务队列的平均长度,三个值分别为1分钟、5分钟、15分钟的平均值
- Tasks: 80 total, 1 running, 79 sleeping, 0 stopped, 0 zombie
- 80 total:当前系统总进程数
- 1 running:正在运行的进程数
- 79 sleeping:睡眠的进程数
- 0 stopped:停止的进程数
- 0 zombie:僵尸进程数
- %Cpu(s): 5.0 us, 3.0 sy, 0.0 ni, 92.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
- 5.0 us:用户空间占用CPU的百分比
- 3.0 sy:内核空间占用CPU的百分比
- 0.0 ni:用户空间内改变过优先级的进程占用CPU的百分比
- 92.0 id:空闲CPU百分比
- 0.0 wa:等待I/O的CPU时间百分比
- 0.0 hi:硬中断占用CPU的百分比
- 0.0 si:软中断占用CPU的百分比
- KiB Mem : 1626844 total, 813420 free, 123372 used, 690052 buff/cache
- 1626844 total:物理内存总量(KB)
- 813420 free:空闲内存总量(KB)
- 123372 used:使用中的内存总量(KB)
- 690052 buff/cache:用作缓存的内存总量(KB)
- 进程列表:每列的含义如下:
- PID:进程的ID。
- USER:进程所有者。
- PR:进程的优先级别,越小越优先被执行。
- NI:nice值。负值表示高优先级,正值表示低优先级。
- VIRT:进程使用的虚拟内存总量,单位kb。VIRT=RES+SWAP
- RES:进程使用的、未被交换出去的物理内存大小,单位kb。RES=CODE+DATA。
- SHR:共享内存大小,单位kb。
- S:进程的状态。S表示休眠,R表示正在运行,Z表示僵死状态,N表示该进程优先值为负数
- %CPU:自从上次屏幕更新以来,进程使用的CPU时间百分比。
- %MEM:进程使用的物理内存百分比。
- TIME+:自从进程启动后,累计的CPU时间,单位为分钟:秒。
- COMMAND:进程启动命令名称
top默认显示所有CPU的平均值,按数字 1,就可以切换到每个CPU的使用率了。每个进程都有一个%CPU列,表示进程的CPU使用率,它是用户态和内核态CPU使用率的综合,包括进程用户空间使用的CPU,通过系统调用执行的内核空间CPU,以及在就绪队列等待运行的CPU。所以top并没有细分进程的用户态CPU和内核态CPU,需要通过其他工具分析如pidstat等。 统计top输出的数据:
class ProcessInfo:
"""
top输出的进程的信息
"""
def __init__(self):
self.user = str() # 进程所有者
self.pr = str() # 进程的优先级别
self.ni = int() # nice值
self.virt = int() # 进程使用的虚拟内存总量
self.res = int() # 进程使用的、未被交换出去的物理内存大小
self.shr = int() # 共享内存大小
self.cpu = float() # 进程使用的CPU时间百分比
self.mem = float() # 进程使用的物理内存百分比
self.time = str() # 自从进程启动后,累计的CPU时间
self.cmd = str() # 进程启动命令名称
class Mem:
"""
用于保存top命令输出的mem的信息
"""
def __init__(self):
self.total = 0 # 物理内存总量
self.free = 0 # 空闲内存总量
self.used = 0 # 使用中的内存总量
self.buff = 0 # 用作缓存的内存总量
class Task:
"""
用于保存top命令输出的task信息
"""
def __init__(self, task_v=0, running_v=0, sleep_v=0, stop_v=0, zombie_v=0):
self.task_num = task_v # 当前系统总进程数
self.running_num = running_v # 正在运行的进程数
self.sleep_num = sleep_v # 睡眠的进程数
self.stop_num = stop_v # 停止的进程数
self.zombie_num = zombie_v # 僵尸进程数
class LoadAverage:
def __init__(self):
self.one_min = 0
self.five_min = 0
self.fifteen_min = 0
class TopItem:
"""
用于保存top命令输出的所有信息,包括task信息,mem信息,进程(一条或多条)的信息
"""
def __init__(self):
self.task_info = Task()
self.mem_info = Mem()
self.load = LoadAverage()
self.run_time = str() # 运行命令的时间
self.ps_info = dict() # key为进程id,val为ProcessInfo
class TopData:
"""
要分析的文本以 ’top - xx:yy:zz‘开头, 以空行的下一行为要分析的数据,以下一个空行为结束行
"""
def __init__(self, top_text):
self.top_text = top_text
self.top_data = list() # list的元素为TopItem对象
# 分割每一次top的输出
self.part_str = r'(top - \d{2}:\d{2}:\d{2}.+?)\n\n(.+?)\n\n'
self.part_pat = re.compile(self.part_str, re.S | re.M)
# top输出中的task内容:
self.task_pat = re.compile(r'\d+')
# top输出中的cpu, mem, swap:
self.com_pat = re.compile(r'[\d\\.]+')
# top输出中的平均负载 r'load average: (?:([\d\\.]+),?\s?){3}'
self.load_pat = re.compile(r'load average: (.+)')
# top的运行时间
self.run_time_pat = re.compile(r'\d{2}:\d{2}:\d{2}')
def read_top_text(self):
with open(self.top_text, 'rt', encoding='utf-8') as f:
top_text_content = f.read() # 将top输出文件中的所有内容读到top_text_content中再进行下一步处理
self.split_part(top_text_content)
def split_part(self, top_context):
top_context_list = self.part_pat.findall(top_context) # top_context_list中的数据为执行一次top的输出
for one_top_data in top_context_list:
top_item = TopItem()
self.parse_header_data(one_top_data[0], top_item)
self.parse_body_data(one_top_data[1], top_item)
self.top_data.append(top_item)
def parse_body_data(self, top_process_data, top_item):
# 按行分割为每个进程的数据列表
top_data_detail = re.split(r'\n', top_process_data)
first_line = False
# 遍历每一个进程的数据
for one_process_data in top_data_detail:
if first_line is False:
first_line = True
continue
process_info = ProcessInfo()
one_process_data = one_process_data.lstrip()
process_data_item = re.split(r'\s+', one_process_data)
pid = process_data_item[0]
process_info.user = process_data_item[1]
process_info.pr = process_data_item[2]
process_info.ni = process_data_item[3]
process_info.virt = process_data_item[4]
process_info.res = process_data_item[5]
process_info.shr = process_data_item[6]
process_info.cpu = process_data_item[8]
process_info.mem = process_data_item[9]
process_info.time = process_data_item[10]
process_info.cmd = process_data_item[11]
top_item.ps_info[pid] = process_info
def parse_header_data(self, data, item):
line_data = re.split('\n', data)
item.load = self.set_load_average(line_data[0])
item.run_time = self.run_time_pat.search(line_data[0]).group()
item.task_info = self.set_task(line_data[1])
item.mem_info = self.set_mem(line_data[3])
def set_load_average(self, line):
load_str = self.load_pat.findall(line)[0]
loads = re.findall(r'([\d\\.]+)', load_str)
load = LoadAverage()
load.one_min = loads[0]
load.five_min = loads[1]
load.fifteen_min = loads[2]
return load
def set_task(self, line):
v = self.task_pat.findall(line)
task = Task(v[0], v[1], v[2], v[3], v[4])
return task
def set_mem(self, line):
v = self.com_pat.findall(line)
mem = Mem()
mem.total = v[0]
mem.free = v[1]
mem.used = v[2]
mem.buff = v[3]
return mem
if __name__ == '__main__':
top_data = TopData('./top.txt')
top_data.read_top_text()
上述代码执行完后所有的top数据都存储在TopData.top_data中,包含了top命令的运行时间(可以计算时间差或运行时长),每次的平均负载,整体内存和cpu使用率,以及每个进程的详细运行信息。可以根据需要使用数据。
ps
pmap
查看进程的内存分布,包括哪些库被加载到内存中,堆和栈的使用情况等
- pmap <pid> 显示进程的内存映射,包括每个区域的大小,权限,和偏移量
- pmap -d <pid> 显示详细信息,包括每个内存区域的设备号和节点号:
- pmap -x <pid> 显示扩展信息,包括每个内存区域的大小,RSS,dirty,mode。
- pmap -A <pid> 显示所有的内存映射,包括那些通常不显示的内存映射。
bcc工具
memleak:可以跟踪系统或指定进程的内存分配和释放,定期输出一个未释放内存和响应调用栈的汇总
Valgrind
valgrind是一个用来分析Linux下程序运行情况的工具链,Valgrind会使程序的运行速度明显变慢,因为它需要拦截并分析每一条指令。包括以下功能:
- 内存泄漏检查
- 缓冲区溢出检查
- 线程错误检查:Valgrind可以检查多线程程序中的数据竞争条件
- 性能分析:Valgrind包含一个名为Callgrind的工具,可以分析程序的运行时间,有助于找出程序中的瓶劲问题(程序的什么运行时间?)
- 指令级模拟:Valgrind可以模拟CPU的指令运行,帮助找出程序中的硬件问题
- 错误注入:Valgrind可以在运行程序时注入错误,以检查程序的错误处理代码是否正确
Valgrind官网和Valgrind在线文档构建加入Valgrind可以参考这里
AddressSanitizer(ASAN)
ASAN时Google的内存检测工具,基于编译器插桩技术实现,需要在编译阶段加入编译选项来启动,具体cmake写法可以参考这里。可以检测到的错误类型和示例代码如下:
- alloc/dealloc 和 new/delete 类型不匹配,报错为:alloc-dealloc-mismatch(new和delete不匹配也是报这个错),如果不想报这个错,可以设置:ASAN_OPTIONS=alloc_dealloc_mismatch=0(在哪设置?)
- allocation-size-too-big:地址擦除系统错误,分配内存太大导致,如 char* buffer = new char[x*y*x*y];这段代码运行会导致std::bad_alloc异常,使用ASAN检测后报该错误,可以使用allocator_may_return_null=1禁用这个检测;
- calloc-overflow:如下示例, calloc 使用初始化为 0 的元素在内存中创建数组
#include <stdio.h>
#include <stdlib.h>
int main() {
int number = -1;
int element_size = 1000;
void *p = calloc(number, element_size); // Boom!
printf("calloc returned: %zu\n", (size_t)p);
return 0;
}
- dynamic-stack-buffer-overflow:地址擦除器错误
- double-free:重复释放已经释放过的内存
int main() {
int *x = new int[42];
delete [] x;
// ... some complex body of code
delete [] x;
return 0;
}
- heap-use-after-free:使用已经释放的内存,会显示出申请和释放的调用栈
#include <stdlib.h>
int main() {
char *x = (char*)malloc(10 * sizeof(char));
free(x);
// ...
return x[5]; // Boom!
}
- global-buffer-overflow:全局变量或静态变量溢出。编译器在.data和.bss段中定义的任何变量,在main之前初始化并分配内存。如果没有asan的检测代码可以正常运行,但数据一定是有问题的。
// static变量
#include <string.h>
int main(int argc, char **argv) {
static char XXX[10];
static char YYY[10];
static char ZZZ[10];
memset(XXX, 0, 10); memset(YYY, 0, 10); memset(ZZZ, 0, 10);
int res = YYY[argc * 10]; // Boom!
res += XXX[argc] + ZZZ[argc];
return res;
}
// 全局变量
int global[10];
int test_memleak() {
memset(global, 0, 10);
int res = global[2 * 10]; // Boom!
return 0;
}
- heap-buffer-overflow:堆缓冲区溢出,有以下三种情形:
// 经典的缓冲区溢出:
void test_memleak() {
char *x = new char[10 * sizeof(char)];
memset(x, 0, 10);
int res = x[2 * 10]; // Boom!
delete x;
}
// 不正确的向下强制转换
class Parent {
public:
int field;
};
class Child : public Parent {
public:
int extra_field;
};
int test_memleak() {
Parent *p = new Parent;
Child *c = (Child*)p; // Intentional error here!
c->extra_field = 42;
return 0;
}
// 拷贝到不够大的内存空间中
int test_memleak() {
char *hello = new char[6];;
strncpy(hello, "hello", 10); // Boom!
return 0;
}
- invalid-posix-memalign-alignment:内存分配不符合其对齐要求时报告的错误,具体来说,当请求的内存分配的对齐方式不符合分配函数(new, malloc)的要求时就会发生这个错误(分配函数的要求是什么?)例如,如果使用posix_memalign请求一个16字节对齐的内存块,但是提供了一个小于16的对齐参数,那么将会报出invalid-allocation-alignment错误。
#include <stdlib.h>
int main() {
void *p;
posix_memalign(&p, 10, 100); // 对齐参数小于指针的大小,这是不正确的
free(p);
return 0;
}
在上述代码中,posix_memalign函数用于分配一块内存,其地址是其第二个参数(即10)的倍数。但是,这个对齐参数必须是一个指针大小的倍数,且为2的幂。因此,使用10作为对齐参数是错误的,这将导致invalid-allocation-alignment错误。
修复这个问题的一种方法是将对齐参数更改为正确的值,例如:
#include <stdlib.h>
int main() {
void *p;
posix_memalign(&p, 16, 100); // 使用正确的对齐参数
free(p);
return 0;
}
- memcpy-param-overlap:这个错误是使用memcpy操作重叠内存。CRT 函数memcpy不支持重叠内存。 CRT 为支持重叠内存的 memcpy 提供了替代方法:memmove。
- 什么是CRT函数:CRT是C运行时(C Runtime)的缩写。CRT函数即C运行时库函数,它们是一组在C语言环境下运行的预定义函数。这些函数提供了诸如输入/输出处理、字符串操作、内存管理等基本操作。例如,printf、scanf、strcpy、malloc等函数都是CRT函数。它们是标准C库的一部分,可以在任何C编译器中使用。
- 什么是重叠内存:重叠内存是指两个或更多的内存块在地址空间中有共享的部分,简单地说,就是他们的地址范围有重叠。这通常在进行内存操作时可能出现,比如在使用像memcpy这样的函数时,如果源地址和目标地址范围有重叠,可能会导致未定义的行为。
char arr[10] = "hello";
memcpy(arr, arr+1, 5);
上述例子中,源地址arr+1和目标地址arr的范围是有重叠的,这就是所谓的重叠内存。使用asan检测会报memcpy-param-overlap错误。这种情况下,应该使用memmove函数,因为它能够正确处理源地址和目标地址重叠的情况。重叠内存在程序中是需要避免的,因为它可能导致数据错误或者其他未定义的行为。
- strncat-param-overlap
- stack-buffer-overflow
- stack-buffer-underflow
- stack-use-after-return
- stack-use-after-scope
- use-after-poison
参考文档: https://learn.microsoft.com/zh-cn/cpp/sanitizers/asan?view=msvc-170