作者:馬宜萱
內(nèi)存檢測
一般的內(nèi)存訪問錯誤如下
越界訪問(out-of-bounds)。
訪問已經(jīng)被釋放的內(nèi)存(use after free)。
重復釋放(double free)。
內(nèi)存泄漏(memory leak)。
棧溢出(stack overflow)。
跟蹤內(nèi)存活動的各種事件源
| 事件類型 | 事件源 |
|---|---|
| 用戶態(tài)內(nèi)存分配 | 使用uprobes跟蹤內(nèi)存分配器函數(shù),使用USDT probes跟蹤libc |
| 內(nèi)核態(tài)內(nèi)存分配 | 使用kprobes跟蹤內(nèi)存分配器函數(shù),以及kmem跟蹤點 |
| 堆內(nèi)存擴展 | brk系統(tǒng)調(diào)用跟蹤點 |
| 共享內(nèi)存函數(shù) | 系統(tǒng)調(diào)用跟蹤點 |
| 缺頁錯誤 | kprobes、軟件事件、exception跟蹤點 |
| 頁面遷移 | migration跟蹤點 |
| 頁面壓縮 | compaction跟蹤點 |
| VM掃描器 | Vmscan跟蹤點 |
| 內(nèi)存訪問周期 | PMC |
對使用libc內(nèi)存分配器的進程來說,libc提供了?系列內(nèi)存分配的函數(shù),包括malloc()和 free()等。在libc庫中已經(jīng)內(nèi)置了一些USDT追蹤點,可以在應用程序中使用這些追蹤點來監(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獲取負載平均值,打印出平均負載等詳細信息。平均負載信息可以在OOM發(fā)生時提供整個系統(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)存分配和釋放事件對應的調(diào)用棧信息。隨著時間的推移,這個工具可以顯示長期不被釋放的內(nèi)存。
在跟蹤用戶態(tài)進程時,memleak跟蹤的是用戶態(tài)內(nèi)存分配函數(shù):malloc()、calloc() 和 free() 等。對內(nèi)核態(tài)內(nèi)存來說,使用的是k跟蹤點:
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)存泄漏:
寫一個c程序:
#include?#include? #include? #include? long?long?*fibonacci(long?long?*n0,?long?long?*n1)?{ ????//?分配1024個長整數(shù)空間方便觀測內(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; }
運行該文件

再開一個終端,使用命令vmstat 3

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

使用命令: sudo /usr/sbin/memleak-bpfcc -p 6867 運行

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

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

改進后,重復上面的操作,結(jié)果如下

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

mmapsnoop
使用syscall:sys_enter_mmap 跟蹤點跟蹤全系統(tǒng)mmap系統(tǒng)調(diào)用并打印映射請求詳細信息。
sys_enter_mmap是一個用于跟蹤mmap系統(tǒng)調(diào)用的跟蹤點的名稱。
syscalls:sys_enter_mmap????????????????????????????[Tracepoint?event]
一個應用程序,特別是在其啟動和初始化期間,可以顯式地使用mmap() 系統(tǒng)調(diào)用來加載數(shù)據(jù)文件或創(chuàng)建各種段,在這個上下文中,我們聚焦于那些比較緩慢的應用增長,這種情況可能是由于分配器函數(shù)調(diào)用了mmap()而不是brk()造成的。而libc通常用mmap()分配較大的內(nèi)存,可以使用munmap()將分配的內(nèi)存返還給系統(tǒng)。
brkstack
一般來說,應用程序的數(shù)據(jù)存放于堆內(nèi)存中,堆內(nèi)存通過 brk 系統(tǒng)調(diào)用進行擴展。跟蹤 brk 調(diào)用,并且展示導致增長的用戶態(tài)調(diào)用棧信息相對來說是很有用的分析信息。同時還有一個 sbrk 變體調(diào)用。在 Linux 中,sbrk 是以庫函數(shù)形式實現(xiàn)的,內(nèi)部仍然使用 brk 系統(tǒng)調(diào)用。
brk 可以用 syscall:sys_enter_brk 跟蹤點來跟蹤,同時該跟蹤點對應的調(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)存的使用情況和信息。

這個輸出顯示了一個Renderer進程通過 shmget 分配了共享內(nèi)存,然后顯示了該 Renderer 進程執(zhí)行了幾種不同的共享內(nèi)存操作,以及對應的參數(shù)信息。shmget 調(diào)用的返回結(jié)果是 0x28,這個標識符接下來被 Renderer 和 Xorg 進程同時使用;換句話說,它們在共享內(nèi)存中。
共享內(nèi)存
共享內(nèi)存就是允許兩個不相關(guān)的進程訪問同一個邏輯內(nèi)存。共享內(nèi)存是在兩個正在運行的進程之間共享和傳遞數(shù)據(jù)的一種非常有效的方式。
不同進程之間共享的內(nèi)存通常安排為同一段物理內(nèi)存。進程可以將同一段共享內(nèi)存連接到它們自己的地址空間中,所有進程都可以訪問共享內(nèi)存中的地址,就好像它們是由用C語言函數(shù)malloc()分配的內(nèi)存一樣。而如果某個進程向共享內(nèi)存寫入數(shù)據(jù),所做的改動將立即影響到可以訪問同一段共享內(nèi)存的任何其他進程。
共享內(nèi)存并未提供同步機制,也就是說,在第一個進程結(jié)束對共享內(nèi)存的寫操作之前,并無自動機制可以阻止第二個進程開始對它進行讀取。所以通常需要用其他的機制來同步對共享內(nèi)存的訪問,例如信號量。
shmget()函數(shù)
得到一個共享內(nèi)存標識符或創(chuàng)建一個共享內(nèi)存對象。
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)存段標識符??出錯:-1
函數(shù)參數(shù):
Key:共享內(nèi)存的鍵值,多個進程可以通過它,來訪問同一個共享內(nèi)存;其中特殊的值IPC_PRIVATE,用于創(chuàng)建當前進程的私有共享內(nèi)存, 多用于父子進程間。
size:共享內(nèi)存區(qū)大小 。
Shmflg:同 open 函數(shù)的權(quán)限位,也可以用八進制表示法
返回值:
shmat( )函數(shù)
連接共享內(nèi)存標識符為shmid的共享內(nèi)存,連接成功后把共享內(nèi)存區(qū)對象映射到調(diào)用進程的地址空間,隨后可像本地空間一樣訪問。
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;
}
成功:被映射的段地址??出錯:-1
函數(shù)原型
shmid:要映射的共享內(nèi)存區(qū)標識符
shmaddr:將共享內(nèi)存映射到指定位置
Shmflg:SHM_RDONLY:共享內(nèi)存只讀,默認0:共享內(nèi)存可讀寫
返回值:
shmdt()函數(shù)
與shmat函數(shù)相反,是用來斷開與共享內(nèi)存附加點的地址,禁止本進程訪問此片共享內(nèi)存。
本函數(shù)調(diào)用并不刪除所指定的共享內(nèi)存區(qū),而只是將先前用shmat函數(shù)連接(attach)好的共享內(nèi)存脫離(detach)目前的進程。
asmlinkage?long?sys_shmdt(char?__user?*shmaddr);
SYSCALL_DEFINE1(shmdt,?char?__user?*,?shmaddr)
{
?return?ksys_shmdt(shmaddr);
}
long?ksys_shmdt(char?__user?*shmaddr)
{
?//?獲取當前進程的內(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)換為無符號長整型
?unsigned?long?addr?=?(unsigned?long)shmaddr;
?//?初始化返回值,默認為無效參數(shù)錯誤
?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)存映射寫鎖,可被信號中斷
?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ù)
完成對共享內(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)存標識符
cmd:IPC_STAT:得到共享內(nèi)存的狀態(tài),把共享內(nèi)存的shmid_ds結(jié)構(gòu)復制到buf中;IPC_SET:改變共享內(nèi)存的狀態(tài),把buf所指的shmid_ds結(jié)構(gòu)中的uid、gid、mode復制到共享內(nèi)存的shmid_ds結(jié)構(gòu)內(nèi);IPC_RMID:刪除這片共享內(nèi)存
buf:共享內(nèi)存管理結(jié)構(gòu)體。
faults
跟蹤缺頁錯誤和對應的調(diào)用棧信息,可以為內(nèi)存使用量分析提供一個新的視角。缺頁錯誤會直接導致 RSS 的增長,所以這里截取的調(diào)用棧信息可以用來解釋進程內(nèi)存使用量的增長。正如 brk() 一樣,可以通過單行程序來直接跟蹤這個事件并進行分析。
跟蹤page_fault_user和page_fault_kernel來對用戶態(tài)和內(nèi)核態(tài)的缺頁錯誤對應的頻率統(tǒng)計信息進行分析。
exceptions:page_fault_user?????????????????????????[Tracepoint?event] exceptions:page_fault_kernel???????????????????????[Tracepoint?event]
vmscan
使用vmscan跟蹤點觀察頁面換出守護進程(kswapd)的操作。這個進程在系統(tǒng)內(nèi)存壓力上升時負責釋放內(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
使用這兩個跟蹤點計算收縮slab所花的全部時間,以毫秒為單位。這是從各種內(nèi)核緩存中回收內(nèi)存。
vmscan:mm_vmscan_direct_reclaim_begin,vmscan:mm_vmscan_direct_reclaim_end
使用這兩個跟蹤點計算直接接回收所花的時間,以毫秒為單位。這是前臺回收過程,在此期間內(nèi)存被換入磁盤中,并且內(nèi)存分配處于阻塞狀態(tài)。
vmscan:mm_vmscan_memcg_reclaim_begin,vmscan:mm_vmscan_memcg_reclaim_end
內(nèi)存cgroup回收所花的時間,以毫秒為單位。如果使用了內(nèi)存cgroups,此列顯示當cgroup超出內(nèi)存限制,導致該cgroup進行內(nèi)存回收的時間。
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 跟蹤點,來跟蹤內(nèi)存釋放過程中的直接回收部分。它能夠顯示受到影響的進程以及對應的延遲,即直接回收所需的時間??梢杂脕矶糠治鰞?nèi)存受限的系統(tǒng)中對應用程序的性能影響。
直接內(nèi)存回收
在直接內(nèi)存回收過程中,有可能會造成當前需要分配內(nèi)存的進程被加入一個等待隊列,當整個node的空閑頁數(shù)量滿足要求時,由kswapd喚醒它重新獲取內(nèi)存。這個等待隊列頭就是node結(jié)點描述符pgdat中的pfmemalloc_wait。如果當前進程加入到了pgdat->pfmemalloc_wait這個等待隊列中,那么進程就不會進行直接內(nèi)存回收,而是由kswapd喚醒后直接進行內(nèi)存分配。
直接內(nèi)存回收執(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)核線程,也可能沒有喚醒,每個node的kswapd是否在__alloc_pages_slowpath()中被喚醒有兩個條件:
而在kswapd中會對node中每一個不平衡的zone進行內(nèi)存回收,直到所有zone都滿足 zone分配頁框后剩余的頁框數(shù)量 > 此zone的high閥值 + 此zone保留的頁框數(shù)量。kswapd就會停止內(nèi)存回收,然后喚醒在等待隊列的進程。
之后進程由于內(nèi)存不足,對zonelist進行直接回收時,會調(diào)用到try_to_free_pages(),在這個函數(shù)內(nèi),決定了進程是否加入到node結(jié)點的pgdat->pfmemalloc_wait這個等待隊列中,如下:
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個頁框?*/
??.nr_to_reclaim?=?SWAP_CLUSTER_MAX,
??.gfp_mask?=?current_gfp_context(gfp_mask),
??.reclaim_idx?=?gfp_zone(gfp_mask),
????????/*?本次內(nèi)存分配的order值?*/
??.order?=?order,
????????/*?允許進行回收的node掩碼?*/
??.nodemask?=?nodemask,
????????/*?優(yōu)先級為默認的12?*/
??.priority?=?DEF_PRIORITY,
????????/*?與/proc/sys/vm/laptop_mode文件有關(guān)
?????????*?laptop_mode為0,則允許進行回寫操作,即使允許回寫,直接內(nèi)存回收也不能對臟文件頁進行回寫
?????????*?不過允許回寫時,可以對非文件頁進行回寫
?????????*/
??.may_writepage?=?!laptop_mode,
????????/*?允許進行unmap操作?*/
??.may_unmap?=?1,
????????/*?允許進行非文件頁的操作?*/
??.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);
????/*?當zonelist中獲取到的第一個node平衡,則返回,如果獲取到的第一個node不平衡,則將當前進程加入到pgdat->pfmemalloc_wait這個等待隊列中?
?????*?這個等待隊列會在kswapd進行內(nèi)存回收時,如果讓node平衡了,則會喚醒這個等待隊列中的進程
?????*?判斷node平衡的標準:
?????*?此node的ZONE_DMA和ZONE_NORMAL的總共空閑頁框數(shù)量?是否大于?此node的ZONE_DMA和ZONE_NORMAL的平均min閥值數(shù)量,大于則說明node平衡
?????*?加入pgdat->pfmemalloc_wait的情況
?????*?1.如果分配標志禁止了文件系統(tǒng)操作,則將要進行內(nèi)存回收的進程設(shè)置為TASK_INTERRUPTIBLE狀態(tài),然后加入到node的pgdat->pfmemalloc_wait,并且會設(shè)置超時時間為1s?
?????*?2.如果分配標志沒有禁止了文件系統(tǒng)操作,則將要進行內(nèi)存回收的進程加入到node的pgdat->pfmemalloc_wait,并設(shè)置為TASK_KILLABLE狀態(tài),表示允許?TASK_UNINTERRUPTIBLE?響應致命信號的狀態(tài)?
?????*?返回真,表示此進程加入過pgdat->pfmemalloc_wait等待隊列,并且已經(jīng)被喚醒
?????*?返回假,表示此進程沒有加入過pgdat->pfmemalloc_wait等待隊列
?????*/
?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);
????/*?進行內(nèi)存回收,有三種情況到這里?
?????*?1.當前進程為內(nèi)核線程
?????*?2.最優(yōu)node是平衡的,當前進程沒有加入到pgdat->pfmemalloc_wait中
?????*?3.當前進程接收到了kill信號
?????*/
?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等待隊列中,主要看此函數(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;
/*?如果標記了PF_KTHREAD,表示此進程是一個內(nèi)核線程,則不會往下執(zhí)行?*/
?if?(current->flags?&?PF_KTHREAD)
??goto?out;
?/*?此進程已經(jīng)接收到了kill信號,準備要被殺掉了?*/
?if?(fatal_signal_pending(current))
??goto?out;
?/*?遍歷zonelist,但是里面只會在獲取到第一個pgdat時就跳出?*/
?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對應的node?*/
??pgdat?=?zone->zone_pgdat;
????????/*?判斷node是否平衡,如果平衡,則返回真
?????????*?如果不平衡,如果此node的kswapd沒有被喚醒,則喚醒,并且這里喚醒kswapd只會對ZONE_NORMAL以下的zone進行內(nèi)存回收
?????????*?node是否平衡的判斷標準是:
?????????*?此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))
????????/*?如果分配標志禁止了文件系統(tǒng)操作,則將要進行內(nèi)存回收的進程設(shè)置為TASK_INTERRUPTIBLE狀態(tài),然后加入到node的pgdat->pfmemalloc_wait,并且會設(shè)置超時時間為1s?
?????????*?1.allow_direct_reclaim(pgdat)為真時被喚醒,而1s沒超時,返回剩余timeout(jiffies)
?????????*?2.睡眠超過1s時會喚醒,而allow_direct_reclaim(pgdat)此時為真,返回1
?????????*?3.睡眠超過1s時會喚醒,而allow_direct_reclaim(pgdat)此時為假,返回0
?????????*?4.接收到信號被喚醒,返回-ERESTARTSYS
?????????*/
??wait_event_interruptible_timeout(pgdat->pfmemalloc_wait,
???allow_direct_reclaim(pgdat),?HZ);
?else
??/*?如果分配標志沒有禁止了文件系統(tǒng)操作,則將要進行內(nèi)存回收的進程加入到node的pgdat->pfmemalloc_wait,并設(shè)置為TASK_KILLABLE狀態(tài),表示允許?TASK_UNINTERRUPTIBLE?響應致命信號的狀態(tài)?
?????*?這些進程在兩種情況下被喚醒
?????*?1.allow_direct_reclaim(pgdat)為真時
?????*?2.接收到致命信號時
?????*/
??wait_event_killable(zone->zone_pgdat->pfmemalloc_wait,
???allow_direct_reclaim(pgdat));
????/*?如果加入到了pgdat->pfmemalloc_wait后被喚醒,就會執(zhí)行到這?*/
????
????/*?喚醒后再次檢查當前進程是否接受到了kill信號,準備退出?*/
?if?(fatal_signal_pending(current))
??return?true;
out:
?return?false;
}
分配標志中沒有__GFP_NO_KSWAPD,只有在透明大頁的分配過程中會有這個標志。
node中有至少一個zone的空閑頁框沒有達到 空閑頁框數(shù)量 >= high閥值 + 1 << order + 保留內(nèi)存,或者有至少一個zone需要進行內(nèi)存壓縮,這兩種情況node的kswapd都會被喚醒。
swapin
使用kprobe跟蹤swap_readpage()內(nèi)核函數(shù),這會在觸發(fā)換頁所在的進程上下文中進行,可以跟蹤觸發(fā)換頁操作的進程的信息。展示了哪個進程正在從換頁設(shè)備中換入頁,前提是系統(tǒng)中有正在使用的換頁設(shè)備。
換頁操作在應用程序使用那些已經(jīng)被換出到換頁設(shè)備上的內(nèi)存時觸發(fā)。這是?個很重要的由于換頁導致的應用性能影響指標。其他的換頁相關(guān)指標,例如掃描和換出操作, 并不直接影響應用程序的性能。
extern?int?swap_readpage(struct?page?*page,?bool?do_poll);
hfaults
使用kprobe跟蹤hugetlb_fault()函數(shù),可以從該函數(shù)的參數(shù)中抓取很多的詳細信息,包括mm_struct結(jié)構(gòu)體和vm_area_struct結(jié)構(gòu)體??梢酝ㄟ^vm_area_struct結(jié)構(gòu)體來抓取文件名信息。
通過跟蹤巨頁相關(guān)的缺頁錯誤信息,按進程展示詳細信息,同時可以用來確保巨頁確實被啟用了。
vm_fault_t?hugetlb_fault(struct?mm_struct?*mm,?struct?vm_area_struct?*vma, ???unsigned?long?address,?unsigned?int?flags);
作者簡介:馬宜萱,西安郵電大學研一在讀,操作系統(tǒng)愛好者,主要方向為內(nèi)存方向。目前在學習操作系統(tǒng)底層原理和內(nèi)核編程。
審核編輯:黃飛
?
電子發(fā)燒友App


















評論