Linux 内存是怎么工作的?

内存管理是操作系统最核心的功能之一。内存主要用来存储系统和应用程序的指令,数据,缓存等。

内存映射

   比如说我现在电脑的内存是8GB,这个内存容量是8GB,指的是我的内存条的容量的大小。指的是物理内存。物理内存也称为主存,大多数计算机用的主存都是动态随机访问内存(DRAM dynamic random access memory)。只有内核才可以直接访问物理内存。那么,进程要访问内存时,该怎么办呢?

   

   linux 内核给每个进程都提供了一个独立的虚拟地址空间。并且这个地址空间是连续的。这样,进程就可以很方便的访问内存了,更确切的说是访问虚拟内存。

   

   虚拟地址空间的内部又被分为**内核空间**和**用户空间**两部分,不同字长(也就是单个 CPU 指令可以处理数据的最大长度)的处理器,地址空间的范围也不同。比如最常见的 32 位和 64 位系统,我画了两张图来分别表示它们的虚拟地址空间,如下所示:

   image.png

通过这里可以看出,32 位系统的内核空间占用 1G,位于最高处,剩下的 3G 是用户空间。而 64 位系统的内核空间和用户空间都是 128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。

还记得进程的用户态和内核态吗?进程在用户态时,只能访问用户空间内存;只有进入内核态后,才可以访问内核空间内存。虽然每个进程的地址空间都包含了内核空间,但这些内核空间,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。

内核态与用户态

内核态:当进程运行在内核空间时就处于内核态

用户态:而进程运行在用户空间时就处于用户态

在内核态下,进程运行在内核地址空间中,此时 CPU 可以执行任何指令。运行的代码也不受任何的限制,可以自由地访问任何有效地址,也可以直接进行端口的访问。

在用户态下,进程运行在用户地址空间中,被执行的代码要受到 CPU 的诸多检查,它们只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址,且只能对任务状态段(TSS)中 I/O 许可位图(I/O Permission Bitmap)中规定的可访问端口进行直接访问。

      既然每个进程都有一个这么大的地址空间,那么所有进程的虚拟内存加起来,自然要比实际的物理内存大得多。所以,并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才分配物理内存,并且分配后的物理内存,是通过内存映射来管理的。

内存映射,其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系,如下图所示:

image.png

页表实际上存储在 CPU 的内存管理单元 MMU(Memory Management Unit) 中,这样,正常情况下,处理器就可以直接通过硬件,找出要访问的内存。

而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核分配物理内存,更新进程页表,最后再返回用户空间,恢复系统的运行。

缺页异常:有些地方翻译成缺页中断,缺页异常才更合适,英文叫做“Page Fault

指的是当软件试图访问已映射在虚拟地址空间中,但是当前并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元所发出的中断。---维基百科

什么是page fault?

当进程访问它的虚拟地址空间中的PAGE时,如果这个PAGE目前还不在物理内存中,此时CPU是不能干活的, 

Linux会产生一个hard page fault中断。 

系统需要从慢速设备(如磁盘)将对应的数据PAGE读入物理内存,并建立物理内存地址与虚拟地址空间PAGE的映射关系。 

然后进程才能访问这部分虚拟地址空间的内存。

page fault 又分为几种,major page fault、 minor page fault、 invalid(segment fault)。

major page fault也称为hard page fault, 指需要访问的内存不在虚拟地址空间,也不在物理内存中,需要从慢速设备载入。从swap回到物理内存也是hard page fault。

minor page fault也称为soft page fault, 指需要访问的内存不在虚拟地址空间,但是在物理内存中,只需要MMU建立物理内存和虚拟地址空间的映射关系即可。 

(通常是多个进程访问同一个共享内存中的数据,可能某些进程还没有建立起映射关系,所以访问时会出现soft page fault)

invalid fault也称为segment fault, 指进程需要访问的内存地址不在它的虚拟地址空间范围内,属于越界访问,内核会报segment fault错误。

TLB(Translation Lookaside Buffer 转译后备缓冲区) 页表缓存。是CPU的一种缓存,由MMU用于改进虚拟地址到物理地址的转译速度。

TLB具有固定数目的空间槽,用于存放将虚拟地址映射至物理地址的标签页表条目。其搜索关键字为虚拟内存地址,其搜索结果为物理地址。如果请求的虚拟地址在TLB中存在,CAM (content-addressable memory 相联存储器)将给出一个非常快速的匹配结果,之后就可以使用得到的物理地址访问存储器。如果请求的虚拟地址不在 TLB 中,就会使用标签页表进行虚实地址转换,而标签页表的访问速度比TLB慢很多。

TLB 其实就是 MMU 中页表的高速缓存。由于进程的虚拟地址空间是独立的,而 TLB 的访问速度又比 MMU 快得多,所以,通过减少进程的上下文切换,减少 TLB 的刷新次数,就可以提高 TLB 缓存的使用率,进而提高 CPU 的内存访问性能。

MMU的规定了一个内存映射的最小单位,也就是页,通常是4KB 大小。这样每次内存映射都需要关联4Kb或者4KB整数倍的内存空间。

(物理内存中对应的单位称为页帧) 他们俩大小总是一样的

 

页的大小只有 4 KB ,导致的另一个问题就是,整个页表会变得非常大。比方说,仅 32 位系统就需要 100 多万个页表项(4GB内存/4KB页大小),才可以实现整个地址空间的映射。为了解决页表项过多的问题,Linux 提供了两种机制,也就是多级页表和大页(HugePage)。(可以尝试理解 页表就像mysql的表,而页就是其中一行)

多级页表

多级页表就是把内存分成区块来管理,将原来的映射关系改成区块索引和区块内的偏移(offset)。由于虚拟内存空间通常只用了很少一部分,那么,多级页表就只保存这些使用中的区块,这样就可以大大地减少页表的项数。

Linux 用的正是四级页表来管理内存页,如下图所示,虚拟地址被分为 5 个部分,前 4 个表项用于选择页,而最后一个索引表示页内偏移。

image.png

大页(Huge page):

在虚拟内存管理中,内核维护一个将虚拟内存地址映射到物理地址的表,对于每个页面操作,内核都需要加载相关的映射。

如果你的内存页很小,那么你需要加载的页就会很多,导致内核会加载更多的映射表。而这会降低性能。

使用“大内存页”,意味着所需要的页变少了。从而大大减少由内核加载的映射表的数量。并且所需的地址转换也减少了,TLB缓存失效的次数就减少了。这提高了内核级别的性能最终有利于应用程序的性能。

简而言之,通过启用“大内存页”,系统具只需要处理较少的页面映射表,从而减少访问/维护它们的开销!

另外,由于地址转换所需的信息一般保存在CPU的缓存中,huge page的使用让地址转换信息减少,从而减少了CPU缓存的使用,减轻了CPU缓存的压力,让CPU缓存能更多地用于应用程序的数据缓存,也能够在整体上提升系统的性能

假设 需要 32Mb 内存  标准页为4kb 假设一个页表只能装10个页,那么需要 32*1024/4 =8192个页 大概需要820个页表。如果提升标准页大小 比如1M,32/1=32/10=3.2 大约需要4个页表就能满足了

二、虚拟内存空间分布

    image.png

           32位系统

通过这张图你可以看到,用户空间内存,从低到高分别是五种不同的内存段。

1 只读段,包括代码和常量等。也可以叫做代码段

2 数据段,包括全局变量,静态变量的空间等

3 堆,包括动态分配的内存,从低地址开始向上增长。

4 文件映射段,包括动态库、共享内存等,从高地址开始向下增长。一般是mmap函数所分配的虚拟地址空间

5 栈 包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是8MB  查看方式ulimit -s

内核空间:

内核总是驻留在内存中,是操作系统的一部分。内核空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数

image.png

在这五个内存段中,堆和文件映射段的内存是动态分配的。比如说,使用 C 标准库的 malloc() 或者 mmap() ,就可以分别在堆和文件映射段动态分配内存。

对小块内存(小于 128K),C 标准库使用 brk() 来分配,也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。

而大块内存(大于 128K),则直接使用内存映射 mmap() 来分配,也就是在文件映射段找一块空闲内存分配出去。

了解这两种调用方式后,我们还需要清楚一点,那就是,当这两种调用发生后,其实并没有真正分配内存。这些内存,都只在首次访问时才分配,也就是通过缺页异常进入内核中,再由内核来分配内存。

你可能会想到一个问题,如果遇到比页更小的对象,比如不到 1K 的时候,该怎么分配内存呢?

实际系统运行中,确实有大量比页还小的对象,如果为它们也分配单独的页,那就太浪费内存了。

所以,在用户空间,malloc 通过 brk() 分配的内存,在释放时并不立即归还系统,而是缓存起来重复利用。在内核空间,Linux 则通过 slab 分配器来管理小内存。你可以把 slab 看成构建在系统上的一个缓存,主要作用就是分配并释放内核中的小对象。

对内存来说,如果只分配而不释放,就会造成内存泄漏,甚至会耗尽系统内存。所以,在应用程序用完内存后,还需要调用 free() 或 unmap() ,来释放这些不用的内存。

当然,系统也不会任由某个进程用完所有内存。在发现内存紧张时,系统就会通过一系列机制来回收内存,比如下面这三种方式:

1 回收缓存,比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面;

2 回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中;

3 杀死进程,内存紧张时系统还会通过 OOM(Out of Memory),直接杀掉占用大量内存的进程。

其中,第二种方式回收不常访问的内存时,会用到交换分区(以下简称 Swap)。Swap 其实就是把一块磁盘空间当成内存来用。它可以把进程暂时不用的数据存储到磁盘中(这个过程称为换出),当进程访问这些内存时,再从磁盘读取这些数据到内存中(这个过程称为换入)。

所以,你可以发现,Swap 把系统的可用内存变大了。不过要注意,通常只在内存不足时,才会发生 Swap 交换。并且由于磁盘读写的速度远比内存慢,Swap 会导致严重的内存性能问题。

第三种方式提到的  OOM(Out of Memory),其实是内核的一种保护机制。它监控进程的内存使用情况,并且使用 oom_score 为每个进程的内存使用情况进行评分:

一个进程消耗的内存越大,oom_score 就越大;

一个进程运行占用的 CPU 越多,oom_score 就越小。

这样,进程的 oom_score 越大,代表消耗的内存越多,也就越容易被 OOM 杀死,从而可以更好保护系统。

当然,为了实际工作的需要,管理员可以通过 /proc 文件系统,手动设置进程的 oom_adj ,从而调整进程的 oom_score。

cat /proc/$pid(具体进程的id)/oom_score

cat /proc/42909/oom_score

这个42909 是php-fpm的oom_score 显示是1  oom_adj 的范围 [-17,15] 数值越大,表示进程越容易被OOM 杀死,数值越小,表示进程越不容易被OOM杀死,其中-17 表示禁止OOM

echo -16 > /proc/$(pidof sshd)/oom_adj   调低进程的oom_score

总结:

内存映射:其实就是将虚拟内存地址映射到物理内存地址。

页:MMU的内存映射的最小单位。也叫逻辑页。大小是4KB

页表: page table 页表是一种特殊的数据结构,放在系统空间的页表区,存放逻辑页与物理页帧的对应关系。

页帧:物理内存中跟逻辑页对应的单位称为页帧。

缺页异常:page fault 分为三种,major page fault、 minor page fault、 invalid(segment fault)。

TLB:转译后备缓冲区--是CPU的一种页表缓存。用来提高虚拟地址转换为物理地址的转换速度。

多级页表:多级页表用来减少页表的项数。

大页:减少由内核加载的映射表的数量。相应的所需的地址转换也减少了,TLB缓存失效的次数就减少了。

虚拟空间分布:

1 只读段,包括代码和常量等。也可以叫做代码段

2 数据段,包括全局变量,静态变量的空间等

3 堆,包括动态分配的内存,从低地址开始向上增长。

4 文件映射段,包括动态库、共享内存等,从高地址开始向下增长。一般是mmap函数所分配的虚拟地址空间

5 栈 包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是8MB  

6 内核空间:内核空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数。

内存分配:内存分配有 brk  mmap  内存释放有 free 和 unmap

内存回收方式:LRU 算法,swap,oom.

这才是个开始~~ 漫漫长路要走很久的。技术的路上可能会很枯燥,沉下去心。

参考资料:

https://www.linuxprobe.com/linux-kernel-user-space.htmL

https://blog.csdn.net/forDreamYue/article/details/78887035 多级页表原理

https://yq.aliyun.com/articles/55820  page fault

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注