这一次,我们了解CPU的另一个模块,内存

同CPU管理一样,内存管理也是操作系统的核心功能之一,内存主要用于存储系统和应用程序的指令,数据,缓存等

那么,Linux如何管理的内存呢?必然不可能让应用程序直接操作物理内存,为了增加一个界限,Linux中增加了内存映射,我们拿一个笔记本来举例说明,一个笔记本的内存是8GB的,那么8GB就是物理内存,物理内存也被称为主存,大多数的主存都是动态随机访问内存 DRAM,直接操作DRAM的是内核,那么 进程要访问内存的话,该怎么办呢?

Linux给每个进程都提供了一个独立的虚拟地址空间,并且这个空间是连续的,那么这个进程就可以很方便的访问内存,或者说是虚拟内存

虚拟地址空间的内部分为了内核空间和用户空间,不同字长的处理器的地址空间的范围也不相同,常见的是32位和64位系统,对应的虚拟地址空间如下

图片

32位系统的内核空间占用1G,处于最高处,剩下的3G是用户空间,64位系统的内核空间和用户空间都是128T,占用了整个内存空间的最高和最低处,剩下的中间部分是未定义的

进程的用户态和内核态就对应的不同的空间内存,进入内核态之后,才可以访问内核空间,但是内核空间之间都是关联的相同的物理内存,进程切换到内核态之后,就可以很方便的访问内核空间内存

那么为了避免虚拟内存过大的问题,并不是所有的虚拟内存都会分配物理内存,只有实际使用的虚拟内存才会分配物理内存,分配后的物理内存,是通过内存映射管理的

内存映射就是将虚拟内存地址映射到物理内存地址,为了完成内存映射,内核为每个进程都维护了一个页表,记录了虚拟地址和物理地址的映射关系,基本如下

图片

页表实际上存储在CPU的内存管理单元MMU中,正常情况下,处理器就可以通过硬件,找到要访问的内存

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

我们也曾经说过,TLB会影响CPU的内存访问性能

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

MMU的最小管理单元是4KB,每一次的内存映射,需要关联4KB及整数倍的内存空间

页的大小只有4KB,导致另一个问题就是整个页表会变得非常大

比如,32位系统需要100多万个页表项 4GB/4KB,才能实现整个地址空间的映射,为了解决页表项过多的,Linux提供了两种机制,就是多级页表和大页 HugePage

多级页表就是将内存分为区块来进行管理,将原来的映射关系改为区块索引和区块内的偏移

由于虚拟内存空间只用了很少的一部分,多级页表只保存这些使用中的区块,大大的减少页表的项数

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

图片

最后是大页,就是比普通也更大的内存块,常见的有2MB和1GB,大页通常用使用大量内存的进程上,就是Oracle DPDK

在这些机制,页表的映射下,进程就可以使用虚拟地址来访问物理内存了

具体的虚拟内存空间分布,即用户空间内存

上面的内核空间不用讲,下面的用户空间内存,分为了多个不同的段,以32位系统为例,我们看对应的关系

图片

用户空间内存,从低到高分别是五种不同的内存段

1.只读段,包括代码和变量等

2.数据段,包括全局变量等

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

4.文件映射段,包括动态库,共享内存等,从高往低增长

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

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

64位的系统和内存分布类似,不过内存空间大的多,如何分配的呢?

malloc()是C标准款提供的内存分配函数,对应到系统调用上,有两种实现方式,即brk()和mmap()

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

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

brk()方式的缓存,就可以减少缺页异常的发生,提高内存访问效率,不过由于内存没有归还系统,在内存工作繁忙的时候,频繁的内存释放和分配会造成内存碎片

mmap()方式分配的内存,会在释放的时候直接归还系统,导致每次mmap都发生缺页异常.所以适合不频繁的内存分配

如果不到1K的时候,如何分配内存呢?

用户空间,malloc通过brk()分配内存,在释放之后不会立刻归还系统,而是缓存起来重复利用,内核空间,Linux通过slab分配器管理小内存,可以将slab看做构建在伙伴系统上的一个缓存

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

而且,如果系统发现内存紧张的时候,也会通过一系列的机制,来回收内存,比如如下的方式

回收缓存 使用LRU算法,回收最近使用最少的内存页面

回收不常访问的内存,将不常用的内存通过交换分区写到磁盘中

杀死进程,内存紧张的时候还会通过OOM ,直接杀死占用大量内存的进程

回收不常访问的内存的时候,会用到SWAP,就是将磁盘当做内存来用的技术

将进程暂时不用的数据存储到磁盘中,当进程访问这些内存的时候,再从磁盘将这些数据读入到内存中

Swap将系统的可用内存变大了,不过一般只在内存不足的时候,才会发生Swap交换,磁盘读写的速度远比内存慢,Swap会导致内存性能问题

还有就是OOM,Out of Memory,内核的一种保护机制,监控进程的内存使用情况,使用oom_score来为每个进程内存的使用情况进行评分

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

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

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

为了实际的工作需要,,可以通过设置/proc,来调整oom_adj,从而调整 oom_score

oom_adj的范围是 [-17,15]数值越大,说明进程越容易被OOM杀死,数值越小,表示进程也不容易被杀死,-17表明禁止OOM

下面就是一个调整oom_adj的实例

echo -16 > /proc/$(pidof sshd)/oom_adj

最后是介绍几个内存空间的使用和分布,以及内存的分配和回收

在实际系统中,我们如果想要查看系统内存使用情况,可以使用如下的一些工具

比如free工具,下面就是一个free的输出实例

# 注意不同版本的free输出可能会有所不同

$ free

total        used        free      shared  buff/cache   available

Mem:        8169348      263524     6875352         668     1030472     7611064

Swap:             0           0           0

free输出的是一个表格,数值默认为字节单位,表格总共有两行六列,分别是物理内存Mem和交换分区Swap的使用情况,每列的含义分别为

total 总内存大小

used 已使用的内存大小,包含了共享内存

free 未使用内存的大小

shared 共享内存的大小

buff/cache 缓存和缓冲区的大小

available是新进程可用内存的大小

available不仅包括未使用内存,还包括了可回收的缓存,所以一般比未使用的内存更大,不过,并不是所有的缓存都可以回收,有些缓存在使用中

free显示的是整个系统内存的使用情况,查看进程的内存使用情况,可以使用top或者ps等工具

# 按下M切换到内存排序

$ top

KiB Mem :  8169348 total,  6871440 free,   267096 used,  1030812 buff/cache

KiB Swap:        0 total,        0 free,        0 used.  7607492 avail Mem

PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND

430 root      19  -1  122360  35588  23748 S   0.0  0.4   0:32.17 systemd-journal

1075 root      20   0  771860  22744  11368 S   0.0  0.3   0:38.89 snapd

1048 root      20   0  170904  17292   9488 S   0.0  0.2   0:00.24 networkd-dispat

1 root      20   0   78020   9156   6644 S   0.0  0.1   0:22.92 systemd

12376 azure     20   0   76632   7456   6420 S   0.0  0.1   0:00.01 systemd

12374 root      20   0  107984   7312   6304 S   0.0  0.1   0:00.00 sshd

top输出界面的顶端,显示了系统整体的内存使用情况,这些数据和free类似,不用重复解释,我们看如下的内容,和内存相关的几列数据,比如VIRT RES SHR 以及 %MEM等

VIRT是进程虚拟内存的大小,进程申请过的内存,还没有分配真正的物理内存,也会计算在内

RES是常驻内存的大小,是进程实际使用的物理内存大小,不包括Swap和共享内存

SHR是共享内存的大小,与其他进程共同使用的共享内存,加载动态链接库和程序的代码段

%MEM是进程使用物理内存占系统总内存的百分比

最后查看,top的时候需要注意,虚拟内存不会全部分配物理内存,上面的输出中可以看出每个进程的虚拟内存都比常驻内存大得多

第二,共享内存SHR不一定是共享的,程序的代码段,非共享的动态链接库,也都算在了SHR中,SHR中包括了进程之间真正共享的内存,计算多个进程内存使用时,不要讲所有的SHR相加得出结果

总结一下,Linux的内存,看到是内核提供的虚拟内存,还需要通过页表,将其转换为物理内存

进程通过malloc()申请内存后,内存不会立刻分配,首次访问时候,才会进行分配

Linux还提供了一系列的机制,应对内存不足的问题,比如缓存的回收,交换分区Swap,OOM等

除此外,还可以使用free,top ps等性能工具,进行分析性能

发表评论

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