大疆嵌入式面試:Linux內(nèi)存泄漏與高占用排查方法
Linux 內(nèi)存泄漏與高占用問題是大疆嵌入式面試中的重點(diǎn)內(nèi)容,掌握有效的排查方法和工具至關(guān)重要。Valgrind 和 AddressSanitizer 是排查內(nèi)存泄漏的利器,top、htop 以及 /proc 文件系統(tǒng)則是監(jiān)控和分析內(nèi)存占用的得力助手 。在實(shí)際面試中,不僅要熟悉這些工具和方法的理論知識(shí),更要能夠結(jié)合具體的場(chǎng)景和問題,靈活運(yùn)用,展示出自己解決問題的能力。
對(duì)于準(zhǔn)備大疆嵌入式面試的同學(xué),建議大家在復(fù)習(xí)時(shí),多進(jìn)行實(shí)際操作和案例分析,加深對(duì)這些排查方法的理解和掌握??梢宰约壕帉懸恍┌瑑?nèi)存問題的測(cè)試程序,然后使用各種工具進(jìn)行排查和解決,積累實(shí)踐經(jīng)驗(yàn)。同時(shí),要關(guān)注內(nèi)存管理在嵌入式系統(tǒng)中的重要性,理解內(nèi)存泄漏和高占用對(duì)系統(tǒng)性能的影響機(jī)制,這樣在面試中才能更加深入地回答問題,展現(xiàn)出自己的專業(yè)素養(yǎng)和技術(shù)能力 。
Part1.內(nèi)存泄漏:悄無聲息的系統(tǒng)殺手
在 Linux 系統(tǒng)中,內(nèi)存泄漏就像是一個(gè)悄無聲息的殺手,慢慢侵蝕著系統(tǒng)的資源。簡單來說,內(nèi)存泄漏是指程序在申請(qǐng)內(nèi)存后,當(dāng)該內(nèi)存不再被使用時(shí),卻沒有將其釋放回系統(tǒng) ,導(dǎo)致這部分內(nèi)存一直被占用,無法被其他程序使用。就好比你向圖書館借了一本書,看完后卻不歸還,隨著時(shí)間推移,越來越多的人借書不還,圖書館的書就會(huì)越來越少,可供其他人借閱的資源也就越來越稀缺。
在嵌入式系統(tǒng)里,內(nèi)存資源本就十分有限,內(nèi)存泄漏帶來的后果往往更加嚴(yán)重。每一次內(nèi)存泄漏,都像是從系統(tǒng)的 “內(nèi)存儲(chǔ)備庫” 中偷走了一部分資源,隨著泄漏的不斷積累,系統(tǒng)可用內(nèi)存越來越少。這會(huì)導(dǎo)致系統(tǒng)頻繁進(jìn)行內(nèi)存交換操作,從磁盤的虛擬內(nèi)存中讀寫數(shù)據(jù),而磁盤的讀寫速度遠(yuǎn)遠(yuǎn)慢于內(nèi)存,從而使得系統(tǒng)性能急劇下降,響應(yīng)變得遲緩,原本流暢運(yùn)行的程序可能變得卡頓甚至無響應(yīng)。當(dāng)內(nèi)存泄漏嚴(yán)重到一定程度,系統(tǒng)再也無法分配到足夠的內(nèi)存來滿足正常的運(yùn)行需求,就如同水庫干涸,無法為下游提供足夠的水源,系統(tǒng)便會(huì)陷入崩潰,造成無人機(jī)飛行異常、工業(yè)控制設(shè)備故障等嚴(yán)重問題。
(1)內(nèi)存占用過大為什么?
內(nèi)存占用過大的原因可能有很多,以下是一些常見的情況:
- 內(nèi)存泄漏:當(dāng)程序在運(yùn)行時(shí)動(dòng)態(tài)分配了內(nèi)存但未正確釋放時(shí),會(huì)導(dǎo)致內(nèi)存泄漏。這意味著那部分內(nèi)存將無法再被其他代碼使用,最終導(dǎo)致內(nèi)存占用增加。
- 頻繁的動(dòng)態(tài)內(nèi)存分配和釋放:如果程序中頻繁進(jìn)行大量的動(dòng)態(tài)內(nèi)存分配和釋放操作,可能會(huì)導(dǎo)致內(nèi)存碎片化問題。這樣系統(tǒng)將難以有效地管理可用的物理內(nèi)存空間。
- 數(shù)據(jù)結(jié)構(gòu)和算法選擇不當(dāng):某些數(shù)據(jù)結(jié)構(gòu)或算法可能對(duì)特定場(chǎng)景具有較高的空間復(fù)雜度,從而導(dǎo)致內(nèi)存占用過大。在設(shè)計(jì)和選擇數(shù)據(jù)結(jié)構(gòu)和算法時(shí)應(yīng)綜合考慮時(shí)間效率和空間效率。
- 緩存未及時(shí)清理:如果程序中使用了緩存機(jī)制,并且沒有及時(shí)清理或管理緩存大小,就會(huì)導(dǎo)致緩存占用過多的內(nèi)存空間。
- 高并發(fā)環(huán)境下資源競(jìng)爭:在高并發(fā)環(huán)境下,多個(gè)線程同時(shí)訪問共享資源(包括對(duì)內(nèi)存的申請(qǐng)和釋放)可能引發(fā)資源競(jìng)爭問題。若沒有適當(dāng)?shù)耐綑C(jī)制或鎖策略,可能導(dǎo)致內(nèi)存占用過大。
- 第三方庫或框架問題:使用的第三方庫或框架可能存在內(nèi)存管理不當(dāng)、內(nèi)存泄漏等問題,從而導(dǎo)致整體程序的內(nèi)存占用過大。
(2)內(nèi)存泄露和內(nèi)存占用過大區(qū)別?
內(nèi)存泄漏指的是在程序運(yùn)行過程中,動(dòng)態(tài)分配的內(nèi)存空間沒有被正確釋放,導(dǎo)致這些內(nèi)存無法再被其他代碼使用。每次發(fā)生內(nèi)存泄漏時(shí),系統(tǒng)可用的物理內(nèi)存空間就會(huì)減少一部分,最終導(dǎo)致整體的內(nèi)存占用量增加。
而內(nèi)存占用過大則是指程序在運(yùn)行時(shí)所消耗的物理內(nèi)存超出了合理范圍或預(yù)期值。除了因?yàn)閮?nèi)存泄漏導(dǎo)致的額外占用外,其他原因如頻繁的動(dòng)態(tài)內(nèi)存分配和釋放、數(shù)據(jù)結(jié)構(gòu)和算法選擇不當(dāng)、緩存管理問題等都可能導(dǎo)致程序的內(nèi)存占用過大。
可以說,內(nèi)存在被正確管理和使用時(shí),即使有一定程度的動(dòng)態(tài)分配和釋放操作,也不會(huì)造成明顯的長期累積效應(yīng),即不會(huì)出現(xiàn)持續(xù)性的內(nèi)存占用過大情況。而如果存在未及時(shí)釋放或回收的資源(即發(fā)生了內(nèi)存泄漏),隨著時(shí)間推移會(huì)逐漸積累并導(dǎo)致整體的內(nèi)存占用越來越高。
因此,在排查和解決內(nèi)存占用過大問題時(shí),需要注意是否存在內(nèi)存泄漏,并且還需綜合考慮其他可能導(dǎo)致內(nèi)存占用過大的因素。
(3)產(chǎn)生的原因
我們?cè)谶M(jìn)行程序開發(fā)的過程使用動(dòng)態(tài)存儲(chǔ)變量時(shí),不可避免地面對(duì)內(nèi)存管理的問題。程序中動(dòng)態(tài)分配的存儲(chǔ)空間,在程序執(zhí)行完畢后需要進(jìn)行釋放。沒有釋放動(dòng)態(tài)分配的存儲(chǔ)空間而造成內(nèi)存泄漏,是使用動(dòng)態(tài)存儲(chǔ)變量的主要問題。
一般情況下,作為開發(fā)人員會(huì)經(jīng)常使用系統(tǒng)提供的內(nèi)存管理基本函數(shù),如malloc、realloc、calloc、free等,完成動(dòng)態(tài)存儲(chǔ)變量存儲(chǔ)空間的分配和釋放。但是,當(dāng)開發(fā)程序中使用動(dòng)態(tài)存儲(chǔ)變量較多和頻繁使用函數(shù)調(diào)用時(shí),就會(huì)經(jīng)常發(fā)生內(nèi)存管理錯(cuò)誤。
Part2.虛擬內(nèi)存泄露
一般來說,我們觀察系統(tǒng)的內(nèi)存占用喜歡用top命令,然后輸入m,對(duì)系統(tǒng)中整體的內(nèi)存占用情況做個(gè)排序,然后在重點(diǎn)觀察,內(nèi)存占用排在前幾位的進(jìn)程,再逐步的分析。
[root@VM-0-2-centos ~]# top -p 5576
top - 18:21:46 up 198 days, 20:07, 2 users, load average: 0.10, 0.04, 0.05
Tasks: 1 total, 0 running, 1 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.7 us, 0.3 sy, 0.0 ni, 99.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 1882008 total, 78532 free, 116516 used, 1686960 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 1606660 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
5576 root 20 0 184064 11248 1124 S 0.0 0.6 10:34.98 nginx
雖然top 也可以觀察到單獨(dú)的進(jìn)程的內(nèi)存變化,不過一般不太好比較內(nèi)存變化的規(guī)律,推薦使用pidstat工具,此工具需要先安裝,通過命令:
yum install sysstat
pidstat 基本說明如下:
- u:默認(rèn)的參數(shù),顯示各個(gè)進(jìn)程的cpu使用統(tǒng)計(jì)
- r:顯示各個(gè)進(jìn)程的內(nèi)存使用統(tǒng)計(jì)
- d:顯示各個(gè)進(jìn)程的IO使用情況
- p:指定進(jìn)程號(hào)
- w:顯示每個(gè)進(jìn)程的上下文切換情況
- t:顯示選擇任務(wù)的線程的統(tǒng)計(jì)信息外的額外信息
- T { TASK | CHILD | ALL }
假如我們觀察到如下的內(nèi)存占用情況:pidstat -r -p pid 5
[root@VM-0-2-centos ~]# pidstat -r -p 5981 5
Linux 3.10.0-1127.19.1.el7.x86_64 (VM-0-2-centos) 07/24/2021 _x86_64_ (1 CPU)
06:25:55 PM UID PID minflt/s majflt/s VSZ RSS %MEM Command
06:26:00 PM 0 5981 0.20 0.00 4416 352 0.02 a.out
06:26:05 PM 0 5981 0.00 0.00 4416 352 0.02 a.out
06:26:10 PM 0 5981 0.20 0.00 4456 352 0.02 a.out
06:26:15 PM 0 5981 0.00 0.00 4456 352 0.02 a.out
06:26:20 PM 0 5981 0.00 0.00 4456 352 0.02 a.out
06:26:25 PM 0 5981 0.20 0.00 4496 352 0.02 a.out
06:26:30 PM 0 5981 0.00 0.00 4496 352 0.02 a.out
06:26:35 PM 0 5981 0.20 0.00 4536 352 0.02 a.out
06:26:40 PM 0 5981 0.00 0.00 4536 352 0.02 a.out
06:26:45 PM 0 5981 0.20 0.00 4576 352 0.02 a.out
06:26:50 PM 0 5981 0.00 0.00 4576 352 0.02 a.out
06:26:55 PM 0 5981 0.20 0.00 4616 352 0.02 a.out
我們注意下,VSZ即虛擬內(nèi)存的占用每10s增加40,單位為k,即10s增加40k的虛擬內(nèi)存,下面來具體分析下這個(gè)程序的內(nèi)存泄露情況。
Part3.分析泄露原因
我們來分析這個(gè)進(jìn)程的內(nèi)存分布情況,來分析這泄露的內(nèi)存有什么特點(diǎn):
[root@VM-0-2-centos ~]# pmap -x 5981
5981: ./a.out
Address Kbytes RSS Dirty Mode Mapping
0000000000400000 4 4 0 r-x-- a.out
0000000000600000 4 4 4 r---- a.out
0000000000601000 4 4 4 rw--- a.out
00007faab436e000 2720 272 272 rw--- [ anon ]
00007faab4616000 1804 260 0 r-x-- libc-2.17.so
00007faab47d9000 2048 0 0 ----- libc-2.17.so
00007faab49d9000 16 16 16 r---- libc-2.17.so
00007faab49dd000 8 8 8 rw--- libc-2.17.so
00007faab49df000 20 12 12 rw--- [ anon ]
00007faab49e4000 136 108 0 r-x-- ld-2.17.so
00007faab4a06000 2012 212 212 rw--- [ anon ]
00007faab4c03000 8 8 8 rw--- [ anon ]
00007faab4c05000 4 4 4 r---- ld-2.17.so
00007faab4c06000 4 4 4 rw--- ld-2.17.so
00007faab4c07000 4 4 4 rw--- [ anon ]
00007ffe0f3f5000 132 16 16 rw--- [ stack ]
00007ffe0f47c000 8 4 0 r-x-- [ anon ]
ffffffffff600000 4 0 0 r-x-- [ anon ]
---------------- ------- ------- -------
total kB 8940 940 564
其中Address為開始的地址,Kbytes是虛擬內(nèi)存的大小,RSS為真實(shí)內(nèi)存的大小,Dirty為未同步到磁盤上的臟頁,Mode為內(nèi)存的權(quán)限,rw為可寫可讀,rx為可讀和可執(zhí)行。通過幾次觀察,我們發(fā)現(xiàn):
00007faab436e000 2720 272 272 rw--- [ anon ]
為泄露部分,此為匿名內(nèi)存區(qū),也就是沒有映射文件,為malloc或mmap分配的內(nèi)存。同樣是每次增加40K。
此時(shí)還是只能大概知道內(nèi)存泄露的位置,我們還先找到具體的代碼位置,這個(gè)該怎么分析?代碼的申請(qǐng),無非是通過malloc和brk這些庫函數(shù)進(jìn)行內(nèi)存調(diào)用,我們可以用strace跟蹤下。
[root@VM-0-2-centos ~]# strace -f -t -p 5981 -o trace.strace
strace: Process 5981 attached
strace: Process 8519 attached
strace: Process 8533 attached
strace: Process 8547 attached
strace: Process 8557 attached
strace: Process 8575 attached
^Cstrace: Process 5981 detached
我們通過-t選項(xiàng)來顯示時(shí)間,-f來跟蹤子進(jìn)程。直接用cat命令查看跟蹤的文件內(nèi)容,會(huì)發(fā)現(xiàn)內(nèi)容相當(dāng)多,只要是系統(tǒng)調(diào)用都打印了出來,可以通過每次增加40k這個(gè)有用的信息搜索下:
[root@VM-0-2-centos ~]# grep 40960 trace.strace
5981 19:01:44 mmap(NULL, 40960, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7faab403a000
5981 19:01:55 mmap(NULL, 40960, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7faab4030000
5981 19:02:06 mmap(NULL, 40960, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7faab4026000
5981 19:02:17 mmap(NULL, 40960, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7faab401c000
5981 19:02:28 mmap(NULL, 40960, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7faab4012000
至此我們找到了具體的泄露代碼位置。看下這個(gè)測(cè)試代碼:
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
#define _SCHED_H
#define __USE_GNU
#include <bits/sched.h>
#define STACK_SIZE 40960
int func(void *arg)
{
printf("thread enter.\n");
sleep(1);
printf("thread exit.\n");
return 0;
}
int main()
{
int thread_pid;
int status;
int w;
while (1) {
void *addr = mmap(NULL, STACK_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0);
if (addr == NULL) {
perror("mmap");
goto error;
}
printf("creat new thread...\n");
thread_pid = clone(&func, addr + STACK_SIZE, CLONE_SIGHAND|CLONE_FS|CLONE_VM|CLONE_FILES, NULL);
printf("Done! Thread pid: %d\n", thread_pid);
if (thread_pid != -1) {
do {
w = waitpid(-1, NULL, __WCLONE | __WALL);
if (w == -1) {
perror("waitpid");
goto error;
}
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
}
sleep(10);
}
error:
return 0;
}
這個(gè)測(cè)試程序利用mmap申請(qǐng)一塊匿名私有的內(nèi)存,clone為系統(tǒng)函數(shù),pthread_create 和fork底層都是調(diào)用它,用來創(chuàng)建進(jìn)程/線程,將func的地址指針存放在子進(jìn)程堆棧的某個(gè)位置處,該位置就是該封裝函數(shù)本身返回地址存放的位置,最后一個(gè)參數(shù)為func的執(zhí)行參數(shù)。clone可以更靈活控制共享,比如可以控制是否共享內(nèi)存空間,是否共享打開文件,是否共享相同的信號(hào)處理函數(shù)等。
我們可以看到,mmap申請(qǐng)內(nèi)存后,需要通過munmap來釋放,這里面沒有釋放,所以導(dǎo)致了虛擬內(nèi)存泄露,這里面申請(qǐng)的內(nèi)存只實(shí)際使用了4個(gè)字節(jié),即復(fù)制了func的指針,其他的內(nèi)存均沒有使用,其實(shí)仔細(xì)觀察會(huì)發(fā)現(xiàn)還有部分的物理內(nèi)存泄露,每次4個(gè)字節(jié),可以通過pmap -x 查到。
在 waitpid后面添加:munmap(addr,STACK_SIZE); 即可以實(shí)現(xiàn)內(nèi)存的釋放。
如何排查內(nèi)存泄漏
我們平時(shí)開發(fā)過程中不可避免的會(huì)遇到內(nèi)存泄漏問題,這是常見的問題。既然發(fā)生了內(nèi)存泄漏,我們就要排查內(nèi)存泄漏的問題。想必大家也經(jīng)常會(huì)用到以下排查內(nèi)存問題的工具,如下:
- memwatch
- mtrace
- dmalloc
- ccmalloc
- valgrind
- debug_new
Part4.Valgrind 排查工具
在眾多排查 Linux 內(nèi)存泄漏與高占用的工具中,Valgrind 堪稱一把鋒利的 “寶劍”,在嵌入式開發(fā)中被廣泛使用。Valgrind 是一套 Linux 下開放源代碼(GPL V2)的仿真調(diào)試工具的集合 ,它由內(nèi)核(core)以及基于內(nèi)核的其他調(diào)試工具組成。其內(nèi)核就像一個(gè)精心搭建的舞臺(tái)框架,模擬出一個(gè) CPU 環(huán)境,為其他工具提供表演的場(chǎng)地和基礎(chǔ)服務(wù);而其他工具則如同一個(gè)個(gè)身懷絕技的演員,在這個(gè)舞臺(tái)上利用內(nèi)核提供的服務(wù),完成各種特定的內(nèi)存調(diào)試任務(wù)。
(1)安裝與準(zhǔn)備
在使用 Valgrind 之前,我們首先得把它安裝到系統(tǒng)中。安裝方法很簡單,對(duì)于基于 Debian 或 Ubuntu 的系統(tǒng),只需在終端中輸入 “sudo apt-get install valgrind” ,系統(tǒng)就會(huì)自動(dòng)從軟件源中下載并安裝 Valgrind 及其依賴項(xiàng),就像從云端倉庫中取來一件趁手的工具。而對(duì)于 CentOS 等基于 Red Hat 的系統(tǒng),安裝命令則變?yōu)?“sudo yum install valgrind” ,同樣能快速將這個(gè)強(qiáng)大的工具收入囊中。
(2)基本使用命令
安裝完成后,就可以使用 Valgrind 來檢測(cè)程序的內(nèi)存問題了。它的基本使用命令格式為:“valgrind [valgrind-options] your-prog [your-prog-options]” 。這里的 “valgrind-options” 是 Valgrind 自身的一些選項(xiàng),用于控制它的行為和輸出;“your-prog” 是你要檢測(cè)的目標(biāo)程序;“your-prog-options” 則是目標(biāo)程序運(yùn)行時(shí)需要的參數(shù)。
其中,最常用的選項(xiàng)是 “--tool=memcheck” ,它指定使用 Memcheck 工具,這是 Valgrind 應(yīng)用最廣泛的工具,一個(gè)重量級(jí)的內(nèi)存檢查器,能夠發(fā)現(xiàn)開發(fā)中絕大多數(shù)內(nèi)存錯(cuò)誤使用情況。比如使用未初始化的內(nèi)存,使用已經(jīng)釋放了的內(nèi)存,內(nèi)存訪問越界等。若想讓 Valgrind 在程序結(jié)束時(shí)詳細(xì)敘述每一個(gè)內(nèi)存泄漏情況,就可以加上 “--leak-check=full” 選項(xiàng);如果希望在錯(cuò)誤報(bào)告中顯示可到達(dá)的內(nèi)存塊(即雖然沒有釋放,但仍有指針指向的內(nèi)存塊),則可以使用 “--show-reachable=yes” 選項(xiàng)。
(3)工作原理
Valgrind 中的 Memcheck 工具能夠檢測(cè)出內(nèi)存問題,關(guān)鍵在于其建立了兩個(gè)全局表。第一個(gè)是 Valid-Value 表,對(duì)于進(jìn)程的整個(gè)地址空間中的每一個(gè)字節(jié) (byte),都有與之對(duì)應(yīng)的 8 個(gè) bits;對(duì)于 CPU 的每個(gè)寄存器,也有一個(gè)與之對(duì)應(yīng)的 bit 向量 。這些 bits 就像一個(gè)個(gè)小衛(wèi)士,負(fù)責(zé)記錄該字節(jié)或者寄存器值是否具有有效的、已初始化的值。第二個(gè)是 Valid-Address 表,對(duì)于進(jìn)程整個(gè)地址空間中的每一個(gè)字節(jié) (byte),還有與之對(duì)應(yīng)的 1 個(gè) bit,它就像一把地址鎖,負(fù)責(zé)記錄該地址是否能夠被讀寫。
當(dāng)程序要讀寫內(nèi)存中某個(gè)字節(jié)時(shí),Memcheck 首先會(huì)檢查這個(gè)字節(jié)對(duì)應(yīng)的 A bit。如果該 A bit 顯示該位置是無效位置,就如同試圖打開一把被鎖住的門,Memcheck 則會(huì)報(bào)告讀寫錯(cuò)誤。內(nèi)核(core)類似于一個(gè)虛擬的 CPU 環(huán)境,這樣當(dāng)內(nèi)存中的某個(gè)字節(jié)被加載到真實(shí)的 CPU 中時(shí),該字節(jié)對(duì)應(yīng)的 V bit 也會(huì)被加載到虛擬的 CPU 環(huán)境中。一旦寄存器中的值被用來產(chǎn)生內(nèi)存地址,或者該值能夠影響程序輸出,Memcheck 就會(huì)像一個(gè)警惕的監(jiān)察員,檢查對(duì)應(yīng)的 V bits,如果該值尚未初始化,則會(huì)報(bào)告使用未初始化內(nèi)存錯(cuò)誤。
(4)實(shí)際案例分析
為了更直觀地感受 Valgrind 的強(qiáng)大功能,我們來看一個(gè)實(shí)際案例。假設(shè)有如下一段簡單的 C 語言代碼:
#include <stdio.h>
#include <stdlib.h>
void leak() {
int* ptr = (int*)malloc(10 * sizeof(int));
// 這里沒有釋放ptr指向的內(nèi)存,造成內(nèi)存泄漏
}
void uninit() {
int x;
if (x > 0) {
// 使用了未初始化的變量x,這是一個(gè)錯(cuò)誤
}
}
int main() {
int* arr = (int*)malloc(20 * sizeof(int));
arr[20] = 0; // 數(shù)組越界訪問,訪問了arr[20],而數(shù)組arr只有20個(gè)元素,合法索引范圍是0到19
leak();
uninit();
free(arr);
return 0;
}
這段代碼中存在內(nèi)存泄漏、使用未初始化變量以及數(shù)組越界訪問的問題。我們使用 Valgrind 來檢測(cè)它。首先,使用 “gcc -g -O0 -Wall test.c -o test” 命令進(jìn)行編譯,其中 “-g” 選項(xiàng)用于生成調(diào)試信息,這對(duì)于 Valgrind 準(zhǔn)確報(bào)告錯(cuò)誤位置至關(guān)重要;“-O0” 選項(xiàng)表示不進(jìn)行優(yōu)化,防止優(yōu)化影響調(diào)試結(jié)果;“-Wall” 選項(xiàng)用于開啟所有常見的警告信息。
編譯完成后,執(zhí)行 “valgrind --tool=memcheck --leak-check=full --show-reachable=yes./test” 命令。Valgrind 運(yùn)行后,會(huì)輸出詳細(xì)的錯(cuò)誤報(bào)告:
==2596== Memcheck, a memory error detector
==2596== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2596== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==2596== Command:./test
==2596==
==2596== Invalid write of size 4
==2596== at 0x4005C3: main (test.c:16)
==2596== Address 0x51f3050 is 0 bytes after a block of size 80 alloc'd
==2596== at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2596== at 0x4005A9: main (test.c:14)
==2596==
==2596== Conditional jump or move depends on uninitialised value(s)
==2596== at 0x4005F5: uninit (test.c:9)
==2596== by 0x40060A: main (test.c:18)
==2596==
==2596== 100 bytes in 1 blocks are definitely lost in loss record 1 of 1
==2596== at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2596== by 0x4005D8: leak (test.c:5)
==2596== by 0x400605: main (test.c:17)
==2596==
==2596== LEAK SUMMARY:
==2596== definitely lost: 100 bytes in 1 blocks
==2596== indirectly lost: 0 bytes in 0 blocks
==2596== possibly lost: 0 bytes in 0 blocks
==2596== still reachable: 0 bytes in 0 blocks
==2596== suppressed: 0 bytes in 0 blocks
==2596==
==2596== For counts of detected and suppressed errors, rerun with: -v
==2596== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 0 from 0)
從報(bào)告中可以清晰地看到,Valgrind 準(zhǔn)確地指出了代碼中的數(shù)組越界訪問錯(cuò)誤(“Invalid write of size 4”),發(fā)生在 “test.c” 文件的 16 行;使用未初始化變量錯(cuò)誤(“Conditional jump or move depends on uninitialised value (s)”),發(fā)生在 “test.c” 文件的 9 行;以及內(nèi)存泄漏錯(cuò)誤(“100 bytes in 1 blocks are definitely lost”),發(fā)生在 “test.c” 文件的 5 行,泄漏的內(nèi)存大小為 100 字節(jié)(因?yàn)榉峙淞?10 個(gè) int 類型的內(nèi)存空間,每個(gè) int 通常為 4 字節(jié),共 40 字節(jié),但這里報(bào)告 100 字節(jié)可能包含了一些內(nèi)部管理信息)。
根據(jù)這些詳細(xì)的錯(cuò)誤信息,我們就可以輕松地定位并修復(fù)代碼中的問題,讓程序更加健壯和穩(wěn)定 。通過這個(gè)案例,我們可以看到 Valgrind 在排查內(nèi)存問題時(shí)的高效和精準(zhǔn),它就像一位經(jīng)驗(yàn)豐富的偵探,能夠在復(fù)雜的代碼迷宮中,準(zhǔn)確地找出內(nèi)存相關(guān)的 “罪犯”,為我們解決內(nèi)存泄漏和高占用問題提供了有力的支持。
Part5.AddressSanitizer(ASan)排查工具
AddressSanitizer(ASAN)是一款由 Google 開發(fā)的內(nèi)存錯(cuò)誤檢測(cè)器 ,在檢測(cè)內(nèi)存泄漏方面展現(xiàn)出獨(dú)特的優(yōu)勢(shì)。它主要通過編譯器插樁的方式,在程序編譯階段將內(nèi)存訪問檢查代碼巧妙地插入到目標(biāo)程序中,就像在程序的關(guān)鍵位置安插了許多 “小哨兵”,時(shí)刻監(jiān)視著內(nèi)存的使用情況。
(1)特點(diǎn)與優(yōu)勢(shì)
與傳統(tǒng)的內(nèi)存檢測(cè)工具相比,AddressSanitizer 最大的特點(diǎn)就是快。它對(duì)程序性能的影響相對(duì)較小,在運(yùn)行時(shí)的開銷僅會(huì)使程序速度減慢約 2 倍 ,而 Valgrind 則可能使程序減慢 10 倍之多。這使得開發(fā)者在使用 ASAN 進(jìn)行內(nèi)存檢測(cè)時(shí),不必花費(fèi)過多時(shí)間等待程序運(yùn)行結(jié)果,大大提高了調(diào)試效率。
ASAN 能夠檢測(cè)到多種類型的內(nèi)存錯(cuò)誤,除了內(nèi)存泄漏,還包括釋放后使用(懸空指針)、堆緩沖區(qū)溢出、堆棧緩沖區(qū)溢出、全局緩沖區(qū)溢出、在作用域之后使用、初始化順序錯(cuò)誤等 。它就像一個(gè)全能的內(nèi)存問題探測(cè)器,能夠全面地掃描程序中的內(nèi)存隱患,為開發(fā)者提供更全面的內(nèi)存錯(cuò)誤信息。
(2)使用步驟
使用 AddressSanitizer 進(jìn)行內(nèi)存檢測(cè),步驟也相對(duì)簡潔明了。首先是編譯階段,需要在編譯命令中添加 “-fsanitize=address” 選項(xiàng)。例如,使用 GCC 編譯 C++ 程序時(shí),可以這樣操作:
gcc -fsanitize=address -g -O0 -Wall test.c -o test
這里的 “-g” 選項(xiàng)用于生成調(diào)試信息,“-O0” 表示不進(jìn)行優(yōu)化,“-Wall” 用于開啟所有常見的警告信息,這些選項(xiàng)與 Valgrind 編譯時(shí)類似,都是為了確保在調(diào)試過程中能夠獲取更準(zhǔn)確的信息。
編譯完成后,直接運(yùn)行生成的可執(zhí)行文件即可。當(dāng)程序運(yùn)行過程中出現(xiàn)內(nèi)存錯(cuò)誤時(shí),AddressSanitizer 會(huì)立即捕獲并在控制臺(tái)上輸出詳細(xì)的錯(cuò)誤報(bào)告。例如,對(duì)于下面這段存在內(nèi)存泄漏的代碼:
#include <stdio.h>
#include <stdlib.h>
int main() {
int* ptr = (int*)malloc(10 * sizeof(int));
// 這里沒有釋放ptr指向的內(nèi)存,造成內(nèi)存泄漏
return 0;
}
運(yùn)行編譯后的程序,ASAN 會(huì)輸出類似如下的錯(cuò)誤報(bào)告:
=================================================================
==1234==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 40 byte(s) in 1 object(s) allocated from:
#0 0x7f919d0c8b97 in malloc (/lib/x86_64-linux-gnu/libasan.so.5+0x10eb97)
#1 0x40059d in main /home/user/test.c:5
SUMMARY: AddressSanitizer: 40 byte(s) leaked in 1 allocation(s).
從報(bào)告中可以清晰地看到,ASAN 準(zhǔn)確地指出了內(nèi)存泄漏的位置在 “test.c” 文件的第 5 行,泄漏的內(nèi)存大小為 40 字節(jié)(因?yàn)榉峙淞?10 個(gè) int 類型的內(nèi)存空間,每個(gè) int 通常為 4 字節(jié),共 40 字節(jié)),并且還給出了內(nèi)存分配的調(diào)用棧信息,方便開發(fā)者快速定位問題根源。
Part6.其他內(nèi)存泄露分析
其實(shí)上面的內(nèi)存泄露是我們知道了具體的泄露的進(jìn)程,然后再做詳細(xì)分析。那么如果不知道哪里內(nèi)存泄露了,有什么辦法,可以通過分析meminfo文件,來觀察泄露的類型。
[root@VM-0-2-centos test]# cat /proc/meminfo
MemTotal: 1882008 kB
MemFree: 752948 kB
MemAvailable: 1610108 kB
Buffers: 564900 kB
Cached: 399584 kB
SwapCached: 0 kB
Active: 808140 kB
Inactive: 220812 kB
Active(anon): 64548 kB
Inactive(anon): 488 kB
Active(file): 743592 kB
Inactive(file): 220324 kB
Unevictable: 0 kB
Mlocked: 0 kB
SwapTotal: 0 kB
SwapFree: 0 kB
....
meminfo文件是Linux系統(tǒng)中用于顯示內(nèi)存使用情況的詳細(xì)信息文件,它位于/proc
目錄下,提供了關(guān)于系統(tǒng)內(nèi)存使用的全面信息。通過查看和分析meminfo文件的內(nèi)容,可以了解系統(tǒng)的內(nèi)存使用狀況,包括總內(nèi)存、空閑內(nèi)存、緩存、交換分區(qū)等信息,這對(duì)于排查內(nèi)存相關(guān)的問題非常有幫助。
meminfo文件包含的主要信息及其含義如下:
- MemTotal:系統(tǒng)總內(nèi)存大小。
- MemFree:系統(tǒng)空閑內(nèi)存大小。
- MemAvailable:可用內(nèi)存大小,包括空閑內(nèi)存和緩存。
- Buffers:用于緩存數(shù)據(jù)的內(nèi)存大小。
- Cached:用于緩存文件系統(tǒng)的內(nèi)存大小。
- SwapCached:用于緩存交換分區(qū)的內(nèi)存大小。
- Active 和 Inactive:分別表示活動(dòng)和非活動(dòng)內(nèi)存大小,即正在使用或最近使用的內(nèi)存和最近沒有使用的內(nèi)存。
- SwapTotal 和 SwapFree:交換分區(qū)總大小和空閑大小。
- Dirty 和 Writeback:等待寫回到磁盤的內(nèi)存大小和正在寫回到磁盤的內(nèi)存大小。
- AnonPages、Mapped、Shmem 等:分別表示用于匿名映射、已映射到文件的內(nèi)存、共享內(nèi)存大小等。
- Slab、SReclaimable、SUnreclaim 等:內(nèi)核數(shù)據(jù)結(jié)構(gòu)緩存的內(nèi)存大小以及可回收和不可回收的Slab內(nèi)存大小。
- KernelStack、PageTables 等:內(nèi)核棧的內(nèi)存大小和頁面表的內(nèi)存大小。
- CommitLimit 和 Committed_AS:可用內(nèi)存可支持的最大內(nèi)存大小和已分配的內(nèi)存大小,包括內(nèi)存和交換分區(qū)。
- VmallocTotal、VmallocUsed 等:虛擬內(nèi)存總大小和已使用的虛擬內(nèi)存大小。
排查內(nèi)存問題時(shí),可以通過以下步驟進(jìn)行:
- 首先,使用
cat /proc/meminfo
命令查看meminfo文件的內(nèi)容,了解系統(tǒng)的整體內(nèi)存使用情況。 - 分析MemTotal和MemFree的值,了解系統(tǒng)的總內(nèi)存和可用空閑內(nèi)存。
- 注意MemAvailable的值,它表示應(yīng)用程序可用的內(nèi)存,與MemFree的區(qū)別在于MemAvailable考慮了Buffers和Cached的大小,這些通常在系統(tǒng)需要時(shí)可以被回收。
- 檢查SwapUsage(雖然meminfo文件中沒有直接顯示SwapUsage,但可以通過SwapTotal和SwapFree計(jì)算得出),如果Swap空間被大量使用,可能意味著物理內(nèi)存不足。
- 注意Active、Inactive、Dirty和Writeback等值,這些指標(biāo)可以幫助你了解系統(tǒng)當(dāng)前的內(nèi)存使用模式和可能的性能瓶頸。
- 如果發(fā)現(xiàn)某些特定類型的內(nèi)存使用異常高(如AnonPages、Shmem等),可能需要進(jìn)一步調(diào)查這些類型的內(nèi)存使用情況,以確定是否存在內(nèi)存泄漏或其他問題。
- 使用其他工具如
free
、vmstat
、top
或htop
等命令提供的信息與meminfo文件的內(nèi)容進(jìn)行對(duì)比,以獲得更全面的系統(tǒng)內(nèi)存使用情況視圖。
通過綜合分析meminfo文件的內(nèi)容和其他相關(guān)工具的輸出,可以有效地排查和解決Linux系統(tǒng)中的內(nèi)存相關(guān)問題
Part7.高內(nèi)存占用:系統(tǒng)性能的隱形障礙
高內(nèi)存占用就像是系統(tǒng)性能的隱形障礙,雖然不像內(nèi)存泄漏那樣直觀,但同樣會(huì)給系統(tǒng)帶來嚴(yán)重的影響。當(dāng)系統(tǒng)內(nèi)存被大量占用時(shí),就好比一個(gè)原本寬敞的房間堆滿了雜物,活動(dòng)空間變得狹小,行動(dòng)也變得困難。
在嵌入式系統(tǒng)中,高內(nèi)存占用可能會(huì)導(dǎo)致系統(tǒng)響應(yīng)速度明顯變慢。當(dāng)系統(tǒng)需要處理新的任務(wù)或響應(yīng)用戶的操作時(shí),由于內(nèi)存中已經(jīng)充滿了各種數(shù)據(jù)和程序,沒有足夠的空閑內(nèi)存來快速加載和運(yùn)行新的任務(wù),就像倉庫里沒有足夠的空間存放新的貨物,只能花費(fèi)時(shí)間去整理和騰出空間,這就使得系統(tǒng)的響應(yīng)變得遲緩。原本能夠迅速執(zhí)行的指令,現(xiàn)在可能需要等待較長時(shí)間才能完成,用戶會(huì)明顯感覺到設(shè)備的操作變得卡頓,例如無人機(jī)在執(zhí)行復(fù)雜飛行指令時(shí),反應(yīng)變得不及時(shí),工業(yè)控制設(shè)備對(duì)傳感器數(shù)據(jù)的處理出現(xiàn)延遲等。
高內(nèi)存占用還會(huì)極大地降低系統(tǒng)的多任務(wù)處理能力。在現(xiàn)代嵌入式應(yīng)用中,很多設(shè)備需要同時(shí)運(yùn)行多個(gè)任務(wù),如無人機(jī)既要實(shí)時(shí)處理飛行姿態(tài)數(shù)據(jù),又要傳輸高清圖像、接收遠(yuǎn)程控制指令等。如果內(nèi)存被某個(gè)或某些任務(wù)大量占用,其他任務(wù)就無法獲得足夠的內(nèi)存資源來正常運(yùn)行,就像多個(gè)工人在一個(gè)狹小的工作空間里工作,互相干擾,效率低下。這可能導(dǎo)致一些任務(wù)被迫暫?;蛑袛?,影響整個(gè)系統(tǒng)的功能完整性和穩(wěn)定性,甚至可能引發(fā)任務(wù)之間的沖突,導(dǎo)致系統(tǒng)出現(xiàn)錯(cuò)誤或異常行為。
高內(nèi)存占用與內(nèi)存泄漏之間也存在著緊密的關(guān)聯(lián)。內(nèi)存泄漏是導(dǎo)致高內(nèi)存占用的一個(gè)重要原因,隨著內(nèi)存泄漏的不斷發(fā)生,系統(tǒng)中被占用卻無法釋放的內(nèi)存越來越多,直接導(dǎo)致了系統(tǒng)內(nèi)存占用率的持續(xù)上升 。就像一個(gè)不斷漏水的水桶,水不斷地流進(jìn)桶里卻無法流出,桶里的水就會(huì)越來越滿。而高內(nèi)存占用又會(huì)加劇內(nèi)存泄漏的影響,因?yàn)楫?dāng)系統(tǒng)內(nèi)存緊張時(shí),程序在分配和釋放內(nèi)存時(shí)更容易出現(xiàn)錯(cuò)誤,進(jìn)一步增加了內(nèi)存泄漏的風(fēng)險(xiǎn),形成一個(gè)惡性循環(huán)。例如,在內(nèi)存緊張的情況下,程序可能會(huì)為了獲取內(nèi)存而采取一些不安全的內(nèi)存分配方式,或者在釋放內(nèi)存時(shí)出現(xiàn)誤判,導(dǎo)致本應(yīng)釋放的內(nèi)存沒有被釋放,從而加重內(nèi)存泄漏問題。
★排查手段一:top 與 htop 實(shí)時(shí)監(jiān)控
在排查 Linux 內(nèi)存泄漏與高占用問題時(shí),top 和 htop 命令是我們最先會(huì)用到的 “偵察兵”,它們就像系統(tǒng)的實(shí)時(shí)監(jiān)控儀表盤,能夠讓我們快速了解系統(tǒng)內(nèi)存的使用狀況,直觀地發(fā)現(xiàn)那些占用內(nèi)存過高的 “嫌疑進(jìn)程”。
①top 命令:系統(tǒng)資源的實(shí)時(shí)監(jiān)視器
top 命令是 Linux 系統(tǒng)中常用的性能分析工具,它能夠?qū)崟r(shí)顯示系統(tǒng)中各個(gè)進(jìn)程的資源占用狀況 ,如同一個(gè)忙碌的指揮中心,時(shí)刻匯報(bào)著系統(tǒng)的 “健康狀態(tài)”。在終端中輸入 “top” 命令,即可打開這個(gè)實(shí)時(shí)監(jiān)控界面,系統(tǒng)默認(rèn)每 3 秒刷新一次信息 ,讓你能夠及時(shí)捕捉到系統(tǒng)狀態(tài)的變化。
在 top 命令的輸出界面中,頭部區(qū)域是系統(tǒng)整體概覽的展示區(qū),這里包含了當(dāng)前時(shí)間、系統(tǒng)已經(jīng)運(yùn)行的時(shí)長、登錄用戶數(shù),以及系統(tǒng)負(fù)載等關(guān)鍵信息 ,就像汽車儀表盤上的時(shí)間、里程和負(fù)載指示燈,讓你對(duì)系統(tǒng)的整體運(yùn)行環(huán)境有一個(gè)初步了解。同時(shí),這里還會(huì)顯示 CPU 和內(nèi)存的使用情況,包括總內(nèi)存、已用內(nèi)存、空閑內(nèi)存等,讓你一眼就能看出系統(tǒng)內(nèi)存的 “庫存” 狀態(tài)。
主體部分則是以列表形式展示系統(tǒng)中的各個(gè)進(jìn)程,每一行都代表著一個(gè)進(jìn)程,詳細(xì)列出了進(jìn)程的各種信息,如進(jìn)程 ID(PID),就像是進(jìn)程的身份證號(hào)碼,用于唯一標(biāo)識(shí)每個(gè)進(jìn)程;用戶(USER),表明該進(jìn)程是由哪個(gè)用戶啟動(dòng)的;優(yōu)先級(jí)(PR),反映了進(jìn)程在系統(tǒng)資源分配中的優(yōu)先程度;虛擬內(nèi)存使用量(VIRT),表示進(jìn)程占用的虛擬內(nèi)存大小,包括進(jìn)程使用的代碼、數(shù)據(jù)和共享庫等;
物理內(nèi)存使用量(RES),即進(jìn)程實(shí)際占用的物理內(nèi)存大??;共享內(nèi)存(SHR),是進(jìn)程與其他進(jìn)程共享的內(nèi)存部分;狀態(tài)(S),顯示進(jìn)程當(dāng)前是正在運(yùn)行(R)、休眠(S)、停止(T)還是處于僵尸狀態(tài)(Z);CPU 使用率(% CPU),直觀地展示了該進(jìn)程占用 CPU 資源的百分比;內(nèi)存使用率(% MEM),表示進(jìn)程占用物理內(nèi)存和總內(nèi)存的比例;累計(jì) CPU 時(shí)間(TIME+),記錄了該進(jìn)程從啟動(dòng)到當(dāng)前累計(jì)使用的 CPU 時(shí)間;以及進(jìn)程名稱(COMMAND),讓你清楚知道這個(gè)進(jìn)程對(duì)應(yīng)的是哪個(gè)程序或服務(wù)。
在實(shí)際使用 top 命令時(shí),我們還可以通過一些選項(xiàng)和交互命令來更靈活地獲取所需信息。比如,使用 “-d” 選項(xiàng)可以設(shè)置更新間隔時(shí)間,例如 “top -d 2” 表示每 2 秒刷新一次信息,就像你可以調(diào)整汽車儀表盤上某些數(shù)據(jù)的更新頻率,以適應(yīng)不同的觀察需求。如果只想監(jiān)控特定的進(jìn)程 ID,可以使用 “-p” 選項(xiàng),如 “top -p 1234”,只關(guān)注 PID 為 1234 的進(jìn)程,這對(duì)于排查某個(gè)關(guān)鍵服務(wù)的內(nèi)存使用情況非常有用。“-u” 選項(xiàng)則用于指定要監(jiān)控的用戶所屬的進(jìn)程,如 “top -u username”,方便查看特定用戶相關(guān)進(jìn)程的內(nèi)存占用。
而在 top 命令的交互界面中,按下 “M” 鍵,就可以按內(nèi)存使用率對(duì)進(jìn)程進(jìn)行排序,讓占用內(nèi)存較多的進(jìn)程排在前面,快速定位到內(nèi)存占用大戶,就像在一個(gè)貨物清單中,按照貨物重量進(jìn)行排序,找出最重的那些貨物。按下 “P” 鍵則可以按 CPU 使用率排序,幫助我們發(fā)現(xiàn)占用 CPU 資源較多的進(jìn)程。如果想要?dú)⑺滥硞€(gè)進(jìn)程,可以按下 “k” 鍵,然后輸入要?dú)⑺赖倪M(jìn)程 ID;若要重新調(diào)整進(jìn)程的優(yōu)先級(jí),按下 “r” 鍵,再輸入進(jìn)程 ID 和新的優(yōu)先級(jí)值即可。
②htop 命令:更直觀強(qiáng)大的監(jiān)控利器
htop 命令是 top 命令的增強(qiáng)版本,它在功能和用戶體驗(yàn)上都有了顯著的提升,就像是在普通相機(jī)的基礎(chǔ)上,增加了更多高級(jí)功能和更清晰的顯示效果,讓我們對(duì)系統(tǒng)內(nèi)存狀況的監(jiān)控更加得心應(yīng)手。
與 top 命令類似,在終端輸入 “htop” 即可啟動(dòng)這個(gè)強(qiáng)大的監(jiān)控工具。htop 的界面布局與 top 有相似之處,但更加美觀和直觀 。頭部同樣展示了系統(tǒng)整體概覽信息,包括 CPU 使用率、內(nèi)存使用率、交換空間使用率等,不過它是以彩色條形圖的形式呈現(xiàn),讓你能更直觀地感受到系統(tǒng)資源的使用比例,就像一個(gè)更生動(dòng)的儀表盤,不同顏色的進(jìn)度條代表著不同資源的使用情況。同時(shí),這里也顯示了系統(tǒng)時(shí)間、運(yùn)行時(shí)間、登錄用戶數(shù)和系統(tǒng)負(fù)載等信息。
主體部分的進(jìn)程列表也列出了與 top 命令類似的進(jìn)程信息,但 htop 的顯示更加清晰,并且支持鼠標(biāo)操作,操作起來更加便捷 。例如,你可以直接用鼠標(biāo)點(diǎn)擊某一列的標(biāo)題,對(duì)進(jìn)程按照該列信息進(jìn)行排序,而無需像 top 命令那樣記住特定的按鍵操作。
htop 命令還提供了許多豐富的功能和交互命令。按下 “F2” 鍵可以進(jìn)入設(shè)置菜單,在這里你可以根據(jù)自己的需求和喜好,自定義顯示的列、顏色主題、排序方式等,就像給你的監(jiān)控工具進(jìn)行個(gè)性化定制,讓它更符合你的使用習(xí)慣。按下 “F3” 鍵可以搜索進(jìn)程,當(dāng)系統(tǒng)中進(jìn)程眾多時(shí),通過輸入進(jìn)程名稱或關(guān)鍵字,就能快速定位到相關(guān)進(jìn)程,節(jié)省查找時(shí)間。如果想要?dú)⑺滥硞€(gè)進(jìn)程,在 htop 中操作更加簡單,直接選擇要?dú)⑺赖倪M(jìn)程,然后按下 “F9” 鍵即可,無需手動(dòng)輸入進(jìn)程 ID,這對(duì)于需要快速處理問題的場(chǎng)景非常實(shí)用。
使用 “+” 和 “-” 鍵可以輕松調(diào)整進(jìn)程的優(yōu)先級(jí),選擇要調(diào)整優(yōu)先級(jí)的進(jìn)程后,按 “+” 鍵提高優(yōu)先級(jí),按 “-” 鍵降低優(yōu)先級(jí),方便你根據(jù)實(shí)際情況優(yōu)化系統(tǒng)資源分配。按下 “F5” 鍵可以切換到樹狀圖模式,顯示進(jìn)程之間的父子關(guān)系,這對(duì)于分析復(fù)雜的進(jìn)程結(jié)構(gòu)和依賴關(guān)系非常有幫助,就像查看一個(gè)家族樹,清晰地了解各個(gè)進(jìn)程之間的傳承和關(guān)聯(lián)。按下 “F6” 鍵可以選擇不同的排序方式,除了按 CPU 使用率和內(nèi)存使用率排序外,還可以根據(jù)其他指標(biāo)進(jìn)行排序,滿足不同的監(jiān)控需求。
通過 top 和 htop 命令的實(shí)時(shí)監(jiān)控,我們能夠快速定位到高內(nèi)存占用的進(jìn)程,為進(jìn)一步深入排查內(nèi)存問題提供了重要線索。它們就像我們?cè)谂挪閮?nèi)存泄漏與高占用問題道路上的得力助手,讓我們?cè)诿鎸?duì)復(fù)雜的系統(tǒng)內(nèi)存狀況時(shí),能夠做到心中有數(shù),有的放矢地進(jìn)行后續(xù)的分析和處理。
★排查手段二:/proc 文件系統(tǒng)深度剖析
在 Linux 系統(tǒng)中,/proc 文件系統(tǒng)是一個(gè)非常強(qiáng)大且特殊的工具,它就像一個(gè)系統(tǒng)信息的寶藏庫,為我們提供了豐富的系統(tǒng)運(yùn)行時(shí)信息,其中與內(nèi)存相關(guān)的文件,更是排查內(nèi)存泄漏與高占用問題的關(guān)鍵線索來源。
①/proc/[PID]/status:進(jìn)程狀態(tài)與內(nèi)存概況
在 /proc 文件系統(tǒng)中,每個(gè)正在運(yùn)行的進(jìn)程都有一個(gè)以其進(jìn)程 ID(PID)命名的目錄 ,如 /proc/1234,其中的 status 文件就像是這個(gè)進(jìn)程的 “健康報(bào)告”,詳細(xì)記錄了進(jìn)程的各種狀態(tài)信息以及內(nèi)存使用的概況。
打開 /proc/[PID]/status 文件,我們可以看到一系列關(guān)鍵信息。其中,“VmPeak” 表示進(jìn)程虛擬內(nèi)存使用量的峰值 ,它記錄了進(jìn)程在運(yùn)行過程中曾經(jīng)達(dá)到的最高虛擬內(nèi)存占用情況,就像一個(gè)運(yùn)動(dòng)員在比賽中跳出的最高紀(jì)錄,反映了進(jìn)程在內(nèi)存需求上的最大值?!癡mSize” 代表當(dāng)前進(jìn)程虛擬內(nèi)存的實(shí)際使用量 ,這是進(jìn)程當(dāng)前占用的虛擬內(nèi)存大小,包括代碼段、數(shù)據(jù)段、堆、棧以及共享庫等所占用的虛擬內(nèi)存空間,是我們了解進(jìn)程當(dāng)前內(nèi)存 “胃口” 大小的重要指標(biāo)?!癡mLck” 表示被鎖定的內(nèi)存大小 ,當(dāng)進(jìn)程使用了 mlock () 函數(shù)等機(jī)制將部分內(nèi)存鎖定,使其不能被交換到磁盤時(shí),這部分內(nèi)存的大小就會(huì)記錄在這里,被鎖定的內(nèi)存就像是被特殊標(biāo)記的 “保險(xiǎn)箱”,始終保留在物理內(nèi)存中。
“VmHWM” 即 High Water Mark,是進(jìn)程實(shí)際使用物理內(nèi)存的峰值 ,它記錄了進(jìn)程在運(yùn)行過程中曾經(jīng)占用物理內(nèi)存的最大值,反映了進(jìn)程在物理內(nèi)存使用上的最高水平?!癡mRSS” 則是進(jìn)程當(dāng)前實(shí)際占用的物理內(nèi)存大小 ,這是我們最關(guān)注的指標(biāo)之一,它直接反映了進(jìn)程當(dāng)前實(shí)實(shí)在在占用的物理內(nèi)存資源,就像一個(gè)房間里實(shí)際擺放的物品所占據(jù)的空間大小。
“VmData” 表示進(jìn)程數(shù)據(jù)段的大小 ,包括已初始化的數(shù)據(jù)和未初始化的數(shù)據(jù),這部分內(nèi)存主要用于存儲(chǔ)進(jìn)程運(yùn)行時(shí)的數(shù)據(jù)變量等信息。“VmStk” 代表進(jìn)程用戶態(tài)棧的大小 ,棧是一種后進(jìn)先出的數(shù)據(jù)結(jié)構(gòu),用于函數(shù)調(diào)用、局部變量存儲(chǔ)等,VmStk 記錄了進(jìn)程棧所占用的內(nèi)存空間。“VmExe” 是進(jìn)程代碼段的大小 ,也就是進(jìn)程可執(zhí)行文件所占用的內(nèi)存空間,這里存放著進(jìn)程運(yùn)行的指令代碼?!癡mLib” 表示進(jìn)程使用的庫映射到虛擬內(nèi)存空間的大小 ,當(dāng)進(jìn)程依賴各種動(dòng)態(tài)鏈接庫時(shí),這些庫會(huì)被映射到進(jìn)程的虛擬內(nèi)存空間中,VmLib 記錄了這部分庫所占用的內(nèi)存大小。
通過分析這些內(nèi)存相關(guān)的字段,我們可以對(duì)進(jìn)程的內(nèi)存使用情況有一個(gè)全面的了解。例如,當(dāng)我們發(fā)現(xiàn) “VmRSS” 不斷增長,而 “VmSize” 也同步增加,且沒有明顯的釋放跡象時(shí),就可能存在內(nèi)存泄漏的隱患。這就好比一個(gè)人不斷地往房間里搬東西,卻從不清理,房間里的物品(內(nèi)存占用)就會(huì)越來越多。而如果 “VmPeak” 與當(dāng)前的 “VmSize” 相差很大,說明進(jìn)程在運(yùn)行過程中曾經(jīng)有過較大的內(nèi)存需求波動(dòng),這也可能是我們需要進(jìn)一步關(guān)注的點(diǎn),也許在某個(gè)特定的操作或時(shí)間段內(nèi),進(jìn)程的內(nèi)存使用出現(xiàn)了異常增長。
②/proc/[PID]/smaps:內(nèi)存映射區(qū)域的詳細(xì)剖析
/proc/[PID]/smaps 文件則像是一個(gè)放大鏡,為我們提供了比 /proc/[PID]/status 文件更加詳細(xì)的內(nèi)存映射區(qū)域信息 ,讓我們能夠深入了解進(jìn)程內(nèi)存使用的每一個(gè)細(xì)節(jié)。
smaps 文件中,每一個(gè)內(nèi)存映射區(qū)域都有詳細(xì)的記錄,包括該區(qū)域的起始地址和結(jié)束地址,以及一系列描述內(nèi)存使用情況的字段?!癝ize” 表示該虛擬內(nèi)存區(qū)域的大小 ,它明確了這個(gè)內(nèi)存塊在虛擬地址空間中的范圍,就像劃定了一塊土地的邊界。“Rss” 是實(shí)際分配給該段落物理頁框的數(shù)量(單位 KB) ,也就是該內(nèi)存區(qū)域當(dāng)前實(shí)際占用的物理內(nèi)存大小,它反映了進(jìn)程在物理內(nèi)存中實(shí)實(shí)在在占據(jù)的 “地盤”?!癙ss(Proportional Set Size)” 表示共享頁面按比例分?jǐn)偤蟮拇笮?,對(duì)于共享內(nèi)存區(qū)域,Pss 能夠更準(zhǔn)確地反映該進(jìn)程實(shí)際使用的內(nèi)存份額。
當(dāng)多個(gè)進(jìn)程共享同一個(gè)動(dòng)態(tài)鏈接庫時(shí),庫所占用的內(nèi)存會(huì)根據(jù)每個(gè)進(jìn)程的使用情況按比例分?jǐn)?,Pss 就是這個(gè)分?jǐn)偤蟮拇笮。苊饬嗽诠蚕韮?nèi)存情況下單純以 Rss 計(jì)算可能導(dǎo)致的內(nèi)存使用統(tǒng)計(jì)偏差。“Shared_Clean” 和 “Shared_Dirty” 分別表示和其它進(jìn)程共享的未修改的 page 的大小以及共享的被改寫的 page 的大小 ,這兩個(gè)字段幫助我們了解共享內(nèi)存中數(shù)據(jù)的狀態(tài)。如果 “Shared_Dirty” 不斷增加,可能意味著多個(gè)進(jìn)程對(duì)共享內(nèi)存的頻繁修改,這可能會(huì)帶來數(shù)據(jù)一致性等問題,也可能影響內(nèi)存的使用效率。
“Private_Clean” 和 “Private_Dirty” 則是未被改寫的私有頁面的大小和已被改寫的私有頁面的大小 ,用于描述進(jìn)程私有的內(nèi)存頁面的狀態(tài)。“Referenced” 表示已經(jīng)被引用過的頁面數(shù)目 ,它反映了內(nèi)存頁面的使用活躍度,被引用次數(shù)多的頁面說明是進(jìn)程運(yùn)行過程中經(jīng)常訪問的數(shù)據(jù)或代碼所在的頁面。“Anonymous” 是不關(guān)聯(lián)任何磁盤文件的匿名映射空間大小 ,例如通過 malloc () 函數(shù)分配的堆內(nèi)存,通常就是匿名映射的,這個(gè)字段可以幫助我們了解進(jìn)程中匿名內(nèi)存的使用情況?!癝wap” 表示當(dāng)前已交換出去至 swap 分區(qū)中的數(shù)據(jù)量 ,當(dāng)物理內(nèi)存不足時(shí),系統(tǒng)會(huì)將一些不常用的內(nèi)存頁面交換到磁盤的 swap 分區(qū)中,Swap 字段記錄了這部分被交換出去的數(shù)據(jù)量。如果 Swap 的值持續(xù)增加,說明系統(tǒng)內(nèi)存緊張,頻繁進(jìn)行內(nèi)存交換,這會(huì)嚴(yán)重影響系統(tǒng)性能,因?yàn)榇疟P的讀寫速度遠(yuǎn)遠(yuǎn)慢于內(nèi)存。
在實(shí)際排查內(nèi)存問題時(shí),我們可以通過監(jiān)控 smaps 文件中相關(guān)字段的變化來發(fā)現(xiàn)異常。比如,當(dāng)某個(gè)進(jìn)程的 “Rss” 持續(xù)上升,而 “Pss” 也同步增加,且 “Anonymous” 區(qū)域不斷擴(kuò)大,同時(shí) “Swap” 也開始出現(xiàn)增長趨勢(shì),這很可能是該進(jìn)程存在內(nèi)存泄漏或者內(nèi)存使用不合理的情況。我們可以進(jìn)一步分析具體是哪些內(nèi)存映射區(qū)域出現(xiàn)了異常,結(jié)合進(jìn)程的功能和代碼邏輯,定位到問題的根源。例如,如果發(fā)現(xiàn)某個(gè)動(dòng)態(tài)庫的內(nèi)存映射區(qū)域中 “Shared_Dirty” 異常高,可能是該庫在使用共享內(nèi)存時(shí)存在頻繁的寫入操作,導(dǎo)致內(nèi)存使用效率低下,甚至可能存在數(shù)據(jù)競(jìng)爭等問題,需要進(jìn)一步檢查庫的使用方式和相關(guān)代碼。
通過深入分析 /proc/[PID]/status 和 /proc/[PID]/smaps 文件,我們能夠獲取到進(jìn)程內(nèi)存使用的詳細(xì)信息,為排查 Linux 內(nèi)存泄漏與高占用問題提供有力的支持,就像一位經(jīng)驗(yàn)豐富的醫(yī)生通過詳細(xì)的檢查報(bào)告,準(zhǔn)確地診斷出病人身體的問題所在。
Part8.面試真題實(shí)戰(zhàn)演練
為了讓大家更清楚地了解在實(shí)際面試中如何運(yùn)用這些排查方法,我們來模擬一個(gè)大疆面試中可能出現(xiàn)的場(chǎng)景。假設(shè)你遇到了一個(gè)多線程程序,在運(yùn)行一段時(shí)間后,系統(tǒng)出現(xiàn)了內(nèi)存泄漏和高內(nèi)存占用的問題,導(dǎo)致系統(tǒng)性能嚴(yán)重下降。現(xiàn)在,面試官要求你找出問題所在并提出解決方案 。
(1)場(chǎng)景描述
這是一個(gè)基于 Linux 系統(tǒng)的多線程數(shù)據(jù)處理程序,用于實(shí)時(shí)處理傳感器采集的數(shù)據(jù)。程序中有多個(gè)線程,分別負(fù)責(zé)數(shù)據(jù)采集、數(shù)據(jù)處理和結(jié)果存儲(chǔ)。在程序運(yùn)行初期,一切正常,但隨著時(shí)間的推移,系統(tǒng)內(nèi)存占用不斷上升,最終導(dǎo)致系統(tǒng)響應(yīng)遲緩,甚至出現(xiàn)死機(jī)現(xiàn)象。
(2)排查步驟
- 初步觀察與進(jìn)程定位:首先,使用 top 或 htop 命令,查看系統(tǒng)中各個(gè)進(jìn)程的內(nèi)存占用情況。通過這一步,我們發(fā)現(xiàn)數(shù)據(jù)處理線程所在的進(jìn)程內(nèi)存占用增長異常明顯,確定了問題所在的進(jìn)程,為后續(xù)的排查工作明確了方向。這就像是在一片森林中找到了冒煙的那片區(qū)域,知道了問題的大致范圍。
- 深入分析進(jìn)程內(nèi)存使用:進(jìn)入 /proc/[PID]/status 文件,查看該進(jìn)程的內(nèi)存使用概況,發(fā)現(xiàn) VmRSS 和 VmSize 持續(xù)增長,這進(jìn)一步驗(yàn)證了內(nèi)存泄漏的可能性。接著,查看 /proc/[PID]/smaps 文件,詳細(xì)分析內(nèi)存映射區(qū)域,發(fā)現(xiàn)某個(gè)動(dòng)態(tài)分配的內(nèi)存區(qū)域 Rss 不斷增加,且沒有釋放的跡象,初步確定了內(nèi)存泄漏的位置就在這個(gè)動(dòng)態(tài)內(nèi)存分配的部分。這就像在鎖定的問題區(qū)域內(nèi),通過更細(xì)致的搜索,找到了具體的 “問題點(diǎn)”。
- 利用工具精確檢測(cè):為了更準(zhǔn)確地定位內(nèi)存泄漏的具體代碼行,使用 Valgrind 工具。在編譯程序時(shí),加上 “-g” 選項(xiàng)生成調(diào)試信息,然后執(zhí)行 “valgrind --tool=memcheck --leak-check=full --show-reachable=yes./your-program” 命令。Valgrind 運(yùn)行后,輸出了詳細(xì)的錯(cuò)誤報(bào)告,指出了在數(shù)據(jù)處理線程中的某個(gè)函數(shù)里,存在內(nèi)存分配后未釋放的問題,就像用高精度的探測(cè)器,精準(zhǔn)地找到了內(nèi)存泄漏的 “漏洞”。
- 分析多線程環(huán)境下的內(nèi)存問題:由于這是一個(gè)多線程程序,線程之間的同步和資源競(jìng)爭也可能導(dǎo)致內(nèi)存問題。檢查線程同步機(jī)制,發(fā)現(xiàn)線程之間在訪問共享內(nèi)存時(shí),沒有正確地使用互斥鎖,導(dǎo)致內(nèi)存訪問混亂,進(jìn)一步加劇了內(nèi)存問題。這就像是在一個(gè)多人合作的項(xiàng)目中,發(fā)現(xiàn)了人員之間的協(xié)作流程出現(xiàn)了混亂,影響了整體的工作效果。
- 解決問題與驗(yàn)證:根據(jù)排查結(jié)果,在數(shù)據(jù)處理線程的對(duì)應(yīng)函數(shù)中,添加正確的內(nèi)存釋放代碼,并確保線程同步機(jī)制的正確性,使用互斥鎖來保護(hù)共享內(nèi)存的訪問。修改代碼后,重新編譯并運(yùn)行程序,再次使用 top、htop 以及 Valgrind 等工具進(jìn)行檢查,發(fā)現(xiàn)內(nèi)存占用穩(wěn)定,沒有再出現(xiàn)泄漏的情況,系統(tǒng)性能也恢復(fù)正常,這表明問題已得到有效解決 。這就像是給出現(xiàn)故障的機(jī)器換上了新的零件,并進(jìn)行了全面的測(cè)試,確保機(jī)器能夠正常運(yùn)轉(zhuǎn)。
通過這個(gè)實(shí)戰(zhàn)演練,我們可以看到,在面對(duì) Linux 內(nèi)存泄漏與高占用問題時(shí),只要掌握了正確的排查方法和工具,按照合理的步驟進(jìn)行分析,就能夠準(zhǔn)確地找到問題的根源,并提出有效的解決方案,這也是在大疆嵌入式面試中,面試官希望看到的解決問題的能力和思路。