Linux内存管理
引言
根据上一节[[010104 - 存储层次]]介绍可以了解到系统分为寄存器,高速缓存,内存,外部存储,这几个组件的传输都比较简单易懂,其中我们会发现内存发挥了至关重要的作用,因为无论是高速缓存还是寄存器,他们提供的性能提升终究有限,如果所有的处理交给高速缓存和寄存器,那么成本和造价都是难以预估的。 所以针对内存的管理才是Linux核心和关键所在。
简单介绍
下面我们就来简单介绍Linux内存管理的,在Linux中内存管理可以大致理解为三个部分:
- 内核使用的内存
- 进程使用的内存
- 可用内存(空闲内存)
其中除开内核使用的内存维持系统正常运行不能被释放之外,其他均可以由操作系统自由支配。
在Linux中拥有free
命令来专门查看内存的使用情况,执行的效果类似如下:
/opt/app/tdev1$free total used free shared buffers cached Mem: 8175320 6159248 2016072 0 310208 5243680 -/+ buffers/cache: 605360 7569960 Swap: 6881272 16196 6865076
各个列的含义如下:
- total:系统搭载物理内存总量,比如上面为8G
- free:表面可用内存
- buff/cache:缓冲区缓存和页面缓存,在[[010104 - 存储层次]]中提到了当内存不够的适合可以使用释放这两个缓存腾出空间给内存使用。
- availiable:实际可以使用的内存,计算公式很简单 内核之外的可用总内存 - (free + buff/cache 最大可以释放的内存)。
除了列数据之外,可以看到还有一个swap的行数据,这个参数的含义将在后文进行介绍。
Linux除了free
命令之外,还有sar -r
命令,并且可以通过这个参数指定采集周期,比如-r 1
就是1秒采集一次。
下面是这两个命令的对应关系:
- total : 无对应
- free :kbememfree
- buff/cache :kbbufferrs + kbcached
- available:无对应
如果内存使用过多,那么系统空出内存,可能会出现强制kill某个进程的操作,这个操作是随机的,无法被监控,如果是个人电脑也许还可以接受,但是如果是商用机器上这种操作就十分危险了,所以有部分的商用机器会开启一旦OO M直接把整个系统强制关闭的操作。
另外Mac系统虽然类Unix系统但是没有free相关的命令,为此可以使用下面的命令进行简单的替代,但是不如free强大。
比如在Mac中使用top -l 1 | head -n 10
查看整体系统运行情况。
MacBook-Pro ~ % top -l 1 | head -n 10 Processes: 604 total, 2 running, 602 sleeping, 3387 threads 2022/04/15 17:29:57 Load Avg: 2.84, 3.27, 5.68 CPU usage: 6.8% user, 14.18% sys, 79.72% idle SharedLibs: 491M resident, 96M data, 48M linkedit. MemRegions: 168374 total, 5515M resident, 235M private, 2390M shared. PhysMem: 15G used (1852M wired), 246M unused. VM: 221T vsize, 3823M framework vsize, 0(0) swapins, 0(0) swapouts. Networks: packets: 312659/297M in, 230345/153M out. Disks: 788193/14G read, 161767/3167M written.
比如在Mac中使用使用diskutil list
:
```shell ~ > diskutil list /dev/disk0 (internal): #: TYPE NAME SIZE IDENTIFIER 0: GUID_partition_scheme 500.3 GB disk0 1: Apple_APFS_ISC 524.3 MB disk0s1 2: Apple_APFS Container disk3 494.4 GB disk0s2 3: Apple_APFS_Recovery 5.4 GB disk0s3 /dev/disk3 (synthesized): #: TYPE NAME SIZE IDENTIFIER 0: APFS Container Scheme - +494.4 GB disk3 Physical Store disk0s2 1: APFS Volume mysystem 15.2 GB disk3s1 2: APFS Snapshot com.apple.os.update-... 15.2 GB disk3s1s1 3: APFS Volume Preboot 529.6 MB disk3s2 4: APFS Volume Recovery 798.6 MB disk3s3 5: APFS Volume Data 455.3 GB disk3s5 6: APFS Volume VM 24.6 KB disk3s6 /dev/disk6 (external, physical): #: TYPE NAME SIZE IDENTIFIER 0: GUID_partition_scheme *512.1 GB disk6 1: Microsoft Basic Data 512.1 GB disk6s1 /dev/disk7 (external, physical): #: TYPE NAME SIZE IDENTIFIER 0: GUID_partition_scheme *1.0 TB disk7 1: Microsoft Basic Data Extreme SSD 1.0 TB disk7s1
简单系统内存分配
内核分配内存的时机大致有下面两种:
- 第一种是创建进程的时候。
- 第二种是创建进程之后进行动态内存分配的时候。
在进程创建之后,如果进程还需要内核提供更多的内存,则可以向内核发出内存的请求申请,内核收到指令之后,则划分可用内存并且把起始结束的地址给进程进行使用。 但是这种要一点给一点的方式有下面几个常见的问题:
- 难以执行多个任务。
- 访问其他用途的内存区域。
- 内存的碎片化。
内存不仅仅需要和CPU通信还需要和其他的控制器和硬件打交道,分配内存给进程只是诸多任务的项目之一。
难以执行多任务可以理解为进程频繁的需要申请内存的情况,这时候内核需要不断的操作分配内存给进程,整个任务被单个进程给拖累了。另外如果多个任务出现分配内存的区域刚好相同,此时需要要完成内存分配给那个进程,则另一个进程等待也是可以理解的。
内存碎片化的原因是进程每次获取内存都需要了解这部分内容要涵盖多少区域否则就不能获取这些内存。 内存碎片化的另一个重大问题是明明有很多富裕的内存但是却拿不出一块完整连续的空间给进程使用,导致不断的回收和分配操作。
虚拟地址和物理地址
首先我们需要了解三个名次的概念:地址空间、虚拟地址、物理地址。 地址空间:指的是可以通过地址访问的范围都统称为地址空间。 虚拟地址:虚拟地址指的是进程无法直接访问到真实的物理内存地址,而是访问和实际内存地址映射的虚拟内存地址,目的是为了保护系统硬件安全。 物理地址:也就是我们实际内存对应的实际的物理地址。 这里举一个简单的例子:如果内核给进程分配一个100地址的虚拟内存地址,那么这个虚拟内存地址可能会指向实际的600物理地址。
页表 完成虚拟地址到物理地址的映射依靠的是页表,在虚拟内存当中所有的内存都被划分为页,一个页对应的数据条目叫做页表项,页表项记录物理地址到虚拟地址的映射关系。 在x86-64的架构当中,一个页的大小为4KB。上面提到了进程神奇的内存是有固定的起止地址的,那么如果出现超出地址的页访问,也就是访问了没有虚拟地址和物理地址映射的空间,会出现什么情况呢? 如果出现越界访问,那么此时CPU会出现缺页中断,并且终止在缺页中进行操作的进程指令,同时启动内核的中断处理机构处理。
虚拟内存的分配操作
虚拟内存的分配操作步骤我们可以理解为几个核心的步骤:
- 内核寻找物理地址,并且把需要的物理地址空间计算之后,请求物理分页。
- 创建进程的页表把物理地址映射到虚拟地址。
- 如果进程需要动态内存管理,内核则会分配新页表并且分配新的可用内存给进程使用,当然同时提供对应的虚拟地址空间。
物理分页使用的是请求分页的方式进行处理,这个分配的操作十分复杂。
内存的上层分配
在经典的编程语言C语言中,分配内存的函数是malloc
函数,而Linux操作系统中用于分配内存的函数是mmap
函数,这两者最大区别是mmap
函数使用的是按页的方式分配,而malloc
是按照字节的方式分配。
glibc通过系统调用向mmap
申请大量的内存空间作为内存池,程序调用malloc
则根据内存池分配出具体的内存使用,如果进程需要再次获取内存则需要再次通过mmap获取内存并且再次进行分配操作。
其实更为上层的编程语言也是使用了类似的操作,首先通过glibc向内核申请内存执行虚拟内存的分配操作,然后malloc
函数再去请求划分具体的内存使用,只不过更上层的语言使用了解析器和脚本进行掩盖而已,实际上通过层层翻译最终的操作依然是上面提到的操作。
虚拟内存是如何解决简单分配的问题的? 这里我们再次把上面三个问题搬出来,再解释虚拟内存是如何处理问题的:
- 难以执行多个任务:每个进程有独立的虚拟地址空间,所以可以编写专用地址空间程序防止多个任务阻塞等待的情况。
- 访问其他用途的内存区域:虚拟地址空间是进程独有,页表也是进程独有。页表的另一个作用是限制可以防止当前的进程访问到其他线程的页表和地址空间。
- 内存的碎片化:内存碎片化使用页表的方式进行分配,因为页表记录了物理地址到虚拟地址的映射,这样就可以很好的知道未使用的空间都干了啥。
虚拟内存的其他作用:
- 文件映射
- 请求分页
- 利用写时复制的方式快速创建进程
- 多级页表
- 标准大页
小结
这一部分简要阐述Linux内存管理的入门理解部分,这一部分主要介绍了简要的内存分配方式,以及Linux对此通过页表的方式实现物理地址和虚拟地址的分配,最后阐述了操作系统和编程语言也就是进程之间是如何分配内存的,具体的分配步骤和交互逻辑介绍。