linux是怎么管理内存的

内存是什么?

内存(英语:Double Data Rate Synchronous Dynamic Random Access Memory,简称DDR SDRAM)

全称:双倍数据率同步动态随机存取存储器

意思是:原来的 内存(SDRAM)在一个时钟周期内只传输一次数据,它是在时钟上升期进行数据传输;

而DDR则是一个时钟周期内可传输两次数据,也就是在时钟的上升期和下降期各传输一次数据。

内存的带宽是怎么计算的?

命名方式因内存技术而异,但对于商用DDR SDRAM , DDR2 SDRAMDDR3 SDRAM内存,总带宽是以下值之乘积:

  • 基本DRAM时钟频率: 一秒钟震荡的次数
  • 每时钟的数据传输次数:带DDR的内存,都是2,时钟震荡一次,数据传2次。比喻为双向车道
  • 内存总线带宽(位宽):每个DDR、DDR2或DDR3内存接口都是64位。有时也被称为1个“行”,传输一次数据的量是64bit,比喻为每个方向都有64个车道。
  • 接口数量 :电脑上的插口,如果你插了两个内存条,那就是2

例如,一个计算机有两个内存,它们都是时钟频率为400MHz的DDR2-800模块(这里的800是400x2,也就是数据传输速率,即一秒钟传输800次数据),则其理论最大内存带宽为:

每秒400,000,000个时钟×每个时钟2个传输次数×64位宽×2个接口=

每秒102,400,000,000(1024亿)比特(也就是12,800MB/s或12.8GB/s)

这个值是理论上的最大值,一般不会达到这个值。

再举个例子 :

上图的1066指的是数据传输速率,也就是时钟频率乘以传输次数的的值 1066MHZ。,根据1066,可以算出来它的时钟频率是533,然后代入上面的公式就能算出带宽。

pc3-8500U ,3是DDR3的意思,8500是带宽8500MB/s的意思。U是UDIMM的意思。说明这是台式机用的内存条。

内存大体可以分为: 服务器用的RDIMM台式电脑用的UDIMM,笔记本用的SO-DIMM。

https://zhuanlan.zhihu.com/p/26255460

1秒钟震荡533000000次×每震荡一次传输2次数据×1次数据的量是64bit=68224000000比特/s

除以8=8528000000B(8个bit比特=1B字节),除以1000=8528000KB,除以1000=8528MB

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

  • 内存是一个连续的线性结构,一个4G的内存有很多个电容,把他们线性排在一起,那么就有34359738367个可以存bit的空位,计算机一般把8个bit合成一个byte存放,那么就有4294967295个byte,写成16进制就是 0x0 ~ 0xFFFFFFFF个地址,这个就是内存地址了,每个地址里面可以取出一个字节的数据。

现在有0xFFFFFFFF个地址,人们是怎么利用操作系统去管理和分配这些地址给程序使用的呢?

  • 为了方便说明问题,我把内存地址用10进制表示,转成16进制也是一样的,16进制不太方便人脑计算。我不用4G的内存讲,因为太多了,用0到99的内存空间即可说明问题。我尽量用通俗的语言说明问题,没有很复杂的概念,复杂的概念请翻看《操作系统原理》。

虚拟内存地址与实际物理地址

  • 在说怎么管理内存之前,先要说一下虚拟内存地址,最开始人们在程序直接使用实际的物理地址,如下图:
    假设程序a第一次启动被装载在1的位置,第二次启动装载在31的位置。
    程序a中有段代码 jmp 3,想去执行3那里的目标代码。

    物理地址.png


    显然第一次jmp是对的,但第二次操作系统把装在了31的位置,显然目标代码应该是33了,就应该把程序改为jmp 33,否则就出错了。
    这就是直接使用物理地址的弊端,每次启动都要重新改代码,或者把所有跳转的地方都+30,很麻烦。所以现代程序都不直接使用物理地址,而是用了虚拟地址,如下图

    地址转换.png


    使用了虚拟地址后,每个程序就认为自己是从0地址开始的就好了,不管加载到哪个地方,都不用在修改代码,通过一个段表就可以把虚拟地址转为实际的物理地址
  • 在编译器debug中可以看到 0x0012fee8 这些都是虚拟地址,物理地址操作系统不允许直接访问了。

     

    vc6.0.png

段式管理

  • 最开始人们用段式管理,但是段式管理会产生内存碎片,过程如下图

    内存碎片.png


    当程序c要加载进内存的时候,程序b前面的空间不够了,只能从b后面分配,于是b前面的空间就不能给c利用,成为了内存碎片
  • 操作系统为了避免这种情况,充分利用内存空间,当内存不够的时候,会采取内存紧缩,就是把所有程序都往左边挪动,全部紧紧的排在一起,但是实际中对4GB的内存空间进行紧缩的时候,需要5秒左右的时间,计算机经常卡机5秒你能忍?
  • 程序c分配内存的策略有首先适配法,最佳适配法等方法,考虑到篇幅就不展开讲了,我上面用的就是首先适配法,从左到右首次找到一个合适位置就分配。

页式管理

  • 由于段式管理的缺点(外部碎片,内存紧缩),人们后来发明了页式管理,通俗来说,页式管理就是把一定大小的物理空间当做一个页框,整个内存中就有很多个这样的页框,比如0到99的内存空间,按10个字节为一个页框,那么整个内存就分成了10页框,0到9是第0个页框,如下图:

    页框.png


    按照页式管理划分空间后,一个程序用一个页框要么使用页框全部空间,要么不能使用,不能说只用一点点,如果一个程序用不上那么多空间,也必须拿完,于是段式管理的外部碎片和内存紧缩的问题被解决了,提高了内存利用率,但是又产生内存内部碎片
  • 程序的页和页框的大小是一样的,页框大小如何确定?如果页框太大,产生的内部碎片也大,如果页框太小,导致页表变大,查找速度降低。例如4GB的内存,按照4KB分为一页,4GB / 4KB = 1048576项,查找起来自然慢了。

虚拟地址到物理地址转换过程

地址转换.png


上图还有个在不在位,这个位表示如果程序的页在页框中,那么直接转换,如果不在页框中,那么引发一个缺页中断,操作系统去磁盘上把缺失的页加载进内存,然后程序才继续往下运行。这里有个重点,运行中的程序不一定全部在内存中,也有可能在磁盘上,在磁盘上的那部分叫做虚拟内存!,那究竟程序的哪些页面在内存中,哪些页面在磁盘上,这里就涉及到页面置换算法

页面置换算法

一个程序的部分页面在内存中,部分页面在磁盘上,究竟怎么确定这些页面?
我选几个来说

  • 先进先出(FIFO)
    最简单的,最先进来的,就最先淘汰出去。缺点:有些频繁访问的页面也可能淘汰出去。
  • 二次机会(SC)
    最先进来的页面不一定最先出去,如果这个页面的访问标志是1,那么把它置为0,再给它一次机会,如果页面访问标志0,那么才置换出去。
  • 最近未使用(NRU)
    每个页面有两个标志位,标记是否访问,是否修改,显然那么没有怎么访问,没有修改的页面会被淘汰出去。
  • 最近最少使用(LRU)
    这个也很好理解,每个页面有个计数器,访问一次就加1,显然把访问次数很少的那些优先淘汰。
  • 此外还有老化算法,工作集算法,等等,限于篇幅(其实是我写累了)就不详细叙说了,我已经写了个实验代码,在这里可以看到。

段页式管理

最后是段页式管理,结合了段式页式的优缺点,把程序先分段,在每个段内再分页,来管理内存,windows操作系统就是用这个方法管理的,当然实际中更加复杂,绝对没有我说的这么简单,我只是通俗的说清原理,详情请看《操作系统原理》



作者:ck2016
链接:https://www.jianshu.com/p/486f48d552e6
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

那么,Linux 到底是怎么管理内存的呢?
 

让我们来回顾一下历史,在早期的计算机中,程序是直接运行在物理内存上的。换句话说,就是程序在运行的过程中访问的都是物理地址。如果这个系统只运行一个程序,那么只要这个程序所需的内存不要超过该机器的物理内存就不会出现问题,我们也就不需要考虑内存管理这个麻烦事了,反正就你一个程序,就这么点内存,吃不吃得饱那是你的事情了。然而现在的系统都是支持多任务,多进程的,这样CPU以及其他硬件的利用率会更高,这个时候我们就要考虑到将系统内有限的物理内存如何及时有效的分配给多个程序了,这个事情本身我们就称之为内存管理。

下面举一个早期的计算机系统中,内存分配管理的例子,以便于大家理解。

加入我们有三个程序,程序1,2,3.程序1运行的过程中需要10M内存,程序2运行的过程中需要100M内存,而程序3运行的过程中需要20M内存。如果系统同时需要运行程序A和B,那么早期的内存管理过程大概是这样的,将物理内存的前10M分配给A, 接下来的10M-110M分配给B。这种内存管理的方法比较直接,好了,假设我们这个时候想让程序C也运行,同时假设我们系统的内存只有128M,显然按照这种方法程序C由于内存不够是不能够运行的。大家知道可以使用虚拟内存的技术,内存空间不够的时候可以将程序不需要用到的数据交换到磁盘空间上去,已达到扩展内存空间的目的。下面我们来看看这种内存管理方式存在的几个比较明显的问题。就像文章一开始提到的,要很深层次的把握某个技术最好搞清楚其发展历程。

1.进程地址空间不能隔离

由于程序直接访问的是物理内存,这个时候程序所使用的内存空间不是隔离的。举个例子,就像上面说的A的地址空间是0-10M这个范围内,但是如果A中有一段代码是操作10M-128M这段地址空间内的数据,那么程序B和程序C就很可能会崩溃(每个程序都可以系统的整个地址空间)。这样很多恶意程序或者是木马程序可以轻而易举的破快其他的程序,系统的安全性也就得不到保障了,这对用户来说也是不能容忍的。

2. 内存使用的效率低

如上面提到的,如果我们要像让程序A、B、C同时运行,那么唯一的方法就是使用虚拟内存技术将一些程序暂时不用的数据写到磁盘上,在需要的时候再从磁盘读回内存。这里程序C要运行,将A交换到磁盘上去显然是不行的,因为程序是需要连续的地址空间的,程序C需要20M的内存,而A只有10M的空间,所以需要将程序B交换到磁盘上去,而B足足有100M,可以看到为了运行程序C我们需要将100M的数据从内存写到磁盘,然后在程序B需要运行的时候再从磁盘读到内存,我们知道IO操作比较耗时,所以这个过程效率将会十分低下。

3. 程序运行的地址不能确定

程序每次需要运行时,都需要在内存中非配一块足够大的空闲区域,而问题是这个空闲的位置是不能确定的,这会带来一些重定位的问题,重定位的问题确定就是程序中引用的变量和函数的地址,如果有不明白童鞋可以去查查编译愿意方面的资料。

内存管理无非就是想办法解决上面三个问题,如何使进程的地址空间隔离,如何提高内存的使用效率,如何解决程序运行时的重定位问题?

这里引用计算机界一句无从考证的名言:“计算机系统里的任何问题都可以靠引入一个中间层来解决。”

现在的内存管理方法就是在程序和物理内存之间引入了虚拟内存这个概念。虚拟内存位于程序和屋里内存之间,程序只能看见虚拟内存,再也不能直接访问物理内存。每个程序都有自己独立的进程地址空间,这样就做到了进程隔离。这里的进程地址空间是指虚拟地址。顾名思义既然是虚拟地址,也就是虚的,不是现实存在的地址空间。

既然我们在程序和物理地址空间之间增加了虚拟地址,那么就要解决怎么从虚拟地址映射到物理地址,因为程序最终肯定是运行在物理内存中的,主要有分段和分页两种技术。

分段(Segmentation):这种方法是人们最开始使用的一种方法,基本思路是将程序所需要的内存地址空间大小的虚拟空间映射到某个
物理地址空间。

段映射机制

每个程序都有其独立的虚拟的独立的进程地址空间,可以看到程序A和B的虚拟地址空间都是从0x00000000开始的。我们将两块大小相同的虚拟地址空间和实际物理地址空间一一映射,即虚拟地址空间中的每个字节对应于实际地址空间中的每个字节,这个映射过程由软件来设置映射的机制,实际的转换由硬件来完成。

这种分段的机制解决了文章一开始提到的3个问题中的进程地址空间隔离和程序地址重定位的问题。程序A和程序B有自己独立的虚拟地址空间,而且该虚拟地址空间被映射到了互相不重叠的物理地址空间,如果程序A访问虚拟地址空间的地址不在0x00000000-0x00A00000这个范围内,那么内核就会拒绝这个请求,所以它解决了隔离地址空间的问题。我们应用程序A只需要关心其虚拟地址空间0x00000000-0x00A00000,而其被映射到哪个物理地址我们无需关心,所以程序永远按照这个虚拟地址空间来放置变量,代码,不需要重新定位。

无论如何分段机制解决了上面两个问题,是一个很大的进步,但是对于内存效率问题仍然无能为力。因为这种内存映射机制仍然是以程序为单位,当内存不足时仍然需要将整个程序交换到磁盘,这样内存使用的效率仍然很低。那么,怎么才算高效率的内存使用呢。事实上,根据程序的局部性运行原理,一个程序在运行的过程当中,在某个时间段内,只有一小部分数据会被经常用到。所以我们需要更加小粒度的内存分割和映射方法,此时是否会想到Linux中的Buddy算法和slab内存分配机制呢,哈哈。另一种将虚拟地址转换为物理地址的方法分页机制应运而生了。

分页机制:

分页机制就是把内存地址空间分为若干个很小的固定大小的页,每一页的大小由内存决定,就像Linux中ext文件系统将磁盘分成若干个Block一样,这样做是分别是为了提高内存和磁盘的利用率。试想以下,如果将磁盘空间分成N等份,每一份的大小(一个Block)是1M,如果我想存储在磁盘上的文件是1K字节,那么其余的999字节是不是浪费了。所以需要更加细粒度的磁盘分割方式,我们可以将Block设置得小一点,这当然是根据所存放文件的大小来综合考虑的,好像有点跑题了,我只是想说,内存中的分页机制跟ext文件系统中的磁盘分割机制非常相似。

Linux中一般页的大小是4KB,我们把进程的地址空间按页分割,把常用的数据和代码页装载到内存中,不常用的代码和数据保存在磁盘中,我们还是以一个例子来说明,如下图:


进程虚拟地址空间、物理地址空间和磁盘之间的页映射关系

我们可以看到进程1和进程2的虚拟地址空间都被映射到了不连续的物理地址空间内(这个意义很大,如果有一天我们的连续物理地址空间不够,但是不连续的地址空间很多,如果没有这种技术,我们的程序就没有办法运行),甚至他们共用了一部分物理地址空间,这就是共享内存。

进程1的虚拟页VP2和VP3被交换到了磁盘中,在程序需要这两页的时候,Linux内核会产生一个缺页异常,然后异常管理程序会将其读到内存中。

这就是分页机制的原理,当然Linux中的分页机制的实现还是比较复杂的,通过了也全局目录,也上级目录,页中级目录,页表等几级的分页机制来实现的,但是基本的工作原理是不会变的。

分页机制的实现需要硬件的实现,这个硬件名字叫做MMU(Memory Management Unit),他就是专门负责从虚拟地址到物理地址转换的,也就是从虚拟页找到物理页。

我们通常所说的内存容量,其实指的是物理内存。物理内存也称为主存,大多数计算机用的主存都是动态随机访问内存(DRAM)。只有内核才可以直接访问物理内存。那么,进程要访问内存时,该怎么办呢?

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

虚拟地址空间的内部又被分为:

  • 内核空间
  • 用户空间

不同字长的处理器,地址空间范围不同,下图是32位和64位系统的虚拟地址空间

34位系统内核占用1g,用户空间占用3g,64位系统内核空间和用户空间占用128T。

进程的用户态和内核态

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

并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才会被分配物理内存,并且分配后的物理内存,是通过内存映射来管理的。

内存映射

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

页表

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

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

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

注意,MMU 并不是以字节为单位来管理内存,而是规定了一个内存映射的最小单位,也就是页,通常是4KB大小,这样,每一次内存映射,都需要关联 4KB或者4KB 整数倍的内存空间。

页的大小只有4KB,因此导致整个页表会变得非常大。一个32位系统就需要100多万个页表项,才能实现整个地址空间的映射,为了解决这个问题,Linux 提供了多级页表大页

什么是多级页表

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

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

什么是大页

大页就是比普通页更大的内存块。常见的有2MB以及1GB。大页通常在使用大量内存的进程上,比如oracle、DPDK等。

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

虚拟内存空间分布

下图,最上方的是内核空间,下方是用户空间。用户空间其实又被分成了多个不同的段。

通过上面这张图,你可以看到用户空间内存从低到高分别分成五个不同的内存块:

  • 只读段,包括代码和常量。
  • 数据段,包括全局变量。
  • 堆,包括动态内存的分配、从低地址开始向上增长。
  • 文件映射段,包括动态库、共享内存等,从高地震向下增长。
  • 栈,包括局部变量和函数调用的上下文等。栈一般大小是固定的,一般是 8MB。

堆和文件映射段的内存是动态分配的。

内存如何分配和回收

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

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

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

这两种方式,自然各有优缺点。

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

而mmap()方式分配的内存,会在释放时直接归还系统,所以每次mmap都会发生缺页异常。在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大。这也是malloc只对大块内存使用mmap的原因。

当这两种调用发生后,其实并没有真正分配内存。这些内存,都只在首次访问时才分配,也就是通过缺页异常进入内核中,再由内核来分配内存。

整体来说,Linux 使用伙伴系统来管理内存分配。这些内存在MMU中以页为单位进行管理,伙伴系统也一样,以页为单位来管理内存,并且会通过相邻页的合并,减少内存碎片化(比如brk方式造成的内存碎片)。

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

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

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

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

  • 回收缓存,比如使用L RU (Least Recently Used) 算法,回收最近使用最少的内存页面;
  • 回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中;

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

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

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

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

  • 一个进程消耗的内存越大,oom_ score就越大;

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

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

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

oom_ adj 的范围是[-17, 15],数值越大,表示进程越容易被00M杀死;数值越小,表示进程越不容易被OOM杀死,其中-17表示禁止0OM。

比如用下面的命令,你就可以把sshd进程的oom_ adj 调小为-16,这样,sshd 进程就不容易被OOM杀死。

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

 

内存调优

在我看来,内存调优最重要的就是,保证应用程序的热点数据放到内存中,并尽量减少换页和交换。

常见的优化思路有这么几种。

  1. 最好禁止 Swap。如果必须开启 Swap,降低 swappiness 的值,减少内存回收时 Swap 的使用倾向。

  2. 减少内存的动态分配。比如,可以使用内存池、大页(HugePage)等。

  3. 尽量使用缓存和缓冲区来访问数据。比如,可以使用堆栈明确声明内存空间,来存储需要缓存的数据;或者用 Redis 这类的外部缓存组件,优化数据的访问。

  4. 使用 cgroups 等方式限制进程的内存使用情况。这样,可以确保系统内存不会被异常进程耗尽。

  5. 通过 /proc/pid/oom_adj ,调整核心应用的 oom_score。这样,可以保证即使内存紧张,核心应用也不会被 OOM 杀死。

转载:

https://awen.me/post/6545.html

https://time.geekbang.org/column/article/76460?utm_term=zeusVZBJU&utm_source=app&utm_medium=geektime&utm_campaign=140-onsell&utm_content=zhuantiye0320

猜你喜欢

转载自blog.csdn.net/fanren224/article/details/89459770
今日推荐