mirror of
https://github.com/eunomia-bpf/bpf-developer-tutorial.git
synced 2026-02-03 18:24:27 +08:00
619 lines
49 KiB
HTML
619 lines
49 KiB
HTML
<!DOCTYPE HTML>
|
||
<html lang="en" class="light" dir="ltr">
|
||
<head>
|
||
<!-- Book generated using mdBook -->
|
||
<meta charset="UTF-8">
|
||
<title>lesson 16-memleak - bpf-developer-tutorial</title>
|
||
|
||
|
||
<!-- Custom HTML head -->
|
||
|
||
<meta name="description" content="">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="theme-color" content="#ffffff">
|
||
|
||
<link rel="icon" href="../favicon.svg">
|
||
<link rel="shortcut icon" href="../favicon.png">
|
||
<link rel="stylesheet" href="../css/variables.css">
|
||
<link rel="stylesheet" href="../css/general.css">
|
||
<link rel="stylesheet" href="../css/chrome.css">
|
||
<link rel="stylesheet" href="../css/print.css" media="print">
|
||
|
||
<!-- Fonts -->
|
||
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
|
||
<link rel="stylesheet" href="../fonts/fonts.css">
|
||
|
||
<!-- Highlight.js Stylesheets -->
|
||
<link rel="stylesheet" href="../highlight.css">
|
||
<link rel="stylesheet" href="../tomorrow-night.css">
|
||
<link rel="stylesheet" href="../ayu-highlight.css">
|
||
|
||
<!-- Custom theme stylesheets -->
|
||
|
||
</head>
|
||
<body class="sidebar-visible no-js">
|
||
<div id="body-container">
|
||
<!-- Provide site root to javascript -->
|
||
<script>
|
||
var path_to_root = "../";
|
||
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
|
||
</script>
|
||
|
||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||
<script>
|
||
try {
|
||
var theme = localStorage.getItem('mdbook-theme');
|
||
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||
|
||
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||
}
|
||
|
||
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||
}
|
||
} catch (e) { }
|
||
</script>
|
||
|
||
<!-- Set the theme before any content is loaded, prevents flash -->
|
||
<script>
|
||
var theme;
|
||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||
if (theme === null || theme === undefined) { theme = default_theme; }
|
||
var html = document.querySelector('html');
|
||
html.classList.remove('light')
|
||
html.classList.add(theme);
|
||
var body = document.querySelector('body');
|
||
body.classList.remove('no-js')
|
||
body.classList.add('js');
|
||
</script>
|
||
|
||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||
|
||
<!-- Hide / unhide sidebar before it is displayed -->
|
||
<script>
|
||
var body = document.querySelector('body');
|
||
var sidebar = null;
|
||
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
|
||
if (document.body.clientWidth >= 1080) {
|
||
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||
sidebar = sidebar || 'visible';
|
||
} else {
|
||
sidebar = 'hidden';
|
||
}
|
||
sidebar_toggle.checked = sidebar === 'visible';
|
||
body.classList.remove('sidebar-visible');
|
||
body.classList.add("sidebar-" + sidebar);
|
||
</script>
|
||
|
||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||
<div class="sidebar-scrollbox">
|
||
<ol class="chapter"><li class="chapter-item expanded affix "><a href="../https://github.com/eunomia-bpf/bpf-developer-tutorial.html">https://github.com/eunomia-bpf/bpf-developer-tutorial</a></li><li class="chapter-item expanded affix "><li class="part-title">入门文档</li><li class="chapter-item expanded "><a href="../0-introduce/index.html"><strong aria-hidden="true">1.</strong> lesson 0-introduce</a></li><li class="chapter-item expanded "><a href="../1-helloworld/index.html"><strong aria-hidden="true">2.</strong> lesson 1-helloworld</a></li><li class="chapter-item expanded "><a href="../2-kprobe-unlink/index.html"><strong aria-hidden="true">3.</strong> lesson 2-kprobe-unlink</a></li><li class="chapter-item expanded "><a href="../3-fentry-unlink/index.html"><strong aria-hidden="true">4.</strong> lesson 3-fentry-unlink</a></li><li class="chapter-item expanded "><a href="../4-opensnoop/index.html"><strong aria-hidden="true">5.</strong> lesson 4-opensnoop</a></li><li class="chapter-item expanded "><a href="../5-uprobe-bashreadline/index.html"><strong aria-hidden="true">6.</strong> lesson 5-uprobe-bashreadline</a></li><li class="chapter-item expanded "><a href="../6-sigsnoop/index.html"><strong aria-hidden="true">7.</strong> lesson 6-sigsnoop</a></li><li class="chapter-item expanded "><a href="../7-execsnoop/index.html"><strong aria-hidden="true">8.</strong> lesson 7-execsnoop</a></li><li class="chapter-item expanded "><a href="../8-exitsnoop/index.html"><strong aria-hidden="true">9.</strong> lesson 8-execsnoop</a></li><li class="chapter-item expanded "><a href="../9-runqlat/index.html"><strong aria-hidden="true">10.</strong> lesson 9-runqlat</a></li><li class="chapter-item expanded "><a href="../10-hardirqs/index.html"><strong aria-hidden="true">11.</strong> lesson 10-hardirqs</a></li><li class="chapter-item expanded affix "><li class="part-title">进阶文档和示例</li><li class="chapter-item expanded "><a href="../11-bootstrap/index.html"><strong aria-hidden="true">12.</strong> lesson 11-bootstrap</a></li><li class="chapter-item expanded "><a href="../12-profile/index.html"><strong aria-hidden="true">13.</strong> lesson 12-profile</a></li><li class="chapter-item expanded "><a href="../13-tcpconnlat/index.html"><strong aria-hidden="true">14.</strong> lesson 13-tcpconnlat</a></li><li class="chapter-item expanded "><a href="../14-tcpstates/index.html"><strong aria-hidden="true">15.</strong> lesson 14-tcpstates</a></li><li class="chapter-item expanded "><a href="../15-javagc/index.html"><strong aria-hidden="true">16.</strong> lesson 15-javagc</a></li><li class="chapter-item expanded "><a href="../16-memleak/index.html" class="active"><strong aria-hidden="true">17.</strong> lesson 16-memleak</a></li><li class="chapter-item expanded "><a href="../17-biopattern/index.html"><strong aria-hidden="true">18.</strong> lesson 17-biopattern</a></li><li class="chapter-item expanded "><a href="../18-further-reading/index.html"><strong aria-hidden="true">19.</strong> lesson 18-further-reading</a></li><li class="chapter-item expanded "><a href="../19-lsm-connect/index.html"><strong aria-hidden="true">20.</strong> lesson 19-lsm-connect</a></li><li class="chapter-item expanded "><a href="../20-tc/index.html"><strong aria-hidden="true">21.</strong> lesson 20-tc</a></li><li class="chapter-item expanded "><a href="../21-xdp/index.html"><strong aria-hidden="true">22.</strong> lesson 21-xdp</a></li><li class="chapter-item expanded affix "><li class="part-title">高级主题</li><li class="chapter-item expanded "><a href="../22-android/index.html"><strong aria-hidden="true">23.</strong> 在 Android 上使用 eBPF 程序</a></li><li class="chapter-item expanded "><a href="../30-sslsniff/index.html"><strong aria-hidden="true">24.</strong> 使用 uprobe 捕获多种库的 SSL/TLS 明文数据</a></li><li class="chapter-item expanded "><a href="../23-http/index.html"><strong aria-hidden="true">25.</strong> 使用 eBPF socket filter 或 syscall trace 追踪 HTTP 请求和其他七层协议</a></li><li class="chapter-item expanded "><a href="../29-sockops/index.html"><strong aria-hidden="true">26.</strong> 使用 sockops 加速网络请求转发</a></li><li class="chapter-item expanded "><a href="../24-hide/index.html"><strong aria-hidden="true">27.</strong> 使用 eBPF 隐藏进程或文件信息</a></li><li class="chapter-item expanded "><a href="../25-signal/index.html"><strong aria-hidden="true">28.</strong> 使用 bpf_send_signal 发送信号终止进程</a></li><li class="chapter-item expanded "><a href="../26-sudo/index.html"><strong aria-hidden="true">29.</strong> 使用 eBPF 添加 sudo 用户</a></li><li class="chapter-item expanded "><a href="../27-replace/index.html"><strong aria-hidden="true">30.</strong> 使用 eBPF 替换任意程序读取或写入的文本</a></li><li class="chapter-item expanded "><a href="../28-detach/index.html"><strong aria-hidden="true">31.</strong> BPF 的生命周期:使用 Detached 模式在用户态应用退出后持续运行 eBPF 程序</a></li><li class="chapter-item expanded "><a href="../18-further-reading/ebpf-security.zh.html"><strong aria-hidden="true">32.</strong> eBPF 运行时的安全性与面临的挑战</a></li><li class="chapter-item expanded "><a href="../34-syscall/index.html"><strong aria-hidden="true">33.</strong> 使用 eBPF 修改系统调用参数</a></li><li class="chapter-item expanded "><a href="../35-user-ringbuf/index.html"><strong aria-hidden="true">34.</strong> eBPF开发实践:使用 user ring buffer 向内核异步发送信息</a></li><li class="chapter-item expanded "><a href="../36-userspace-ebpf/index.html"><strong aria-hidden="true">35.</strong> 用户空间 eBPF 运行时:深度解析与应用实践</a></li><li class="chapter-item expanded "><a href="../37-uprobe-rust/index.html"><strong aria-hidden="true">36.</strong> 使用 uprobe 追踪 Rust 应用程序</a></li><li class="chapter-item expanded "><a href="../38-btf-uprobe/index.html"><strong aria-hidden="true">37.</strong> 借助 eBPF 和 BTF,让用户态也能一次编译、到处运行</a></li><li class="chapter-item expanded affix "><li class="part-title">bcc 和 bpftrace 教程与文档</li><li class="chapter-item expanded "><a href="../bcc-documents/kernel-versions.html"><strong aria-hidden="true">38.</strong> BPF Features by Linux Kernel Version</a></li><li class="chapter-item expanded "><a href="../bcc-documents/kernel_config.html"><strong aria-hidden="true">39.</strong> Kernel Configuration for BPF Features</a></li><li class="chapter-item expanded "><a href="../bcc-documents/reference_guide.html"><strong aria-hidden="true">40.</strong> bcc Reference Guide</a></li><li class="chapter-item expanded "><a href="../bcc-documents/special_filtering.html"><strong aria-hidden="true">41.</strong> Special Filtering</a></li><li class="chapter-item expanded "><a href="../bcc-documents/tutorial.html"><strong aria-hidden="true">42.</strong> bcc Tutorial</a></li><li class="chapter-item expanded "><a href="../bcc-documents/tutorial_bcc_python_developer.html"><strong aria-hidden="true">43.</strong> bcc Python Developer Tutorial</a></li><li class="chapter-item expanded "><a href="../bpftrace-tutorial/index.html"><strong aria-hidden="true">44.</strong> bpftrace Tutorial</a></li></ol>
|
||
</div>
|
||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||
<div class="sidebar-resize-indicator"></div>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- Track and set sidebar scroll position -->
|
||
<script>
|
||
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
|
||
sidebarScrollbox.addEventListener('click', function(e) {
|
||
if (e.target.tagName === 'A') {
|
||
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
|
||
}
|
||
}, { passive: true });
|
||
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
|
||
sessionStorage.removeItem('sidebar-scroll');
|
||
if (sidebarScrollTop) {
|
||
// preserve sidebar scroll position when navigating via links within sidebar
|
||
sidebarScrollbox.scrollTop = sidebarScrollTop;
|
||
} else {
|
||
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
|
||
var activeSection = document.querySelector('#sidebar .active');
|
||
if (activeSection) {
|
||
activeSection.scrollIntoView({ block: 'center' });
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<div id="page-wrapper" class="page-wrapper">
|
||
|
||
<div class="page">
|
||
<div id="menu-bar-hover-placeholder"></div>
|
||
<div id="menu-bar" class="menu-bar sticky">
|
||
<div class="left-buttons">
|
||
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||
<i class="fa fa-bars"></i>
|
||
</label>
|
||
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||
<i class="fa fa-paint-brush"></i>
|
||
</button>
|
||
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
||
</ul>
|
||
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||
<i class="fa fa-search"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<h1 class="menu-title">bpf-developer-tutorial</h1>
|
||
|
||
<div class="right-buttons">
|
||
<a href="../print.html" title="Print this book" aria-label="Print this book">
|
||
<i id="print-button" class="fa fa-print"></i>
|
||
</a>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div id="search-wrapper" class="hidden">
|
||
<form id="searchbar-outer" class="searchbar-outer">
|
||
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||
</form>
|
||
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||
<div id="searchresults-header" class="searchresults-header"></div>
|
||
<ul id="searchresults">
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||
<script>
|
||
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||
});
|
||
</script>
|
||
|
||
<div id="content" class="content">
|
||
<main>
|
||
<h1 id="ebpf-入门实践教程十六编写-ebpf-程序-memleak-监控内存泄漏"><a class="header" href="#ebpf-入门实践教程十六编写-ebpf-程序-memleak-监控内存泄漏">eBPF 入门实践教程十六:编写 eBPF 程序 Memleak 监控内存泄漏</a></h1>
|
||
<p>eBPF(扩展的伯克利数据包过滤器)是一项强大的网络和性能分析工具,被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。</p>
|
||
<p>在本篇教程中,我们将探讨如何使用 eBPF 编写 Memleak 程序,以监控程序的内存泄漏。</p>
|
||
<h2 id="背景及其重要性"><a class="header" href="#背景及其重要性">背景及其重要性</a></h2>
|
||
<p>内存泄漏是计算机编程中的一种常见问题,其严重程度不应被低估。内存泄漏发生时,程序会逐渐消耗更多的内存资源,但并未正确释放。随着时间的推移,这种行为会导致系统内存逐渐耗尽,从而显著降低程序及系统的整体性能。</p>
|
||
<p>内存泄漏有多种可能的原因。这可能是由于配置错误导致的,例如程序错误地配置了某些资源的动态分配。它也可能是由于软件缺陷或错误的内存管理策略导致的,如在程序执行过程中忘记释放不再需要的内存。此外,如果一个应用程序的内存使用量过大,那么系统性能可能会因页面交换(swapping)而大幅下降,甚至可能导致应用程序被系统强制终止(Linux 的 OOM killer)。</p>
|
||
<h3 id="调试内存泄漏的挑战"><a class="header" href="#调试内存泄漏的挑战">调试内存泄漏的挑战</a></h3>
|
||
<p>调试内存泄漏问题是一项复杂且挑战性的任务。这涉及到详细检查应用程序的配置、内存分配和释放情况,通常需要应用专门的工具来帮助诊断。例如,有一些工具可以在应用程序启动时将 malloc() 函数调用与特定的检测工具关联起来,如 Valgrind memcheck,这类工具可以模拟 CPU 来检查所有内存访问,但可能会导致应用程序运行速度大大减慢。另一个选择是使用堆分析器,如 libtcmalloc,它相对较快,但仍可能使应用程序运行速度降低五倍以上。此外,还有一些工具,如 gdb,可以获取应用程序的核心转储并进行后处理以分析内存使用情况。然而,这些工具通常在获取核心转储时需要暂停应用程序,或在应用程序终止后才能调用 free() 函数。</p>
|
||
<h2 id="ebpf-的作用"><a class="header" href="#ebpf-的作用">eBPF 的作用</a></h2>
|
||
<p>在这种背景下,eBPF 的作用就显得尤为重要。eBPF 提供了一种高效的机制来监控和追踪系统级别的事件,包括内存的分配和释放。通过 eBPF,我们可以跟踪内存分配和释放的请求,并收集每次分配的调用堆栈。然后,我们可以分</p>
|
||
<p>析这些信息,找出执行了内存分配但未执行释放操作的调用堆栈,这有助于我们找出导致内存泄漏的源头。这种方式的优点在于,它可以实时地在运行的应用程序中进行,而无需暂停应用程序或进行复杂的前后处理。</p>
|
||
<p><code>memleak</code> eBPF 工具可以跟踪并匹配内存分配和释放的请求,并收集每次分配的调用堆栈。随后,<code>memleak</code> 可以打印一个总结,表明哪些调用堆栈执行了分配,但是并没有随后进行释放。例如,我们运行命令:</p>
|
||
<pre><code class="language-console"># ./memleak -p $(pidof allocs)
|
||
Attaching to pid 5193, Ctrl+C to quit.
|
||
[11:16:33] Top 2 stacks with outstanding allocations:
|
||
80 bytes in 5 allocations from stack
|
||
main+0x6d [allocs]
|
||
__libc_start_main+0xf0 [libc-2.21.so]
|
||
|
||
[11:16:34] Top 2 stacks with outstanding allocations:
|
||
160 bytes in 10 allocations from stack
|
||
main+0x6d [allocs]
|
||
__libc_start_main+0xf0 [libc-2.21.so]
|
||
</code></pre>
|
||
<p>运行这个命令后,我们可以看到分配但未释放的内存来自于哪些堆栈,并且可以看到这些未释放的内存的大小和数量。</p>
|
||
<p>随着时间的推移,很显然,<code>allocs</code> 进程的 <code>main</code> 函数正在泄漏内存,每次泄漏 16 字节。幸运的是,我们不需要检查每个分配,我们得到了一个很好的总结,告诉我们哪个堆栈负责大量的泄漏。</p>
|
||
<h2 id="memleak-的实现原理"><a class="header" href="#memleak-的实现原理">memleak 的实现原理</a></h2>
|
||
<p>在基本层面上,<code>memleak</code> 的工作方式类似于在内存分配和释放路径上安装监控设备。它通过在内存分配和释放函数中插入 eBPF 程序来达到这个目标。这意味着,当这些函数被调用时,<code>memleak</code> 就会记录一些重要信息,如调用者的进程 ID(PID)、分配的内存地址以及分配的内存大小等。当释放内存的函数被调用时,<code>memleak</code> 则会在其内部的映射表(map)中删除相应的内存分配记录。这种机制使得 <code>memleak</code> 能够准确地追踪到哪些内存块已被分配但未被释放。</p>
|
||
<p>对于用户态的常用内存分配函数,如 <code>malloc</code> 和 <code>calloc</code> 等,<code>memleak</code> 利用了用户态探测(uprobe)技术来实现监控。uprobe 是一种用于用户空间应用程序的动态追踪技术,它可以在运行时不修改二进制文件的情况下在任意位置设置断点,从而实现对特定函数调用的追踪。Uprobe 在内核态 eBPF 运行时,也可能产生比较大的性能开销,这时候也可以考虑使用用户态 eBPF 运行时,例如 <a href="https://github.com/eunomia-bpf/bpftime">bpftime</a>。bpftime 是一个基于 LLVM JIT/AOT 的用户态 eBPF 运行时,它可以在用户态运行 eBPF 程序,和内核态的 eBPF 兼容,避免了内核态和用户态之间的上下文切换,从而提高了 eBPF 程序的执行效率。对于 uprobe 而言,bpftime 的性能开销比 kernel 小一个数量级。</p>
|
||
<p>对于内核态的内存分配函数,如 <code>kmalloc</code> 等,<code>memleak</code> 则选择使用了 tracepoint 来实现监控。Tracepoint 是一种在 Linux 内核中提供的动态追踪技术,它可以在内核运行时动态地追踪特定的事件,而无需重新编译内核或加载内核模块。</p>
|
||
<h2 id="内核态-ebpf-程序实现"><a class="header" href="#内核态-ebpf-程序实现">内核态 eBPF 程序实现</a></h2>
|
||
<h2 id="memleak-内核态-ebpf-程序实现"><a class="header" href="#memleak-内核态-ebpf-程序实现"><code>memleak</code> 内核态 eBPF 程序实现</a></h2>
|
||
<p><code>memleak</code> 的内核态 eBPF 程序包含一些用于跟踪内存分配和释放的关键函数。在我们深入了解这些函数之前,让我们首先观察 <code>memleak</code> 所定义的一些数据结构,这些结构在其内核态和用户态程序中均有使用。</p>
|
||
<pre><code class="language-c">#ifndef __MEMLEAK_H
|
||
#define __MEMLEAK_H
|
||
|
||
#define ALLOCS_MAX_ENTRIES 1000000
|
||
#define COMBINED_ALLOCS_MAX_ENTRIES 10240
|
||
|
||
struct alloc_info {
|
||
__u64 size; // 分配的内存大小
|
||
__u64 timestamp_ns; // 分配时的时间戳,单位为纳秒
|
||
int stack_id; // 分配时的调用堆栈ID
|
||
};
|
||
|
||
union combined_alloc_info {
|
||
struct {
|
||
__u64 total_size : 40; // 所有未释放分配的总大小
|
||
__u64 number_of_allocs : 24; // 所有未释放分配的总次数
|
||
};
|
||
__u64 bits; // 结构的位图表示
|
||
};
|
||
|
||
#endif /* __MEMLEAK_H */
|
||
</code></pre>
|
||
<p>这里定义了两个主要的数据结构:<code>alloc_info</code> 和 <code>combined_alloc_info</code>。</p>
|
||
<p><code>alloc_info</code> 结构体包含了一个内存分配的基本信息,包括分配的内存大小 <code>size</code>、分配发生时的时间戳 <code>timestamp_ns</code>,以及触发分配的调用堆栈 ID <code>stack_id</code>。</p>
|
||
<p><code>combined_alloc_info</code> 是一个联合体(union),它包含一个嵌入的结构体和一个 <code>__u64</code> 类型的位图表示 <code>bits</code>。嵌入的结构体有两个成员:<code>total_size</code> 和 <code>number_of_allocs</code>,分别代表所有未释放分配的总大小和总次数。其中 40 和 24 分别表示 total_size 和 number_of_allocs这两个成员变量所占用的位数,用来限制其大小。通过这样的位数限制,可以节省combined_alloc_info结构的存储空间。同时,由于total_size和number_of_allocs在存储时是共用一个unsigned long long类型的变量bits,因此可以通过在成员变量bits上进行位运算来访问和修改total_size和number_of_allocs,从而避免了在程序中定义额外的变量和函数的复杂性。</p>
|
||
<p>接下来,<code>memleak</code> 定义了一系列用于保存内存分配信息和分析结果的 eBPF 映射(maps)。这些映射都以 <code>SEC(".maps")</code> 的形式定义,表示它们属于 eBPF 程序的映射部分。</p>
|
||
<pre><code class="language-c">const volatile size_t min_size = 0;
|
||
const volatile size_t max_size = -1;
|
||
const volatile size_t page_size = 4096;
|
||
const volatile __u64 sample_rate = 1;
|
||
const volatile bool trace_all = false;
|
||
const volatile __u64 stack_flags = 0;
|
||
const volatile bool wa_missing_free = false;
|
||
|
||
struct {
|
||
__uint(type, BPF_MAP_TYPE_HASH);
|
||
__type(key, pid_t);
|
||
__type(value, u64);
|
||
__uint(max_entries, 10240);
|
||
} sizes SEC(".maps");
|
||
|
||
struct {
|
||
__uint(type, BPF_MAP_TYPE_HASH);
|
||
__type(key, u64); /* address */
|
||
__type(value, struct alloc_info);
|
||
__uint(max_entries, ALLOCS_MAX_ENTRIES);
|
||
} allocs SEC(".maps");
|
||
|
||
struct {
|
||
__uint(type, BPF_MAP_TYPE_HASH);
|
||
__type(key, u64); /* stack id */
|
||
__type(value, union combined_alloc_info);
|
||
__uint(max_entries, COMBINED_ALLOCS_MAX_ENTRIES);
|
||
} combined_allocs SEC(".maps");
|
||
|
||
struct {
|
||
__uint(type, BPF_MAP_TYPE_HASH);
|
||
__type(key, u64);
|
||
__type(value, u64);
|
||
__uint(max_entries, 10240);
|
||
} memptrs SEC(".maps");
|
||
|
||
struct {
|
||
__uint(type, BPF_MAP_TYPE_STACK_TRACE);
|
||
__type(key, u32);
|
||
} stack_traces SEC(".maps");
|
||
|
||
static union combined_alloc_info initial_cinfo;
|
||
</code></pre>
|
||
<p>这段代码首先定义了一些可配置的参数,如 <code>min_size</code>, <code>max_size</code>, <code>page_size</code>, <code>sample_rate</code>, <code>trace_all</code>, <code>stack_flags</code> 和 <code>wa_missing_free</code>,分别表示最小分配大小、最大分配大小、页面大小、采样率、是否追踪所有分配、堆栈标志和是否工作在缺失释放(missing free)模式。</p>
|
||
<p>接着定义了五个映射:</p>
|
||
<ol>
|
||
<li><code>sizes</code>:这是一个哈希类型的映射,键为进程 ID,值为 <code>u64</code> 类型,存储每个进程的分配大小。</li>
|
||
<li><code>allocs</code>:这也是一个哈希类型的映射,键为分配的地址,值为 <code>alloc_info</code> 结构体,存储每个内存分配的详细信息。</li>
|
||
<li><code>combined_allocs</code>:这是另一个哈希类型的映射,键为堆栈 ID,值为 <code>combined_alloc_info</code> 联合体,存储所有未释放分配的总大小和总次数。</li>
|
||
<li><code>memptrs</code>:这也是一个哈希类型的映射,键和值都为 <code>u64</code> 类型,用于在用户空间和内核空间之间传递内存指针。</li>
|
||
<li><code>stack_traces</code>:这是一个堆栈追踪类型的映射,键为 <code>u32</code> 类型,用于存储堆栈 ID。</li>
|
||
</ol>
|
||
<p>以用户态的内存分配追踪部分为例,主要是挂钩内存相关的函数调用,如 <code>malloc</code>, <code>free</code>, <code>calloc</code>, <code>realloc</code>, <code>mmap</code> 和 <code>munmap</code>,以便在调用这些函数时进行数据记录。在用户态,<code>memleak</code> 主要使用了 uprobes 技术进行挂载。</p>
|
||
<p>每个函数调用被分为 "enter" 和 "exit" 两部分。"enter" 部分记录的是函数调用的参数,如分配的大小或者释放的地址。"exit" 部分则主要用于获取函数的返回值,如分配得到的内存地址。</p>
|
||
<p>这里,<code>gen_alloc_enter</code>, <code>gen_alloc_exit</code>, <code>gen_free_enter</code> 是实现记录行为的函数,他们分别用于记录分配开始、分配结束和释放开始的相关信息。</p>
|
||
<p>函数原型示例如下:</p>
|
||
<pre><code class="language-c">SEC("uprobe")
|
||
int BPF_KPROBE(malloc_enter, size_t size)
|
||
{
|
||
// 记录分配开始的相关信息
|
||
return gen_alloc_enter(size);
|
||
}
|
||
|
||
SEC("uretprobe")
|
||
int BPF_KRETPROBE(malloc_exit)
|
||
{
|
||
// 记录分配结束的相关信息
|
||
return gen_alloc_exit(ctx);
|
||
}
|
||
|
||
SEC("uprobe")
|
||
int BPF_KPROBE(free_enter, void *address)
|
||
{
|
||
// 记录释放开始的相关信息
|
||
return gen_free_enter(address);
|
||
}
|
||
</code></pre>
|
||
<p>其中,<code>malloc_enter</code> 和 <code>free_enter</code> 是分别挂载在 <code>malloc</code> 和 <code>free</code> 函数入口处的探针(probes),用于在函数调用时进行数据记录。而 <code>malloc_exit</code> 则是挂载在 <code>malloc</code> 函数的返回处的探针,用于记录函数的返回值。</p>
|
||
<p>这些函数使用了 <code>BPF_KPROBE</code> 和 <code>BPF_KRETPROBE</code> 这两个宏来声明,这两个宏分别用于声明 kprobe(内核探针)和 kretprobe(内核返回探针)。具体来说,kprobe 用于在函数调用时触发,而 kretprobe 则是在函数返回时触发。</p>
|
||
<p><code>gen_alloc_enter</code> 函数是在内存分配请求的开始时被调用的。这个函数主要负责在调用分配内存的函数时收集一些基本的信息。下面我们将深入探讨这个函数的实现。</p>
|
||
<pre><code class="language-c">static int gen_alloc_enter(size_t size)
|
||
{
|
||
if (size < min_size || size > max_size)
|
||
return 0;
|
||
|
||
if (sample_rate > 1) {
|
||
if (bpf_ktime_get_ns() % sample_rate != 0)
|
||
return 0;
|
||
}
|
||
|
||
const pid_t pid = bpf_get_current_pid_tgid() >> 32;
|
||
bpf_map_update_elem(&sizes, &pid, &size, BPF_ANY);
|
||
|
||
if (trace_all)
|
||
bpf_printk("alloc entered, size = %lu\n", size);
|
||
|
||
return 0;
|
||
}
|
||
|
||
SEC("uprobe")
|
||
int BPF_KPROBE(malloc_enter, size_t size)
|
||
{
|
||
return gen_alloc_enter(size);
|
||
}
|
||
</code></pre>
|
||
<p>首先,<code>gen_alloc_enter</code> 函数接收一个 <code>size</code> 参数,这个参数表示请求分配的内存的大小。如果这个值不在 <code>min_size</code> 和 <code>max_size</code> 之间,函数将直接返回,不再进行后续的操作。这样可以使工具专注于追踪特定范围的内存分配请求,过滤掉不感兴趣的分配请求。</p>
|
||
<p>接下来,函数检查采样率 <code>sample_rate</code>。如果 <code>sample_rate</code> 大于1,意味着我们不需要追踪所有的内存分配请求,而是周期性地追踪。这里使用 <code>bpf_ktime_get_ns</code> 获取当前的时间戳,然后通过取模运算来决定是否需要追踪当前的内存分配请求。这是一种常见的采样技术,用于降低性能开销,同时还能够提供一个代表性的样本用于分析。</p>
|
||
<p>之后,函数使用 <code>bpf_get_current_pid_tgid</code> 函数获取当前进程的 PID。注意这里的 PID 实际上是进程和线程的组合 ID,我们通过右移 32 位来获取真正的进程 ID。</p>
|
||
<p>函数接下来更新 <code>sizes</code> 这个 map,这个 map 以进程 ID 为键,以请求的内存分配大小为值。<code>BPF_ANY</code> 表示如果 key 已存在,那么更新 value,否则就新建一个条目。</p>
|
||
<p>最后,如果启用了 <code>trace_all</code> 标志,函数将打印一条信息,说明发生了内存分配。</p>
|
||
<p><code>BPF_KPROBE</code> 宏用于</p>
|
||
<p>最后定义了 <code>BPF_KPROBE(malloc_enter, size_t size)</code>,它会在 <code>malloc</code> 函数被调用时被 BPF uprobe 拦截执行,并通过 <code>gen_alloc_enter</code> 来记录内存分配大小。
|
||
我们刚刚分析了内存分配的入口函数 <code>gen_alloc_enter</code>,现在我们来关注这个过程的退出部分。具体来说,我们将讨论 <code>gen_alloc_exit2</code> 函数以及如何从内存分配调用中获取返回的内存地址。</p>
|
||
<pre><code class="language-c">static int gen_alloc_exit2(void *ctx, u64 address)
|
||
{
|
||
const pid_t pid = bpf_get_current_pid_tgid() >> 32;
|
||
struct alloc_info info;
|
||
|
||
const u64* size = bpf_map_lookup_elem(&sizes, &pid);
|
||
if (!size)
|
||
return 0; // missed alloc entry
|
||
|
||
__builtin_memset(&info, 0, sizeof(info));
|
||
|
||
info.size = *size;
|
||
bpf_map_delete_elem(&sizes, &pid);
|
||
|
||
if (address != 0) {
|
||
info.timestamp_ns = bpf_ktime_get_ns();
|
||
|
||
info.stack_id = bpf_get_stackid(ctx, &stack_traces, stack_flags);
|
||
|
||
bpf_map_update_elem(&allocs, &address, &info, BPF_ANY);
|
||
|
||
update_statistics_add(info.stack_id, info.size);
|
||
}
|
||
|
||
if (trace_all) {
|
||
bpf_printk("alloc exited, size = %lu, result = %lx\n",
|
||
info.size, address);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
static int gen_alloc_exit(struct pt_regs *ctx)
|
||
{
|
||
return gen_alloc_exit2(ctx, PT_REGS_RC(ctx));
|
||
}
|
||
|
||
SEC("uretprobe")
|
||
int BPF_KRETPROBE(malloc_exit)
|
||
{
|
||
return gen_alloc_exit(ctx);
|
||
}
|
||
</code></pre>
|
||
<p><code>gen_alloc_exit2</code> 函数在内存分配操作完成时被调用,这个函数接收两个参数,一个是上下文 <code>ctx</code>,另一个是内存分配函数返回的内存地址 <code>address</code>。</p>
|
||
<p>首先,它获取当前线程的 PID,然后使用这个 PID 作为键在 <code>sizes</code> 这个 map 中查找对应的内存分配大小。如果没有找到(也就是说,没有对应的内存分配操作的入口),函数就会直接返回。</p>
|
||
<p>接着,函数清除 <code>info</code> 结构体的内容,并设置它的 <code>size</code> 字段为之前在 map 中找到的内存分配大小。并从 <code>sizes</code> 这个 map 中删除相应的元素,因为此时内存分配操作已经完成,不再需要这个信息。</p>
|
||
<p>接下来,如果 <code>address</code> 不为 0(也就是说,内存分配操作成功了),函数就会进一步收集一些额外的信息。首先,它获取当前的时间戳作为内存分配完成的时间,并获取当前的堆栈跟踪。这些信息都会被储存在 <code>info</code> 结构体中,并随后更新到 <code>allocs</code> 这个 map 中。</p>
|
||
<p>最后,函数调用 <code>update_statistics_add</code> 更新统计数据,如果启用了所有内存分配操作的跟踪,函数还会打印一些关于内存分配操作的信息。</p>
|
||
<p>请注意,<code>gen_alloc_exit</code> 函数是 <code>gen_alloc_exit2</code> 的一个包装,它将 <code>PT_REGS_RC(ctx)</code> 作为 <code>address</code> 参数传递给 <code>gen_alloc_exit2</code>。<code>在我们的讨论中,我们刚刚提到在</code>gen_alloc_exit2<code>函数中,调用了</code>update_statistics_add` 函数以更新内存分配的统计数据。下面我们详细看一下这个函数的具体实现。</p>
|
||
<pre><code class="language-c">static void update_statistics_add(u64 stack_id, u64 sz)
|
||
{
|
||
union combined_alloc_info *existing_cinfo;
|
||
|
||
existing_cinfo = bpf_map_lookup_or_try_init(&combined_allocs, &stack_id, &initial_cinfo);
|
||
if (!existing_cinfo)
|
||
return;
|
||
|
||
const union combined_alloc_info incremental_cinfo = {
|
||
.total_size = sz,
|
||
.number_of_allocs = 1
|
||
};
|
||
|
||
__sync_fetch_and_add(&existing_cinfo->bits, incremental_cinfo.bits);
|
||
}
|
||
</code></pre>
|
||
<p><code>update_statistics_add</code> 函数接收两个参数:当前的堆栈 ID <code>stack_id</code> 以及内存分配的大小 <code>sz</code>。这两个参数都在内存分配事件中收集到,并且用于更新内存分配的统计数据。</p>
|
||
<p>首先,函数尝试在 <code>combined_allocs</code> 这个 map 中查找键值为当前堆栈 ID 的元素,如果找不到,就用 <code>initial_cinfo</code>(这是一个默认的 combined_alloc_info 结构体,所有字段都为零)来初始化新的元素。</p>
|
||
<p>接着,函数创建一个 <code>incremental_cinfo</code>,并设置它的 <code>total_size</code> 为当前内存分配的大小,设置 <code>number_of_allocs</code> 为 1。这是因为每次调用 <code>update_statistics_add</code> 函数都表示有一个新的内存分配事件发生,而这个事件的内存分配大小就是 <code>sz</code>。</p>
|
||
<p>最后,函数使用 <code>__sync_fetch_and_add</code> 函数原子地将 <code>incremental_cinfo</code> 的值加到 <code>existing_cinfo</code> 中。请注意这个步骤是线程安全的,即使有多个线程并发地调用 <code>update_statistics_add</code> 函数,每个内存分配事件也能正确地记录到统计数据中。</p>
|
||
<p>总的来说,<code>update_statistics_add</code> 函数实现了内存分配统计的更新逻辑,通过维护每个堆栈 ID 的内存分配总量和次数,我们可以深入了解到程序的内存分配行为。
|
||
在我们对内存分配的统计跟踪过程中,我们不仅要统计内存的分配,还要考虑内存的释放。在上述代码中,我们定义了一个名为 <code>update_statistics_del</code> 的函数,其作用是在内存释放时更新统计信息。而 <code>gen_free_enter</code> 函数则是在进程调用 <code>free</code> 函数时被执行。</p>
|
||
<pre><code class="language-c">static void update_statistics_del(u64 stack_id, u64 sz)
|
||
{
|
||
union combined_alloc_info *existing_cinfo;
|
||
|
||
existing_cinfo = bpf_map_lookup_elem(&combined_allocs, &stack_id);
|
||
if (!existing_cinfo) {
|
||
bpf_printk("failed to lookup combined allocs\n");
|
||
return;
|
||
}
|
||
|
||
const union combined_alloc_info decremental_cinfo = {
|
||
.total_size = sz,
|
||
.number_of_allocs = 1
|
||
};
|
||
|
||
__sync_fetch_and_sub(&existing_cinfo->bits, decremental_cinfo.bits);
|
||
}
|
||
</code></pre>
|
||
<p><code>update_statistics_del</code> 函数的参数为堆栈 ID 和要释放的内存块大小。函数首先在 <code>combined_allocs</code> 这个 map 中使用当前的堆栈 ID 作为键来查找相应的 <code>combined_alloc_info</code> 结构体。如果找不到,就输出错误信息,然后函数返回。如果找到了,就会构造一个名为 <code>decremental_cinfo</code> 的 <code>combined_alloc_info</code> 结构体,设置它的 <code>total_size</code> 为要释放的内存大小,设置 <code>number_of_allocs</code> 为 1。然后使用 <code>__sync_fetch_and_sub</code> 函数原子地从 <code>existing_cinfo</code> 中减去 <code>decremental_cinfo</code> 的值。请注意,这里的 <code>number_of_allocs</code> 是负数,表示减少了一个内存分配。</p>
|
||
<pre><code class="language-c">static int gen_free_enter(const void *address)
|
||
{
|
||
const u64 addr = (u64)address;
|
||
|
||
const struct alloc_info *info = bpf_map_lookup_elem(&allocs, &addr);
|
||
if (!info)
|
||
return 0;
|
||
|
||
bpf_map_delete_elem(&allocs, &addr);
|
||
update_statistics_del(info->stack_id, info->size);
|
||
|
||
if (trace_all) {
|
||
bpf_printk("free entered, address = %lx, size = %lu\n",
|
||
address, info->size);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
SEC("uprobe")
|
||
int BPF_KPROBE(free_enter, void *address)
|
||
{
|
||
return gen_free_enter(address);
|
||
}
|
||
</code></pre>
|
||
<p>接下来看 <code>gen_free_enter</code> 函数。它接收一个地址作为参数,这个地址是内存分配的结果,也就是将要释放的内存的起始地址。函数首先在 <code>allocs</code> 这个 map 中使用这个地址作为键来查找对应的 <code>alloc_info</code> 结构体。如果找不到,那么就直接返回,因为这意味着这个地址并没有被分配过。如果找到了,那么就删除这个元素,并且调用 <code>update_statistics_del</code> 函数来更新统计数据。最后,如果启用了全局追踪,那么还会输出一条信息,包括这个地址以及它的大小。
|
||
在我们追踪和统计内存分配的同时,我们也需要对内核态的内存分配和释放进行追踪。在Linux内核中,kmem_cache_alloc函数和kfree函数分别用于内核态的内存分配和释放。</p>
|
||
<pre><code class="language-c">SEC("tracepoint/kmem/kfree")
|
||
int memleak__kfree(void *ctx)
|
||
{
|
||
const void *ptr;
|
||
|
||
if (has_kfree()) {
|
||
struct trace_event_raw_kfree___x *args = ctx;
|
||
ptr = BPF_CORE_READ(args, ptr);
|
||
} else {
|
||
struct trace_event_raw_kmem_free___x *args = ctx;
|
||
ptr = BPF_CORE_READ(args, ptr);
|
||
}
|
||
|
||
return gen_free_enter(ptr);
|
||
}
|
||
</code></pre>
|
||
<p>上述代码片段定义了一个函数memleak__kfree,这是一个bpf程序,会在内核调用kfree函数时执行。首先,该函数检查是否存在kfree函数。如果存在,则会读取传递给kfree函数的参数(即要释放的内存块的地址),并保存到变量ptr中;否则,会读取传递给kmem_free函数的参数(即要释放的内存块的地址),并保存到变量ptr中。接着,该函数会调用之前定义的gen_free_enter函数来处理该内存块的释放。</p>
|
||
<pre><code class="language-c">SEC("tracepoint/kmem/kmem_cache_alloc")
|
||
int memleak__kmem_cache_alloc(struct trace_event_raw_kmem_alloc *ctx)
|
||
{
|
||
if (wa_missing_free)
|
||
gen_free_enter(ctx->ptr);
|
||
|
||
gen_alloc_enter(ctx->bytes_alloc);
|
||
|
||
return gen_alloc_exit2(ctx, (u64)(ctx->ptr));
|
||
}
|
||
</code></pre>
|
||
<p>这段代码定义了一个函数 memleak__kmem_cache_alloc,这也是一个bpf程序,会在内核调用 kmem_cache_alloc 函数时执行。如果标记 wa_missing_free 被设置,则调用 gen_free_enter 函数处理可能遗漏的释放操作。然后,该函数会调用 gen_alloc_enter 函数来处理内存分配,最后调用gen_alloc_exit2函数记录分配的结果。</p>
|
||
<p>这两个 bpf 程序都使用了 SEC 宏定义了对应的 tracepoint,以便在相应的内核函数被调用时得到执行。在Linux内核中,tracepoint 是一种可以在内核中插入的静态钩子,可以用来收集运行时的内核信息,它在调试和性能分析中非常有用。</p>
|
||
<p>在理解这些代码的过程中,要注意 BPF_CORE_READ 宏的使用。这个宏用于在 bpf 程序中读取内核数据。在 bpf 程序中,我们不能直接访问内核内存,而需要使用这样的宏来安全地读取数据。</p>
|
||
<h3 id="用户态程序"><a class="header" href="#用户态程序">用户态程序</a></h3>
|
||
<p>在理解 BPF 内核部分之后,我们转到用户空间程序。用户空间程序与BPF内核程序紧密配合,它负责将BPF程序加载到内核,设置和管理BPF map,以及处理从BPF程序收集到的数据。用户态程序较长,我们这里可以简要参考一下它的挂载点。</p>
|
||
<pre><code class="language-c">int attach_uprobes(struct memleak_bpf *skel)
|
||
{
|
||
ATTACH_UPROBE_CHECKED(skel, malloc, malloc_enter);
|
||
ATTACH_URETPROBE_CHECKED(skel, malloc, malloc_exit);
|
||
|
||
ATTACH_UPROBE_CHECKED(skel, calloc, calloc_enter);
|
||
ATTACH_URETPROBE_CHECKED(skel, calloc, calloc_exit);
|
||
|
||
ATTACH_UPROBE_CHECKED(skel, realloc, realloc_enter);
|
||
ATTACH_URETPROBE_CHECKED(skel, realloc, realloc_exit);
|
||
|
||
ATTACH_UPROBE_CHECKED(skel, mmap, mmap_enter);
|
||
ATTACH_URETPROBE_CHECKED(skel, mmap, mmap_exit);
|
||
|
||
ATTACH_UPROBE_CHECKED(skel, posix_memalign, posix_memalign_enter);
|
||
ATTACH_URETPROBE_CHECKED(skel, posix_memalign, posix_memalign_exit);
|
||
|
||
ATTACH_UPROBE_CHECKED(skel, memalign, memalign_enter);
|
||
ATTACH_URETPROBE_CHECKED(skel, memalign, memalign_exit);
|
||
|
||
ATTACH_UPROBE_CHECKED(skel, free, free_enter);
|
||
ATTACH_UPROBE_CHECKED(skel, munmap, munmap_enter);
|
||
|
||
// the following probes are intentinally allowed to fail attachment
|
||
|
||
// deprecated in libc.so bionic
|
||
ATTACH_UPROBE(skel, valloc, valloc_enter);
|
||
ATTACH_URETPROBE(skel, valloc, valloc_exit);
|
||
|
||
// deprecated in libc.so bionic
|
||
ATTACH_UPROBE(skel, pvalloc, pvalloc_enter);
|
||
ATTACH_URETPROBE(skel, pvalloc, pvalloc_exit);
|
||
|
||
// added in C11
|
||
ATTACH_UPROBE(skel, aligned_alloc, aligned_alloc_enter);
|
||
ATTACH_URETPROBE(skel, aligned_alloc, aligned_alloc_exit);
|
||
|
||
return 0;
|
||
}
|
||
</code></pre>
|
||
<p>在这段代码中,我们看到一个名为<code>attach_uprobes</code>的函数,该函数负责将uprobes(用户空间探测点)挂载到内存分配和释放函数上。在Linux中,uprobes是一种内核机制,可以在用户空间程序中的任意位置设置断点,这使得我们可以非常精确地观察和控制用户空间程序的行为。</p>
|
||
<p>这里,每个内存相关的函数都通过两个uprobes进行跟踪:一个在函数入口(enter),一个在函数退出(exit)。因此,每当这些函数被调用或返回时,都会触发一个uprobes事件,进而触发相应的BPF程序。</p>
|
||
<p>在具体的实现中,我们使用了<code>ATTACH_UPROBE</code>和<code>ATTACH_URETPROBE</code>两个宏来附加uprobes和uretprobes(函数返回探测点)。每个宏都需要三个参数:BPF程序的骨架(skel),要监视的函数名,以及要触发的BPF程序的名称。</p>
|
||
<p>这些挂载点包括常见的内存分配函数,如malloc、calloc、realloc、mmap、posix_memalign、memalign、free等,以及对应的退出点。另外,我们也观察一些可能的分配函数,如valloc、pvalloc、aligned_alloc等,尽管它们可能不总是存在。</p>
|
||
<p>这些挂载点的目标是捕获所有可能的内存分配和释放事件,从而使我们的内存泄露检测工具能够获取到尽可能全面的数据。这种方法可以让我们不仅能跟踪到内存分配和释放,还能得到它们发生的上下文信息,例如调用栈和调用次数,从而帮助我们定位和修复内存泄露问题。</p>
|
||
<p>注意,一些内存分配函数可能并不存在或已弃用,比如valloc、pvalloc等,因此它们的附加可能会失败。在这种情况下,我们允许附加失败,并不会阻止程序的执行。这是因为我们更关注的是主流和常用的内存分配函数,而这些已经被弃用的函数往往在实际应用中较少使用。</p>
|
||
<p>完整的源代码:<a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/16-memleak">https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/16-memleak</a> 关于如何安装依赖,请参考:<a href="https://eunomia.dev/tutorials/11-bootstrap/">https://eunomia.dev/tutorials/11-bootstrap/</a></p>
|
||
<h2 id="编译运行"><a class="header" href="#编译运行">编译运行</a></h2>
|
||
<pre><code class="language-console">$ make
|
||
$ sudo ./memleak
|
||
using default object: libc.so.6
|
||
using page size: 4096
|
||
tracing kernel: true
|
||
Tracing outstanding memory allocs... Hit Ctrl-C to end
|
||
[17:17:27] Top 10 stacks with outstanding allocations:
|
||
1236992 bytes in 302 allocations from stack
|
||
0 [<ffffffff812c8f43>] <null sym>
|
||
1 [<ffffffff812c8f43>] <null sym>
|
||
2 [<ffffffff812a9d42>] <null sym>
|
||
3 [<ffffffff812aa392>] <null sym>
|
||
4 [<ffffffff810df0cb>] <null sym>
|
||
5 [<ffffffff81edc3fd>] <null sym>
|
||
6 [<ffffffff82000b62>] <null sym>
|
||
...
|
||
</code></pre>
|
||
<h2 id="总结"><a class="header" href="#总结">总结</a></h2>
|
||
<p>通过本篇 eBPF 入门实践教程,您已经学习了如何编写 Memleak eBPF 监控程序,以实时监控程序的内存泄漏。您已经了解了 eBPF 在内存监控方面的应用,学会了使用 BPF API 编写 eBPF 程序,创建和使用 eBPF maps,并且明白了如何用 eBPF 工具监测和分析内存泄漏问题。我们展示了一个详细的例子,帮助您理解 eBPF 代码的运行流程和原理。</p>
|
||
<p>您可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。</p>
|
||
<p>接下来的教程将进一步探讨 eBPF 的高级特性,我们会继续分享更多有关 eBPF 开发实践的内容。希望这些知识和技巧能帮助您更好地了解和使用 eBPF,以解决实际工作中遇到的问题。</p>
|
||
<p>参考资料:<a href="https://github.com/iovisor/bcc/blob/master/libbpf-tools/memleak.c">https://github.com/iovisor/bcc/blob/master/libbpf-tools/memleak.c</a></p>
|
||
|
||
</main>
|
||
|
||
<nav class="nav-wrapper" aria-label="Page navigation">
|
||
<!-- Mobile navigation buttons -->
|
||
<a rel="prev" href="../15-javagc/index.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||
<i class="fa fa-angle-left"></i>
|
||
</a>
|
||
|
||
<a rel="next prefetch" href="../17-biopattern/index.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||
<i class="fa fa-angle-right"></i>
|
||
</a>
|
||
|
||
<div style="clear: both"></div>
|
||
</nav>
|
||
</div>
|
||
</div>
|
||
|
||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||
<a rel="prev" href="../15-javagc/index.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||
<i class="fa fa-angle-left"></i>
|
||
</a>
|
||
|
||
<a rel="next prefetch" href="../17-biopattern/index.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||
<i class="fa fa-angle-right"></i>
|
||
</a>
|
||
</nav>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
|
||
<script>
|
||
window.playground_copyable = true;
|
||
</script>
|
||
|
||
|
||
<script src="../elasticlunr.min.js"></script>
|
||
<script src="../mark.min.js"></script>
|
||
<script src="../searcher.js"></script>
|
||
|
||
<script src="../clipboard.min.js"></script>
|
||
<script src="../highlight.js"></script>
|
||
<script src="../book.js"></script>
|
||
|
||
<!-- Custom JS scripts -->
|
||
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|