LINUX存储管理
地址空间 地址空间(address space)是一段表示内存位置的地址范围。地址空间有两种: 物理地址空间 逻辑地址空间,也被称为虚拟地址空间 在逻辑地址和物理地址之间相互转换的工作是由内核和硬件内存管理单元(MMU—memory management unit)共同完成的。MMU是被集成进现代的CPU里的,它们都是同一块CPU芯片内的一个部分。内核告诉MMU如何为每个进程把某逻辑页面映射到某特定物理页面,而MMU在进程提出内存请求时完成实际的转换工作。
LINUX的虚拟地址划分 每一个用户进程都可以访问4GB的线性虚拟内存空间。 从3GB到4GB的虚拟内存地址为核心态空间,存放仅供核心态访问的代码和数据,用户态进程不可访问。 所有进程从3GB到4GB的虚拟空间都是一样的,有同样的页目录项,同样的页表,对应到同样的物理内存段。LINUX以此方式让内核态进程共享代码段和数据段。 内核态虚拟空间从3GB到3GB+4M的一段(也就是进程页目录第768项所管辖的范围),被映射到物理空间0到4M段。因此,进程处于核心态时,只要通过访问虚拟空间3GB到3GB+4M段,偏移地址0到4M,即访问了物理空间0到4M段。
虚拟地址转换 页目录索引,页面中间目录、页表索引和偏移量 PGD PMD PTE Page Frame 页目录索引,页面中间目录、页表索引和偏移量 虚拟内存和物理内存都分为大小固定的块,叫做页面。每一个页面有一个唯一的页面号,叫做PFN(page frame number)。
虚拟地址转换 转换后备缓存(Translation Lookaside Buffers:TLBs) 如果简单的执行从逻辑地址到物理地址的转换过程,在跟踪指针链时将会需要几个内存引用。RAM虽然不像磁盘那么慢,但是仍然比CPU要慢的多,这样就容易形成性能的瓶颈。为了减少这种开销,最近被执行过的地址转换结果将被存储在MMU的转换后备缓存(translation lookaside buffers:TLBs)内。
虚存段(VMA)的组织和管理 VMA:vitual memory area 一个vma段是某个进程的一段连续的虚存空间;在这段虚存里的所有单元拥有相同的特征。例如:属于同一进程,相同的访问权限,同时被锁定(locked),同时受保护(protected),等等。 进程通常占用几个vma段,分别用于代码段、数据段、堆栈段等。属于同一进程的vma段通过vm_next指针连接,组成链表。对于一个给定的进程,两个VMAs决不会重叠,一个地址最多被一个VMA所覆盖;进程从未访问过的的一个地址将不会在任何一个VMA中。 struct mm_struct结构的成员struct vm_area_struct * mmap 表示进程的vma链表的表头。
虚存段(VMA)的组织和管理 当一个进程有多于avl_min_map_count数目的VMA时,内核也会创建一个AVL树来存储它们,此时仍然是使用VMAs自己的指针对该树进行管理。AVL树是一个平衡二叉树结构,因此这种方法在VMA数量巨大时查找效率十分高。
AVL树
vm_operations_struct PCB对内存的控制 Task struct mm_struct vm_area_struct _ vm_mm pgd mm vm_operations_struct vm_next *open mmap vm_ops pgd vm_area_struct vm_next pte pte page page frame frame
物理空间管理 物理空间的组织 空闲物理内存管理 空闲内存的组织 分配 回收
物理空间的组织 (include/linux/fs.h,struct page) 物理内存以页帧(page frame)为单位,页帧的长度固定,等于页长,对INTEL CPU缺省为4K字节。 LINUX对物理内存的管理通过mem_map表描述(mm/memory.c)。 mem_map在系统初始化时由free_area_init()函数创建(mm/page_alloc.c)。 它本身是关于struct page mem_map_t (linux/mm.h)的数组,每项mem_map_t对应一个关于核心态、用户态代码和数据等的页帧。
mem_map在物理空间的位置 0X000000( 0K ) e mpty_Zero_Page 由 mem_init 初始化 swapper_pg_dir 核心态访问空间的页目录 0X00 2 000( 8K pg0 0X003000( 12K bad_pages 0X004000( 16K bad_pg_table 0X005000( 20K floppy_track_buffer 0X006000( 24K kernel_code+text FREE 0X0A0000( 640K RESERVED 0X100000( 1M pg_tables(4K) swap_cache mem_map bitmap
struct page 134 typedef struct page { 135 struct list_head list; 136 struct address_space *mapping; 137 unsigned long index; /* 若该页帧的内容是文件,则index指出文件的inode和偏移位置 */ 138 struct page *next_hash; 139 atomic_t count; /* 指明目前使用该页面的用户数。count==0意味着此页空闲 */ 140 unsigned long flags; /* atomic flags, some possibly updated asynchronously */ 141 struct list_head lru; 142 unsigned long age; /* 页帧的年龄,越小越先换出 */ 143 wait_queue_head_t wait; 144 struct page **pprev_hash; 145 struct buffer_head * buffers; /* 若该页帧作为缓冲区,则指示地址 */ 146 void *virtual; /* non-NULL if kmapped */ 147 struct zone_struct *zone; 148 } mem_map_t;
空闲内存的组织
bitmap 表 在物理内存低端,紧跟mem_map表的bitmap表以位图方式记录了所有物理内存的空闲状况。 与mem_map一样,bitmap表在系统初始化时由free_area_init()函数创建(mm/page_alloc.c)。 与一般性位图不同的是,bitmap表分割成NR_MEM_LISTS组(缺省值6)。
bitmap 表 首先是第0组,初始化时设定了长度为(end_mem-start_mem) / PAGE_SIZE/20+3,每位表示20个页帧的空闲状况,置位表示已被占用。 接着是第1组,初始化时设定了长度为: (end_mem-start_mem) / PAGE_SIZE/21+3 ,每位表示连续21个页帧的空闲状况,置位表示其中1页或2页已被占用。 类似地,对第i组,初始化时设定了长度为: (end_mem-start_mem) / PAGE_SIZE / 2i+3 ,每位表示连续2i个页帧的空闲状况,置位表示其中1页或几页已被占用。 例如对第5组,某个bit所对应的连续32页帧中只要有一个被占用,此位即置1,只有当所有32页帧全部回收后才清0。
free_area数组 LINUX用free_area数组记录空闲的物理页帧。free_area数组由NR_MEM_LISTS个free_area_struct结构类型的数组元素构成,每个元素均作为一条空闲块链表的表头。 struct free_area_struct { struct page *next; /* 此结构的next,prev指针与struct page匹配 */ struct page *prev; unsigned int * map; /* 指向bitmap */ }; static struct free_area_struct free_area[NR_MEM_LISTS]; 所有单个空闲页帧组成的链表挂到free_area数组的第0项后面。连续2 i个空闲页帧则被挂到free_area数组的第i项后面。
操作函数 分配内存块由__get_free_pages()函数和宏定义__get_free_page()执行
分配算法 LINUX采用buddy算法分配空闲块,块长可以是2i个 (0<= i< NR_MEM_LISTS) 页帧。 当分配长度是2i页帧的块时,从free_area数组的第i条链表开始搜索,找不到再搜索第i+1条链表,以此类推。 若找到的空闲块长正好等于需求的块长,则直接将它从free_area删除,返回首地址。 若找到的空闲块长大于需求的块长,则将空闲块一分为二,前半部分插入free_area中前一条链表,取后半部分。 若还大,则继续对半分,留一半取一半,直至相等。 bitmap表也相应调整。每分配一个2i页帧长的块,都要将bitmap表从第i组到第NR_MEM_LISTS组的对应的bit置1。
释放算法 回收空闲块时,change_bit()函数根据bitmap表的对应组,判断回收块的前后邻居是否也为空。 若空则合并,即修改bitmap表中对应位,从free_area的空闲链表中取下该相邻块。 此判断是个递归过程,直至找不到空闲邻居为止。 将最后合并的最大块插入free_area的相应链表中。
交换空间 两种交换空间: 一种用整个块设备,如硬盘的一个分区,称作交换设备,效率较高; 另一种用文件系统中固定长度的文件,称作交换文件,效率较低。 LINUX允许并行管理MAX_SWAPFILES个交换空间(MAX_SWAPFILES的缺省值为8)。
交换空间的格式 前4096字节是一个以字符串 “SWAP_SPACE”结尾的位图。位图的每一位(bit)对应一个交换空间的页面,置位表示对应的页面可用于换页操作。 第4096字节之后则是真正存放换出页面的空间。 这样,每个交换空间最多可容纳 (4096-10)* 8 – 1 = 32687个页面。
启用交换空间 int sys_swapon(const char * swapfile, int swapflags); swapflags规定交换空间的优先数。该参数中,SWAP_FLAG_PREFER(0X8000)必须置位,SWAP_FLAG_PRIO_MASK(0X7FFF)指定一个正的优先数。如果没有指定优先数,swapon自动给出一个负的优先数,负优先数的取值决定于swapon的调用次数。
每注册一个交换空间,就在swap_info表中填一项swap_info_struct结构 25 struct swap_info_struct swap_info[MAX_SWAPFILES]; 49 struct swap_info_struct { 50 unsigned int flags; /* 如果SWP_USED位置位,则被占用。如果SWP_WRITEOK,则该交换空间准备就绪。 */ 51 kdev_t swap_device; /* 对于交换设备,swap_device属性表示交换设备的主、次设备号 */ 52 spinlock_t sdev_lock; /* 对于此设备的互斥锁 */ 53 struct dentry * swap_file; /* 对于交换文件,swap_file属性指向该文件的inode */ 54 struct vfsmount *swap_vfsmnt; 55 unsigned short * swap_map;/* 指向一张表,其每一字节按顺序对应交换空间的一个页面,字节的值代表了引用该页面的进程数 */ 56 unsigned int lowest_bit; /*交换空间中的第一个没有被任何进程使用的交换页在swap_map数组中的下标 */ 57 unsigned int highest_bit; /* 交换空间中最后一个没被任何进程使用的交换页的下标 */ 58 unsigned int cluster_next; /*上次从当前的cluster中成功分配的交换页面的后继页面在swap_map数组中的下标 */ 59 unsigned int cluster_nr; /* 当前cluster中可供使用的交换页面的个数 */ 60 int prio; /*交换空间的优先级。优先级越高,交换文件申请交换页面的时候越优先考虑 */ 61 int pages; /* 表示该交换空间尚有多少空闲空间可供保存进程换出的物理页 */ 62 unsigned long max; 63 int next; /*指向下一项交换空间的的指针 */ 64 }; 153 struct swap_list_t { 154 int head; /* head of priority-ordered swapfile list */ 155 int next; /* swapfile to be used next */ 156 }; 23 struct swap_list_t swap_list = {-1, -1};
int sys_swapoff(const char * swapfile); 注销交换空间 int sys_swapoff(const char * swapfile);
交换空间的工作 kswapd进程换出页面时,调用try_to_swap_out() 测试页面的年龄。如果某物理页面可以换出,则调用get_swap_page向swap_list.next指示的交换空间申请空闲页面,得到一地址entry。该地址写入进程页表中那个原来描述换出物理页面的页表项,替换了其中的页帧地址。最后,调用rw_swap_page(),将换出的物理页面写到entry指定的交换空间某个页面中。 反过来,当缺页中断发生时,缺页中断服务程序可以根据产生缺页的地址(由CR2寄存器给出),找到描述该页面的页表项。页表项的Present位应该为0,最高20位指出该页面保存在哪个交换空间的哪个页面中。然后,经一系列函数调用后,读入该页面。
kswapd 当物理页面不够时,利用kswapd释放部分物理页面,将它们的内容写到交换空间。 kswapd是一特殊的进程,称内核态线程(kernel thread)。 注意,kernel thread完全不同于通常意义上的线程。它是没有虚拟存储空间的进程,它只运行在内核态,直接使用物理地址空间。同类型的进程还有bdflush和init。 kswapd的作用超越了字面上的描述。它不仅能将页面换出到交换空间(交换区或交换文件),它也保证系统中有足够的空闲页面以保持存储系统高效地运行。
请求调页 一旦一个可执行镜像映射到了一个进程的虚拟内存中,它就可以开始执行了。因为开始时只有镜像开头的一小部分装入到了系统的物理内存中,所以不久进程就会存取一些不在物理内存中的虚拟内存页,这时处理器会通知L i n u x发生了页面错误。页面错误将会描述页面错误发生时的虚拟内存地址和存取内存操作的类型。
产生缺页中断 当一个进程访问了一个还没有有效页表项的虚拟地址时(即页表项的P位为0),处理器将产生缺页中断,通知操作系统,并将出现缺页的虚存地址(在CR2寄存器中)和缺页时访问虚存的模式一并传递给LINUX的缺页中断服务程序。
缺页中断服务程序为do_page_fault() set_trap_gate(14, &page_fault); /* arch/i386/kernel/trap.c */ ENTRY(page_fault) /* arch/i386/kernel/entry.S */ pushl $SYMBOL_NAME(do_page_fault) jmp error_code /* 异常中断服务程序的统一入口 */
中断服务流程 根据控制寄存器CR2传递的缺页地址,找到用来表示出现缺页的虚拟存储区的vm_area_struct结构。 如果没有找到与缺页相对应的vm_area_struct结构,那么说明进程访问了一个非法存储区,LINUX向进程发送信号SIGSEGV。 接着检测缺页时访问模式是否合法。如果进程对该页的访问超越权限,例如试图对只允许读操作的页面进行写操作,系统也将向该进程发送一个信号,通知进程的存储访问出错。 如果Linux认为此页面错误是合法的,它将处理此页面错误。 Linux还必须区分页面是在交换文件中还是作为文件镜像的一部分存在于磁盘中。它靠检查出错页面的页面表来区分:如果页面表的入口是无效的,但非空,说明页面在交换文件中。 最后,Linux调入所需的页面并更新进程的页面表。
页面置换 当一个进程需要把一个虚拟内存页面装入到物理内存而又没有空闲的物理内存时,操作系统必须将一个现在不用的页面从物理内存中扔掉以便为将要装入的虚拟内存页腾出空间。 选择换出页 对换出页的处理
页面置换 检查是否存在可以从缓冲区中回收的块 若无,试图回收共享内存保留的页框 若无,用近似的LRU(全局的最近最少使用)替代算法找到换出页。
页面换出 如果将要扔掉的物理内存页一直没有被改写过,则操作系统将不保存此内存页,而只是简单地将它扔掉。如果再需要此内存页时,再从文件镜像中装入。 但是,如果此页面已经被修改过,操作系统就需要把页面的内容保存起来。这些页面称为“脏页面”(dirty page)。当它们从内存中移走时,将会被保存到一个特殊的交换文件中。
内存管理中的高速缓存 硬件高速缓存:一个常用的硬件高速缓存是在处理器中,它一般保存着页表的入口。(TLB) 页面高速缓存:它的作用是加快对磁盘中的文件的存取 交换高速缓存:它只保存那些被修改过的页面。只要在页面被写入到交换文件中后没有被修改过,那么此页面下一次从内存中交换出来时就不用再写入到交换文件中了,因为交换文件中已经有了该页面。这样,该页面就可以简单地扔掉,节省了大量的系统操作。 缓冲区高速缓存
页面高速缓存 对于已经作好了磁盘映射的文件,L i n u x每次读取一页,并将读取的页面存储到页面高速缓存中。 页面高速缓存由page_hash_table组成,page_hash_table 是一个包含指向mem_map_t结构指针的数组。 每当从一个内存映射文件中读取一个页面时,页面都要从页面高速缓存中读取。如果页面在高速缓存中,则将一个指向mem_map_t的指针返回给页面错误处理程序。否则,页面必须从磁盘上读入到内存中。 如果可能,L i n u x系统将会提前读取文件中的下一个页面,这样,如果文件是顺序执行的,那么下一个页面就已经在内存中了。随着文件的读入和执行,页面高速缓存也将变得越来越大。不用的页面将被移出高速缓存。
缓冲区高速缓存 缓冲区高速缓存中包含了用于块设备驱动程序的数据缓冲区。这些缓冲区大小固定(例如 512 字节),包括从块设备读出的数据或者要写到块设备的数据。块设备用设备标识符和要访问的数据块编号作为索引,用来快速定位数据块。 块设备只能通过缓冲区高速缓存存取。如果数据可以在缓冲区高速缓存中找到,那就不需要从物理块设备如硬盘上读取,从而使访问加快。 参见 fs/buffer.c
内存管理的常用命令