说到内存我们都知道,机器插多大的内存条就有多大的内存,这也是我们常说的物理内存。而 linux 为了更好地管理和使用内存,定义了虚拟内存的概念。

虚拟内存

Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自互不干涉的进程地址空间。32位操作系统的虚拟内存是大小为4G的线性虚拟空间,也就是2^32,64位操作系统一般是2^48。用户所看到和接触到的都是虚拟地址,无法看到实际的物理内存地址。

32位操作系统4G的虚拟内存又被认为的分为了两部分:用户空间和内核空间。用户空间占3G,内核空间占1G,即3G到4G之间的1G空间。而64位操作系统则是用户空间和内核空间各占2^47=128T,下文都以32位操作系统进行讨论。

centos虚拟内存占用少 linux虚拟内存大小_linux

当进程需要内存时,从内核获取的地址都是虚拟地址,当需进程要实际访问内存的时候,会由内核的 “请求页机制” 产生 “缺页异常”,从而分配物理内存页。

Linux内核管理物理内存是通过分页机制实现的,它将整个内存划分成无数个4k大小的内存页,作为分配和回收内存的基本单位。

centos虚拟内存占用少 linux虚拟内存大小_centos虚拟内存占用少_02

地址转换和页表

从上面知道,逻辑地址到物理地址需要进行地址转换,其实逻辑地址需要先转换成线性地址,再由线性地址转换成物理地址,只不过Linux中的逻辑地址等于线性地址,所以后面直接讨论线性地址到物理地址的转换。

在32位的线性地址中,前10位表示页目录,中间10位表示页表,后12位表示页内偏移。

centos虚拟内存占用少 linux虚拟内存大小_centos虚拟内存占用少_03

地址转换时,先从寄存器CR3获得当前页面目录指针(页目录基地址),从页目录表中找到对应的页目录项(页表基地址),再从页表中找到对应的页表项,根据页表项中存储的地址(物理内存基地址)再加偏移值就可以转换成物理地址,从而得到最终的存储信息。

这个过程的硬件部分由MMU内存管理单元完成。

进程与内存

说完虚拟内存和地址转换,下面看下进程具体是如何使用虚拟内存的。

进程在使用用户空间的虚拟内存时,按照访问属性一致的地址空间存放在一起,划分成几个区域。

  • 代码段:代码段是用来存放可执行文件的操作指令,也就是说是它是可执行程序在内存中的镜像。代码段需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作。
  • 数据段:数据段用来存放可执行文件中已初始化的全局变量和静态变量。
  • BSS段:BSS段包含了程序中未初始化或值为0的全局变量的内存映射。
  • 堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用系统函数分配内存时,新分配的内存就被动态添加到堆上;当利用函数释放内存时,被释放的内存从堆中被剔除。
  • 栈:栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。
  • 除了上面的五个区域,还有lib库的代码段、数据段、bss段、内存映射文件段、共享内存段。

centos虚拟内存占用少 linux虚拟内存大小_centos虚拟内存占用少_04

内存分配算法

伙伴算法

虽然内存可以不连续使用,不过我们还是倾向于分配连续的内存地址,这样在做地址转换时,查询页表不需要修改,可以提高地址转换的速度。所以出现了内存分配的伙伴算法。

伙伴系统是一个内存池,它将空闲的内存页分成了11个块链表,第一个链表包含1个连续页(4k),第二个链表包含2个连续的页……第11个链表包含2^10个连续页(4M)。

centos虚拟内存占用少 linux虚拟内存大小_centos虚拟内存占用少_05

按照这样的规则对空闲的页进行拆分或合并,当需要获取或回收物理内存时时,从链表上获取和归还。

slab

伙伴算法是为大内存需求时申请连续的内存,slab则是以byte为单位,为小于一个内存页的数据分配内存。

slab把不同的对象划分为高速缓存(cache)组,每种对象类型对应一个高速缓存。例如一个高速缓存存放task_struct,另一个存放struct inode。slab由一个或多个物理连续的页组成,每个高速缓存由多个slab组成。

centos虚拟内存占用少 linux虚拟内存大小_服务器_06

slab分为三种状态,如果slab满了,状态为full,使用了一部分,状态为partial,没有使用状态为free。分配时候先从partial获取内存,没有partial再从free中取。

slab可以减少伙伴算法在分配小块连续内存时产生的碎片,减少空间浪费。将频繁使用的对象缓存起来,减少分配、初始化和释放对象的时间开销。可以更好地使用硬件高速缓存。

内存映射

内存映射(mmap)是Linux操作系统的一个很大特色,它可以将系统内存映射到一个文件上,以便可以通过访问文件内容来达到访问内存的目的。这样做的最大好处是提高了内存访问速度,并且可以利用文件系统的接口编程(设备在Linux中作为特殊文件处理)访问内存,降低了开发难度。许多设备驱动程序便是利用内存映射功能将用户空间的一段地址关联到设备内存上,无论何时,只要内存在分配的地址范围内进行读写,实际上就是对设备内存的访问。同时对设备文件的访问也等同于对内存区域的访问,也就是说,通过文件操作接口可以访问内存。

这里就可以和之前文章中介绍的伪文件系统、inode、文件描述符、page cache等等概念关联起来了。

内存回收

文件页内存可以直接和硬盘对应的文件进行交换,如果进程修改了文件内容,直接修改的是page cache,并将修改后的内存页标记为脏,在内存回收时必须将脏页回写到磁盘后才可以进行回收。

没有文件背景的内存页我们叫它匿名页,例如进程堆、栈、数据段使用的内存页等,无法直接跟磁盘文件进行交换,但是可以跟swap区进行交换,其实也是将这部分内存页的内容存到磁盘上。

当匿名页交换到swap分区的时候,就实现了这部分页的内存回收。再次访问时再从swap交换回内存。

说到内存回收,首先想到的还有各种编程语言的垃圾回收,也就是堆内存部分的内存页回收,这部分将在以后的文章中进行介绍,欢迎关注~