说到内存我们都知道,机器插多大的内存条就有多大的内存,这也是我们常说的物理内存。而 linux 为了更好地管理和使用内存,定义了虚拟内存的概念。
32位操作系统4G的虚拟内存又被认为的分为了两部分:用户空间和内核空间。用户空间占3G,内核空间占1G,即3G到4G之间的1G空间。而64位操作系统则是用户空间和内核空间各占2^47=128T,下文都以32位操作系统进行讨论。
Linux内核管理物理内存是通过分页机制实现的,它将整个内存划分成无数个4k大小的内存页,作为分配和回收内存的基本单位。
地址转换和页表
这个过程的硬件部分由MMU内存管理单元完成。
进程与内存
说完虚拟内存和地址转换,下面看下进程具体是如何使用虚拟内存的。
代码段:代码段是用来存放可执行文件的操作指令,也就是说是它是可执行程序在内存中的镜像。代码段需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作。
数据段:数据段用来存放可执行文件中已初始化的全局变量和静态变量。
BSS段:BSS段包含了程序中未初始化或值为0的全局变量的内存映射。
堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用系统函数分配内存时,新分配的内存就被动态添加到堆上;当利用函数释放内存时,被释放的内存从堆中被剔除。
栈:栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。
除了上面的五个区域,还有lib库的代码段、数据段、bss段、内存映射文件段、共享内存段。
伙伴算法
虽然内存可以不连续使用,不过我们还是倾向于分配连续的内存地址,这样在做地址转换时,查询页表不需要修改,可以提高地址转换的速度。所以出现了内存分配的伙伴算法。
伙伴系统是一个内存池,它将空闲的内存页分成了11个块链表,第一个链表包含1个连续页(4k),第二个链表包含2个连续的页……第11个链表包含2^10个连续页(4M)。
按照这样的规则对空闲的页进行拆分或合并,当需要获取或回收物理内存时时,从链表上获取和归还。
slab
伙伴算法是为大内存需求时申请连续的内存,slab则是以byte为单位,为小于一个内存页的数据分配内存。
slab把不同的对象划分为高速缓存(cache)组,每种对象类型对应一个高速缓存。例如一个高速缓存存放task_struct,另一个存放struct inode。slab由一个或多个物理连续的页组成,每个高速缓存由多个slab组成。
slab分为三种状态,如果slab满了,状态为full,使用了一部分,状态为partial,没有使用状态为free。分配时候先从partial获取内存,没有partial再从free中取。
slab可以减少伙伴算法在分配小块连续内存时产生的碎片,减少空间浪费。将频繁使用的对象缓存起来,减少分配、初始化和释放对象的时间开销。可以更好地使用硬件高速缓存。
内存映射
内存映射(mmap)是Linux操作系统的一个很大特色,它可以将系统内存映射到一个文件上,以便可以通过访问文件内容来达到访问内存的目的。这样做的最大好处是提高了内存访问速度,并且可以利用文件系统的接口编程(设备在Linux中作为特殊文件处理)访问内存,降低了开发难度。许多设备驱动程序便是利用内存映射功能将用户空间的一段地址关联到设备内存上,无论何时,只要内存在分配的地址范围内进行读写,实际上就是对设备内存的访问。同时对设备文件的访问也等同于对内存区域的访问,也就是说,通过文件操作接口可以访问内存。
这里就可以和之前文章中介绍的伪文件系统、inode、文件描述符、page cache等等概念关联起来了。
内存回收
文件
页内存
可以直接和硬盘对应的
文件进行交换,如果进程修改了文件内容,直接修改的是page cache,并将修改后的内存页标记为脏,在内存回收时必须将脏页回写到磁盘后才可以进行回收。具体回收算法在 中也有介绍。
没有文件背景的内存页我们叫它匿名页,例如进程堆、栈、数据段使用的内存页等,无法直接跟磁盘文件进行交换,但是可以跟swap区进行交换,其实也是将这部分内存页的内容存到磁盘上。
当匿名页
交换到swap分区的时候,就实现了这部分页的内存回收。再次访问时再从swap交换回内存。
说到内存回收,首先想到的还有各种编程语言的垃圾回收,也就是堆内存部分的内存页回收,这部分将在以后的文章中进行介绍,欢迎关注~