作者:馬宜萱
內(nèi)存檢測(cè)
一般的內(nèi)存訪問錯(cuò)誤如下
越界訪問(out-of-bounds)。
訪問已經(jīng)被釋放的內(nèi)存(use after free)。
重復(fù)釋放(double free)。
內(nèi)存泄漏(memory leak)。
棧溢出(stack overflow)。
跟蹤內(nèi)存活動(dòng)的各種事件源
事件類型 | 事件源 |
---|---|
用戶態(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) |
缺頁錯(cuò)誤 | kprobes、軟件事件、exception跟蹤點(diǎn) |
頁面遷移 | migration跟蹤點(diǎn) |
頁面壓縮 | compaction跟蹤點(diǎn) |
VM掃描器 | Vmscan跟蹤點(diǎn) |
內(nèi)存訪問周期 | PMC |
對(duì)使用libc內(nèi)存分配器的進(jìn)程來說,libc提供了?系列內(nèi)存分配的函數(shù),包括malloc()和 free()等。在libc庫(kù)中已經(jīng)內(nèi)置了一些USDT追蹤點(diǎn),可以在應(yīng)用程序中使用這些追蹤點(diǎn)來監(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來跟蹤oom_kill_process()函數(shù),來跟蹤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可以用來跟蹤內(nèi)存分配和釋放事件對(duì)應(yīng)的調(diào)用棧信息。隨著時(shí)間的推移,這個(gè)工具可以顯示長(zhǎng)期不被釋放的內(nèi)存。
在跟蹤用戶態(tài)進(jìn)程時(shí),memleak跟蹤的是用戶態(tài)內(nèi)存分配函數(shù):malloc()、calloc() 和 free() 等。對(duì)內(nèi)核態(tài)內(nèi)存來說,使用的是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)存泄漏:
寫一個(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)行該文件
再開一個(gè)終端,使用命令vmstat 3
上面的 "free", "buff", "cache" 欄目分別以 KB 為單位顯示了空閑內(nèi)存、存儲(chǔ) I/O 緩沖區(qū)占用的內(nèi)存,以及文件系統(tǒng)緩存占用的內(nèi)存數(shù)量。"si" 和 "so" 欄目分別展示了頁換入和頁換出操作的數(shù)量,如果系統(tǒng)中存在這些操作的話。
第一行輸出的是"自系統(tǒng)啟動(dòng)以來"的統(tǒng)計(jì)信息,這一行的大部分欄目是自從系統(tǒng)啟動(dòng)以來的平均值。然而,"memory"欄顯示的仍然是系統(tǒng)內(nèi)存的當(dāng)前狀態(tài)。而第二行和之后的行顯示的都是一秒之內(nèi)的統(tǒng)計(jì)信息。
可以看出free(可用內(nèi)存)上下浮動(dòng)慢慢減少,而buff(磁盤緩存),cache(文件緩存)上下浮動(dòng)基本保持不變。
再次使用命令運(yùn)行上面C程序
在打開第二個(gè)終端中使用命令:ps aux | grep app查看進(jìn)程id
使用命令: sudo /usr/sbin/memleak-bpfcc -p 6867 運(yùn)行
從圖中可以看出來泄露位置:
fibonacci+0x23 [leak]child+0x5a [leak]
可以看出代碼中的*v,沒有釋放,造成內(nèi)存泄漏。
改后代碼:
改進(jìn)后,重復(fù)上面的操作,結(jié)果如下
單靠memleak無法判斷這些內(nèi)存分配操作是真正的內(nèi)存泄漏(即,分配的內(nèi)存沒有任何引用,永遠(yuǎn)不會(huì)被釋放),還是只是內(nèi)存用量的正常增長(zhǎng),或者僅僅是真正的長(zhǎng)期內(nèi)存。為了區(qū)分這幾種類型,需要閱讀和理解這些代碼路徑的真正意圖。
如果沒有 -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)的名稱。
syscalls:sys_enter_mmap????????????????????????????[Tracepoint?event]
一個(gè)應(yīng)用程序,特別是在其啟動(dòng)和初始化期間,可以顯式地使用mmap() 系統(tǒng)調(diào)用來加載數(shù)據(jù)文件或創(chuàng)建各種段,在這個(gè)上下文中,我們聚焦于那些比較緩慢的應(yīng)用增長(zhǎng),這種情況可能是由于分配器函數(shù)調(diào)用了mmap()而不是brk()造成的。而libc通常用mmap()分配較大的內(nèi)存,可以使用munmap()將分配的內(nèi)存返還給系統(tǒng)。
brkstack
一般來說,應(yīng)用程序的數(shù)據(jù)存放于堆內(nèi)存中,堆內(nèi)存通過 brk 系統(tǒng)調(diào)用進(jìn)行擴(kuò)展。跟蹤 brk 調(diào)用,并且展示導(dǎo)致增長(zhǎng)的用戶態(tài)調(diào)用棧信息相對(duì)來說是很有用的分析信息。同時(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)來跟蹤,同時(shí)該跟蹤點(diǎn)對(duì)應(yīng)的調(diào)用棧信息,可以用 bpftrace 版本的單行程序等方式來獲取。
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。可以用來調(diào)試共享內(nèi)存的使用情況和信息。
這個(gè)輸出顯示了一個(gè)Renderer進(jìn)程通過 shmget 分配了共享內(nèi)存,然后顯示了該 Renderer 進(jìn)程執(zhí)行了幾種不同的共享內(nèi)存操作,以及對(duì)應(yīng)的參數(shù)信息。shmget 調(diào)用的返回結(jié)果是 0x28,這個(gè)標(biāo)識(shí)符接下來被 Renderer 和 Xorg 進(jìn)程同時(shí)使用;換句話說,它們?cè)诠蚕韮?nèi)存中。
共享內(nèi)存
共享內(nèi)存就是允許兩個(gè)不相關(guān)的進(jì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)程都可以訪問共享內(nèi)存中的地址,就好像它們是由用C語言函數(shù)malloc()分配的內(nèi)存一樣。而如果某個(gè)進(jìn)程向共享內(nèi)存寫入數(shù)據(jù),所做的改動(dòng)將立即影響到可以訪問同一段共享內(nèi)存的任何其他進(jìn)程。
共享內(nèi)存并未提供同步機(jī)制,也就是說,在第一個(gè)進(jìn)程結(jié)束對(duì)共享內(nèi)存的寫操作之前,并無自動(dòng)機(jī)制可以阻止第二個(gè)進(jìn)程開始對(duì)它進(jìn)行讀取。所以通常需要用其他的機(jī)制來同步對(duì)共享內(nèi)存的訪問,例如信號(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)程可以通過它,來訪問同一個(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)程的地址空間,隨后可像本地空間一樣訪問。
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)存可讀寫
返回值:
shmdt()函數(shù)
與shmat函數(shù)相反,是用來斷開與共享內(nèi)存附加點(diǎn)的地址,禁止本進(jì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)換為無符號(hào)長(zhǎng)整型 ?unsigned?long?addr?=?(unsigned?long)shmaddr; ?//?初始化返回值,默認(rèn)為無效參數(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)存映射寫鎖,可被信號(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?條件下,必須給出要銷毀的確切地址 ?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
跟蹤缺頁錯(cuò)誤和對(duì)應(yīng)的調(diào)用棧信息,可以為內(nèi)存使用量分析提供一個(gè)新的視角。缺頁錯(cuò)誤會(huì)直接導(dǎo)致 RSS 的增長(zhǎng),所以這里截取的調(diào)用棧信息可以用來解釋進(jìn)程內(nèi)存使用量的增長(zhǎng)。正如 brk() 一樣,可以通過單行程序來直接跟蹤這個(gè)事件并進(jìn)行分析。
跟蹤page_fault_user和page_fault_kernel來對(duì)用戶態(tài)和內(nèi)核態(tài)的缺頁錯(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)觀察頁面換出守護(hù)進(jìn)程(kswapd)的操作。這個(gè)進(jìn)程在系統(tǒng)內(nèi)存壓力上升時(shí)負(fù)責(zé)釋放內(nèi)存以便重用。值得注意的是,盡管內(nèi)核函數(shù)的名稱仍然使用scanner,但為了提高效率,內(nèi)核已經(jīng)采用鏈表方式來管理活躍內(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)回收過程,在此期間內(nèi)存被換入磁盤中,并且內(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寫入頁的數(shù)量。
drsnoop
drsnoop使用mm_vmscan_direct_reclaim_begin 和 mm_vmscan_direct_reclaim_end 跟蹤點(diǎn),來跟蹤內(nèi)存釋放過程中的直接回收部分。它能夠顯示受到影響的進(jìn)程以及對(duì)應(yīng)的延遲,即直接回收所需的時(shí)間。可以用來定量分析內(nèi)存受限的系統(tǒng)中對(duì)應(yīng)用程序的性能影響。
直接內(nèi)存回收
在直接內(nèi)存回收過程中,有可能會(huì)造成當(dāng)前需要分配內(nèi)存的進(jìn)程被加入一個(gè)等待隊(duì)列,當(dāng)整個(gè)node的空閑頁數(shù)量滿足要求時(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)核線程,也可能沒有喚醒,每個(gè)node的kswapd是否在__alloc_pages_slowpath()中被喚醒有兩個(gè)條件:
而在kswapd中會(huì)對(duì)node中每一個(gè)不平衡的zone進(jìn)行內(nèi)存回收,直到所有zone都滿足 zone分配頁框后剩余的頁框數(shù)量 > 此zone的high閥值 + 此zone保留的頁框數(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è)頁框?*/ ??.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)行回寫操作,即使允許回寫,直接內(nèi)存回收也不能對(duì)臟文件頁進(jìn)行回寫 ?????????*?不過允許回寫時(shí),可以對(duì)非文件頁進(jìn)行回寫 ?????????*/ ??.may_writepage?=?!laptop_mode, ????????/*?允許進(jìn)行unmap操作?*/ ??.may_unmap?=?1, ????????/*?允許進(jìn)行非文件頁的操作?*/ ??.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的總共空閑頁框數(shù)量?是否大于?此node的ZONE_DMA和ZONE_NORMAL的平均min閥值數(shù)量,大于則說明node平衡 ?????*?加入pgdat->pfmemalloc_wait的情況 ?????*?1.如果分配標(biāo)志禁止了文件系統(tǒng)操作,則將要進(jìn)行內(nèi)存回收的進(jìn)程設(shè)置為TASK_INTERRUPTIBLE狀態(tài),然后加入到node的pgdat->pfmemalloc_wait,并且會(huì)設(shè)置超時(shí)時(shí)間為1s? ?????*?2.如果分配標(biāo)志沒有禁止了文件系統(tǒng)操作,則將要進(jìn)行內(nèi)存回收的進(jìn)程加入到node的pgdat->pfmemalloc_wait,并設(shè)置為TASK_KILLABLE狀態(tài),表示允許?TASK_UNINTERRUPTIBLE?響應(yīng)致命信號(hào)的狀態(tài)? ?????*?返回真,表示此進(jìn)程加入過pgdat->pfmemalloc_wait等待隊(duì)列,并且已經(jīng)被喚醒 ?????*?返回假,表示此進(jìn)程沒有加入過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)程沒有加入到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; }
主要通過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沒有被喚醒,則喚醒,并且這里喚醒kswapd只會(huì)對(duì)ZONE_NORMAL以下的zone進(jìn)行內(nèi)存回收 ?????????*?node是否平衡的判斷標(biāo)準(zhǔn)是: ?????????*?此node的ZONE_DMA和ZONE_NORMAL的總共空閑頁框數(shù)量?是否大于?此node的ZONE_DMA和ZONE_NORMAL的平均min閥值數(shù)量,大于則說明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è)置為TASK_INTERRUPTIBLE狀態(tài),然后加入到node的pgdat->pfmemalloc_wait,并且會(huì)設(shè)置超時(shí)時(shí)間為1s? ?????????*?1.allow_direct_reclaim(pgdat)為真時(shí)被喚醒,而1s沒超時(shí),返回剩余timeout(jiffies) ?????????*?2.睡眠超過1s時(shí)會(huì)喚醒,而allow_direct_reclaim(pgdat)此時(shí)為真,返回1 ?????????*?3.睡眠超過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)志沒有禁止了文件系統(tǒng)操作,則將要進(jìn)行內(nèi)存回收的進(jìn)程加入到node的pgdat->pfmemalloc_wait,并設(shè)置為TASK_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)志中沒有__GFP_NO_KSWAPD,只有在透明大頁的分配過程中會(huì)有這個(gè)標(biāo)志。
node中有至少一個(gè)zone的空閑頁框沒有達(dá)到 空閑頁框數(shù)量 >= high閥值 + 1 << order + 保留內(nèi)存,或者有至少一個(gè)zone需要進(jìn)行內(nèi)存壓縮,這兩種情況node的kswapd都會(huì)被喚醒。
swapin
使用kprobe跟蹤swap_readpage()內(nèi)核函數(shù),這會(huì)在觸發(fā)換頁所在的進(jìn)程上下文中進(jìn)行,可以跟蹤觸發(fā)換頁操作的進(jìn)程的信息。展示了哪個(gè)進(jìn)程正在從換頁設(shè)備中換入頁,前提是系統(tǒng)中有正在使用的換頁設(shè)備。
換頁操作在應(yīng)用程序使用那些已經(jīng)被換出到換頁設(shè)備上的內(nèi)存時(shí)觸發(fā)。這是?個(gè)很重要的由于換頁導(dǎo)致的應(yīng)用性能影響指標(biāo)。其他的換頁相關(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)體。可以通過vm_area_struct結(jié)構(gòu)體來抓取文件名信息。
通過跟蹤巨頁相關(guān)的缺頁錯(cuò)誤信息,按進(jìn)程展示詳細(xì)信息,同時(shí)可以用來確保巨頁確實(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)愛好者,主要方向?yàn)閮?nèi)存方向。目前在學(xué)習(xí)操作系統(tǒng)底層原理和內(nèi)核編程。
審核編輯:黃飛
?
評(píng)論
查看更多