这一次,我们了解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等性能工具,进行分析性能