目录

  • 内存分配
  • 工具
  • 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

查看内存变化情况

获取 lua 内存_linux

通过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

查看进程的内存分布,包括哪些库被加载到内存中,堆和栈的使用情况等

  1. pmap <pid> 显示进程的内存映射,包括每个区域的大小,权限,和偏移量
  2. pmap -d <pid> 显示详细信息,包括每个内存区域的设备号和节点号:
  3. pmap -x <pid> 显示扩展信息,包括每个内存区域的大小,RSS,dirty,mode。
  4. 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