Linux内存分配与回收

2023-09-24 1239阅读

之前实习的时候,听了OOM的分享,对Linux内核内存管理产生了浓厚的兴趣。不过这块知识面很大,我还没有积累足够的知识。所以经过一段时间的积累,对内核内存有了一定的了解后,今天写了这篇博客,记录一下,分享一下。我之前写过关于Linux内存管理的文章。本文主要分析单个进程空间的内存布局和分配。然后返回到用户态,通过内存分配器算法管理分配的内存,返回用户所需的内存。当进程在用户态通过调用free释放内存时,如果通过mmap分配内存,则调用munmap并直接返回给系统。否则,先将内存返回给内存分配器,然后内存分配器统一返回给系统。

原文链接:

之前实习的时候,听了OOM的分享,对Linux内核内存管理产生了浓厚的兴趣。 不过这块知识面很大,我还没有积累足够的知识。 我不敢写下来,怕误导别人。 所以经过一段时间的积累,对内核内存有了一定的了解后,今天写了这篇博客,记录一下,分享一下。

我之前写过关于Linux内存管理的文章。 那篇文章主要是关于Linux内存管理的。 本文主要分析单个进程空间的内存布局和分配。 今天的博客主要从全局角度分析内核的内存管理。 。

复制文件显示内存不足_发现内存复制错误_复制文件存储空间不足

下面主要从以下几个方面介绍Linux内存管理:

1.进程内存申请与分配

之前有一篇文章介绍了hello world程序如何加载内存以及如何申请内存。 我在这里再解释一下:同样,最好先给出进程的地址空间。 我认为这张图片对于任何开发人员来说都是必须的。 记住,还有一个操作磁盘、内存和CPU缓存的时序图。

复制文件显示内存不足_复制文件存储空间不足_发现内存复制错误

当我们在终端中启动一个程序时,终端进程会调用exec函数将可执行文件加载到内存中。 此时代码段、数据段、bbs段、堆栈段都通过mmap函数映射到内存空间。 堆根据堆上是否有Allocate内存来决定是否进行map。

exec执行后,此时进程的执行并没有真正开始,而是将cpu控制权交给了动态链接库加载器,它将进程所需的动态链接库加载到内存中。 然后流程开始执行。 可以通过使用strace命令来跟踪该进程调用的系统函数来分析该进程。

复制文件显示内存不足_复制文件存储空间不足_发现内存复制错误

这是我上一篇博客中关于了解管道的程序。 从这个输出过程可以看出,和我上面描述的是一致的。

当第一次调用malloc申请内存时,通过系统调用brk嵌入到内核中。 首先会判断是否有关于堆的vma。 如果没有,则通过mmap匿名映射一块内存到堆上,建立vma结构。 到mm_struct描述符上的红黑树和链表。

然后返回到用户态,通过内存分配器(ptmalloc、tcmalloc、jemalloc)算法管理分配的内存,返回用户所需的内存。

如果用户态申请大内存,则直接调用mmap分配内存。 此时返回用户态的内存仍然是虚拟内存。 直到第一次访问返回的内存时,才真正分配内存。

其实通过brk返回的也是虚拟内存,只不过内存分配器进行切割分配后(切割时必须访问内存),就全部分配给物理内存了。

当进程在用户态通过调用free释放内存时,如果通过mmap分配内存,则调用munmap并直接返回给系统。

否则,先将内存返回给内存分配器,然后内存分配器统一返回给系统。 这就是为什么当我们调用free回收内存时,再次访问这块内存时可能不会报错。

当然,当整个进程退出时,这个进程所占用的内存就会归还给系统。

2、内存不足后OOM

实习期间,测试机上有一个MySQL实例经常被OOM杀死。 OOM(Out of Memory)是内存耗尽时系统的自救措施。 它将选择一个进程,杀死它,并释放内存。 ,很明显哪个进程占用内存最多,最有可能被杀死,但事实真的是这样吗?

今天早上上班,碰巧遇到了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。 man proc的说明如下:

复制文件存储空间不足_复制文件显示内存不足_发现内存复制错误

意思是当overcommit_memory为0时,是启发式oom,即当申请的虚拟内存没有夸张地大于物理内存时,系统允许申请,但是当进程申请的虚拟内存夸张地大时超过物理内存,就会产生OOM。

比如物理内存只有8G,那么redis虚拟内存占用24G,物理内存占用3G。 如果此时执行bgsave,子进程和父进程共享物理内存,但虚拟内存是自己的,即子进程会申请24G的虚拟内存。 内存比物理内存过大,会导致OOM。

当overcommit_memory为1时,overcommit_memory内存申请始终是允许的,也就是说,无论你的虚拟内存申请有多大,都是允许的,但是当系统内存耗尽时,就会出现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。 这里主要分析缓存和普通内存。 。

通过第一部分,我们知道进程的地址空间几乎都是由mmap函数请求的,有两种类型:文件映射和匿名映射。

3.1 共享文件映射

我们先看一下代码段和动态链接库映射段。 两者都属于共享文件映射。 也就是说,同一个可执行文件启动的两个进程共享这两个段,并映射到同一块物理内存,那么这块内存在哪里呢? 我写了一个程序来测试如下:

复制文件显示内存不足_复制文件存储空间不足_发现内存复制错误

我们首先看一下当前系统的内存使用情况:

复制文件显示内存不足_复制文件存储空间不足_发现内存复制错误

当我在本地新建一个1G文件时:

dd if=/dev/zero of=fileblock bs=M 计数=1024

发现内存复制错误_复制文件存储空间不足_复制文件显示内存不足

然后调用上述程序进行共享文件映射。 此时内存使用情况为:

复制文件显示内存不足_复制文件存储空间不足_发现内存复制错误

我们可以发现buff/cache增长了1G左右,因此可以得出代码段和动态链接库段映射到了内核缓存,这意味着在进行共享文件映射时,先读取文件是缓存,而然后映射到用户进程空间。

3.2 私有文件映射段

对于进程空间中的数据段,必须是私有文件映射,因为如果是共享文件映射,那么同一个可执行文件启动的两个进程对该数据段的任何修改都会影响另一个进程。 我将上面的测试程序重写为匿名文件映射:

复制文件显示内存不足_复制文件存储空间不足_发现内存复制错误

在执行程序之前,需要先释放之前的缓存,否则会影响结果。

echo 1 >> /proc/sys/vm/drop_caches

然后执行程序并查看内存使用情况:

发现内存复制错误_复制文件显示内存不足_复制文件存储空间不足

对比use前后,可以发现used和buff/cache各增加了1G,说明在进行私有文件映射时,首先将文件映射到cache中,然后如果有文件修改了该文件,则将其映射到cache中。将要从其他内存中分配一块内存时,首先将文件数据复制到新分配的内存中,然后在新分配的内存上进行修改。 这也是写时复制。

这也很容易理解,因为如果同一个可执行文件打开多个实例,内核首先将可执行数据段映射到缓存中,然后如果数据段被修改,每个实例都会分配一块内存存储。 数据段,毕竟数据段也是进程私有的。

通过上面的分析可以得出,如果是文件映射,则将文件映射到缓存中,然后根据是共享还是私有进行不同的操作。

3.3 私有匿名映射

bbs段、堆、栈都是匿名映射,因为可执行文件中没有对应的段,必须是私有映射。 否则,如果当前进程fork出一个子进程,则父进程和子进程将共享这些段。 一个修改都会互相影响,这是不合理的。

ok,现在我把上面的测试程序改成私有匿名映射

发现内存复制错误_复制文件显示内存不足_复制文件存储空间不足

现在让我们看一下内存使用情况。

发现内存复制错误_复制文件存储空间不足_复制文件显示内存不足

我们可以看到只有used增加了1G,但是buff/cache并没有增加。 这意味着在执行匿名私有映射时,不占用缓存。 事实上,这是有道理的,因为只有当前进程正在使用这个块。 内存,没有必要占用宝贵的缓存。

3.4 共享匿名映射

当我们需要在父子进程之间共享内存时,可以使用mmap来共享匿名映射。 那么共享匿名映射的内存存储在哪里呢? 我继续重写上面的测试程序来共享匿名地图。

复制文件存储空间不足_发现内存复制错误_复制文件显示内存不足

现在我们来看看内存使用情况:

复制文件显示内存不足_复制文件存储空间不足_发现内存复制错误

从上面的结果可以看出,仅buff/cache增加了1G,即进行共享匿名映射时,向cache请求内存。 原因也很明显,因为父子进程共享这块内存,共享匿名映射。 存在于缓存中,然后各个进程映射到彼此的虚拟内存空间,这样就可以操作同一个内存了。

4.系统回收内存

当系统内存不足时,释放内存有两种方式,一种是手动,一种是系统本身触发的内存回收。 我们先看一下手动触发方式。

4.1 手动回收内存

手动回收内存之前已经演示过,即

echo 1 >> /proc/sys/vm/drop_caches

我们可以在man proc下看到对此的介绍

复制文件存储空间不足_发现内存复制错误_复制文件显示内存不足

从这个介绍可以看出,当drop_caches文件为1时,pagecache中可释放的部分就会被释放(有些cache不能通过这个释放)。 当drop_caches为2时,dentry和inode缓存将被释放。 当 drop_caches 为 3 时,会同时释放上述两项。

关键是最后一句,意思是如果pagecache中有脏数据,则drop_caches操作无法释放。 必须通过sync命令将脏数据刷新到磁盘,然后才能通过drop_caches操作释放pagecache。

ok,之前提到过有些pagecache是​​无法通过drop_caches释放的,那么除了上面提到的文件映射和共享匿名映射之外,pagecache中还存在哪些东西呢?

4.2 临时文件系统

我们先看一下tmpfs。 与 procfs、sysfs 和 ramfs 一样,tmpfs 是基于内存的文件系统。 tmpfs 和 ramfs 的区别在于 ramfs 文件是基于纯内存的。 除了纯内存之外,tmpfs 还使用了 swap。 space,而ramfs可能会耗尽内存,而tmpfs可以限制内存大小。 可以使用命令 df -T -h 查看系统中的一些文件系统,其中有一些是tmpfs,比较出名的就是目录/dev/shm

tmpfs文件系统的源文件在内核源代码mm/shmem.c中。 tmpfs的实现非常复杂。 虚拟文件系统之前已经介绍过。 基于 tmpfs 文件系统创建文件与其他基于磁盘的文件系统相同。 还有 inode、super_block 和 identry。 文件与其他结构的区别主要在于读写,因为读写只涉及文件载体是内存还是磁盘。

至于tmpfs文件读取函数shmem_file_read,其过程主要是通过inode结构体找到address_space地址空间,实际上就是磁盘文件的pagecache,然后通过读取offset来定位cache页和页偏移量。

这时我们可以通过函数__copy_to_user直接将缓存页中的数据从这个pagecache复制到用户空间。 当我们要读取的数据不在pagecache中时,我们必须判断它是否在swap中。 如果是,则先交换内存页。 入,然后阅读。

写入tmpfs文件的shmem_file_write函数主要判断要写入的页面是否在内存中。 如果是,则通过函数__copy_from_user将用户态数据直接复制到内核pagecache中覆盖旧数据,并标记为dirty。

如果要写入的数据已经不在内存中,则判断是否在swap中。 如果是,先读出,用新数据覆盖旧数据,标记为脏。 如果不在内存或磁盘中,则生成新的pagecache。 存储用户数据。

从上面的分析我们知道基于tmpfs的文件也是使用cache的。 我们可以在/dev/shm上创建一个文件来检测:

发现内存复制错误_复制文件显示内存不足_复制文件存储空间不足

可以看到,缓存增长了1G,这验证了tmpfs确实使用了缓存内存。

其实mmap匿名映射原理也是利用了tmpfs。 mm/mmap.c->do_mmap_pgoff函数内部判断,如果文件结构为空且SHARED映射,则调用shmem_zero_setup(vma)函数在tmpfs上创建新文件。

Linux内存分配与回收

复制文件显示内存不足_发现内存复制错误_复制文件存储空间不足

这就解释了为什么共享匿名映射内存被初始化为0,但是我们知道用mmap分配的内存被初始化为0,也就是说mmap私有匿名映射也是0,那么体现在哪里呢?

这在do_mmap_pgoff函数内部没有体现出来,而是发生了缺页异常,然后分配了一个初始化为0的特殊页。

那么这个tmpfs占用的内存页可以回收吗?

发现内存复制错误_复制文件存储空间不足_复制文件显示内存不足

也就是说,tmpfs文件占用的pagecache无法回收。 原因很明显。 因为有文件引用这些页面,所以无法回收它们。

4.3 共享内存

posix共享内存实际上与mmap共享映射相同。 它们都使用 tmpfs 文件系统上的新文件,然后将其映射到用户模式。 最终,两个进程操作的是同一个物理内存。 那么System V共享内存也使用吗? tmpfs 文件系统?

我们可以追踪以下函数

发现内存复制错误_复制文件存储空间不足_复制文件显示内存不足

该函数是创建一个新的共享内存段,其中函数

shmem_kernel_file_setup

就是在tmpfs文件系统上创建一个文件,然后通过这个内存文件实现进程通信。 我不会写测试程序,而且这个不能回收,因为共享内存IPC机制生命周期依赖于内核,也就是说你创建共享内存后,如果不显式删除的话,共享内存仍然会进程退出后存在。

之前看过一些技术博客,提到Poxic和System V的两套IPC机制(消息队列、信号量和共享内存)都使用了tmpfs文件系统,也就是说最终内存使用的是pagecache,但是我在源码中可以看到两个共享内存是基于tmpfs文件系统的,其他的信号量和消息队列还没见过(待会儿研究)。

posix消息队列的实现有点类似于pipe的实现。 它也有自己的mqueue文件系统,然后将消息队列属性mqueue_inode_info挂在inode上的i_private上。 关于这个属性,在内核2.6中,使用数组来存储消息。 ,而在4.6中,红黑树用于存储消息(我下载了这两个版本,但没有详细介绍我什么时候开始使用红黑树的)。

然后两个进程的每次操作都会操作这个mqueue_inode_info中的消息数组或者红黑树,从而实现进程通信。 与这个mqueue_inode_info类似的还有tmpfs文件系统属性shmem_inode_info和为epoll服务的文件系统eventloop,也有一个特殊的属性。 struct eventpoll,这是挂在文件结构中的private_data等。

说到这里,我们可以总结一下,进程空间中的代码段、数据段、动态链接库(共享文件映射)、mmap共享匿名映射都存在于缓存中,但是这些内存页是被进程引用的,所以他们不能被释放。 是的,基于tmpfs的ipc进程间通信机制的生命周期是跟内核在一起的,所以不能通过drop_caches来释放。

上面提到的缓存虽然不能释放,但是后面提到当内存不足时,可以将内存换出。

因此,drop_caches能释放的是从磁盘读取文件时以及进程将文件映射到内存后进程退出时的缓存页。 此时映射文件的缓存页如果没有被引用也可以释放。 。

4.4 自动内存释放方法

当系统内存不够的时候,操作系统有一种机制可以自组织内存,释放尽可能多的内存。 如果这个机制不能释放足够的内存,那么就只能发生OOM。

之前提到OOM的时候,说过redis是因为OOM而被杀掉的,如下:

复制文件显示内存不足_复制文件存储空间不足_发现内存复制错误

第二句话的后半部分,

总计 vm:186660kB,匿名 rss:9388kB,文件 rss:4kB

进程的内存使用情况用三个属性来描述,即所有虚拟内存、常驻内存匿名映射页和常驻内存文件映射页。

其实从上面的分析我们也可以知道,一个进程其实就是文件映射和匿名映射:

事实上,内核是根据文件映射和匿名映射来回收内存的。 mmzone.h中有如下定义:

复制文件存储空间不足_发现内存复制错误_复制文件显示内存不足

LRU_UNEVICTABLE 是不能被驱逐的页列表。 我的理解是,当调用mlock时,内存被锁定,页表不允许被系统换出。

下面简单说一下Linux内核自动内存回收的原理。 内核中有一个kswapd会定期检查内存使用情况。 如果发现空闲内存设置在pages_low,kswapd就会扫描lru_list中的前4个lru队列,并在活动链表中找到未使用的内存。 活动页面并添加非活动链接列表。

然后遍历inactive链表,通过回收将32个页面一页一页地释放,直到空闲页数达到pages_high。 不同页面的回收方式不同。

当然,当内存水平低于某个限制阈值时,会直接发出内存回收。 原理和kswapd一样,只不过这次的回收强度更大,需要回收更多的内存。

文档页面:

如果是脏页,则直接写回磁盘,然后回收内​​存。

如果不是脏页,就直接释放回收,因为如果是IO读缓存,就直接释放。 下次读取时会出现缺页异常,可以直接读回磁盘。 如果是文件映射页,则直接释放。 下次访问时,还会产生两个缺页异常,一个是将文件内容读入磁盘,另一个是与进程虚拟内存关联。

匿名页:因为匿名页没有地方写回,如果释放了,数据就找不到了。 因此,匿名页通过换出到磁盘的方式被回收,并在页表项中做一个标记。 下次发生页面错误时,从磁盘换入内存。

Swap的换入换出实际上占用了大量的系统IO。 如果系统内存需求突然快速增加,CPU就会被IO占用,系统就会卡住,导致无法对外提供服务。 因此,系统提供了一个参数来设置当前进行内存回收时,回收cache和swap匿名页。 这个参数是:

复制文件显示内存不足_发现内存复制错误_复制文件存储空间不足

这意味着该值越高,越有可能使用swap来回收内存。 最大值为100。如果设置为0,将通过回收cache尽可能地释放内存。

5. 总结

这篇文章主要写Linux内存管理相关的东西:

首先是进程地址空间的回顾;

其次,当进程消耗大量内存导致内存不足时,我们可以有两种方法:第一种是手动回收缓存;第二种是手动回收缓存。 另一种是利用系统后台线程swap来进行内存回收。

最后,当请求的内存大于系统剩余内存时,才会发生OOM,杀死进程,释放内存。 从这个过程中,我们可以看到系统为了释放足够的内存付出了多大的努力。

复制文件显示内存不足_发现内存复制错误_复制文件存储空间不足

复制文件存储空间不足_复制文件显示内存不足_发现内存复制错误

###答案1:libcurl是一个功能强大的开源网络传输库,支持多种协议和数据传输方式,广泛应用于各种编程语言和操作系统。 关于64位版本的libcurl,这里有一些关键信息:首先,64位版本的libcurl可以在64位操作系统上使用,这意味着它可以更好地利用计算机的硬件资源,包括更大的内存地址空间和更多可用的处理器寄存器。 它还可以处理比 32 位版本更大的文件和数据。 其次,64位版本的libcurl和32位版本在功能和性能上没有本质区别。 它提供相同的一组API和功能,可以执行各种网络传输操作,例如HTTP请求、FTP文件传输、SMTP电子邮件发送等。64位版本在处理更大的数据量时更有效,并提供更好的性能和可扩展性。 此外,64 位版本的 libcurl 通常随编程语言和操作系统一起分发。 无论您使用 C、C++、Python 还是其他语言,您都会找到适用于 64 位操作系统的版本。 此外,它还可以在Windows、macOS、Linux等常见操作系统上使用。 总而言之,64位版本的libcurl是一个强大且可靠的适合64位操作系统的网络传输库。 它可以提供更大的内存地址空间和更好的性能,并支持各种传输协议和操作系统。

Linux内存分配与回收

无论您是通过 C、C++ 还是其他编程语言使用 libcurl,您都应该选择适用于 64 位操作系统的版本,以获得更好的性能和功能。 ###答案2:libcurl是一个开源的多协议文件传输库,支持各种常见的网络协议和模拟器,包括HTTP、HTTPS、FTP、SMTP等。它是用C语言编写的,运行在不同的操作系统上,并且可以与多种编程语言集成。 关于64位版本的libcurl,指的是64位版本的libcurl库。 64位版本的libcurl主要是为了充分利用64位操作系统的优点,比如更大的内存寻址空间、更高的性能等。使用64位版本的libcurl有以下优点: 1更大的内存寻址空间:64位应用程序可以使用更大的内存空间,因此可以处理更大的数据集,特别是对于需要处理大量数据的应用程序。 非常重要。 2、性能更高:由于64位处理器的数据通道较宽,寄存器较大,64位版本的libcurl可以在一次计算中处理更多的数据,从而提高程序的运行速度和性能。 3.支持更大的文件:传统的32位系统对单个文件的大小有限制,而64位版本的libcurl可以处理更大的文件,使文件传输更加灵活和高效。

电脑系统可以装u盘么_电脑可以安装系统吗_电脑64位能装32位系统

综上所述,64位版本的libcurl在性能和功能上比32位版本更加强大和灵活。 能够更好地满足大型应用处理大量数据和高性能的需求。 因此,在选择使用libcurl时,根据具体需求,可以考虑使用64位版本,以优化程序的性能和功能。 ###答案3:libcurl是一个开源的网络传输库,可以使用多种编程语言,支持多种网络协议。 libcurl提供了一组简单易用的API,可以用来发送和接收网络请求,实现下载网页、上传下载文件等操作。 在 64 位系统中使用 libcurl 时,您需要安装 64 位版本的 libcurl 库并将其链接到您的编程环境。 64位版本的libcurl和32位版本在功能上没有太大区别,都可以满足网络传输的需求。 使用64位版本的libcurl可以带来以下优势: 1.内存管理:64位系统可以利用更大的内存空间,并提供更好的性能和稳定性。 64位版本的libcurl在处理大量数据时可以更好地利用系统的内存资源。 2.大文件支持:64位版本的libcurl可以处理更大的文件,并提供更大的文件传输能力。 这对于需要传输大文件的应用程序很重要。 3、数据类型:64位系统可以支持更广泛的数据类型,使得数据传输更加灵活高效。 4.性能优化:64位版本的libcurl针对64位系统进行了性能优化,可以更好地利用系统资源,提供更高效的网络传输。 总之,libcurl在64位系统中的应用与在32位系统中的应用没有太大区别,但是通过使用64位版本的libcurl,我们可以更好地利用系统资源,提供更好的性能和功能。

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]