进程与内存
进程的5种不同的数据区
代码段
代码段是用来存放可执行文件的操作指令,也就是说它是可执行程序在内存中的镜像。代码段需要防止在运行时被非法修改,所以只允许读取操作,儿不允许写入(修改)操作--它是不可写的。
数据段
数据段用来存放可执行文件中已初始化全局变量,换句话说就是存放程序静态分配的变量和全局变量。
BSS段
BSS段包含了程序未初始化的全局变量,在内存中bss段全部置零。
堆(heap)
堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
栈
栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧'{ }'中定义的变量,(但不包括static声明的变量,static意味着在数据段中存放变量),除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数返回值也会被存放回栈中。由于栈的先进后出的特点,所以栈特别方便用来保存/恢复现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
进程内存的分配与回收
创建进程fork()、程序载入execve()、映射文件mmap()、动态内存分配malloc()/brk()等进程相关操作都需要分配内存给进程。不过这时进程申请和获得的还不是实际内存,而是虚拟内存准确的说是"内存区域".进程对内存区域的分配最终都会归结到do_map()函数上来(brk调用被单独一系统调用实现,不用do_map()),内核使用do_map()函数创建一个新的线性地址区间。但是说该函数创建了一个新VMA并不准确,因为如果创建的地址区间和一个已经存在的地址区间相邻,并且它们具有相同的访问权限的话,那么两个区间将合并为一个。如果不能合并,那么就确实需要创建一个新的VMA了。但无论哪种情况,do_map()函数都会将一个地址区间加入到进程的地址区间中--无论是扩展已存在的内存区域还是创建一个新的区域。同样,释放一个内存区域应使用函数do_ummap(),它会销毁对应的内存区域。
如何由虚变实
从上面已经看到进程所能直接操作的地址都为虚地址。当进程需要内存时,从内核获得的仅仅是虚拟的内存区域,而不是实际的物理地址,进程并没有获得物理内存(物理页面),获得的仅仅是对一个新的线性地址区间的使用权。实际物理内存只有当进程真的去访问新获取的虚拟地址时,才会由“请求页机制”产生“缺页”异常,从而进入分配实际页面的例程。该异常时虚拟内存机制赖以存在的基本保证--它会告诉内核去真正为进程分配物理页面,并建立对应的页表,这之后虚拟地址才实实在在的映射到了系统的物理内存上。(当然,如果页被换出到磁盘,也会产生缺页异常,不过这时不用再建立页表了)。这种请求页机制把页面的分配推迟到不能再推迟为止,并不急于把所有的事情都一次做完。之所以能这么做是利用了内存访问的"局部性原理",请求页带来的好处是节约了空闲内存,提高了系统的吞吐率。
物理内存管理
Linux内核管理物理内存是通过分页机制实现的,它将整个内存划分成无数个4k(在i386体系结构中)大小的页,从而分配和回收内存的基本单位便是内存页了。利用分页管理有助于灵活分配内存地址,因为分配时不必要求有大块的连续内存,系统可以东一页、西一页的凑出所需要的内存供进程使用。虽然如此,但是实际上系统使用内存时还是倾向于分配连续的内存块,因为分配连续内存时,页表不需要更改,因此能降低TLB的刷新率(频繁刷新会在很大程度上降低访问速度)。
鉴于上述需求,内核分配物理页面是为了尽量减少不连续的情况,采用了"伙伴"关系来管理空闲页面。Linux中空闲页面的组织和管理利用了伙伴关系,因此空闲页面分配时页需要遵循伙伴关系,最小单位只能是2的幂倍页面大小。内核中分配空闲页面的基本函数是get_free_page/get_free_pages,它们或是分配单页或是分配指定的页面(2,4,8,...,512页)。注意:get_free_page是在内核中分配内存,不同于malloc在用户空间中分配,malloc利用堆动态分配,实际上是调用brk()系统调用,该调用的作用是扩大或缩小进程堆空间。如果现有的内存区域不够容纳堆空间,则会以页面大小的倍数为单位,扩张或收缩对应的内存区域,但brk值并非以页面大小为倍数修改,而是按实际请求修改。因此malloc在用户空间分配内存可以以字节为单位分配,但内核在内部仍然是以页为单位分配的。
内核内存使用
slab:所谓尺有所长,寸有所短。以页为最小单位分配内存对于内核管理系统中的物理内存来说的确比较方便,但内核自身最常使用的内存却往往很小(远远小于一页)的内存块--比如存放文件描述符、进程描述符、虚拟内存区域描述符等行为所需的内存都不足一页。这些用来存放描述符的内存相比页面而言,就好比是面包屑和面包。一个整页中可以聚集多个这些小块内存;而且这些小块内存也和面包屑一样频繁的生成/销毁。
为了满足内核对这种小内存块的需要,Linux系统采用了一种被称为slab分配器的技术。slab分配器的实现相当复杂,但原理不难,其核心思想就是"存储池"的运用。内存片段(小内存块)被看作对象,当被使用完后,并不是直接释放而是被缓存到“存储池”里,留做下次使用,这无疑避免了频繁创建与销毁对象所带来的额外的负载。
slab技术不但避免了内存内部分片带来的不便(引入slab分配器的主要目的是为了减少对伙伴系统分配算法的调用次数--频繁分配和回收必然会导致内存碎片--难以找到大块连续的可用内存),而且可以很好的利用硬件缓存提高访问速度。
slab并非是脱离伙伴关系而独立存在的一种内存分配方式,slab仍然是建立在页面基础之上,换句话说,slab将页面(来自于伙伴关系管理的空闲页面链表)撕碎成众多小内存块以供分配,slab中的对象分配和销毁使用kmem_cache_alloc与kmem_cache_free。
Kmalloc
slab分配器不仅仅只用来存放内核专用的结构体,它还被用来处理内核对小块内存的请求。当然鉴于slab分配器的特点,一般来说内核程序中对小于一页的小块内存的请求才通过slab分配器提供的接口kmalloc来完成(虽然它可分配32到131072字节的内存)。从内核内存分配的角度来讲,kmalloc可被看成是get_free_page(s)的一个有效补充,内存分配粒度更灵活了。
内核非连续内存分配(Vmalloc)
伙伴关系也好、slab技术也好,从内存管理理论角度而言目的基本是一致的,它们都是为了防止"分片",不过分片又分为外部分片和内部分片之说,所谓内部分片是说系统为了满足一小段内存区(连续)的需要,不得不分配一大区域连续内存给他,从而造成了空间浪费;外部分片是指系统虽有足够的内存,但却是分散的碎片,无法满足对大块"连续内存"的需求。无论何种分片都是系统有效利用内存的障碍。slab分配器使得一个页面包含的众多小块内存可独立被分配使用,避免了内部分片,节约了空闲内存。伙伴关系把内存块按大小分组管理,一定程度上减轻了外部分片的危害,因为页框分配不再盲目,而是按照大小依次有序进行,不过伙伴关系只是减轻了外部分片,但并未彻底消除。
所以避免外部分片的最终思路还是落到了如何利用不连续的内存块组合成“看起来很大的内存块”--这里的情况很类似于用户空间分配虚拟内存,内存逻辑上连续,其实映射到并不一定连续的物理内存上,Linux内核借用了这个技术,允许内核程序在内核地址空间中分配虚拟地址,同样也利用页表(内核页表)将虚拟地址映射到分散的内存页上。以此完美的解决了内核内存中使用外部分片问题。内核提供vmalloc函数分配内核虚拟内存,该函数不同于kmalloc,它可以分配较Kmalloc大得多的内存空间(可远大于128K,但必须是页大小的倍数),但相比kmalloc来说,vmalloc需要对内核虚拟地址进行重映射,必须更新内核页表,因此分配效率上要低一些。