作者:馬宜萱
內存檢測
一般的內存訪問錯誤如下
越界訪問(out-of-bounds)。
訪問已經被釋放的內存(use after free)。
重復釋放(double free)。
內存泄漏(memory leak)。
棧溢出(stack overflow)。
跟蹤內存活動的各種事件源
事件類型 | 事件源 |
---|---|
用戶態內存分配 | 使用uprobes跟蹤內存分配器函數,使用USDT probes跟蹤libc |
內核態內存分配 | 使用kprobes跟蹤內存分配器函數,以及kmem跟蹤點 |
堆內存擴展 | brk系統調用跟蹤點 |
共享內存函數 | 系統調用跟蹤點 |
缺頁錯誤 | kprobes、軟件事件、exception跟蹤點 |
頁面遷移 | migration跟蹤點 |
頁面壓縮 | compaction跟蹤點 |
VM掃描器 | Vmscan跟蹤點 |
內存訪問周期 | PMC |
對使用libc內存分配器的進程來說,libc提供了?系列內存分配的函數,包括malloc()和 free()等。在libc庫中已經內置了一些USDT追蹤點,可以在應用程序中使用這些追蹤點來監視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()函數,來跟蹤OOM Killer事件的信息,以及可以從/proc/loadavg獲取負載平均值,打印出平均負載等詳細信息。平均負載信息可以在OOM發生時提供整個系統狀態的一些上下文信息,展示出系統整體是正在變忙還是處于穩定狀態。
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可以用來跟蹤內存分配和釋放事件對應的調用棧信息。隨著時間的推移,這個工具可以顯示長期不被釋放的內存。
在跟蹤用戶態進程時,memleak跟蹤的是用戶態內存分配函數:malloc()、calloc() 和 free() 等。對內核態內存來說,使用的是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]
使用工具模擬內存泄漏:
寫一個c程序:
#include?#include? #include? #include? long?long?*fibonacci(long?long?*n0,?long?long?*n1)?{ ????//?分配1024個長整數空間方便觀測內存的變化情況 ????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 為單位顯示了空閑內存、存儲 I/O 緩沖區占用的內存,以及文件系統緩存占用的內存數量。"si" 和 "so" 欄目分別展示了頁換入和頁換出操作的數量,如果系統中存在這些操作的話。
第一行輸出的是"自系統啟動以來"的統計信息,這一行的大部分欄目是自從系統啟動以來的平均值。然而,"memory"欄顯示的仍然是系統內存的當前狀態。而第二行和之后的行顯示的都是一秒之內的統計信息。
可以看出free(可用內存)上下浮動慢慢減少,而buff(磁盤緩存),cache(文件緩存)上下浮動基本保持不變。
再次使用命令運行上面C程序
在打開第二個終端中使用命令:ps aux | grep app查看進程id
使用命令: sudo /usr/sbin/memleak-bpfcc -p 6867 運行
從圖中可以看出來泄露位置:
fibonacci+0x23 [leak]child+0x5a [leak]
可以看出代碼中的*v,沒有釋放,造成內存泄漏。
改后代碼:
改進后,重復上面的操作,結果如下
單靠memleak無法判斷這些內存分配操作是真正的內存泄漏(即,分配的內存沒有任何引用,永遠不會被釋放),還是只是內存用量的正常增長,或者僅僅是真正的長期內存。為了區分這幾種類型,需要閱讀和理解這些代碼路徑的真正意圖。
如果沒有 -p PID 命令行參數,那么memleak跟蹤的是內核中的內存分配信息:
mmapsnoop
使用syscall:sys_enter_mmap 跟蹤點跟蹤全系統mmap系統調用并打印映射請求詳細信息。
sys_enter_mmap是一個用于跟蹤mmap系統調用的跟蹤點的名稱。
syscalls:sys_enter_mmap????????????????????????????[Tracepoint?event]
一個應用程序,特別是在其啟動和初始化期間,可以顯式地使用mmap() 系統調用來加載數據文件或創建各種段,在這個上下文中,我們聚焦于那些比較緩慢的應用增長,這種情況可能是由于分配器函數調用了mmap()而不是brk()造成的。而libc通常用mmap()分配較大的內存,可以使用munmap()將分配的內存返還給系統。
brkstack
一般來說,應用程序的數據存放于堆內存中,堆內存通過 brk 系統調用進行擴展。跟蹤 brk 調用,并且展示導致增長的用戶態調用棧信息相對來說是很有用的分析信息。同時還有一個 sbrk 變體調用。在 Linux 中,sbrk 是以庫函數形式實現的,內部仍然使用 brk 系統調用。
brk 可以用 syscall:sys_enter_brk 跟蹤點來跟蹤,同時該跟蹤點對應的調用棧信息,可以用 bpftrace 版本的單行程序等方式來獲取。
sudo?bpftrace?-e?'tracepointsys_enter_brk?{?printf("%s ",?comm);?}'
上面命令可以跟蹤brk系統調用。
shmsnoop
shmsnoop跟蹤 System V 的共享內存系統調用:shmget、shmat、shmdt以及shmctl。可以用來調試共享內存的使用情況和信息。
這個輸出顯示了一個Renderer進程通過 shmget 分配了共享內存,然后顯示了該 Renderer 進程執行了幾種不同的共享內存操作,以及對應的參數信息。shmget 調用的返回結果是 0x28,這個標識符接下來被 Renderer 和 Xorg 進程同時使用;換句話說,它們在共享內存中。
共享內存
共享內存就是允許兩個不相關的進程訪問同一個邏輯內存。共享內存是在兩個正在運行的進程之間共享和傳遞數據的一種非常有效的方式。
不同進程之間共享的內存通常安排為同一段物理內存。進程可以將同一段共享內存連接到它們自己的地址空間中,所有進程都可以訪問共享內存中的地址,就好像它們是由用C語言函數malloc()分配的內存一樣。而如果某個進程向共享內存寫入數據,所做的改動將立即影響到可以訪問同一段共享內存的任何其他進程。
共享內存并未提供同步機制,也就是說,在第一個進程結束對共享內存的寫操作之前,并無自動機制可以阻止第二個進程開始對它進行讀取。所以通常需要用其他的機制來同步對共享內存的訪問,例如信號量。
shmget()函數
得到一個共享內存標識符或創建一個共享內存對象。
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); }
成功:共享內存段標識符??出錯:-1
函數參數:
Key:共享內存的鍵值,多個進程可以通過它,來訪問同一個共享內存;其中特殊的值IPC_PRIVATE,用于創建當前進程的私有共享內存, 多用于父子進程間。
size:共享內存區大小 。
Shmflg:同 open 函數的權限位,也可以用八進制表示法
返回值:
shmat( )函數
連接共享內存標識符為shmid的共享內存,連接成功后把共享內存區對象映射到調用進程的地址空間,隨后可像本地空間一樣訪問。
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
函數原型
shmid:要映射的共享內存區標識符
shmaddr:將共享內存映射到指定位置
Shmflg:SHM_RDONLY:共享內存只讀,默認0:共享內存可讀寫
返回值:
shmdt()函數
與shmat函數相反,是用來斷開與共享內存附加點的地址,禁止本進程訪問此片共享內存。
本函數調用并不刪除所指定的共享內存區,而只是將先前用shmat函數連接(attach)好的共享內存脫離(detach)目前的進程。
asmlinkage?long?sys_shmdt(char?__user?*shmaddr);
SYSCALL_DEFINE1(shmdt,?char?__user?*,?shmaddr) { ?return?ksys_shmdt(shmaddr); }
long?ksys_shmdt(char?__user?*shmaddr) { ?//?獲取當前進程的內存管理結構 ?struct?mm_struct?*mm?=?current->mm; ?//?定義虛擬內存區域結構體指針 ?struct?vm_area_struct?*vma; ?//?將共享內存地址轉換為無符號長整型 ?unsigned?long?addr?=?(unsigned?long)shmaddr; ?//?初始化返回值,默認為無效參數錯誤 ?int?retval?=?-EINVAL; #ifdef?CONFIG_MMU ?//?定義大小變量和文件指針 ?loff_t?size?=?0; ?struct?file?*file; ?struct?vm_area_struct?*next; #endif ?//?檢查共享內存地址是否有效 ?if?(addr?&?~PAGE_MASK) ??return?retval; ?//?嘗試獲取內存映射寫鎖,可被信號中斷 ?if?(mmap_write_lock_killable(mm)) ??return?-EINTR; ?//?查找給定地址的虛擬內存區域 ?vma?=?find_vma(mm,?addr); #ifdef?CONFIG_MMU ?while?(vma)?{ ??next?=?vma->vm_next; ??//?檢查地址是否匹配,并且?vma?與?shm?相關 ??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); ???//?設置返回值為成功 ???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?相關 ??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); ??//?設置返回值為成功 ??retval?=?0; ?} #endif ?//?解鎖內存映射 ?mmap_write_unlock(mm); ?return?retval; }
函數原型
shmaddr:連接的共享內存的起始地址
shmctl函數
完成對共享內存的控制
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; ?} }
函數原型
shmid:共享內存標識符
cmd:IPC_STAT:得到共享內存的狀態,把共享內存的shmid_ds結構復制到buf中;IPC_SET:改變共享內存的狀態,把buf所指的shmid_ds結構中的uid、gid、mode復制到共享內存的shmid_ds結構內;IPC_RMID:刪除這片共享內存
buf:共享內存管理結構體。
faults
跟蹤缺頁錯誤和對應的調用棧信息,可以為內存使用量分析提供一個新的視角。缺頁錯誤會直接導致 RSS 的增長,所以這里截取的調用棧信息可以用來解釋進程內存使用量的增長。正如 brk() 一樣,可以通過單行程序來直接跟蹤這個事件并進行分析。
跟蹤page_fault_user和page_fault_kernel來對用戶態和內核態的缺頁錯誤對應的頻率統計信息進行分析。
exceptions:page_fault_user?????????????????????????[Tracepoint?event] exceptions:page_fault_kernel???????????????????????[Tracepoint?event]
vmscan
使用vmscan跟蹤點觀察頁面換出守護進程(kswapd)的操作。這個進程在系統內存壓力上升時負責釋放內存以便重用。值得注意的是,盡管內核函數的名稱仍然使用scanner,但為了提高效率,內核已經采用鏈表方式來管理活躍內存和不活躍內存。
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所花的全部時間,以毫秒為單位。這是從各種內核緩存中回收內存。
vmscan:mm_vmscan_direct_reclaim_begin,vmscan:mm_vmscan_direct_reclaim_end
使用這兩個跟蹤點計算直接接回收所花的時間,以毫秒為單位。這是前臺回收過程,在此期間內存被換入磁盤中,并且內存分配處于阻塞狀態。
vmscan:mm_vmscan_memcg_reclaim_begin,vmscan:mm_vmscan_memcg_reclaim_end
內存cgroup回收所花的時間,以毫秒為單位。如果使用了內存cgroups,此列顯示當cgroup超出內存限制,導致該cgroup進行內存回收的時間。
vmscan:mm_vmscan_wakeup_kswapd
kswapd 喚醒的次數。
vmscan:mm_vmscan_writepage
kswapd寫入頁的數量。
drsnoop
drsnoop使用mm_vmscan_direct_reclaim_begin 和 mm_vmscan_direct_reclaim_end 跟蹤點,來跟蹤內存釋放過程中的直接回收部分。它能夠顯示受到影響的進程以及對應的延遲,即直接回收所需的時間。可以用來定量分析內存受限的系統中對應用程序的性能影響。
直接內存回收
在直接內存回收過程中,有可能會造成當前需要分配內存的進程被加入一個等待隊列,當整個node的空閑頁數量滿足要求時,由kswapd喚醒它重新獲取內存。這個等待隊列頭就是node結點描述符pgdat中的pfmemalloc_wait。如果當前進程加入到了pgdat->pfmemalloc_wait這個等待隊列中,那么進程就不會進行直接內存回收,而是由kswapd喚醒后直接進行內存分配。
直接內存回收執行路徑是:
__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內核線程,也可能沒有喚醒,每個node的kswapd是否在__alloc_pages_slowpath()中被喚醒有兩個條件:
而在kswapd中會對node中每一個不平衡的zone進行內存回收,直到所有zone都滿足 zone分配頁框后剩余的頁框數量 > 此zone的high閥值 + 此zone保留的頁框數量。kswapd就會停止內存回收,然后喚醒在等待隊列的進程。
之后進程由于內存不足,對zonelist進行直接回收時,會調用到try_to_free_pages(),在這個函數內,決定了進程是否加入到node結點的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), ????????/*?本次內存分配的order值?*/ ??.order?=?order, ????????/*?允許進行回收的node掩碼?*/ ??.nodemask?=?nodemask, ????????/*?優先級為默認的12?*/ ??.priority?=?DEF_PRIORITY, ????????/*?與/proc/sys/vm/laptop_mode文件有關 ?????????*?laptop_mode為0,則允許進行回寫操作,即使允許回寫,直接內存回收也不能對臟文件頁進行回寫 ?????????*?不過允許回寫時,可以對非文件頁進行回寫 ?????????*/ ??.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進行內存回收時,如果讓node平衡了,則會喚醒這個等待隊列中的進程 ?????*?判斷node平衡的標準: ?????*?此node的ZONE_DMA和ZONE_NORMAL的總共空閑頁框數量?是否大于?此node的ZONE_DMA和ZONE_NORMAL的平均min閥值數量,大于則說明node平衡 ?????*?加入pgdat->pfmemalloc_wait的情況 ?????*?1.如果分配標志禁止了文件系統操作,則將要進行內存回收的進程設置為TASK_INTERRUPTIBLE狀態,然后加入到node的pgdat->pfmemalloc_wait,并且會設置超時時間為1s? ?????*?2.如果分配標志沒有禁止了文件系統操作,則將要進行內存回收的進程加入到node的pgdat->pfmemalloc_wait,并設置為TASK_KILLABLE狀態,表示允許?TASK_UNINTERRUPTIBLE?響應致命信號的狀態? ?????*?返回真,表示此進程加入過pgdat->pfmemalloc_wait等待隊列,并且已經被喚醒 ?????*?返回假,表示此進程沒有加入過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); ????/*?進行內存回收,有三種情況到這里? ?????*?1.當前進程為內核線程 ?????*?2.最優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()函數判斷是否加入到pgdat->pfmemalloc_wait等待隊列中,主要看此函數:
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,表示此進程是一個內核線程,則不會往下執行?*/ ?if?(current->flags?&?PF_KTHREAD) ??goto?out; ?/*?此進程已經接收到了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區?*/ ????????if?(zone_idx(zone)?>?ZONE_NORMAL) ???continue; ??/*?獲取zone對應的node?*/ ??pgdat?=?zone->zone_pgdat; ????????/*?判斷node是否平衡,如果平衡,則返回真 ?????????*?如果不平衡,如果此node的kswapd沒有被喚醒,則喚醒,并且這里喚醒kswapd只會對ZONE_NORMAL以下的zone進行內存回收 ?????????*?node是否平衡的判斷標準是: ?????????*?此node的ZONE_DMA和ZONE_NORMAL的總共空閑頁框數量?是否大于?此node的ZONE_DMA和ZONE_NORMAL的平均min閥值數量,大于則說明node平衡 ?????????*/ ??if?(allow_direct_reclaim(pgdat)) ???goto?out; ??break; ?} ? ?if?(!pgdat) ??goto?out; ?count_vm_event(PGSCAN_DIRECT_THROTTLE); ? ?if?(!(gfp_mask?&?__GFP_FS)) ????????/*?如果分配標志禁止了文件系統操作,則將要進行內存回收的進程設置為TASK_INTERRUPTIBLE狀態,然后加入到node的pgdat->pfmemalloc_wait,并且會設置超時時間為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 ??/*?如果分配標志沒有禁止了文件系統操作,則將要進行內存回收的進程加入到node的pgdat->pfmemalloc_wait,并設置為TASK_KILLABLE狀態,表示允許?TASK_UNINTERRUPTIBLE?響應致命信號的狀態? ?????*?這些進程在兩種情況下被喚醒 ?????*?1.allow_direct_reclaim(pgdat)為真時 ?????*?2.接收到致命信號時 ?????*/ ??wait_event_killable(zone->zone_pgdat->pfmemalloc_wait, ???allow_direct_reclaim(pgdat)); ????/*?如果加入到了pgdat->pfmemalloc_wait后被喚醒,就會執行到這?*/ ???? ????/*?喚醒后再次檢查當前進程是否接受到了kill信號,準備退出?*/ ?if?(fatal_signal_pending(current)) ??return?true; out: ?return?false; }
分配標志中沒有__GFP_NO_KSWAPD,只有在透明大頁的分配過程中會有這個標志。
node中有至少一個zone的空閑頁框沒有達到 空閑頁框數量 >= high閥值 + 1 << order + 保留內存,或者有至少一個zone需要進行內存壓縮,這兩種情況node的kswapd都會被喚醒。
swapin
使用kprobe跟蹤swap_readpage()內核函數,這會在觸發換頁所在的進程上下文中進行,可以跟蹤觸發換頁操作的進程的信息。展示了哪個進程正在從換頁設備中換入頁,前提是系統中有正在使用的換頁設備。
換頁操作在應用程序使用那些已經被換出到換頁設備上的內存時觸發。這是?個很重要的由于換頁導致的應用性能影響指標。其他的換頁相關指標,例如掃描和換出操作, 并不直接影響應用程序的性能。
extern?int?swap_readpage(struct?page?*page,?bool?do_poll);
hfaults
使用kprobe跟蹤hugetlb_fault()函數,可以從該函數的參數中抓取很多的詳細信息,包括mm_struct結構體和vm_area_struct結構體。可以通過vm_area_struct結構體來抓取文件名信息。
通過跟蹤巨頁相關的缺頁錯誤信息,按進程展示詳細信息,同時可以用來確保巨頁確實被啟用了。
vm_fault_t?hugetlb_fault(struct?mm_struct?*mm,?struct?vm_area_struct?*vma, ???unsigned?long?address,?unsigned?int?flags);
作者簡介:馬宜萱,西安郵電大學研一在讀,操作系統愛好者,主要方向為內存方向。目前在學習操作系統底層原理和內核編程。
審核編輯:黃飛
?
評論
查看更多