29 Jan 2022
记录这篇文章的目的是为了linux的性能调优,所以并不会记录详细的理论细节,只会将必要的过程进行个人语言的简单概要。
linux中,只有内核可以直接访问物理内存。进程无法直接访问物理内存,linux内核给每个进程都分配了一个独立的虚拟内存地址空间。这样进程就可以通过访问虚拟内存来达到使用内存的目的。
虚拟内存地址空间分为两部分,内核空间和地址空间。如果我们把内存地址由高到低排列,那么以64位系统举例,低位的128T被分配为用户空间,而高位的128T被分配为内核空间,高位和低位中间的部分,为未定义的部分。
进程在用户态时,只能访问用户空间内存;只有进入内核态后,才可以访问内核空间内存。虽然每个进程的地址空间都包含了内核空间,但这些内核空间,其实关联的都是相同的物理内存。
系统为每一个进程都分配了这么大的地址空间,那么所有进程的虚拟内存加起来,会比实际的物理内存大特别多。所以,并不是所有的虚拟空间都会分配到实际的物理内存,只有被实际使用的虚拟内存才会分配到物理内存。这个过程是通过内存映射管理的。
所谓内存映射,其实就是虚拟内存地址映射到物理内存地址。内核会为每一个进程维护一张页表,记录虚拟内存地址和物理内存地址的映射关系。这些页表实际上存储在CPU的内存管理单元MMU中,CPU就可以直接通过硬件,找出要访问的内存。
缺页异常,指的是进程访问的虚拟地址在页表中查不到其与物理地址的映射,进而进入内核空间分配物理内存给这个虚拟地址,然后更新进程页表,最后再返回用户空间,恢复进程的运行。
MMU的单位是页,页的大小通常为4KB大小。如果每个页是4KB,那么会导致页表非常巨大。为了解决这个问题,linux提供了两种机制,多级页表和大页。多级页表就是把内存分成区块管理;大页就是比普通也更大的内存块,常见的大小有2MB和1GB。大页通常用在使用大量内存的进程上,比如Oracle等
虚拟内存空间地址由高到低顺序排布,由上至下分别为内核空间、栈、文件映射、堆、数据段、只读段。
上面五个内存段中,堆和文件映射段的内存是动态分布的。C标准库中的malloc()或者mmap(),就分别可以在堆和文件映射段中动态分配内存
malloc()是C标准库提供的内存分配函数,对应到系统调用上,有两种方式,即brk()和mmap()。
对小块内存(小于128K),会使用系统调用blk(),通过移动堆顶的方式在堆上分配。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样可以重复利用。
对于大块内存(大于128K),会使用内存映射mmap()在文件映射段分配空闲内存。
通过mmap()的参数,在文件关联性和映射区域是否共享两个维度
分为以下四种类型:
- 私有匿名映射,常用于内存分配
- 私有文件映射,常用于加载动态库
- 共享匿名映射,常用于进程间共享内存
- 共享文件映射,常用于内存映射IO,进程间通信
可通过
fd=-1和flags=MAP_ANONYMOUS | MAP_PRIVATE来分配私有匿名内存可通过
fd=-1和flags=MAP_ANONYMOUS | MAP_SHARED来分配共享匿名内存
以上两种方式各有优缺点,brk()方式的缓存,可以减少缺页异常的发生,提高内存访问率。不过由于内存并未归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。而mmap()方式分配的内存,会在释放的时候直接归还系统,所以每次mmap必然会发生缺页异常。在内存工作繁忙时,频繁的内存分配和释放会导致大量的缺页异常,使内核的管理负担增大。这也是malloc()只对大块内存使用mmap()的原因。
此外,需要注意的是,当两种调用发生后,并未真正的分配内存。只有在内存被首次访问时,才会通过触发缺页异常进入内核中,再由内核真正的分配内存。
如果遇到比页更小的对象,比如不到1K的时候,该怎么分配内存呢?
实际系统运行中,确实有大量比页还小的对象,如果为它们分配单独的页,那么就存在大量的系统浪费。所以在
用户空间,malloc()通过brk()分配的内存,在释放时并不立即归还系统,而是缓存起来重复利用。在内核空间,linux则通过slab分配器来管理小内存,其主要作用就是分配并释放内核中的小对象。
对内存来说,如果一直分配却不释放,就会造成内存泄漏,甚至会耗尽系统内存。所以,在进程用完内存后,需要调用free()或unmap()来释放不用的内存。
当然,系统也不会任由某个进程用完所有的内存。在发现内存紧张时,系统就会通过一系列机制来回收内存,比如:
回收缓存,比如使用LRU(Least Recently Used)算法,回收最近使用最少的内存页;
回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中。也就是使用所谓的swap(交换分区),swap其实就是把一块磁盘空间当做内存来用,可以把暂时用不到的内存数据存入到磁盘中(swap out),当进程需要访问这部分数据时,再将数据从磁盘读取到内存中(swap in)。使用swap分区,虽然可以增大系统的可用内存,但是会造成严重的性能问题,因为磁盘的读写速度远低于内存的写入速度。
杀死进程,内存紧张时系统还会通过OOM(Out of Memory),直接杀掉占用大量内存的进程。这是内核的一种保护机制,它监控各个进程的内存使用情况,并且使用oom_score为每个进程的内存使用情况进行评分,评分的规则如下:
可以通过
/proc/<pid>/oom_adj来手动调整OOM,取值范围为[-17,15],数值越大,表示越容易被OOM杀死,其中-17代表禁止OOM。