linux內核給每個進程都提供了一個獨立的虛擬地址空間,而且這個地址空間是連續的。這樣,進程就能夠很方便地訪問內存,也就是虛擬內存。linux
虛擬地址空間分爲:內核空間和用戶空間,不一樣字長(cpu指令能夠處理數據的最大長度)的處理器,地址空間的範圍也不一樣。如:32位和64位git
進程在用戶態時,只能訪問用戶空間的內存;只有進入內核態時才能訪問內核空間內存。雖然每一個進程的地址空間都包含了內核空間,但這些內核空間,其實關聯的都是相同的物理內存。這樣,進程切換到內核態後,就能夠很方便地訪問內核空間內存。github
每個進程都有一個這麼大的地址空間,那麼全部進程的虛擬內存加起來,天然要比實際的物理內存大的多。並非全部的虛擬內存都會分配物理內存,只有那些實際使用的虛擬內存才分配物理內存,而且分配後的物理內存,是經過內存映射來管理的。算法
內存映射,其實就是將虛擬內存地址映射到物理內存地址。爲了完成內存映射,內核爲每個進程都維護了一張頁表,記錄虛擬地址與物理地址的映射關係docker
頁表實際存儲在cpu的內存管理單元MMU中,這樣,正常狀況下,處理器就能夠直接經過硬件,找到要訪問的內存。當進程訪問的虛擬地址在頁表中查不到時,系統會產生一個缺頁異常,進入內核空間分配物理內存、更新進程頁表,最後再返回用戶空間,恢復進程的運行。shell
TLB(備緩衝器),就是MMU中頁表的高速緩存。因爲進程的虛擬地址空間是獨立的,而TLB的訪問速度又比MMU快得多,因此,經過減小進程的上下文切換,減小TLB的刷新次數,就能夠提升TLB緩存的使用率,進而提升cpu的內存訪問性能。ubuntu
MMU不是以字節爲單位來管理內存,而是規定了一個內存映射的最小單位,也就是頁,大小4KB。每一次內存映射,都須要關聯4KB或者4KB整數倍的內存空間。centos
爲了解決頁表項過多的問題,linux提供了兩種機制,也就是多級頁和大頁(HugePage)。緩存
多級頁:就是把內存分紅區塊來管理,將原來的映射關係改爲塊索引和區塊內的偏移。因爲虛擬內存空間一般只用了不多一部分,那麼,多級頁就保存這些使用中的區塊,這樣就能夠大大地減小頁表的項數。app
linux使用四級頁表來管理內存頁。前4個表項用於選擇頁,最後一個索引表示頁內偏移。
大頁:比普通頁更大的內存塊,常見的有2MB和1GB。大頁一般用在使用大量內存的進程上,如:Oracle等
虛擬內存空間分佈
一、只讀段:包括代碼和常量
二、數據段:包括全局變量
三、堆:包括動態分配內存,從低地址開始向上增加
四、文件映射段:包括動態庫、共享內存等,從高地址開始向下增加
五、棧:包括局部變量和函數調用的上下文等。棧的大小都是固定的,通常都是8MB。
內存分配和回收
malloc()是C標準庫提供的內存分配函數,對應到系統調用上,有兩種實現方式:brk()和mmap()。
brk():小塊內存(小於128k),C標準庫使用brk()來分配,也就是經過移動堆頂的位置來分配內存。這些內存釋放後並不會當即歸還系統,而是被緩存起來,這樣就能夠重複使用。
MMap():大塊內存(大於128k),則直接使用內存映射mmap()來分配,也就是在文件映射段找一塊空閒內存分配出去,釋放後直接歸還系統。
可是,這兩種分配都有優缺點,brk能夠減小缺頁異常的發生,提升內存訪問效率。這些內存沒有歸還系統,在內存工做繁忙時,在內存工做繁忙時,頻繁的內存分配和釋放會形成內存碎片。mmap會直接歸還系統,因此每次mmap都會發生缺頁異常。在內存工做繁忙時,頻繁的內存分配會致使大量的缺頁異常,使內核的管理負擔增大。
linux經過三種方式回收內存:
回收緩存:使用LRU算法,回收最近使用最少的內存頁面;
回收不常訪問的內存:把不經常使用的內存經過交換分區直接寫在磁盤中;
殺死進程:內存緊張時系統會經過oom,直接殺死佔用大量內存的進程
其中,第二種方式回收不常訪問的內存時,會用到交換分區swap。swap其實就是把一塊磁盤空間看成內存使用。它能夠把進程暫時不用的數據存儲到磁盤中(這個過程就是換出)。當進程訪問這些內存時,再從磁盤讀取這些數據到內存中(這個過程稱換入)。一般在內存不足時,纔會使用swap。
第三種方式oom,實際上是內核的一種保護機制。它監控進程的內存使用狀況,而且使用oom_score爲每個進程的內存使用狀況進行評分:
一個進程消耗的內存越大,oom_score就越大;
一個進程運行佔用用的cpu越多,oom_score就越小。
能夠經過手動設置進程的oom_adj,從而調整進程的oom_score。oom_adj的範圍【-17,15】,數值越大,表示進程越容易被oom殺死;數值越小,表示進程越不容易被oom殺死,其中-17表示禁止oom。
[root@test proc]# lsof -i:22 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME sshd 3043 root 3u IPv4 21799 0t0 TCP *:ssh (LISTEN) sshd 3499 root 3u IPv4 23448 0t0 TCP test:ssh->123.139.156.118:53251 (ESTABLISHED) sshd 3552 root 3u IPv4 23652 0t0 TCP test:ssh->123.139.156.118:53254 (ESTABLISHED) [root@test proc]# cat /proc/3043/oom_adj -17 #此時ssh進程的oom_adj爲-17,ssh就不容易被殺死
查看內存總體狀況
free
[root@test ~]# free -hm total used free shared buff/cache available Mem: 991M 93M 399M 484K 498M 737M Swap: 0B 0B 0B [root@test ~]# grep Cached /proc/meminfo Cached: 408672 kB SwapCached: 0 kB [root@test ~]# grep Buffer /proc/meminfo Buffers: 41252 kB
total:總內存大小;
used:已經使用的內存大小,包含了共享內存;
free:未使用內存大小;
shared:共享內存大小;
buff/cache:緩存和緩衝區的大小;
available:新進程可用內存的大。
注意:available包含了可回收的緩存,因此大於free未使用的內存。並非全部緩存均可以回收。
top
[root@test proc]# top top - 11:42:36 up 2:10, 2 users, load average: 0.00, 0.01, 0.05 Tasks: 77 total, 1 running, 76 sleeping, 0 stopped, 0 zombie %Cpu(s): 0.3 us, 0.3 sy, 0.0 ni, 98.0 id, 1.3 wa, 0.0 hi, 0.0 si, 0.0 st KiB Mem : 1015024 total, 352724 free, 94188 used, 568112 buff/cache KiB Swap: 0 total, 0 free, 0 used. 747996 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 3476 root 20 0 611432 13556 2364 S 0.3 1.3 0:23.47 barad_agent 3775 root 20 0 571352 7168 2528 S 0.3 0.7 0:10.34 YDService 1 root 20 0 125476 3916 2592 S 0.0 0.4 0:01.45 systemd 2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd 3 root 20 0 0 0 0 S 0.0 0.0 0:00.10 ksoftirqd/0 5 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kworker/0:0H 7 root rt 0 0 0 0 S 0.0 0.0 0:00.00 migration/0 8 root 20 0 0 0 0 S 0.0 0.0 0:00.00 rcu_bh 9 root 20 0 0 0 0 S 0.0 0.0 0:00.61 rcu_sched
VIRT:進程虛擬內存的大小,只要申請過的內存,即使尚未真正分配物理內存,也會計算在內。
RES:常駐內存大小,也就是進程實際使用的物理內存大小,但不包括swap和共享內存
SHR:共享內存大小,如與其餘進程共同使用的共享內存、加載的動態連接庫以及程序的代碼段等。
%MEM:進程使用物理內存佔系統內存的百分比。
Buffers and Cached
Buffers是對原始磁盤塊的臨時存儲,也就是用來緩存磁盤的數據,經過不會特別大(幾十MB)。這樣內核就能夠把分散的寫集中起來,統一優化磁盤寫入,屢次小的寫合併成單次大的寫。
Cached是從磁盤讀取文件的頁緩存,也就是用來緩存從文件讀取的數據。這樣下次訪問的時候直接從內存讀取。
SReclaimable是Slab的一部分。Slab包括兩部分,可回收用:SReclaimable。不可回收:SUnreclaim。
注意:buffers和cache既有讀又有寫。不是單一的讀或者寫
進程緩存命中率
查看緩存命中率須要安裝bcc軟件包
centos7系統安裝
[root@centos ~]# yum update
[root@centos ~]# rpm --import
https://www.elrepo.org/RPM-GPG-KEY-elrepo.org && rpm -Uvh
http://www.elrepo.org/elrepo-release-7.0-2.el7.elrepo.noarch.rpm
[root@centos ~]# uname -r ##
3.10.0-862.el7.x86_64
[root@centos ~]# yum remove kernel-headers kernel-tools kernel-tools-libs
[root@centos ~]# yum --disablerepo="" --enablerepo="elrepo-kernel" install
kernel-ml kernel-ml-devel kernel-ml-headers kernel-ml-tools
kernel-ml-tools-libs kernel-ml-tools-libs-devel
[root@centos ~]# sed -i '/GRUB_DEFAULT/s/=./=0/' /etc/default/grub
[root@centos ~]# grub2-mkconfig -o /boot/grub2/grub.cfg
[root@centos ~]# reboot
[root@centos ~]# uname -r ## 升級成功
4.20.0-1.el7.elrepo.x86_64
[root@centos ~]# yum install -y bcc-tools
[root@centos ~]# echo 'export PATH=$PATH:/usr/share/bcc/tools' > /etc/profile.d/bcc-tools.sh
[root@centos ~]# . /etc/profile.d/bcc-tools.sh
[root@centos ~]# cachestat 1 1 ## 測試安裝是否成功Ubuntu系統安裝步驟
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD echo "deb https://repo.iovisor.org/apt/xenial xenial main" | sudo tee /etc/apt/sources.list.d/iovisor.list sudo apt-get update sudo apt-get install -y bcc-tools libbcc-examples linux-headers-$(uname -r) #bcc安裝到/usr/share/bcc/tools這個目錄中。須要配置系統的path路徑 export PATH=$PATH:/usr/share/bcc/tools注意:bcc軟件包,必須是內核4.1以上。
cachestat/cachetop進程的緩存命中
root@VM-16-7-ubuntu:~# cachestat 1 3 HITS MISSES DIRTIES HITRATIO BUFFERS_MB CACHED_MB 3414 0 5 100.00% 32 563 59 0 4 100.00% 32 563 62 0 4 100.00% 32 563 root@VM-16-7-ubuntu:~# cachetop 12:50:16 Buffers MB: 80 / Cached MB: 572 / Sort: HITS / Order: ascending PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT% 3267 root YDService 2 0 0 100.0% 0.0% 1843 root barad_agent 3 0 1 66.7% 0.0% 20042 root barad_agent 8 0 3 62.5% 0.0% 323 root jbd2/vda1-8 8 8 6 12.5% 12.5% 20041 root barad_agent 9 2 4 45.5% 9.1% 20034 root cachetop 30 0 0 100.0% 0.0% 20044 root sh 158 0 0 100.0% 0.0% 20045 root sh 158 0 0 100.0% 0.0% 20046 root sh 158 0 0 100.0% 0.0% 20043 root sh 392 0 0 100.0% 0.0% 20044 root cat 491 0 0 100.0% 0.0% 20043 root barad_agent 496 0 0 100.0% 0.0% 20045 root grep 638 0 0 100.0% 0.0% 20046 root awk 915 0 0 100.0% 0.0% #指標 TOTAL:總的IO次數; MISSES:緩存未命中的次數; HITS:緩存命中次數; DIRTIES:新增到緩存中的髒頁數; CACHED_MB:buffer的大小,MB爲單位; BUFFERS_MB:cache的大小,MB爲單位;
pcstat文件緩存查看
#pcstat 安裝: if [ $(uname -m) == "x86_64" ] ; then curl -L -o pcstat https://github.com/tobert/pcstat/raw/2014-05-02-01/pcstat.x86_64 else curl -L -o pcstat https://github.com/tobert/pcstat/raw/2014-05-02-01/pcstat.x86_32 fi chmod 755 pcstat ./pcstat #便可使用 #### root@VM-16-7-ubuntu:~# ls pcstat root@VM-16-7-ubuntu:~# ./pcstat pcstat |----------+----------------+------------+-----------+---------| | Name | Size | Pages | Cached | Percent | |----------+----------------+------------+-----------+---------| | pcstat | 3049296 | 745 | 745 | 100.000 | |----------+----------------+------------+-----------+---------| ##指標
當進程經過malloc()申請虛擬內存後,系統並不會當即爲其分配物理內存,而是在首次訪問時,才經過缺頁異常陷入內核中分配內存。爲了協調快速cpu和慢速磁盤的性能差別,linux還會使用cache和buffer,分別把文件和磁盤讀寫的數據緩存到內存中。因此,對於程序來講,動態分配內存和回收,就是事故的地點。
沒有正確回收分配後的內存,致使了泄漏。
訪問的是已分配內存邊界外的地址,致使程序異常退出等等
內存的分配和回收
進程的內存空間
一、只讀段:包括代碼和常量。不會再去分配新的內存,因此不會產生內存泄漏
二、數據段:包括全局變量。變量在定義的時候就肯定了大小,因此不會產生內存泄漏
三、堆:包括動態分配內存,從低地址開始向上增加。應用程分配管理,沒有正確釋放堆內存,內存泄漏
四、文件映射段:包括動態庫、共享內存等,從高地址開始向下增加。共享內存也是程序管理,內存泄漏
五、棧:包括局部變量和函數調用的上下文等。棧的大小都是固定的,通常都是8MB。系統管理,不會泄漏
環境
2cpu 8GB內存
預先安裝sysstat docker bcc
#安裝docker、sysstat sudo apt-get install -y sysstat docker.io #安裝bcc sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD echo "deb https://repo.iovisor.org/apt/bionic bionic main" | sudo tee /etc/apt/sources.list.d/iovisor.list sudo apt-get update sudo apt-get install -y bcc-tools libbcc-examples linux-headers-$(uname -r) #拉取鏡像 docker run --name=app -itd feisk /app:mem-leak #k後面加y #檢查 root@test:~# docker logs app 2th => 1 3th => 2 4th => 3 5th => 5 6th => 8 7th => 13 8th => 21 9th => 34 10th => 55
排查
vmstat
root@test:~# vmstat 3 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 0 0 0 143036 108584 558084 0 0 20 63 146 313 1 0 99 0 0 0 0 0 142904 108584 558068 0 0 0 0 173 400 0 0 100 0 0 0 0 0 142440 108588 558072 0 0 0 271 159 333 1 1 96 2 0 0 0 0 142472 108588 558072 0 0 0 17 128 313 1 0 99 0 0 1 0 0 142472 108588 558072 0 0 0 5 115 283 0 0 100 0 0 .... .... 0 0 0 142412 108596 558076 0 0 0 29 297 708 1 1 98 0 0 0 0 0 141480 108628 558152 0 0 0 12 170 404 0 0 99 0 0 0 0 0 141512 108628 558152 0 0 0 4 172 390 0 0 100 0 0 0 0 0 141512 108628 558152 0 0 0 16 176 399 1 1 98 0 0 #觀察一段時間,發現free不斷減小,buffer和cache基本不變。說明系統內存一直在使用,可是,沒法說明內存泄漏
memleak
bcc軟件包中的
root@test:~# /usr/share/bcc/tools/memleak -a -p 17050 Attaching to pid 17050, Ctrl+C to quit. cannot attach uprobe, Device or resource busy [19:26:43] Top 10 stacks with outstanding allocations: addr = 7f389c2fc700 size = 8192 addr = 7f389c2f86e0 size = 8192 addr = 7f389c300720 size = 8192 addr = 7f389c2fa6f0 size = 8192 addr = 7f389c2fe710 size = 8192 40960 bytes in 5 allocations from stack fibonacci+0x1f [app] child+0x4f [app] start_thread+0xdb [libpthread-2.27.so] [19:26:48] Top 10 stacks with outstanding allocations: # -a 表示顯示每一個內存分配請求的大小以及地址 # -p 指定案例應用的 PID 號 # /usr/share/bcc/tools/ # -a 表示顯示每一個內存分配請求的大小以及地址 # -p 指定案例應用的 PID 號 #app進程一直在分配內存,而且fibonacci()函數分配的內存沒有釋放。 ===== #檢查代碼 root@test:~# docker exec app cat /app.c #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> long long *fibonacci(long long *n0, long long *n1) { 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; for (int n = 2; n > 0; n++) { v = fibonacci(&n0, &n1); n0 = n1; n1 = *v; printf("%dth => %lld\n", n, *v); sleep(1); } } int main(void) { pthread_t tid; pthread_create(&tid, NULL, child, NULL); pthread_join(tid, NULL); printf("main thread exit\n"); return 0; }root@test:~# #發現child()調用了fibonacci函數,可是並無釋放fibonacci返回的內存。在child函數加free(v);釋放內存