序言之前在实习时,听了OOM的分享以后,就对Linux内核显存管理饱含兴趣,然而这块知识十分庞大,没有一定积累,不敢写下,害怕误人子弟,所以经过一个一段时间的积累,对内核显存有一定了解过后,明天才写下这篇文章记录,分享。这篇文章主要是剖析了单个进程空间的显存布局与分配,是从全局的视角剖析下内核对显存的管理;
下边主要从以下方面介绍Linux显存管理:。
1、进程的显存申请与分配
之前文章介绍helloworld程序是怎样载入显存以及是怎样申请显存的,我在这,再度说明下:同样,还是先给出进程的地址空间,我认为对于任何开发人员这张图是必须记住的,还有一张就是操作disk,memory以及cpucache的时间图。
当我们在终端启动一个程序时,终端进程调用exec函数将可执行文件载入显存,此时代码段,数据段,bbs段,stack段都通过mmap函数映射到显存空间,堆则要依据是否有在堆上申请显存来决定是否映射。exec执行以后,此时并未真正开始执行进程,而是将cpu控制权交给了动态链接库装载器,由它来将该进程须要的动态链接库装载进显存。以后才开始进程的执行,这个过程可以通过strace命令跟踪进程调用的系统函数来剖析。
这是认识pipe中的程序,从这个输出过程,可以看出和我上述描述的一致。当第一次调用malloc申请显存时,通过系统调用brk嵌入到内核,首先会进行一次判定,是否有关于堆的vma,假如没有,则通过mmap匿名映射一块显存给堆,并完善vma结构,挂到mm_struct描述符上的黑红树和数组上。之后回到用户态,通过显存分配器(ptmaloc,tcmalloc,jemalloc)算法将分配到的显存进行管理,返回给用户所须要的显存。假如用户态申请大显存时,是直接调用mmap分配显存,此时返回给用户态的显存还是虚拟显存,直至第一次访问返回的显存时,才真正进行显存的分配。虽然通过brk返回的也是虚拟显存,并且经过显存分配器进行切割分配以后(切割就必须访问显存),全都分配到了化学显存当进程在用户态通过调用free释放显存时,假若这块显存是通过mmap分配,则调用munmap直接返回给系统。否则显存是先返回给显存分配器,之后由显存分配器统一退还给系统,这就是为何当我们调用free回收显存以后,再度访问这块显存时,可能不会报错的诱因。其实,当整个进程退出以后linux是什么,这个进程占用的显存就会归还给系统。
Linux内核源码学习地址:/course/4032547?flowToken=1041043
【文章福利】小编推荐自己的Linux内核源码剖析交流群:【点击1095678385加入】整理了一些个人认为比较好的学习书籍、视频资料共享在群文件上面,有须要的可以自行添加哦!
2、内存用尽以后OOM
在实习期间,有一台测试机上的mysql实例常常被oom杀害,OOM(outofmemory)即为系统在显存用尽时的自我挽救举措,他会选择一个进程,将其杀害,释放出显存,很显著,那个进程占用的显存最多,即最可能被杀害,但事实是这样的吗?昨天晚上去下班,正好遇到了一起OOM,忽然发觉,OOM一次,世界都安静出来了,哈哈,测试机上的redis被杀害了。
OOM关键文件oom_kill.c,上面介绍了当显存不够时,系统怎样选择最应当被杀害的进程,选择诱因有挺多的,不仅进程占用的显存外,还有进程运行的时间,进程的优先级,是否为root用户进程,子进程个数和占用显存以及用户控制参数oom_adj都相关。当形成oom以后,函数select_bad_process会遍历所有进程,通过之前提到的这些诱因,每位进程就会得到一个oom_score分数,分数最高,则被选为杀害的进程。我们可以通过设置/proc/
/oom_adj分数来干预系统选择杀害的进程。
这是内核关于这个oom_adj调整值的定义,最大可以调整为15,最小为-16,倘若为-17,则该进程如同买了vip会员一样,不会被系统驱逐杀害了,为此,假如在一台机器上有跑好多服务器,且你不希望自己的服务被杀害的话,就可以设置自己服务的oom_adj为-17。其实,说到这,就必须提及另一个参数/proc/sys/vm/overcommit_memory,manproc说明如下:
意思就是当overcommit_memory为0时,则为启发式oom,即当申请的虚拟显存不是很夸张的小于化学显存,则系统容许申请,并且当进程申请的虚拟显存很夸张的小于化学显存,则都会形成OOM。诸如只有8g的化学显存,之后redis虚拟显存占用了24G,化学显存占用3glinux 文件空间容量,假如这时执行bgsave,子进程和父进程共享化学显存,而且虚拟显存是自己的,即子进程会申请24g的虚拟显存,这很夸张小于化学显存,才会形成一次OOM。当overcommit_memory为1时,则永远都容许overmemory显存申请,即不管你多大的虚拟显存申请都容许,然而当系统显存用尽时,这时才会形成oom,即上述的redis事例,在overcommit_memory=1时,是不会形成oom的,由于化学显存足够。当overcommit_memory为2时,永远都不能超出某个限定额的显存申请,这个限定额为swap+RAM*系数(/proc/sys/vm/overcmmit_ratio,默认50%,可以自己调整),倘若如此多资源早已用光,这么前面任何尝试申请显存的行为就会返回错误,这一般意味着此时无法运行任何新程序以上就是OOM的内容,了解原理,以及怎样按照自己的应用,合理的设置OOM。
3、系统申请的显存都在哪?
我们了解了一个进程的地址空间以后,是否会好奇,申请到的数学显存都存在哪了?可能好多人认为,不就是化学显存吗?我这儿说申请的显存在哪,是由于化学显存有分为cache和普通化学显存,可以通过free命令查看,但是化学显存还有分DMA,NORMAL,HIGH三个区,这儿主要剖析cache和普通显存。通过第一部份,我们晓得一个进程的地址空间几乎都是mmap函数申请,有文件映射和匿名映射两种。
3.1共享文件映射
我们先来看下代码段和动态链接库映射段,这两个都是属于共享文件映射,也就是说由同一个可执行文件启动的两个进程是共享这两个段,都是映射到同一块化学显存,这么这块显存在哪了?我写了个程序测试如下:
我们先看下当前系统的显存使用情况:
当我在本地新建一个1G的文件:ddif=/dev/zeroof=fileblockbs=Mcount=1024之后调用上述程序,进行共享文件映射,此时显存使用情况为:
我们可以发觉,buff/cache下降了大约1G,因而我们可以得出推论,代码段和动态链接库段是映射到内核cache中,也就是说当执行共享文件映射时,文件是先被读取到cache中,之后再映射到用户进程空间中。
3.2私有文件映射段
对于进程空间中的数据段,其必须是私有文件映射,由于假如是共享文件映射,这么同一个可执行文件启动的两个进程,任何一个进程更改数据段,都将影响另一个进程了,我将上述测试程序改写成匿名文件映射:
在执行程序执行,须要先将之前的cache释放掉,否则会影响结果echo1>>/proc/sys/vm/drop_caches接着执行程序,看下显存使用情况:
从使用前和使用后对比,可以发觉used和buff/cache分别下降了1G,说明当进行私有文件映射时,首先是将文件映射到cache中,之后假如某个文件对这个文件进行更改,则会从其他显存中分配一块显存先将文件数据拷贝至新分配的显存,之后再在新分配的显存上进行更改,这也就是写时复制。这也挺好理解,由于假如同一个可执行文件开启多个实例,这么内核先将这个可执行的数据段映射到cache,之后每位实例假如有更改数据段,则都将分配一个一块显存储存数据段,虽然数据段也是一个进程私有的。通过上述剖析,可以得出推论,倘若是文件映射,则都是将文件映射到cache中,之后按照共享还是私有进行不同的操作。
3.3私有匿名映射
像bbs段,堆,栈那些都是匿名映射,由于可执行文件中没有相应的段,并且必须是私有映射,否则假如当前进程fork出一个子进程,这么母子进程将会共享那些段linux 文件空间容量,一个更改就会影响到彼此,这是不合理的。ok,如今我把上述测试程序改成私有匿名映射
这时再来看下显存的使用情况
我们可以看见,只有used降低了1G,而buff/cache并没有下降;说明,在进行匿名私有映射时,并没有占用cache,虽然这也是有道理,由于就只有当前进程在使用这块这块显存,没有必要占用宝贵的cache。
3.4共享匿名映射
当我们须要在母子进程共享显存时,就可以用到mmap共享匿名映射,这么共享匿名映射的显存是储存在哪了?我继续改写上述测试程序为共享匿名映射。
这时来看下显存的使用情况:
从上述结果,我们可以看出,只有buff/cache下降了1G,即当进行共享匿名映射时,这时是从cache中申请显存,道理也很显著,由于兄妹进程共享这块显存,共享匿名映射存在于cache,之后每位进程再映射到彼此的虚存空间,这样即可操作的是同一块显存。
4、系统回收显存
当系统显存不足时,有两种方法进行显存释放,一种是自动的方法,另一种是系统自己触发的显存回收,先来看下自动触发形式。4.1自动回收显存
自动回收显存,之前也有演示过,即echo1>>/proc/sys/vm/drop_caches我们可以在manproc下边听到关于这个的简介
从这个介绍可以看出,当drop_caches文件为1时,这时将释放pagecache中可释放的部份(有些cache是不能通过这个释放的),当drop_caches为2时,这时将释放dentries和inodes缓存,当drop_caches为3时,这同时释放上述两项。关键还有最后一句,意思是说假如pagecache中有脏数据时,操作drop_caches是不能释放的,必须通过sync命令将脏数据刷新到c盘,能够通过操作drop_caches释放pagecache。ok,之前有提及有些pagecache是不能通过drop_caches释放的,这么不仅上述提文件映射和共享匿名映射外,还有有什么东西是存在pagecache了?4.2tmpfs我们先来看下tmpfs,tmpfs和procfs,sysfs以及ramfs一样,都是基于显存的文件系统,tmpfs和ramfs的区别就是ramfs的文件基于纯显存的,和tmpfs不仅纯显存外,都会使用swap交换空间,以及ramfs可能会把显存用尽,而tmpfs可以限定使用显存大小,可以用命令df-T-h查看系统一些文件系统,其中就有一些是tmpfs,比较出名的是目录/dev/shmtmpfs文件系统源文件在内核源码mm/shmem.c,tmpfs实现很复杂,之前有介绍虚拟文件系统,基于tmpfs文件系统创建文件和其他基于c盘的文件系统一样,也会有inode,super_block,identry,file等结构,区别主要是在读写上,由于读写才涉及到文件的载体是显存还是c盘。
而tmpfs文件的读函数shmem_file_read,过程主要为通过inode结构找到address_space地址空间,虽然就是c盘文件的pagecache,之后通过读偏斜定位cache页以及页内偏斜。这时就可以直接从这个pagecache通过函数__copy_to_user将缓存页内数据拷贝到用户空间,当我们要读物的数据不pagecache中时,这时要判别是否在swap中,假如在则先将显存页swapin,再读取。tmpfs文件的写函数shmem_file_write,过程主要为先判别要写的页是否在显存中,假如在,则直接将用户态数据通过函数__copy_from_user拷贝至内核pagecache中覆盖老数据,并标为dirty。假如要写的数据不再显存中,则判定是否在swap中,假如在,则先读取下来,用新数据覆盖老数据并标为脏,假如即不在显存也不在c盘,则新生成一个pagecache储存用户数据。由前面剖析,我们晓得基于tmpfs的文件也是使用cache的,我们可以在/dev/shm上创建一个文件来测量下:
见到了吧,cache下降了1G,验证了tmpfs的确使用的cache显存。虽然mmap匿名映射原理也是用了tmpfs,在mm/mmap.c->do_mmap_pgoff函数内部,有判别假如file结构为空以及为SHARED映射,则调用shmem_zero_setup(vma)函数在tmpfs上用新建一个文件
这儿就解释了为何共享匿名映射显存初始化为0了,而且我们晓得用mmap分配的显存初始化为0,就是说mmap私有匿名映射也为0,这么彰显在哪了?这个在do_mmap_pgoff函数内部可没有彰显下来,而是在缺页异常,之后分配一种特殊的初始化为0的页。这么这个tmpfs占有的显存页可以回收吗?
也就是说tmpfs文件占有的pagecache是不能回收的,道理也很显著,由于有文件引用那些页linux系统好用吗,就不能回收。
4.3共享显存
posix共享显存虽然和mmap共享映射是同一个道理,都是借助在tmpfs文件系统上新建一个文件,之后再映射到用户态,最后两个进程操作同一个数学显存,这么SystemV共享显存是否也是借助tmpfs文件系统了?我们可以跟踪到下列函数
这个函数就是新建一个共享显存段,其中函数shmem_kernel_file_setup就是在tmpfs文件系统上创建一个文件,之后通过这个显存文件实现进程通讯,这我就不写测试程序了,但是这也是不能回收的,由于共享显存ipc机制生命周期是随内核的,也就是说你创建共享显存以后,假如不显示删掉的话,进程退出以后,共享显存还是存在的。之前看了一些技术博客,说到Poxic和SystemV两套ipc机制(消息队列,讯号量以及共享显存)都是使用tmpfs文件系统,也就是说最终显存使用的都是pagecache,并且我在源码中看出了两个共享显存是基于tmpfs文件系统,其他讯号量和消息队列还没看下来(有待后续考究)。posix消息队列的实现有点类似与pipe的实现,也是自己一套mqueue文件系统,之后在inode上的i_private上挂上关于消息队列属性mqueue_inode_info,在这个属性上,内核2.6时,是用一个链表储存消息,而到了4.6则用黑红树了储存消息(我下载了这两个版本,具体哪些时侯开始用黑红树,没考量)。之后两个进程每次操作都是操作这个mqueue_inode_info中的消息链表或则黑红树,实现进程通讯,和这个mqueue_inode_info类似的还有tmpfs文件系统属性shmem_inode_info和为epoll服务的文件系统eventloop,也有一个特殊属性structeventpoll,这个是挂在file结构的private_data等等。说到这,可以小结下,进程空间中代码段,数据段,动态链接库(共享文件映射),mmap共享匿名映射都存在于cache中,并且这种显存页都有被进程引用,所以是不能释放的,基于tmpfs的ipc进程间通讯机制的生命周期是随内核,因而也是不能通过drop_caches释放。其实上述提到的cache不能释放,并且前面有提及,当显存不足时,这种显存是可以swapout的。因而drop_caches能释放的就是当从c盘读取文件时的缓存页以及某个进程将某个文件映射到显存以后,进程退出,这时映射文件的的缓存页假如没有被引用,也是可以被释放的。
4.4显存手动释放形式
当系统显存不够时,操作系统有一套自我整理显存,并尽可能的释放显存机制,假如这套机制不能释放足够多的显存,这么只能OOM了。之前在提到OOM时,说道redis由于OOM被杀害,如下:
第二句后半部份,total-vm:186660kB,anon-rss:9388kB,file-rss:4kB把一个进程显存使用情况,用三个属性进行了说明,即所有虚拟显存,常驻显存匿名映射页以及常驻显存文件映射页。虽然从上述的剖析,我们也可以晓得一个进程也许就是文件映射和匿名映射:
虽然内核回收显存就是按照文件映射和匿名映射来进行的,在mmzone.h有如下定义:
LRU_UNEVICTABLE即为不可驱逐页lru,我的理解就是当调用mlock锁住显存,不让系统swapout出去的页列表。简单说下linux内核手动回收显存原理,内核有一个kswapd会周期性的检测显存使用情况,若果发觉空闲显存定于pages_low,则kswapd会对lru_list前四个lru队列进行扫描,在活跃数组中查找不活跃的页,并添加不活跃数组。之后再遍历不活跃数组,挨个进行回收释放出32个页,晓得freepage数目达到pages_high,针对不同的页,回收方法也不一样。其实,当显存水平高于某个极限阀值时,会直接发出显存回收,原理和kswapd一样,而且此次回收力度更大,须要回收更多的显存。文件页:假如是脏页,则直接回写进c盘,再回收显存。假如不是脏页,则直接释放回收,由于假如是io读缓存,直接释放掉,上次读时,缺页异常,直接到c盘读回去即可,假如是文件映射页,直接释放掉,上次访问时,也是形成两个缺页异常,一次将文件内容读取进c盘,另一次与进程虚拟显存关联。匿名页:由于匿名页没有回写的地方,假如释放掉,这么就找不到数据了,所以匿名页的回收是采取swapout到c盘,并在页表项做个标记,上次缺页异常在从c盘swapin进显存。swap换进换出虽然是很占用系统IO的,假如系统显存需求忽然间迅速下降,这么cpu将被io占用,系统会卡死,致使不能对外提供服务,因而系统提供一个参数,用于设置当进行显存回收时,执行回收cache和swap匿名页的,这个参数为:
意思就是说这个值越高,越可能使用swap的形式回收显存,最大值为100,假如设为0,则尽可能使用回收cache的形式释放显存。
5、总结
这篇文章主要是写了linux显存管理相关的东西:首先是回顾了进程地址空间;其次当进程消耗大量显存而造成显存不足时,我们可以有两种形式:第一是自动回收cache;另一种是系统后台线程swapd执行显存回收工作。最后当申请的显存小于系统剩余的显存时,这时就只会形成OOM,杀害进程,释放显存,从这个过程,可以看出系统为了腾出足够的显存,是多么的努力啊。