Linux存储管理
(一)Intel X86的存储管理(硬件层次)
(二)Linux存储管理基本框架(软件实现)
(三)Linux存储管理实现(实现中使用到的的数据结构)
(四)Linux存储管理实现(实现中使用到的函数)
(一)Intel X86的存储管理(硬件层次)
1、虚拟存储器
(1)基本的思想:把地址空间与主存容量区分开,程序员在地址空间写程序,程序在真正的内存中运行。由一个专门的机制实现地址空间(虚拟地址)和内存(物理地址)之间的映射。
(2)许多概念如进程、进程的上下文切换、存储器分配、虚拟地址空间、缺页处理都与虚拟存储器机制有关。
2、分页机制
(1)进程和内存都被划分为固定的块。
(2)无需用连续的叶框来存放一个进程,操作系统为每个进程生成一个页表。
(4)通过页表实现逻辑地址到物理地址的转换。
(5)无需将一个进程的全部装入主存,程序访问的局部性。
3、分段机制
(1) 将程序模块和数据模块分配给不同的主存段,一个程序有多个代码段和多个数据段构成。
(2)段通常有段名和基地址.
(3)分段系统将主存空间按实际程序中的段来划分,每个段在主存中的位置记录在段表中,并附以段长项。
4、IA-32 x86体系采用段页式虚拟存储管理方式
(1)在保护模式下IA32采用段页式虚拟存储管理方式。
(2)存储地址用逻辑地址(48位)、线性地址(32位)、物理地址(32位)进行描述.
逻辑地址---- 》线性地址(分段来完成)------》物理地址(分页来完成)
(3)分段过程(48位转换成32位 逻辑地址到线性地址)
Intel在原有的四个段寄存器的基础上,又添加了两个段寄存器,来存放GDT(全局描述符表)和LDT(局部描述符表),实质上就是一个索引,根据这个索引找到对应的段表中的某一个段表项(指向的是段表项中的段基址)获得段基址,段基址+有效地址即为线性地址。
(4)分页过程
采用了多级页表方式,32位的线性地址的划分即为:
22-31位 页目录索引
12-21 位 页表索引
0-11位 页内偏移量
(二)Linux存储管理基本框架(软件实现)
1、Linux为了考虑到不同的CPU上的实现,linux的内核映射机制设计成三层,在页目录和页表间设了一层中间目录。
页面目录PGD 中间目录 PMD 页表PT 页表项PTE
2、地址映射的全过程(linux基于硬件的实现地址映射)
A、段式映射
Linux系统会简化硬件提供的分段过程,设置基地址均为0,操作系统会把CS或DS这些段寄存器进行填充,包括这些寄存器对应的描述符cache,所以通过指令得到的虚拟地址就是偏移量即 线性地址
虚拟地址是链接的时候链接器给的,即线性地址,这个线性地址也是段的线性地址,因为段的基地址为0,所以链接器给出的指令的地址就是这个指令的有效地址,链接器给出的操作数的地址经过就算就是操作数的有效地址(线性地址)。
B、页式映射
概括的说就是将虚拟地址分段,使每段虚拟地址都作为一个索引指向页表,而页表则指向下一级别的页表或指向最终的物理页面。
(1)虚拟地址是链接的时候链接器给的,即线性地址,对照线性地址的格式,最高的10位对应的值去页目录中找到目录项,这个目录项的高20位指向一个页面表,CPU在20位后边填上12个0就得到该页面表的指针,找到页面表之后通过线性地址的中间10位对应的值找到对应的表项,页面表项的p标志位为1表示所映射的页面在内存中了。32位页面表项的高20位指向一个物理内存页面,在后边填上12个0就得到物理内存页面的起始地址,起始地址加上线性地址的低12位,就得到了物理内存地址。这里有两次在得到的高20位后面填12个0,因为页面表和页占一个页面,都是4K字节对齐的,其起始地址的低12位一定全是0.
页面映射过程中要访存3次(页面目录 页面表 真正的目标),所以虚存的高效实现有赖于高速缓存(快表)的实现。
(三)Linux内核实现内存管理的基本数据结构
A、有关物理空间的数据结构
(page是管理每一个物理页面的,zone是把一堆的物理页面进行分区的,pgd_t,pmd_t,pte_t分别代表页面目录、中间目录和页面表)
1、pgd_t pmd_t pte_t(页目录、页表)
既然页式映射用到了页目录(PGD)和页表(PT),linux内核准备了pgd_t,pmd_t,pte_t构成的数组.
#if CONFIG_X86_PAE
typedef struct { unsigned long pte_low, pte_high; } pte_t;
typedef struct { unsigned long long pmd; } pmd_t;
typedef struct { unsigned long long pgd; } pgd_t;
#define pte_val(x) ((x).pte_low | ((unsigned long long)(x).pte_high << 32))
#else
typedef struct { unsigned long pte_low; } pte_t;
typedef struct { unsigned long pmd; } pmd_t;
typedef struct { unsigned long pgd; } pgd_t;
#define pte_val(x) ((x).pte_low)
#endif
#define PTE_MASK PAGE_MASK
PGD包含了一个pgd_t类型的数组,多数体系结构中pgd_t类型等同于无符号的长整型类型,PGD中的表项指向二级页目录表项PMD,他是一个 pmd_t类型的数组,其中的表项指向PTE的表项 是一个pte_t类型的数组,该表项指向物理页面。如图所示:
其中pte_t的高20位用来看做物理页面的序号,低12位用于页面的状态信息和访问权限,而这低12 位另外定义了一个页面保护结构pgprot_t来说明,这与MMU的页面表项的低12位相对应,其中9位是标志位。
2、page(每个物理页)
内核用struct page结构表示系统中的每个物理页(和虚拟页无关),内核中有个全局变量mem_map,指向page数据结构数组,内核用page结构来管理系统中的所有的页,内核需要知道一个也是否空闲,是谁拥有这个页。
typedef struct page
{
struct list_head list;
struct address_space *mapping;
unsigned long index;
struct page *next_hash;
atomic_t count;
unsigned long flags;
struct list_head lru;
unsigned long age;
wait_queue_head_t wait;
struct page **pprev_hash;
struct buffer_head * buffers;
void *virtual;
struct zone_struct *zone;
} mem_map_t;
flags :页的状态
count :页的引用计数,当为0 时内核并没有引用它,在新的分配中就可以使用它。一个页可以由页缓存使用(mapping域指向和这个页相关的address_sapce对象或者作为私有数据private指向)或者作为进程页表的映射。
virtual:页的虚拟地址
3、zone
有些页位于内存的特定的物理地址,不能将其用于特定的任务,所以内核把页划分为不同的区(zone),内核使用区对相似特性的页进行分组。
(1)一些硬件只能用某些特定的内存地址来执行DMA(直接内存访问) 分配了ZONE_DMA
(2)一些体系结构其内存的物理寻址范围比虚拟寻址的范围大,这样有一些内存不能永久的映射到内核空间上 ZONE_HIGHMEM
(3)ZONE_NOMAL
(4)每个管理区有一个数据结构zone_struct,zone_struct结构中有一组空闲队列。因为常常成块的分配物理空间内的连续的多个页面。即维持一个连续长度为(2、4、8、16)的页面块队列。
B、有关虚存空间的数据结构
虚拟空间是以进程为基础的,虚拟空间管理不像物理空间那样有一个总的物理仓库。如下图为虚存空空间的数据结构描述图
1、mm_struct(虚拟内存描述符 进程整个地址空间的抽象)
struct mm_struct {
//虚拟内存区域的构建的链表
struct vm_area_struct * mmap; /* list of VMAs */
//虚拟内存区域的构建的树(可能是红黑树也可能是AVL树)
struct vm_area_struct * mmap_avl; /* tree of VMAs */
//指向最近一次用到那个虚存结构
struct vm_area_struct * mmap_cache; /* last find_vma result */
//pgd指向进程的页目录额,当内核调度一个进程的时候,就将这个指针转换成物理地址,并写入CR3寄存器中。
pgd_t * pgd;
//计数器,一个mm_struct可能有多个进程共享,列如fork之后父子进程共享,
atomic_t mm_users; /* How many users with user space? */
//主引用计数只要mm_users不为0, mm_atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */
int map_count; /* number of VMAs */
//互斥,PV操作的信号量
struct semaphore mmap_sem;//自旋锁 检索和操作页表时必须使用锁,以防止竞争条件
spinlock_t page_table_lock;
//所有的mm_sturuct结构体通过自身的mmlist域连接在一个双向链表中,该链表的首元素是init_mm描述符,即init进程代表的地址空间
struct list_head mmlist; /* List of all active mm's */
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long rss, total_vm, locked_vm;
unsigned long def_flags;
unsigned long cpu_vm_mask;
unsigned long swap_cnt; /* number of pages to swap on next pass */
unsigned long swap_address;
/* Architecture-specific MM context */
mm_context_t context;
};
2、vm_area_struct(虚拟内存区域 VMA, 地址空间内连续区间上的一个独立虚拟内存范围)
struct vm_area_struct {
struct mm_struct * vm_mm;
unsigned long vm_start;
unsigned long vm_end;
//end和strat决定了一个虚存空间,划分取决于页面访问权限,如果一个地址范围内的前一半页面和后一半页面有不同的访问权限,则应分为两个区间。
//虚存区间的线性队列
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot;
unsigned long vm_flags;
/* AVL tree of VM areas per task, sorted by address */
//虚存区间建立的AVL树,提高搜索效率
short vm_avl_height;
struct vm_area_struct * vm_avl_left;
struct vm_area_struct * vm_avl_right;
///用于管理磁盘和虚存之间发生关系的结构(mmap函数文件和虚存进行映射和盘区交换)
struct vm_area_struct *vm_next_shae;
struct vm_area_struct **vm_pprev_share;
//指向vm_operations_struct数据结构的指针(里面全是函数指针),用于虚存的打开关闭和建立映射,页面出错时就要用到这里面函数
struct vm_operations_struct * vm_ops;
unsigned long vm_pgoff; /* offset in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */
struct file * vm_file;
unsigned long vm_raend;
void * vm_private_data; /* was vm_pte (shared mem) */
};
(2)VMA标志 unsigned long vm_flags;
比较重要的VM_READ VM__WRITE VM_EXEC
(3)VMA操作 struct vm_operations_struct * vm_ops; (页缺失调用的函数就在这里)
(4)内存区域的树形结构(short vm_avl_height; 用于快速搜索)和链表表结构 ( struct vm_area_struct *vm_next; 用于遍历)用了两套数据结构
(四)Linux存储管理实现(实现中使用到的函数)
所有进程使用一个页目录表,每个进程都拥有一个页表
linux调用这些函数,把上述提到的有关内存管理的数据结构联系起来,完成映射。
我们先从图上看一下这几类数据结构之间的联系
1、设置CR3(CR3:页目录基址寄存器 保存页目录的起始地址。)
(1)一个系统只能有一个页面目录,每个进程都有其自身的页面目录项就是PGD中的其中一项pgd_t,存放在mm_struct中,每当调度一个进程的时候,内核都要为即将运行的进程设置好CR3,MMU的硬件会从CR3中取得指向当前页目录的指针。在该进程运行之前,CR3已经设置好了。MMU在进行映射是所用的是物理地址,既有下面的转换:
内核中切换进程时将CR3设置为指向新的进程的页目录PGD,而该目录的起始地址在内核代码中是虚地址,但CR3需要的是物理地址。
static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk,
unsigned cpu)
{
asm volatile("movl %0,%%cr3": :"r" (__pa(next->pgd)));
}
注:
#define __PAGE_OFFSET (0xC0000000)
#define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET)
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
系统空间占据了每个虚存空间的最高的1G字节,但是在物理内存中却是从0地址开始。所以,对于内核来说其地址映射是很简单的线性映射,(0xC0000000) 就是两者之间的位移量。对于系统空间而言,给定一个虚地址x,其物理地址就是__pa(x)( 内核代码中当需要知道与一个虚地址对应的物理地址时提供方便)
???通过PGD和虚拟地址的高10位找到某一个pgd_t是通过MMU来完成的吗
2、设置页面表项
每个页表项中的内容是随机的,是由物理页地址内容确定的,即由内存管理程序通过设置页表项来确定,
叶框地址指定了一页物理内存的起始地址,因为内存是4K字节对齐的,所以其12位用作标志位等
在上一节中讲到作为指针只需要它的高20位,低12位用于页面状态信息和页面的访问权限,但是这12位的信息作为页面保护定义到pgport_t结构中,将页面序号左移12位,再与页面控制/状况位段相或,就得到了相对应的表项的值。
在linux中宏操作mk_pte()来完成。
#define __mk_pte(page_nr,pgprot) __pte(((page_nr) << PAGE_SHIFT) | pgprot_val(pgprot))
3、pte和mem_map结合找到对应的某个物理页面
pte_t高20位用来看做物理页面的序号。内核中有个全局变量mem_map,指向page数据结构数组,页面表项(pte_t)的高20位对于软件是一个物理页面的序号,将这个序号用作数组下标就可以从mem_map找到这个代表物理页面的page数据结构。(对于硬件,则在地位补上12个0后就是物理页面的起始地址)。对应的宏操作
#define pte_page(x) (mem_map+((unsigned long)(((x).pte_low >> PAGE_SHIFT))))与&mem_map[x] 一样的