作者:馬宜萱
內(nèi)存檢測(cè)
一般的內(nèi)存訪問(wèn)錯(cuò)誤如下
越界訪問(wèn)(out-of-bounds)。
訪問(wèn)已經(jīng)被釋放的內(nèi)存(use after free)。
重復(fù)釋放(double free)。
內(nèi)存泄漏(memory leak)。
棧溢出(stack overflow)。
跟蹤內(nèi)存活動(dòng)的各種事件源
| 事件類(lèi)型 | 事件源 |
|---|---|
| 用戶(hù)態(tài)內(nèi)存分配 | 使用uprobes跟蹤內(nèi)存分配器函數(shù),使用USDT probes跟蹤libc |
| 內(nèi)核態(tài)內(nèi)存分配 | 使用kprobes跟蹤內(nèi)存分配器函數(shù),以及kmem跟蹤點(diǎn) |
| 堆內(nèi)存擴(kuò)展 | brk系統(tǒng)調(diào)用跟蹤點(diǎn) |
| 共享內(nèi)存函數(shù) | 系統(tǒng)調(diào)用跟蹤點(diǎn) |
| 缺頁(yè)錯(cuò)誤 | kprobes、軟件事件、exception跟蹤點(diǎn) |
| 頁(yè)面遷移 | migration跟蹤點(diǎn) |
| 頁(yè)面壓縮 | compaction跟蹤點(diǎn) |
| VM掃描器 | Vmscan跟蹤點(diǎn) |
| 內(nèi)存訪問(wèn)周期 | PMC |
對(duì)使用libc內(nèi)存分配器的進(jìn)程來(lái)說(shuō),libc提供了?系列內(nèi)存分配的函數(shù),包括malloc()和 free()等。在libc庫(kù)中已經(jīng)內(nèi)置了一些USDT追蹤點(diǎn),可以在應(yīng)用程序中使用這些追蹤點(diǎn)來(lái)監(jiān)視libc的行為。
以下是libc中可用的USDT探針:
#?sudo?bpftrace?-l?usdt:/lib/x86_64-linux-gnu/libc-2.31.so? usdt:/lib/x86_64-linux-gnu/libc-2.31.sosetjmp usdt:/lib/x86_64-linux-gnu/libc-2.31.solongjmp usdt:/lib/x86_64-linux-gnu/libc-2.31.solongjmp_target usdt:/lib/x86_64-linux-gnu/libc-2.31.solll_lock_wait_private usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_mallopt_arena_max usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_mallopt_arena_test usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_tunable_tcache_max_bytes usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_tunable_tcache_count usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_tunable_tcache_unsorted_limit usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_mallopt_trim_threshold usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_mallopt_top_pad usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_mallopt_mmap_threshold usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_mallopt_mmap_max usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_mallopt_perturb usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_mallopt_mxfast usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_heap_new usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_arena_reuse_free_list usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_arena_reuse usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_arena_reuse_wait usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_arena_new usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_arena_retry usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_sbrk_less usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_heap_free usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_heap_less usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_tcache_double_free usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_heap_more usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_sbrk_more usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_malloc_retry usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_memalign_retry usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_mallopt_free_dyn_thresholds usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_realloc_retry usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_calloc_retry usdt:/lib/x86_64-linux-gnu/libc-2.31.somemory_mallopt
oomkill
使用kprobes來(lái)跟蹤oom_kill_process()函數(shù),來(lái)跟蹤OOM Killer事件的信息,以及可以從/proc/loadavg獲取負(fù)載平均值,打印出平均負(fù)載等詳細(xì)信息。平均負(fù)載信息可以在OOM發(fā)生時(shí)提供整個(gè)系統(tǒng)狀態(tài)的一些上下文信息,展示出系統(tǒng)整體是正在變忙還是處于穩(wěn)定狀態(tài)。
static?void?oom_kill_process(struct?oom_control?*oc,?const?char?*message)
#?cat?/proc/loadavg? 0.05?0.10?0.13?1/875?23359
memleak
memleak可以用來(lái)跟蹤內(nèi)存分配和釋放事件對(duì)應(yīng)的調(diào)用棧信息。隨著時(shí)間的推移,這個(gè)工具可以顯示長(zhǎng)期不被釋放的內(nèi)存。
在跟蹤用戶(hù)態(tài)進(jìn)程時(shí),memleak跟蹤的是用戶(hù)態(tài)內(nèi)存分配函數(shù):malloc()、calloc() 和 free() 等。對(duì)內(nèi)核態(tài)內(nèi)存來(lái)說(shuō),使用的是k跟蹤點(diǎn):
kmem:kfree?????????????????????????????????????????[Tracepoint?event] kmem:kmalloc???????????????????????????????????????[Tracepoint?event] kmem:kmalloc_node??????????????????????????????????[Tracepoint?event] kmem:kmem_cache_alloc??????????????????????????????[Tracepoint?event] kmem:kmem_cache_alloc_node?????????????????????????[Tracepoint?event] kmem:kmem_cache_free???????????????????????????????[Tracepoint?event] kmem:mm_page_alloc?????????????????????????????????[Tracepoint?event] kmem:mm_page_free??????????????????????????????????[Tracepoint?event] percpu:percpu_alloc_percpu?????????????????????????[Tracepoint?event] percpu:percpu_free_percpu??????????????????????????[Tracepoint?event]
使用工具模擬內(nèi)存泄漏:
寫(xiě)一個(gè)c程序:
#include?#include? #include? #include? long?long?*fibonacci(long?long?*n0,?long?long?*n1)?{ ????//?分配1024個(gè)長(zhǎng)整數(shù)空間方便觀測(cè)內(nèi)存的變化情況 ????long?long?*v?=?(long?long?*)?calloc(1024,?sizeof(long?long)); ????*v?=?*n0?+?*n1; ????return?v; } void?*child(void?*arg)?{ ????long?long?n0?=?0; ????long?long?n1?=?1; ????long?long?*v?=?NULL; ????int?n?=?2; ????for?(n?=?2;?n?>?0;?n++)?{ ????????v?=?fibonacci(&n0,?&n1); ????????n0?=?n1; ????????n1?=?*v; ????????printf("%dth?=>?%lld ",?n,?*v); ????????sleep(1); ????} } int?main(void)?{ ????pthread_t?tid; ????pthread_create(&tid,?NULL,?child,?NULL); ????pthread_join(tid,?NULL); ????printf("main?thread?exit "); ????return?0; }
運(yùn)行該文件

再開(kāi)一個(gè)終端,使用命令vmstat 3

上面的 "free", "buff", "cache" 欄目分別以 KB 為單位顯示了空閑內(nèi)存、存儲(chǔ) I/O 緩沖區(qū)占用的內(nèi)存,以及文件系統(tǒng)緩存占用的內(nèi)存數(shù)量。"si" 和 "so" 欄目分別展示了頁(yè)換入和頁(yè)換出操作的數(shù)量,如果系統(tǒng)中存在這些操作的話(huà)。
第一行輸出的是"自系統(tǒng)啟動(dòng)以來(lái)"的統(tǒng)計(jì)信息,這一行的大部分欄目是自從系統(tǒng)啟動(dòng)以來(lái)的平均值。然而,"memory"欄顯示的仍然是系統(tǒng)內(nèi)存的當(dāng)前狀態(tài)。而第二行和之后的行顯示的都是一秒之內(nèi)的統(tǒng)計(jì)信息。
可以看出free(可用內(nèi)存)上下浮動(dòng)慢慢減少,而buff(磁盤(pán)緩存),cache(文件緩存)上下浮動(dòng)基本保持不變。
再次使用命令運(yùn)行上面C程序
在打開(kāi)第二個(gè)終端中使用命令:ps aux | grep app查看進(jìn)程id

使用命令: sudo /usr/sbin/memleak-bpfcc -p 6867 運(yùn)行

從圖中可以看出來(lái)泄露位置:
fibonacci+0x23 [leak]child+0x5a [leak]

可以看出代碼中的*v,沒(méi)有釋放,造成內(nèi)存泄漏。
改后代碼:

改進(jìn)后,重復(fù)上面的操作,結(jié)果如下

單靠memleak無(wú)法判斷這些內(nèi)存分配操作是真正的內(nèi)存泄漏(即,分配的內(nèi)存沒(méi)有任何引用,永遠(yuǎn)不會(huì)被釋放),還是只是內(nèi)存用量的正常增長(zhǎng),或者僅僅是真正的長(zhǎng)期內(nèi)存。為了區(qū)分這幾種類(lèi)型,需要閱讀和理解這些代碼路徑的真正意圖。
如果沒(méi)有 -p PID 命令行參數(shù),那么memleak跟蹤的是內(nèi)核中的內(nèi)存分配信息:

mmapsnoop
使用syscall:sys_enter_mmap 跟蹤點(diǎn)跟蹤全系統(tǒng)mmap系統(tǒng)調(diào)用并打印映射請(qǐng)求詳細(xì)信息。
sys_enter_mmap是一個(gè)用于跟蹤mmap系統(tǒng)調(diào)用的跟蹤點(diǎn)的名稱(chēng)。
syscalls:sys_enter_mmap????????????????????????????[Tracepoint?event]
一個(gè)應(yīng)用程序,特別是在其啟動(dòng)和初始化期間,可以顯式地使用mmap() 系統(tǒng)調(diào)用來(lái)加載數(shù)據(jù)文件或創(chuàng)建各種段,在這個(gè)上下文中,我們聚焦于那些比較緩慢的應(yīng)用增長(zhǎng),這種情況可能是由于分配器函數(shù)調(diào)用了mmap()而不是brk()造成的。而libc通常用mmap()分配較大的內(nèi)存,可以使用munmap()將分配的內(nèi)存返還給系統(tǒng)。
brkstack
一般來(lái)說(shuō),應(yīng)用程序的數(shù)據(jù)存放于堆內(nèi)存中,堆內(nèi)存通過(guò) brk 系統(tǒng)調(diào)用進(jìn)行擴(kuò)展。跟蹤 brk 調(diào)用,并且展示導(dǎo)致增長(zhǎng)的用戶(hù)態(tài)調(diào)用棧信息相對(duì)來(lái)說(shuō)是很有用的分析信息。同時(shí)還有一個(gè) sbrk 變體調(diào)用。在 Linux 中,sbrk 是以庫(kù)函數(shù)形式實(shí)現(xiàn)的,內(nèi)部仍然使用 brk 系統(tǒng)調(diào)用。
brk 可以用 syscall:sys_enter_brk 跟蹤點(diǎn)來(lái)跟蹤,同時(shí)該跟蹤點(diǎn)對(duì)應(yīng)的調(diào)用棧信息,可以用 bpftrace 版本的單行程序等方式來(lái)獲取。
sudo?bpftrace?-e?'tracepointsys_enter_brk?{?printf("%s
",?comm);?}'
上面命令可以跟蹤brk系統(tǒng)調(diào)用。

shmsnoop
shmsnoop跟蹤 System V 的共享內(nèi)存系統(tǒng)調(diào)用:shmget、shmat、shmdt以及shmctl??梢杂脕?lái)調(diào)試共享內(nèi)存的使用情況和信息。

這個(gè)輸出顯示了一個(gè)Renderer進(jìn)程通過(guò) shmget 分配了共享內(nèi)存,然后顯示了該 Renderer 進(jìn)程執(zhí)行了幾種不同的共享內(nèi)存操作,以及對(duì)應(yīng)的參數(shù)信息。shmget 調(diào)用的返回結(jié)果是 0x28,這個(gè)標(biāo)識(shí)符接下來(lái)被 Renderer 和 Xorg 進(jìn)程同時(shí)使用;換句話(huà)說(shuō),它們?cè)诠蚕韮?nèi)存中。
共享內(nèi)存
共享內(nèi)存就是允許兩個(gè)不相關(guān)的進(jìn)程訪問(wèn)同一個(gè)邏輯內(nèi)存。共享內(nèi)存是在兩個(gè)正在運(yùn)行的進(jìn)程之間共享和傳遞數(shù)據(jù)的一種非常有效的方式。
不同進(jìn)程之間共享的內(nèi)存通常安排為同一段物理內(nèi)存。進(jìn)程可以將同一段共享內(nèi)存連接到它們自己的地址空間中,所有進(jìn)程都可以訪問(wèn)共享內(nèi)存中的地址,就好像它們是由用C語(yǔ)言函數(shù)malloc()分配的內(nèi)存一樣。而如果某個(gè)進(jìn)程向共享內(nèi)存寫(xiě)入數(shù)據(jù),所做的改動(dòng)將立即影響到可以訪問(wèn)同一段共享內(nèi)存的任何其他進(jìn)程。
共享內(nèi)存并未提供同步機(jī)制,也就是說(shuō),在第一個(gè)進(jìn)程結(jié)束對(duì)共享內(nèi)存的寫(xiě)操作之前,并無(wú)自動(dòng)機(jī)制可以阻止第二個(gè)進(jìn)程開(kāi)始對(duì)它進(jìn)行讀取。所以通常需要用其他的機(jī)制來(lái)同步對(duì)共享內(nèi)存的訪問(wèn),例如信號(hào)量。
shmget()函數(shù)
得到一個(gè)共享內(nèi)存標(biāo)識(shí)符或創(chuàng)建一個(gè)共享內(nèi)存對(duì)象。
asmlinkage?long?sys_shmget(key_t?key,?size_t?size,?int?flag);
SYSCALL_DEFINE3(shmget,?key_t,?key,?size_t,?size,?int,?shmflg)
{
?return?ksys_shmget(key,?size,?shmflg);
}
long?ksys_shmget(key_t?key,?size_t?size,?int?shmflg)
{
?struct?ipc_namespace?*ns;
?static?const?struct?ipc_ops?shm_ops?=?{
??.getnew?=?newseg,
??.associate?=?security_shm_associate,
??.more_checks?=?shm_more_checks,
?};
?struct?ipc_params?shm_params;
?ns?=?current->nsproxy->ipc_ns;
?shm_params.key?=?key;
?shm_params.flg?=?shmflg;
?shm_params.u.size?=?size;
?return?ipcget(ns,?&shm_ids(ns),?&shm_ops,?&shm_params);
}
成功:共享內(nèi)存段標(biāo)識(shí)符??出錯(cuò):-1
函數(shù)參數(shù):
Key:共享內(nèi)存的鍵值,多個(gè)進(jìn)程可以通過(guò)它,來(lái)訪問(wèn)同一個(gè)共享內(nèi)存;其中特殊的值IPC_PRIVATE,用于創(chuàng)建當(dāng)前進(jìn)程的私有共享內(nèi)存, 多用于父子進(jìn)程間。
size:共享內(nèi)存區(qū)大小 。
Shmflg:同 open 函數(shù)的權(quán)限位,也可以用八進(jìn)制表示法
返回值:
shmat( )函數(shù)
連接共享內(nèi)存標(biāo)識(shí)符為shmid的共享內(nèi)存,連接成功后把共享內(nèi)存區(qū)對(duì)象映射到調(diào)用進(jìn)程的地址空間,隨后可像本地空間一樣訪問(wèn)。
asmlinkage?long?sys_shmat(int?shmid,?char?__user?*shmaddr,?int?shmflg);
SYSCALL_DEFINE3(shmat,?int,?shmid,?char?__user?*,?shmaddr,?int,?shmflg)
{
?unsigned?long?ret;
?long?err;
?err?=?do_shmat(shmid,?shmaddr,?shmflg,?&ret,?SHMLBA);
?if?(err)
??return?err;
?force_successful_syscall_return();
?return?(long)ret;
}
成功:被映射的段地址??出錯(cuò):-1
函數(shù)原型
shmid:要映射的共享內(nèi)存區(qū)標(biāo)識(shí)符
shmaddr:將共享內(nèi)存映射到指定位置
Shmflg:SHM_RDONLY:共享內(nèi)存只讀,默認(rèn)0:共享內(nèi)存可讀寫(xiě)
返回值:
shmdt()函數(shù)
與shmat函數(shù)相反,是用來(lái)斷開(kāi)與共享內(nèi)存附加點(diǎn)的地址,禁止本進(jìn)程訪問(wèn)此片共享內(nèi)存。
本函數(shù)調(diào)用并不刪除所指定的共享內(nèi)存區(qū),而只是將先前用shmat函數(shù)連接(attach)好的共享內(nèi)存脫離(detach)目前的進(jìn)程。
asmlinkage?long?sys_shmdt(char?__user?*shmaddr);
SYSCALL_DEFINE1(shmdt,?char?__user?*,?shmaddr)
{
?return?ksys_shmdt(shmaddr);
}
long?ksys_shmdt(char?__user?*shmaddr)
{
?//?獲取當(dāng)前進(jìn)程的內(nèi)存管理結(jié)構(gòu)
?struct?mm_struct?*mm?=?current->mm;
?//?定義虛擬內(nèi)存區(qū)域結(jié)構(gòu)體指針
?struct?vm_area_struct?*vma;
?//?將共享內(nèi)存地址轉(zhuǎn)換為無(wú)符號(hào)長(zhǎng)整型
?unsigned?long?addr?=?(unsigned?long)shmaddr;
?//?初始化返回值,默認(rèn)為無(wú)效參數(shù)錯(cuò)誤
?int?retval?=?-EINVAL;
#ifdef?CONFIG_MMU
?//?定義大小變量和文件指針
?loff_t?size?=?0;
?struct?file?*file;
?struct?vm_area_struct?*next;
#endif
?//?檢查共享內(nèi)存地址是否有效
?if?(addr?&?~PAGE_MASK)
??return?retval;
?//?嘗試獲取內(nèi)存映射寫(xiě)鎖,可被信號(hào)中斷
?if?(mmap_write_lock_killable(mm))
??return?-EINTR;
?//?查找給定地址的虛擬內(nèi)存區(qū)域
?vma?=?find_vma(mm,?addr);
#ifdef?CONFIG_MMU
?while?(vma)?{
??next?=?vma->vm_next;
??//?檢查地址是否匹配,并且?vma?與?shm?相關(guān)
??if?((vma->vm_ops?==?&shm_vm_ops)?&&
???(vma->vm_start?-?addr)/PAGE_SIZE?==?vma->vm_pgoff)?{
???//?記錄?shm?段的文件和大小
???file?=?vma->vm_file;
???size?=?i_size_read(file_inode(vma->vm_file));
???//?取消映射?shm?段
???do_munmap(mm,?vma->vm_start,?vma->vm_end?-?vma->vm_start,?NULL);
???//?設(shè)置返回值為成功
???retval?=?0;
???vma?=?next;
???break;
??}
??vma?=?next;
?}
?//?遍歷所有可能的?vma
?size?=?PAGE_ALIGN(size);
?while?(vma?&&?(loff_t)(vma->vm_end?-?addr)?<=?size)?{
??next?=?vma->vm_next;
??//?檢查地址是否匹配,并且?vma?與?shm?相關(guān)
??if?((vma->vm_ops?==?&shm_vm_ops)?&&
??????((vma->vm_start?-?addr)/PAGE_SIZE?==?vma->vm_pgoff)?&&
??????(vma->vm_file?==?file))
???//?取消映射?shm?段
???do_munmap(mm,?vma->vm_start,?vma->vm_end?-?vma->vm_start,?NULL);
??vma?=?next;
?}
#else?/*?CONFIG_MMU?*/
?//?在?NOMMU?條件下,必須給出要銷(xiāo)毀的確切地址
?if?(vma?&&?vma->vm_start?==?addr?&&?vma->vm_ops?==?&shm_vm_ops)?{
??//?取消映射?shm?段
??do_munmap(mm,?vma->vm_start,?vma->vm_end?-?vma->vm_start,?NULL);
??//?設(shè)置返回值為成功
??retval?=?0;
?}
#endif
?//?解鎖內(nèi)存映射
?mmap_write_unlock(mm);
?return?retval;
}
函數(shù)原型
shmaddr:連接的共享內(nèi)存的起始地址
shmctl函數(shù)
完成對(duì)共享內(nèi)存的控制
asmlinkage?long?sys_shmctl(int?shmid,?int?cmd,?struct?shmid_ds?__user?*buf);
SYSCALL_DEFINE3(shmctl,?int,?shmid,?int,?cmd,?struct?shmid_ds?__user?*,?buf)
{
?return?ksys_shmctl(shmid,?cmd,?buf,?IPC_64);
}
static?long?ksys_shmctl(int?shmid,?int?cmd,?struct?shmid_ds?__user?*buf,?int?version)
{
?int?err;
?struct?ipc_namespace?*ns;
?struct?shmid64_ds?sem64;
?if?(cmd?0?||?shmid?0)
??return?-EINVAL;
?ns?=?current->nsproxy->ipc_ns;
?switch?(cmd)?{
?case?IPC_INFO:?{
??struct?shminfo64?shminfo;
??err?=?shmctl_ipc_info(ns,?&shminfo);
??if?(err?0)
???return?err;
??if?(copy_shminfo_to_user(buf,?&shminfo,?version))
???err?=?-EFAULT;
??return?err;
?}
?case?SHM_INFO:?{
??struct?shm_info?shm_info;
??err?=?shmctl_shm_info(ns,?&shm_info);
??if?(err?0)
???return?err;
??if?(copy_to_user(buf,?&shm_info,?sizeof(shm_info)))
???err?=?-EFAULT;
??return?err;
?}
?case?SHM_STAT:
?case?SHM_STAT_ANY:
?case?IPC_STAT:?{
??err?=?shmctl_stat(ns,?shmid,?cmd,?&sem64);
??if?(err?0)
???return?err;
??if?(copy_shmid_to_user(buf,?&sem64,?version))
???err?=?-EFAULT;
??return?err;
?}
?case?IPC_SET:
??if?(copy_shmid_from_user(&sem64,?buf,?version))
???return?-EFAULT;
??fallthrough;
?case?IPC_RMID:
??return?shmctl_down(ns,?shmid,?cmd,?&sem64);
?case?SHM_LOCK:
?case?SHM_UNLOCK:
??return?shmctl_do_lock(ns,?shmid,?cmd);
?default:
?return?-EINVAL;
?}
}
函數(shù)原型
shmid:共享內(nèi)存標(biāo)識(shí)符
cmd:IPC_STAT:得到共享內(nèi)存的狀態(tài),把共享內(nèi)存的shmid_ds結(jié)構(gòu)復(fù)制到buf中;IPC_SET:改變共享內(nèi)存的狀態(tài),把buf所指的shmid_ds結(jié)構(gòu)中的uid、gid、mode復(fù)制到共享內(nèi)存的shmid_ds結(jié)構(gòu)內(nèi);IPC_RMID:刪除這片共享內(nèi)存
buf:共享內(nèi)存管理結(jié)構(gòu)體。
faults
跟蹤缺頁(yè)錯(cuò)誤和對(duì)應(yīng)的調(diào)用棧信息,可以為內(nèi)存使用量分析提供一個(gè)新的視角。缺頁(yè)錯(cuò)誤會(huì)直接導(dǎo)致 RSS 的增長(zhǎng),所以這里截取的調(diào)用棧信息可以用來(lái)解釋進(jìn)程內(nèi)存使用量的增長(zhǎng)。正如 brk() 一樣,可以通過(guò)單行程序來(lái)直接跟蹤這個(gè)事件并進(jìn)行分析。
跟蹤page_fault_user和page_fault_kernel來(lái)對(duì)用戶(hù)態(tài)和內(nèi)核態(tài)的缺頁(yè)錯(cuò)誤對(duì)應(yīng)的頻率統(tǒng)計(jì)信息進(jìn)行分析。
exceptions:page_fault_user?????????????????????????[Tracepoint?event] exceptions:page_fault_kernel???????????????????????[Tracepoint?event]
vmscan
使用vmscan跟蹤點(diǎn)觀察頁(yè)面換出守護(hù)進(jìn)程(kswapd)的操作。這個(gè)進(jìn)程在系統(tǒng)內(nèi)存壓力上升時(shí)負(fù)責(zé)釋放內(nèi)存以便重用。值得注意的是,盡管內(nèi)核函數(shù)的名稱(chēng)仍然使用scanner,但為了提高效率,內(nèi)核已經(jīng)采用鏈表方式來(lái)管理活躍內(nèi)存和不活躍內(nèi)存。
vmscan:mm_shrink_slab_end??????????????????????????[Tracepoint?event] vmscan:mm_shrink_slab_start????????????????????????[Tracepoint?event] vmscan:mm_vmscan_direct_reclaim_begin??????????????[Tracepoint?event] vmscan:mm_vmscan_direct_reclaim_end????????????????[Tracepoint?event] vmscan:mm_vmscan_memcg_reclaim_begin???????????????[Tracepoint?event] vmscan:mm_vmscan_memcg_reclaim_end?????????????????[Tracepoint?event] vmscan:mm_vmscan_wakeup_kswapd?????????????????????[Tracepoint?event] vmscan:mm_vmscan_writepage?????????????????????????[Tracepoint?event]
vmscan:mm_shrink_slab_end,vmscan:mm_shrink_slab_start
使用這兩個(gè)跟蹤點(diǎn)計(jì)算收縮slab所花的全部時(shí)間,以毫秒為單位。這是從各種內(nèi)核緩存中回收內(nèi)存。
vmscan:mm_vmscan_direct_reclaim_begin,vmscan:mm_vmscan_direct_reclaim_end
使用這兩個(gè)跟蹤點(diǎn)計(jì)算直接接回收所花的時(shí)間,以毫秒為單位。這是前臺(tái)回收過(guò)程,在此期間內(nèi)存被換入磁盤(pán)中,并且內(nèi)存分配處于阻塞狀態(tài)。
vmscan:mm_vmscan_memcg_reclaim_begin,vmscan:mm_vmscan_memcg_reclaim_end
內(nèi)存cgroup回收所花的時(shí)間,以毫秒為單位。如果使用了內(nèi)存cgroups,此列顯示當(dāng)cgroup超出內(nèi)存限制,導(dǎo)致該cgroup進(jìn)行內(nèi)存回收的時(shí)間。
vmscan:mm_vmscan_wakeup_kswapd
kswapd 喚醒的次數(shù)。
vmscan:mm_vmscan_writepage
kswapd寫(xiě)入頁(yè)的數(shù)量。
drsnoop
drsnoop使用mm_vmscan_direct_reclaim_begin 和 mm_vmscan_direct_reclaim_end 跟蹤點(diǎn),來(lái)跟蹤內(nèi)存釋放過(guò)程中的直接回收部分。它能夠顯示受到影響的進(jìn)程以及對(duì)應(yīng)的延遲,即直接回收所需的時(shí)間??梢杂脕?lái)定量分析內(nèi)存受限的系統(tǒng)中對(duì)應(yīng)用程序的性能影響。
直接內(nèi)存回收
在直接內(nèi)存回收過(guò)程中,有可能會(huì)造成當(dāng)前需要分配內(nèi)存的進(jìn)程被加入一個(gè)等待隊(duì)列,當(dāng)整個(gè)node的空閑頁(yè)數(shù)量滿(mǎn)足要求時(shí),由kswapd喚醒它重新獲取內(nèi)存。這個(gè)等待隊(duì)列頭就是node結(jié)點(diǎn)描述符pgdat中的pfmemalloc_wait。如果當(dāng)前進(jìn)程加入到了pgdat->pfmemalloc_wait這個(gè)等待隊(duì)列中,那么進(jìn)程就不會(huì)進(jìn)行直接內(nèi)存回收,而是由kswapd喚醒后直接進(jìn)行內(nèi)存分配。
直接內(nèi)存回收?qǐng)?zhí)行路徑是:
__alloc_pages_slowpath() -> __alloc_pages_direct_reclaim() -> __perform_reclaim() ->try_to_free_pages() -> do_try_to_free_pages() -> shrink_zones() -> shrink_zone()
在__alloc_pages_slowpath()中可能喚醒了所有node的kswapd內(nèi)核線程,也可能沒(méi)有喚醒,每個(gè)node的kswapd是否在__alloc_pages_slowpath()中被喚醒有兩個(gè)條件:
而在kswapd中會(huì)對(duì)node中每一個(gè)不平衡的zone進(jìn)行內(nèi)存回收,直到所有zone都滿(mǎn)足 zone分配頁(yè)框后剩余的頁(yè)框數(shù)量 > 此zone的high閥值 + 此zone保留的頁(yè)框數(shù)量。kswapd就會(huì)停止內(nèi)存回收,然后喚醒在等待隊(duì)列的進(jìn)程。
之后進(jìn)程由于內(nèi)存不足,對(duì)zonelist進(jìn)行直接回收時(shí),會(huì)調(diào)用到try_to_free_pages(),在這個(gè)函數(shù)內(nèi),決定了進(jìn)程是否加入到node結(jié)點(diǎn)的pgdat->pfmemalloc_wait這個(gè)等待隊(duì)列中,如下:
unsigned?long?try_to_free_pages(struct?zonelist?*zonelist,?int?order,
????gfp_t?gfp_mask,?nodemask_t?*nodemask)
{
?unsigned?long?nr_reclaimed;
?struct?scan_control?sc?=?{
????????/*?打算回收32個(gè)頁(yè)框?*/
??.nr_to_reclaim?=?SWAP_CLUSTER_MAX,
??.gfp_mask?=?current_gfp_context(gfp_mask),
??.reclaim_idx?=?gfp_zone(gfp_mask),
????????/*?本次內(nèi)存分配的order值?*/
??.order?=?order,
????????/*?允許進(jìn)行回收的node掩碼?*/
??.nodemask?=?nodemask,
????????/*?優(yōu)先級(jí)為默認(rèn)的12?*/
??.priority?=?DEF_PRIORITY,
????????/*?與/proc/sys/vm/laptop_mode文件有關(guān)
?????????*?laptop_mode為0,則允許進(jìn)行回寫(xiě)操作,即使允許回寫(xiě),直接內(nèi)存回收也不能對(duì)臟文件頁(yè)進(jìn)行回寫(xiě)
?????????*?不過(guò)允許回寫(xiě)時(shí),可以對(duì)非文件頁(yè)進(jìn)行回寫(xiě)
?????????*/
??.may_writepage?=?!laptop_mode,
????????/*?允許進(jìn)行unmap操作?*/
??.may_unmap?=?1,
????????/*?允許進(jìn)行非文件頁(yè)的操作?*/
??.may_swap?=?1,
?};
?BUILD_BUG_ON(MAX_ORDER?>?S8_MAX);
?BUILD_BUG_ON(DEF_PRIORITY?>?S8_MAX);
?BUILD_BUG_ON(MAX_NR_ZONES?>?S8_MAX);
????/*?當(dāng)zonelist中獲取到的第一個(gè)node平衡,則返回,如果獲取到的第一個(gè)node不平衡,則將當(dāng)前進(jìn)程加入到pgdat->pfmemalloc_wait這個(gè)等待隊(duì)列中?
?????*?這個(gè)等待隊(duì)列會(huì)在kswapd進(jìn)行內(nèi)存回收時(shí),如果讓node平衡了,則會(huì)喚醒這個(gè)等待隊(duì)列中的進(jìn)程
?????*?判斷node平衡的標(biāo)準(zhǔn):
?????*?此node的ZONE_DMA和ZONE_NORMAL的總共空閑頁(yè)框數(shù)量?是否大于?此node的ZONE_DMA和ZONE_NORMAL的平均min閥值數(shù)量,大于則說(shuō)明node平衡
?????*?加入pgdat->pfmemalloc_wait的情況
?????*?1.如果分配標(biāo)志禁止了文件系統(tǒng)操作,則將要進(jìn)行內(nèi)存回收的進(jìn)程設(shè)置為T(mén)ASK_INTERRUPTIBLE狀態(tài),然后加入到node的pgdat->pfmemalloc_wait,并且會(huì)設(shè)置超時(shí)時(shí)間為1s?
?????*?2.如果分配標(biāo)志沒(méi)有禁止了文件系統(tǒng)操作,則將要進(jìn)行內(nèi)存回收的進(jìn)程加入到node的pgdat->pfmemalloc_wait,并設(shè)置為T(mén)ASK_KILLABLE狀態(tài),表示允許?TASK_UNINTERRUPTIBLE?響應(yīng)致命信號(hào)的狀態(tài)?
?????*?返回真,表示此進(jìn)程加入過(guò)pgdat->pfmemalloc_wait等待隊(duì)列,并且已經(jīng)被喚醒
?????*?返回假,表示此進(jìn)程沒(méi)有加入過(guò)pgdat->pfmemalloc_wait等待隊(duì)列
?????*/
?if?(throttle_direct_reclaim(sc.gfp_mask,?zonelist,?nodemask))
??return?1;
?set_task_reclaim_state(current,?&sc.reclaim_state);
?trace_mm_vmscan_direct_reclaim_begin(order,?sc.gfp_mask);
????/*?進(jìn)行內(nèi)存回收,有三種情況到這里?
?????*?1.當(dāng)前進(jìn)程為內(nèi)核線程
?????*?2.最優(yōu)node是平衡的,當(dāng)前進(jìn)程沒(méi)有加入到pgdat->pfmemalloc_wait中
?????*?3.當(dāng)前進(jìn)程接收到了kill信號(hào)
?????*/
?nr_reclaimed?=?do_try_to_free_pages(zonelist,?&sc);
?trace_mm_vmscan_direct_reclaim_end(nr_reclaimed);
?set_task_reclaim_state(current,?NULL);
?return?nr_reclaimed;
}
主要通過(guò)throttle_direct_reclaim()函數(shù)判斷是否加入到pgdat->pfmemalloc_wait等待隊(duì)列中,主要看此函數(shù):
static?bool?throttle_direct_reclaim(gfp_t?gfp_mask,?struct?zonelist?*zonelist,
?????nodemask_t?*nodemask)
{
?struct?zoneref?*z;
?struct?zone?*zone;
pg_data_t?*pgdat?=?NULL;
/*?如果標(biāo)記了PF_KTHREAD,表示此進(jìn)程是一個(gè)內(nèi)核線程,則不會(huì)往下執(zhí)行?*/
?if?(current->flags?&?PF_KTHREAD)
??goto?out;
?/*?此進(jìn)程已經(jīng)接收到了kill信號(hào),準(zhǔn)備要被殺掉了?*/
?if?(fatal_signal_pending(current))
??goto?out;
?/*?遍歷zonelist,但是里面只會(huì)在獲取到第一個(gè)pgdat時(shí)就跳出?*/
?for_each_zone_zonelist_nodemask(zone,?z,?zonelist,
?????gfp_zone(gfp_mask),?nodemask)?{
??/*?只遍歷ZONE_NORMAL和ZONE_DMA區(qū)?*/
????????if?(zone_idx(zone)?>?ZONE_NORMAL)
???continue;
??/*?獲取zone對(duì)應(yīng)的node?*/
??pgdat?=?zone->zone_pgdat;
????????/*?判斷node是否平衡,如果平衡,則返回真
?????????*?如果不平衡,如果此node的kswapd沒(méi)有被喚醒,則喚醒,并且這里喚醒kswapd只會(huì)對(duì)ZONE_NORMAL以下的zone進(jìn)行內(nèi)存回收
?????????*?node是否平衡的判斷標(biāo)準(zhǔn)是:
?????????*?此node的ZONE_DMA和ZONE_NORMAL的總共空閑頁(yè)框數(shù)量?是否大于?此node的ZONE_DMA和ZONE_NORMAL的平均min閥值數(shù)量,大于則說(shuō)明node平衡
?????????*/
??if?(allow_direct_reclaim(pgdat))
???goto?out;
??break;
?}
?
?if?(!pgdat)
??goto?out;
?count_vm_event(PGSCAN_DIRECT_THROTTLE);
?
?if?(!(gfp_mask?&?__GFP_FS))
????????/*?如果分配標(biāo)志禁止了文件系統(tǒng)操作,則將要進(jìn)行內(nèi)存回收的進(jìn)程設(shè)置為T(mén)ASK_INTERRUPTIBLE狀態(tài),然后加入到node的pgdat->pfmemalloc_wait,并且會(huì)設(shè)置超時(shí)時(shí)間為1s?
?????????*?1.allow_direct_reclaim(pgdat)為真時(shí)被喚醒,而1s沒(méi)超時(shí),返回剩余timeout(jiffies)
?????????*?2.睡眠超過(guò)1s時(shí)會(huì)喚醒,而allow_direct_reclaim(pgdat)此時(shí)為真,返回1
?????????*?3.睡眠超過(guò)1s時(shí)會(huì)喚醒,而allow_direct_reclaim(pgdat)此時(shí)為假,返回0
?????????*?4.接收到信號(hào)被喚醒,返回-ERESTARTSYS
?????????*/
??wait_event_interruptible_timeout(pgdat->pfmemalloc_wait,
???allow_direct_reclaim(pgdat),?HZ);
?else
??/*?如果分配標(biāo)志沒(méi)有禁止了文件系統(tǒng)操作,則將要進(jìn)行內(nèi)存回收的進(jìn)程加入到node的pgdat->pfmemalloc_wait,并設(shè)置為T(mén)ASK_KILLABLE狀態(tài),表示允許?TASK_UNINTERRUPTIBLE?響應(yīng)致命信號(hào)的狀態(tài)?
?????*?這些進(jìn)程在兩種情況下被喚醒
?????*?1.allow_direct_reclaim(pgdat)為真時(shí)
?????*?2.接收到致命信號(hào)時(shí)
?????*/
??wait_event_killable(zone->zone_pgdat->pfmemalloc_wait,
???allow_direct_reclaim(pgdat));
????/*?如果加入到了pgdat->pfmemalloc_wait后被喚醒,就會(huì)執(zhí)行到這?*/
????
????/*?喚醒后再次檢查當(dāng)前進(jìn)程是否接受到了kill信號(hào),準(zhǔn)備退出?*/
?if?(fatal_signal_pending(current))
??return?true;
out:
?return?false;
}
分配標(biāo)志中沒(méi)有__GFP_NO_KSWAPD,只有在透明大頁(yè)的分配過(guò)程中會(huì)有這個(gè)標(biāo)志。
node中有至少一個(gè)zone的空閑頁(yè)框沒(méi)有達(dá)到 空閑頁(yè)框數(shù)量 >= high閥值 + 1 << order + 保留內(nèi)存,或者有至少一個(gè)zone需要進(jìn)行內(nèi)存壓縮,這兩種情況node的kswapd都會(huì)被喚醒。
swapin
使用kprobe跟蹤swap_readpage()內(nèi)核函數(shù),這會(huì)在觸發(fā)換頁(yè)所在的進(jìn)程上下文中進(jìn)行,可以跟蹤觸發(fā)換頁(yè)操作的進(jìn)程的信息。展示了哪個(gè)進(jìn)程正在從換頁(yè)設(shè)備中換入頁(yè),前提是系統(tǒng)中有正在使用的換頁(yè)設(shè)備。
換頁(yè)操作在應(yīng)用程序使用那些已經(jīng)被換出到換頁(yè)設(shè)備上的內(nèi)存時(shí)觸發(fā)。這是?個(gè)很重要的由于換頁(yè)導(dǎo)致的應(yīng)用性能影響指標(biāo)。其他的換頁(yè)相關(guān)指標(biāo),例如掃描和換出操作, 并不直接影響應(yīng)用程序的性能。
extern?int?swap_readpage(struct?page?*page,?bool?do_poll);
hfaults
使用kprobe跟蹤hugetlb_fault()函數(shù),可以從該函數(shù)的參數(shù)中抓取很多的詳細(xì)信息,包括mm_struct結(jié)構(gòu)體和vm_area_struct結(jié)構(gòu)體。可以通過(guò)vm_area_struct結(jié)構(gòu)體來(lái)抓取文件名信息。
通過(guò)跟蹤巨頁(yè)相關(guān)的缺頁(yè)錯(cuò)誤信息,按進(jìn)程展示詳細(xì)信息,同時(shí)可以用來(lái)確保巨頁(yè)確實(shí)被啟用了。
vm_fault_t?hugetlb_fault(struct?mm_struct?*mm,?struct?vm_area_struct?*vma, ???unsigned?long?address,?unsigned?int?flags);
作者簡(jiǎn)介:馬宜萱,西安郵電大學(xué)研一在讀,操作系統(tǒng)愛(ài)好者,主要方向?yàn)閮?nèi)存方向。目前在學(xué)習(xí)操作系統(tǒng)底層原理和內(nèi)核編程。
審核編輯:黃飛
?
電子發(fā)燒友App


















評(píng)論