当前位置: 首页 > 科技观察

80%的 Linux 运维都不懂的内核问题_0

时间:2023-03-11 21:29:47 科技观察

Linux运维80%不懂的内核问题写下来,担心误导别人,所以经过一段时间的积累和对内核内存有了一定的了解,记录下来分享。【OOM-OutofMemory】内存溢出和内存溢出的解决方法:1.按比例缩小图片2.对图片使用软引用,及时进行recycle()操作。3.使用图片加载框架处理图片,比如专业图片处理的ImageLoader图片加载框架,XUtils的BitMapUtils进行处理。本文主要分析单个进程空间的内存布局和分配,从全局的角度分析内核的内存管理;下面主要从以下几个方面介绍Linux内存管理:进程内存申请与分配;内存耗尽后OOM;请求的内存在哪里?系统回收内存;1.进程内存申请与分配之前有一篇文章介绍了helloworld程序是如何加载到内存中,如何申请内存的。这里我再说明一下:同理,先给进程的地址空间,我想这张图对于任何一个开发者来说都一定要记住,另外还有一张操作磁盘、内存和cpu缓存的时间图。当我们在终端启动一个程序时,终端进程调用exec函数将可执行文件加载到内存中。此时代码段、数据段、bbs段、栈段都通过mmap函数映射到内存空间,堆根据堆上是否有申请内存来决定是否映射。exec执行后,此时进程并没有真正执行,而是将cpu控制权交给了动态链接库加载器,动态链接库加载器将进程所需的动态链接库加载到内存中。之后,进程开始执行。这个进程可以通过strace命令跟踪进程调用的系统函数来分析。这是我上一篇博客中理解管道的程序。从这个输出过程可以看出和我上面的描述是一致的。第一次调用malloc申请内存时,通过系统调用brk嵌入到内核中,首先会判断是否有关于堆的vma。如果没有,它会通过mmap匿名映射一块内存到堆上,并创建一个vma结构。挂在mm_struct描述符上的红黑树和链表。然后回到用户态,通过内存分配器(ptmaloc、tcmalloc、jemalloc)算法管理分配的内存,返回用户需要的内存。如果用户态申请大内存,直接调用mmap分配内存。此时返回给用户态的内存仍然是虚拟内存,直到第一次访问返回的内存时才真正分配内存。其实brk返回的也是虚拟内存,只是内存分配器切割分配后(切割必须访问内存),全部分配到物理内存。allocation,直接调用munmap返回给系统。否则,内存会先归还给内存分配器,再由内存分配器统一归还给系统。这就是为什么当我们调用free回收内存的时候,我们再次访问这块内存时可能不会报错。当然,当整个进程退出后,这个进程占用的内存会归还给系统。2、内存耗尽后OOM实习期间,测试机上的一个mysql实例经常被oomkill掉。OOM(outofmemory)是内存耗尽时系统的一种自救措施。它将选择一个进程并杀死并释放内存。很明显哪个进程占用内存最多,也就是最有可能被kill掉,但是真的是这样吗?今天早上上班,刚好遇到OOM,突然发现OOM了世界就安静了,哈哈,测试机上的redis被kill掉了。OOM关键文件oom_kill.c,介绍内存不足时系统如何选择应该杀掉的进程。选择因素有很多。除了进程占用的内存外,还有进程的运行时间,进程的优先级Level,是否是root用户进程,子进程的数量,占用的内存以及用户控制参数oom_adj都是相关的。oom产生后,函数select_bad_process会遍历所有进程。通过前面提到的因素,每个进程都会得到一个oom_score分数。如果得分为***,则将其选为killed进程。我们可以通过设置/proc//oom_adj分数来干预系统选择杀死哪些进程。这是内核定义的oom_adj调整值,***可以调整到15,最小为-16,如果是-17,进程就不会像买VIP会员一样被系统驱逐杀死,所以,如果一台机器上运行的服务器很多,又不想自己的服务被kill掉,可以把自己服务的oom_adj设置为-17。当然,说到这里,就不得不提到另一个参数/proc/sys/vm/overcommit_memory。manproc描述如下:表示当overcommit_memory为0时,是heuristicoom,即当申请的虚拟内存不是很夸张时,如果大于物理内存,系统允许申请,但是当进程申请的虚拟内存比物理内存夸张的大,就会出现OOM。比如物理内存只有8g,然后redis虚拟内存占用24G,物理内存占用3g。如果此时执行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、系统申请的内存在哪里?当我们了解了一个进程的地址空间后,我们是否想知道申请的物理内存在哪里?很多人可能会想,不就是物理内存吗?我说的是请求的内存在哪,因为物理内存分为缓存和普通物理内存,可以通过free命令查看,物理内存分为三个区域:DMA、NORMAL、HIGH。这里主要分析缓存和普通内存。.通过***部分我们知道一个进程的地址空间几乎都是由mmap函数申请的,有文件映射和匿名映射两种。3.1共享文件映射先来看代码段和动态链接库映射段,这两个段都属于共享文件映射,也就是说同一个可执行文件启动的两个进程共享这两个段,都被映射到相同的物理内存,那么这个内存在哪里?我写了一个程序测试如下:先看一下当前系统的内存使用情况:当我在本地新建一个1G的文件时:ddif=/dev/zeroof=fileblockbs=Mcount=1024然后调用上面的程序执行sharedfilemapping,此时的内存使用情况为:我们可以发现buff/cache增加了1G左右,所以可以断定代码段和动态链接库段映射到内核缓存,即比如说,在进行共享文件映射时,先将文件读入缓存,再映射到用户进程空间。3.2私有文件映射段对于进程空间中的数据段,它必须是私有文件映射,因为如果是共享文件映射,那么两个进程启动同一个可执行文件,一个进程对数据段的任何修改会影响对方。一个过程,我把上面的测试程序改写成了匿名文件映射:在执行程序之前,需要先释放之前的缓存,否则会影响结果echo1>>/proc/sys/vm/drop_caches再执行程序,查看内存使用情况:从使用前后对比可以发现,used和buff/cache分别增加了1G,说明在进行私有文件映射时,先将文件映射到缓存中,然后如果是一个文件执行修改时,会从其他内存中分配一块内存,将文件数据复制到新分配的内存中,然后在新分配的内存上进行修改,即copy-on-write。这也很好理解,因为如果打开同一个可执行文件的多个实例,内核首先将可执行数据段映射到缓存中,然后每个实例如果有修改的数据段就会分配一块内存存储。数据段,毕竟数据段也是进程私有的。通过上面的分析可以得出,如果是文件映射,就是将文件映射到缓存中,然后根据是共享还是私有进行不同的操作。3.3私有匿名映射,如bbs段、堆、栈等都是匿名映射,因为可执行文件中没有对应的段,必须是私有映射,否则如果当前进程fork子进程,父进程而且子进程会共享这些段,一个修改就会互相影响,这是不合理的。ok,现在我把上面的测试程序改成私有的匿名映射,再看看内存占用情况。可以看到只used增加了1G,buff/cache没有增加;意思是在进行匿名私有映射的时候,是不占用缓存的。其实这也是合理的,因为只有当前进程在使用这块内存,没有必要去占用宝贵的缓存。3.4共享匿名映射当我们需要在父子进程之间共享内存时,可以使用mmap共享匿名映射,那么共享匿名映射的内存存放在哪里呢?我继续将上面的测试程序重写为共享匿名地图。再来看内存使用情况:从上面的结果可以看出,只有buff/cache增加了1G,也就是说,在进行共享匿名映射的时候,此时是从缓存中申请内存的,原因是很明显,因为父子进程共享这块内存,共享的匿名映射存在于缓存中,然后各个进程映射到彼此的虚拟内存空间,这样就可以操作同一块内存了。4.系统回收内存。当系统内存不足时,有两种释放内存的方式,一种是手动释放,另一种是系统自己触发的内存回收。我们先来看看手动触发的方法。echo1>>/proc/sys/vm/drop_caches4.1ManuallyreclaimmemoryManuallyreclaimmemory,前面已经演示过,即echo1>>/proc/sys/vm/drop_cachesmanprocfrom下可以看到关于这个的介绍从这个介绍可以看出,当drop_caches文件为1时,此时会释放pagecache中可释放的部分(有些缓存不能通过这个释放),当drop_caches为2时,会释放dentries和inodes缓存此时,当drop_caches为3时,这将同时释放上述两者。重点是最后一句,意思是如果pagecache中有脏数据,是不能通过操作drop_caches来释放的,必须先通过sync命令把脏数据flush到磁盘,才能通过操作drop_caches释放pagecache.ok,之前提到过有些pagecache是??不能通过drop_caches释放的,那么除了上面提到的文件映射和共享匿名映射之外,pagecache中还存在什么呢?4.2tmpfs我们先来看看tmpfs。与procfs、sysfs和ramfs一样,tmpfs是一个基于内存的文件系统。tmpfs和ramfs的区别在于ramfs文件是基于纯内存的。除了纯内存,tmpfs还使用swap空间,ramfs可能会耗尽内存,tmpfs可以限制所使用内存的大小。可以使用命令df-T-h查看系统中的一些文件系统,其中有一些是tmpfs,比较出名的目录是/dev/shmtmpfs文件系统源文件在内核源码mm/shmem。C。tmpfs的实现非常复杂。之前介绍过虚拟文件系统。基于tmpfs文件系统创建文件与其他基于磁盘的文件系统相同。还会有inode、super_block、identity、File等结构,区别主要在读写上,因为读写只涉及到文件的载体是内存还是磁盘。tmpfs文件读取函数shmem_file_read主要是通过inode结构找到address_space地址空间,其实就是磁盘文件的pagecache,然后通过读取offset定位到cachepage和pageoffset。这时可以通过函数__copy_to_user将缓存页中的数据直接从pagecache复制到用户空间。当我们要读取的数据不在pagecache中时,我们需要判断是否在swap中。如果是,先换内存页,再读。tmpfs文件shmem_file_write的写入函数,该过程主要是判断要写入的page是否在内存中,如果是,则直接通过函数__copy_from_user将用户态数据复制到内核pagecache中覆盖旧数据,以及将其标记为脏。如果要写入的数据已经不在内存中,判断是否在swap中,如果在,先读取,用新数据覆盖旧数据并标记为脏,如果都不在内存中也不在磁盘上,然后生成一个新的pagecache存储用户数据。通过上面的分析,我们知道基于tmpfs的文件也是使用缓存的。我们可以在/dev/shm上新建一个文件查看:看,缓存增加了1G,验证了tmpfs使用的缓存内存。其实mmap的匿名映射原理也是用到了tmpfs。mm/mmap.c->do_mmap_pgoff函数内部,判断如果文件结构为空,映射为SHARED,则调用shmem_zero_setup(vma)函数,在此处tmpfs上新建文件。解释了为什么共享匿名映射内存初始化为0,但是我们知道mmap分配的内存初始化为0,也就是说mmap的私有匿名映射也是0,那么体现在哪里呢?这在do_mmap_pgoff函数内部并没有体现出来,而是出现了pagefault异常,然后分配了一个初始化为0的特殊page。那么这个tmpfs占用的内存页是否可以回收呢?也就是说,tmpfs文件占用的pagecache不能回收,原因很明显,因为有文件引用了这些page,所以不能回收。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,这个就是挂在文件结构中的private_data等等。说到这里,可以总结一下,进程空间中的代码段、数据段、动态链接库(共享文件映射)、mmap共享匿名映射都存在缓存中,但是这些内存页是被进程引用的,所以它们不能被释放是的,基于tmpfs的ipc进程间通信机制的生命周期跟随内核,所以不能通过drop_caches释放。上面说的缓存虽然不能释放,但是后面会提到,当内存不足的时候,可以把内存换出。因此,drop_caches能够释放的是从磁盘读取文件时,进程将文件映射到内存后,进程退出时的缓存页。这时映射文件的缓存页如果没有被引用也可以释放。.4.4自动内存释放法当系统内存不足时,操作系统有自组织内存,尽可能多地释放内存。如果这种机制不能释放足够的内存,那只能是OOM。之前提到OOM的时候说redis因为OOM被kill了,如下:第二句后半部分,total-vm:186660kB,anon-rss:9388kB,file-rss:4kB用三个这个属性来描述,即所有虚拟内存、常驻内存匿名映射页和常驻内存文件映射页。其实从上面的分析我们也可以知道一个进程其实就是文件映射和匿名映射:文件映射:代码段、数据段、动态链接库共享存储段和用户程序的文件映射段;匿名映射:bbs段,堆,malloc用mmap分配内存时,还有mmap共享内存段;其实内核是根据文件映射和匿名映射来回收内存的,在mmzone.h中定义如下:不允许系统换出的列表。简单说一下Linux内核自动回收内存的原理。内核有一个kswapd,它会定期检查内存使用情况。如果发现freememory设置为pages_low,那么kswapd会扫描lru_list的前四个lru队列,在active链表中寻找unavailable。活动页面,并添加非活动链表。然后遍历inactive链表,逐个回收释放32页,直到空闲页数达到pages_high,不同页回收方式不同。当然,当内存级别低于某个限制阈值时,会直接发出内存回收。原理和kswapd一样,但是这次回收更强,需要回收更多的内存。文件页:如果是脏页,直接写回磁盘,然后回收内??存。如果不是脏页,则直接释放回收,因为如果是io读缓存,则直接释放。下次缺页的时候就异常了,直接从磁盘读取即可。如果是文件映射页面,则直接发布。下次访问时,也会产生两个pagefault异常,一个是将文件内容读入磁盘,一个是和进程虚拟内存有关。匿名页:因为匿名页没有地方回写,如果释放了,数据就找不到了,所以匿名页的回收就是把swap取出来磁盘,在页表项中标记出来,下一个页面错误将异常从磁盘换入内存。换入和换出实际上占用了大量的系统IO。如果系统内存需求突然快速增加,CPU就会被IO占用,系统就会卡死,导致无法对外提供服务。因此,系统提供了一个参数,用于设置当前执行内存回收时,回收缓存和交换匿名页面。这个参数是:值越大,内存越有可能被swap回收。最大值为100。如果设置为0,则尽可能通过回收缓存来释放内存。5.小结本文主要写Linux内存管理相关的东西:首先回顾进程地址空间;其次,当进程消耗大量内存导致内存不足时,我们可以有两种方式:最后是手动回收缓存;另一个是系统后台线程swapd,执行内存回收工作。***当申请的内存大于系统剩余内存时,只会产生OOM,杀死进程,释放内存。从这个过程中,我们可以看出系统试图释放足够内存的努力程度。