一、内存映射
内存管理也是操作系统最核心的功能之一,内存主要用来存储系统和应用程序的指令、数据、缓存等
1、我们通说的内存指的是物理内存还是虚拟内存?
我们通常说的内存容量,其实这指的是物理内存,物理内存也称为主存,大多数计算机用的主存都是动态随机访问内存(DRAM)。只有内核才可以直接访问物理内存。
那么,进程要访问内存时,该怎么办呢?
2、进程是如何访问内存的?
Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。这样,进程就可以很方便地访问内存,更确切地说是访问虚拟内存
3、虚拟内存的内核空间和用户空间分布图
4、进程是如何访问内核空间内存的?
进程在用户态时,只能访问用户内存;只有进入内核态后,才可以访问内核空间内存,虽然每个进程的地址空间都包含了内核空间,但这些内核空间,其实关联的都是想同的物理内存
这样、进程切换到内核态后,就可以很方便地访问内核空间内存
5、并不是所有的虚拟机内存都会分配物理内存
既然每个进程都有一个这么大的地址空间,那么所有进程的虚拟内存加起来,自然要比实际的物理内存大得多
所以并不是所有的虚拟机内存都会分配物理内存,只有那些实际使用的虚拟机内存才分配物理内存,并且分配后的物理内存,是通过内存映射来管理的
6、什么是内存映射?
内存映射,其实就是将虚拟内存地址映射到物理内存地址,为了完成内存映射,内核为每个进程都维护了一张页表,
记录虚拟地址与物理地址的映射关系,如下图所示:
页表实际上存储在 CPU 的内存管理单元 MMU 中,这样,正常情况下,处理器就可以直接通过硬件,找出要访问的内存
7、进程访问的虚拟地址在页表中查不到,怎么办?
当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间、恢复进程运行
8、什么是TLB?TLB的作用是什么
TLB 其实就是 MMU 中页表的高速缓存,由于进程的虚拟地址空间是独立的,而 TLB 的访问速度又比MMU 快得多,
所以,通过减少进程的上下文切换,减少 TLB 的刷新次数,就可以提高 TLB 缓存的使用率,进而提高 CPU 的内存访问性能。
9、MMU 是以什么为单位来管理内存?
不过要注意,MMU 并不以字节为单位来管理内存,而是规定了一个内存映射的最小单位,也就是页,通常是 4 KB大小,
这样,每一次内存映射,都需要关联 4 KB 或者 4KB 整数倍的内存空间
10、如何减少页表的项数
页的大小只有 4 KB ,导致的另一个问题就是,整个页表会变得非常大。比方说,仅 32 位系统就需要 100 多万个页表项(4GB/4KB),才可以实现整个地址空间的映射。
为了解决页表项过多的问题,Linux 提供了两种机制,也就是多级页表和大页(HugePage)。
11、多级页
多级页表就是把内存分成区块来管理,将原来的映射关系改成区块索引和区块内的偏移。由于虚拟内存空间通常只用了很少一部分,
那么多级页表就只保存这些使用中的区块,这样就可以大大地减少页表的项数
Linux 用的正是四级页表来管理内存页,如下图所示,虚拟地址被分为 5 个部分,前 4 个表项用于选择页,而最后一个索引表示页内偏移。
12、大页
再看大页,顾名思义,就是比普通页更大的内存块,常见的大小有 2MB 和 1GB。大页通常用在使用大量内存的的进程上,比如 Oracle、DPDK 等
通过这些机制,在页表的映射下,进程就可以通过虚拟地址来访问物理内存了。那么具体到一个 Linux 进程中,这些内存又是怎么使用的呢
二、虚拟内存空间分布
1、虚拟内存空间分布图
通过这张图你可以看到,用户空间内存,从低到高分别是五种不同的的内存段
2、虚拟内存空间分布详解
1、在这五个内存段中,堆和文件映射段的内存是动态分配的,比如说,使用 C 标准库的 malloc() 或者mmap() ,就可以分别在堆和文件映射段动态分配内存
2、其实 64 位系统的内存分布也类似,只不过内存空间要大得多,那么,更重要的问题来了,内存究竟是怎么分配的呢?
三、内存分配与回收
malloc() 是 C 标准库提供的内存分配函数,对应到系统调用上,有两种实现方式,即 brk() 和 mmap()。
1、内存分配
都只在首次访问时才分配,也就是通过缺页异常进入内核中,再由内内核来分配内存。
1、如何减少内存碎片?
整体来说,Linux 使用伙伴系统来管理内存分配。前面我们提到过,这些内存在 MMU 中以页为单位进行管理,伙伴系统也一系统也一样,以页为单位来管理内存,并且会通过相邻页的合并,
减少内存碎片化(比如 brk 方式造成的内存碎片)。
2、比页更小的内存(不到 1K ),该怎么分配内存呢?
实际系统运行中,确实有大量比页还小的对象如果为它们也分配单独的页,那就太浪费内存了。所以,在用户空间,malloc 通过 brk() 分配的内存,在释放时并不立即归还系统,
而是缓存起来重复利用。在内核空间,Linux 则通过 slab 分配器来管理小内存。你可以把slab 看成构建在伙伴系统上的一个缓存,主要作用就是分配并分配并释放内核中的小对象。
2、内存回收
对内存来说,如果只分配而不释放,就会造成内存泄漏,甚至会耗尽系统内存。所以,在应用程序用完内存后,还需要调用free() 或 unmap() ,来释放这些不用的内存。
当然,系统也不会任由某个进程用完所有内存,在发现内存紧张时,系统就会通过一系列机制来回收内存,比如下面下面这三种方式:
第三种方式提到的 OOM(Out of Memory),其实是内核的一种保护机制它监控进程的内存使用情况,并且使用 oom_score 为每个进程的内存使用情况进行评分:
一个进程消耗的内存越大,oom_score 就越大;
一个进程运行占用的 CPU 越多,oom_score 就越小
这样,进程的 oom_score 越大,代表消耗的内存越多,也就越容易被 OOM 杀死,从而可以更好保护系统。
当然,为了实际工作的需要,管理员可以通过 /proc 文件系统,手动设置进程的 oom_adj ,从而调整进程的 oom_score。
oom_adj 的范围是 [-17, 15],数值越大,表示进程越容易被 OOM 杀死;数值越小,表示进程越不容易被 OOM 杀死,其中 -17 表示禁止 OOM。
比如用下面的命令,你就可以把 sshd 进程的调小为 -16,这样, sshd 进程就不容易被 OOM 杀死
echo -16 > /proc/$(pidof sshd)/oom_adj
四、如何查看内存使用情况
1、free图解
[root@api ~]# free
total used free shared buff/cache available
Mem: 8010968 935180 5968260 16668 1107528 6835728
Swap: 4194300 0 4194300
这里尤其注意一下,最后一列的可用内存 available,available 不仅包含未使用内存,还包括了可回收的缓存,所以一般会比未使用内存更大。
不过,并不是所有缓存都可以回收,所以一般会比未使用内存更大
2、top图解
top - 11:52:35 up 16 days, 55 min, 1 user, load average: 0.00, 0.01, 0.05
Tasks: 101 total, 1 running, 100 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.1 us, 0.1 sy, 0.0 ni, 99.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 14.7/8010968 [||||||||||||||| ]
KiB Swap: 0.0/4194300 [ ] PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 41348 3580 2332 S 0.0 0.0 0:36.59 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd
3 root 20 0 0 0 0 S 0.0 0.0 0:00.00 ksoftirqd/0
VIRT 是进程虚拟内存的大小:只要是进程申请过的内存,即便还没有真正分配物理内存,也会计算在内。
RES 是常驻内存的大小:也就是进程实际使用的物理内存大小,但不包括 Swap 和共享内存。
SHR 是共享内存的大小:比如与其他进程共同使用的共享内存、加载的动态链接库以及程序的代码段等。
%MEM 是进程使用物理内存占系统总内存的百分比。
3、需要注意两点
第一:虚拟内存通常并不会全部分配物理内存。从上面的输出,你可以发现每个进程的虚拟内存都比常驻内存大得多。
第二:共享内存 SHR 并不一定是共享的,比方说,程序的代码、非共享的动态链接库,也都算在 SHR 里。当然,
SHR 也包括了进程间真正共享的内存。所以在计算多个进程的内存使用时,不要把所有进程的 SHR 直接相加得出结果。