MM基础教程

来源:互联网 发布:域名过户需要什么资料 编辑:程序博客网 时间:2024/04/30 01:32
 一、存储管理

存储管理是我第一个阅读的部分,因为涉及到很多与进程控制,文件系统相关的知识,以及对代码中的C语言不适应等诸多原因,这一段读的很费力,当然,学到的东西也就更多。

我会从三个部分总结每部分的内容, 1.重要的数据结构相关 2.重要的函数相关 3.重要的程序流程以及特性
下面开始:
1. 重要的数据结构相关
1)32bit地址的得出
Linux的内存管理采用的是一种多层的概念,就好像一个中学一样,都只为了一个目标,实现存储功能,但为了便于管理和效率等原因,内存管理也是 一样,他们被分成了几个层,页面目录PGD, 中间目录PMD和页面表PTE,对于32bit的CPU来说,PGD占10bit有效位(其余的32bit-10bit全部都是0),也就是1024个目 录项,每个目录项是一个32bit的pointer,它指向一个(不占位的)PMD,然后PMD指向1024个PTE,每个PTE中存放的pointer 指向一个大小为4k的物理页面。(下面的所占位数指的是有效位)

+++++++++++++++++++++++++++++++++++++++++++++++++++
+ 10bit PGD + 0bit PMD + 10bit PT + 12bit OFFSET +
+++++++++++++++++++++++++++++++++++++++++++++++++++
32bit地址,两层映射

而对于大于32bit的CPU来说,为了完成这个任务,只需改变PMD即可…
32bit的CPU支持4G的内存空间,其中3G为用户空间,1G为内核系统空间,这1G空间位于虚拟空间的最顶部,它通过一个PAGE_OFFSET的偏移量映射出它的物理地址。
--------------------------------------------------------------------------------------------------------
#define __PAGE_OFFSET (0xc0000000)//3G
#define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET)
#define __pa(x) ((unsigned long)(x) – PAGE_OFFSET)//物理地址
#define __va(x)((void *)((unsigned long)(x) + PAGE_OFFSET)) //虚拟地址

#define TASK_SIZE (PAGE_OFFSET) //用户空间的上限
---------------------------------------------------------------------------------------------------------
而地址映射在这里起什么作用呢?我将会在后面讲道…
PGD, PMD和PT的数据结构 (定义于include/asm-i386/page.h)
32bit时,使用两层结构
-----------------------------------------------------------------------------------------------------------
struct pte_t {unsigned long pte_low; /*低位12bit有效,作为页面信息位*/};
struct pmd_t{unsigned long pmd;}
struct pgd_t{unsigned long pgd;}

#define pte_val(x)((x).pte_low)
------------------------------------------------------------------------------------------------------------
32bit以上时,将使用三层结构
-------------------------------------------------------------------------------------------------------------
struct pte_t{unsigned long pte_low, pte_high;}
struct pmd_t{unsigned long long pmd;}
struct pgd_t{unsigned long long pgd;}

#define pte_val(x)((x).pte_low | ((unsigned long long)(x).pte_high << 32)) //得到36bit地址
-------------------------------------------------------------------------------------------------------------
上面已经谈到pte_t并不用于页面位置的指定,这是因为所有的物理页面都是以4k为边界对齐的,因此32bit地址的前20bit可以看作是物 理地址的序号,通过他就已经可以找到相应的物理页面。而另外的12bit用于显示状态信息和访问权限,程序在page.h中定义了一个说明页面信息的结构 pgprot_t:
struct pgprot_t{unsigned long pgprot;}
prprot的值对应于i386MMU的页面表项的低12bit,其中9位是标志位,在代码中给出了定义。
(include/asm-i386/pgtable.h)
--------------------------------------------------------------------------------------------------------------
#define _PAGE_PRESENT0x001 // 常用到的P标志位
#define _PAGE_RW0x002
#define _PAGE_USER0x004
#define _PAGE_PWT0x008
#define _PAGE_PCD0x010
#define _PAGE_ACCESSED0x020
#define _PAGE_DIRTY0x040
#define _PAGE_PSE0x080
#define _PAGE_GLOBAL0x100
#define _PAGE_PROTNONE 0x080 //这一位并不起作用
-------------------------------------------------------------------------------------------------------------
到此,我们已经知道了32bit(或更多位)包含的内容,我们把他们合在一起就得到了代表一个表项的地址,具体的操作如下:
-------------------------------------------------------------------------------------------------------------
#define __mk_pte(page_nr, prprot) /
__pte(((page_nr) << PAGE_SHIFT) | prprot_val(pgprot))
// 将页面页号左移12bit,然后与页面信息位合在一起
#define pgprot_val(x)((x).pgprot)
#define __pte(x)((pte_t) {(x)})
--------------------------------------------------------------------------------------------------------------
在此,我们就得到了有效的32bit地址。

2)基于32bit地址的物理内存管理
我们前面已经提到过32bit地址的前20bit可以看作是物理页面的序号,而事实上这二十位还可以作为下标,以便page结构的数组确定相应的 物理页面,每个工作的物理页面对应一个page结构,所以page结构数组代表了正在工作中的全部的物理页面,内核中有个全局量mem_map指向这个结 构数组。
--------------------------------------------------------------------------------------------------------------
#define set_pte(pteptr, pteval)(*(pteptr) = pteval)
#define pte_none(x)(!(x).pte_low)
// 判断P标志位是否为1
#define pte_present(x)((x).pte_low & (_PAGE_PROTNONE | _PAGE_PRESENT))

static inline int pte_dirty(pte_t pte) /
{return (pte).pte_low & _PAGE_DIRTY;}
static inline int pte_young(pte_t pte) /
{return (pte).pte_low & _PAGE_ACCESSED;}
static inline int pte_write(pte_t pte) /
{return (pte).pte_low & _PAGE_RW;}
// mem_map+x = &mem[x]
#define pte_page(x) /
(mem_map + ((unsigned long)(((x).pte_low >> PAGE_SHIFT)))
// 转换成代表物理页面的page数组下标
#define virt_to_page(kaddr)(mem_map + (__pa(kaddr) >> PAGE_SHIFT))
---------------------------------------------------------------------------------------------------------------
下面介绍代表物理页面的page数据结构:
mem_mep -----------&#61664; page[0] (随着不断的深入,会渐渐给出完整的注释)

typedef struct page { // page, mem_map_t
struct list_head list; // 指向上下相邻的两个page结构,构成双向链表
struct address_space *mapping; // 指向一个address_space结构,用来指定映射节点
// 当页面内容来自一个文件时,index代表着该页面在文件中的序号,当页面内容被换出到// 交换设备上时,但还保留着内容作为缓存时,则index指向了页面的去向
unsigned long index;
struct page *next_hash; // 指向在hash表中的排列的下一个page结构,以便快速查找
atomic_t count; // 如果该页框空闲,则为0;如果被分配给一个或多个进程使用,或用 //于某些内核数据结构,则该域为一个>0的值
unsigned long flags;
struct list_head lru; // 应该是链入某个运行队列中
unsigned long age;
wait_queue_head_t wait; // 等待此页面的等待队列
struct page **pprev_hash; // 作为next_hash的补充
struct buffer_head *buffers;
void *virtual; // 内核虚拟地址
struct zone_struct *zone; // 此page所在的管理的数据结构zone,我们马上就要讲到
}mem_map_t;
---------------------------------------------------------------------------------------------------------------------------
这就是2.4内核的page结构,在这里我想先详细讲两个域,flags域和zone_struct管理域:

unsigned long flags ---
一个由32个标志组成的数组,用来描述页框的状态:
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
标志名 + 意义
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
PG_decr_after + read_swap_cache_async()函数将会用到
PG_dirty + 没有使用
PG_error + 在调页时发生I/O错误
PG_free_after + 从正规文件读取数据时用到
PG_DMA + 由ISA DMA使用
PG_locked + 也不能被换出
PG_referenced + 通过页高速缓存的散列表来访问页框
PG_reserved + 页框留给内核代码使用或不能使用
PG_skip + 用于SPARC/SPARC64结构以“跳过”一些空间地址
PG_Slab + Slab结构用到
PG_swap_cache + 交换高速缓冲用到
PG_swap_unlock_after + read_swap_cache_async()函数将会用到
PG_uptodate + 在完成读操作后置位,除非发生磁盘I/O出错
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
在内核中,对于每一个PG_xyz标志,都定义了一个相应的宏PageXyz来读和设置它的值。
它标识出了每一个物理页面的各个状态。

Zone_struct---
用于内存管理的数据结构。
---------------------------------------------------------------------------------------------------------------------------
/* Free memory management – zoned buddy allocator */

#define MAX_ORDER 10
typedef struct free_area_struct {
struct list_head free_list;
unsigned int *map; // 可能是指向空闲的page结构(物理页面)链表的指针
} free_area_t;

struct pglist_data;

typedef struct zone_struct {
/* Commonly accessed fields; */
spinlock_tlock; // 自旋锁
unsigned long offset; // 该分区在mem_map中的起始页面号
unsigned long free_pages; // 该分区空闲页面数
unsigned longinactive_clean_pages; // 在inactive_clean队列中的页面数
unsigned longinactive_dirty_pages; // 在inactive_dirty队列中的页面数
// pages_low物理页面的下限,page_high为内存充裕的标准
unsigned long pages_min, pages_low, pages_high;
/* free areas of different different sizes */
struct list_head inactive_clean_list; // inactive_clean队列指针
free_area_t free_area[MAX_ORDER]; // buddy管理的空闲页面队列
/* rarely used fields */
char*name; // 此zone的名字
unsigned long size; // 此zone所管理的物理内存的大小
/* Discontig memory support fields. */
struct pglist_data*zone_pgdat; // 存储节点
unsigned longzone_start_paddr; // 此zone在物理内存中的起始位置
unsigned long zone_start_mapnr; // zone在mem_map中的索引
struct page *zone_mem_map;
} zone_t;

#define ZONE_DMA0
#define ZONE_NORMAL 1
#define ZONE_HIGHMEM 2
#define MAX_NR_ZONES3
---------------------------------------------------------------------------------------------------------------------------
为了能详细的讲清这个复杂的结构,我们再看一个结构,struct pglist_data.
---------------------------------------------------------------------------------------------------------------------------
typedef struct pglist_data {
zone_tnode_zones[MAX_NR_ZONES]; // 该节点的三个管理区
zonelist_tnode_zonelists[NR_GFPINDEX]; // 多种管理策略
struct page*node_mem_map; // 此节点的包含的物理页面
unsigned long*valid_addr_bitmap; // 关于可用和不可用的页面的bitmap
struct bootmem_data*bdata;
unsigned long node_start_paddr; // 此节点开始的物理地址
unsigned longnode_start_mapnr; // 此节点所指向的第一个page的page number
unsigned long node_size; // 在此节点上的页面总数
intnode_id; // 当前节点的序号
struct pglist_data*node_next; // 指向下一个节点
} pg_data_t;

/* 其中包含一个重要的数据结构zonelist_t */

typedef struct zonelist_struct {
zone_t*zones[MAX_NR_ZONES + 1];
int gfp_mask;
} zonelist_t;

/* 另外的一个数据结构bootmem */
// 此结构只用于boot时的内存存储和分配,它通过一个bitmap来回收和分配内存,此bitmap
// 被放在kernel后的空间,只能管理896M以下的low addr部分
typedef struct bootmem_data {
unsigned long node_boot_start; // bootmem bitmap的开始位置,在紧挨着kernel end的页面
unsigned long node_low_pfn;
void *node_bootmem_map;
unsigned long last_offset;
unsigned long last_pos;
} bootmem_data_t;

---------------------------------------------------------------------------------------------------------------------------
在传统计算机体系中,整个物理空间都是均匀一致的,也就是CPU访问某一个地址的时间与位置无关,于是称为“均匀存储结构”(Uniform Memory Architecture),简称UMA。但现在这种传统被打破了,特别表现在多CPU结构的系统中,均匀的存储结构已经被破坏了,这就导致了一种可能, 比如有4个页面的需要要储存,但本地的空闲空间却只有3页,如果按照UMA的做法,会把前三个页面存入本地,而剩下的一个存入公用存储区,但由于是不均匀 的,这显然不适合。而为了解决NUMA这种问题,便提出了pglist_data这种结构,对存储节点进行可选择的管理。
Zone_t 结构数组包含了3个管理区,具体要用到那个管理区,还得看具体情况
Zonelist_t 结构数组规定了NR_GFPINDEX种页面存储分配策略

好了,关于物理空间管理的数据结构相关我们就讲到这里,下面开始将虚拟空间管理的数据结构相关


3)虚拟空间管理
按照《情景》所讲的,虚拟内存的空间管理和物理页面完全是两种不同的概念,物理页面更像一个仓库,管理它拥有的资源,而虚拟页面管理的是它从仓库得到的资源。
这时,我们面临着这样的一个问题:虚拟空间从仓库中得到的资源往往是若干离散的虚存空间,Linux为了解决这个问题,又构建了一个对此抽象的数据结构vm_area_struct
---------------------------------------------------------------------------------------------------------------------------
struct vm_area_struct {
struct mm_struct *vm_mm; // 虚拟内存空间表
unsigned long vm_start;
unsigned long vm_end;
struct vm_area_struct *vm_next; // 对于每个任务都形成一个VM的链接表
pgprot_t vm_page_prot; // 虚拟页面的保护结构
unsigned long vm_flags; // 对于此页面特性的描述(祥见第四页的flags表)

/* AVL树结构 */
short vm_avl_high;
struct vm_area_struct *vm_avl_left;
struct vm_area_struct*vm_avl_right;

/* 一些特殊的成分,用来记录和管理虚存页面和磁盘文件之间的联系 */
struct vm_area_struct *vm_next_share;
struct vm_area_struct**vm_pprev_share;

struct vm_operations_struct *vm_ops;
unsigned long vm_pgoff; // offset in PAGE_SIZE units

struct file *vm_file;
unsigned long vm_raend;
void *vm_private_data; // vm_pte(share mem)
}; // 此结构变量名常为vma
---------------------------------------------------------------------------------------------------------------------------
结构中的vm_start和vm_end决定了一个虚存空间,vm属于[vm_start, vm_end),也就是vm_end不包含在区间内。对于某一个区间,它必须保证自己的访问权限和其它属性始终一致,这是通过pgprot_t和 flags来保证的。而对于某个任务(进程)的所有虚存区间,都要按其地址的高低顺序链接在一起,结构中的vm_next指针就是用于这个目的。
内核中给定一个虚拟地址而要找出其所属区间是一个频繁要用的操作,如果每次都要顺着vm_next在链中作线性搜索的话,会对内核的效率产生明显的影响。所以在vm链较长的情况下,建立一个AVL树用来存储和搜索,速度快的多,而代价只是O(lg n)。
有一些特殊成分,是与文件管理相关的,我们在这里不再赘述,以后遇到再做详解。
我们在这里还要讲解一个重要的成分vm_ops,它是一个指向vm_operations_struct结构的指针。
此结构也是在include/linux/mm.h种定义的。
---------------------------------------------------------------------------------------------------------------------------
struct vm_operations_struct {
void (*open) (struct vm_area_struct *area);
void (*close) (struct vm_area_struct *area);
struct page *(*nopage) (struct vm_area_struct *area, unsigned long address, int write_access);
};
---------------------------------------------------------------------------------------------------------------------------
此结构全是函数指针。其中open, close, nopage分别用于虚存区间的打开,关闭和建立映射。
其中nopage是当虚存页面不在物理内存中而引起“页面错误”异常时所应调用的函数。
最后,vm_area_struct中还有一个很重要的指针vm_mm,该指针指向一个重要的数据结构mm_struct,它是在include/linux/sched.h中定义的:
---------------------------------------------------------------------------------------------------------------------------
struct mm_struct {
struct vm_area_struct*mmap; // 指向VMAs的单链表的头节点
struct vm_area_struct*mmap_avl; // 指向VMAs的AVL树的头节点
struct vm_area_struct*mmap_cache; // find_vma()最近找到的节点
pgd_d*pgd;
atomic_tmm_users; // 拥有用户空间的用户数量
atomic_tmm_count; // 与此结构相关的进程或结构的数量
intmap_count; // VMAs元素数量
struct semaphoremmap_sem;
spinlock_t page_table_lock;

struct list_headmmlist; // 所有active的mm_struct的双向链表
unsigned long start_code, end_code, start_data, end_data;
unsigned longstart_brk, brk, start_stack;
unsigned longarg_start, arg_end, env_start, env_end;
unsigned longrss, total_vm, locked_vm;
unsigned longdef_flags;
unsigned long cpu_vm_maskl;
unsigned long swap_cnt; // 下一个过程需要交换的page数量
unsigned long swap_address;
mm_context_t context;
}; // 一般用于这个数据结构的变量名是mm
---------------------------------------------------------------------------------------------------------------------------
mm_struct是比vm_area_struct更高一层的数据结构,每一个进程只有一个mm_struct,但每个mm_struct可以被多个进程所共享,mm_struct所设立的mm_users和mm_count就是来记录与此相关的信息的。
mm_struct的头三个指针都是关于虚存空间的。第一个mmap用来建立一个虚存区间结构的单链性队列。第二个mmap_avl用来建立一个 虚存区间结构的AVL树,第三个指针mmap_cache,用来指向最近一次用到的虚存区间,这是因为程序中的地址常常带有局部性。
另外还有一个成分map_count,用来说明队列中或AVL树中到底有几个虚存区间结构,也就是说该进程有几个虚存空间。
指针pgd是指向该进程的页面目录的,当内核调度一个进程进入运行时,就将这个指针转换成物理地址,并写入CPU内部的控制寄存器CR3中。
mm_struct和vm_area_struct只是表明了对页面的要求,一个虚拟地址有相应的虚拟区间存在,并不能保证该地址所在的页面已经 映射到某个物理页面。当访问失败时,会因为”page fault”异常导致一个服务程序来处理这个问题。这是我们以后会具体涉及的问题。

mm数据结构的部分,到此也就讲完了,如果有什么遗漏,以后我会一点一点补上&#61514;

一点小的总结:
task_struct --&#61664; mm_struct
mm_struct --&#61664; pgd_t --&#61664; pte_t
mm_struct --&#61664; vm_area_struct

2. 函数相关
在我看来,这是一个很冗长的过程,需要有耐心看下去,并看懂,这才是最重要的。

随着不断的阅读各种书籍,对于某一方面的知识也有更深的理解,对于内存管理的部分,思路开始变得越来越清晰。我把内存管理理解成三个部分:
(1)内核级的页面管理,对物理页面进行仓库式的总体管理,分配,回收以及其他,其中包含了很多细节和巧妙的方法,以达到对内存最有效率的使用。
(2)进程级的内存管理,对每一个进程的虚拟内存区间进行管理,但我觉得进程级的内存管理,只不过是用来使内存被程序正确的使用,而并不涉及到回收分配等细节。
(3)两个级别管理的联系,进程通过系统调用,异常服务等手段从物理空间中申请页面。

2.1 内核级的页面管理(这个标题似乎不太恰当)
他可能是Linux内核中相当复杂的一个部分,至今我仍未掌握,只能边记边想&#61514;
那好,我们现在开始介绍第一个函数。
2.1.1 mem_init( )
start_mem表示动态内存的第一个线性地址,紧挨着内核所占内存
end_mem为动态内存的最后一个地址+1
其中涉及到一个函数free_area_init()负责给mem_map分配合适大小的内存区。
mem_init()所作的是把有关页面的PG_reserved标志清除,以使他们用作动态内存。这部分页面的范围是start_low_mem ~~ i386_endbase和start_mem ~~ end_mem,它们会在这步之后用作动态内存。
mem_init()还会把物理地址大于或等于0x1000000的所有页框的PG_DMA标志清除。
首先,mem_init()函数确定num_physpages的值,也就是系统现有的页框总数,然后再计算出PG_reserved的页框数。 编译内核时产生的一些符号能使这个函数计算出保留给硬件、内核代码、内核数据的页框数,还能计算出内核初始化期间使用的随后又被释放的页框数。最后, mem_init()函数把与动态内存相关连的每一个页框描述符count域的置为1,并调用函数free_page()。
因为free_page函数增加变量nr_free_pages的值,因此在循环结束时,这个变量将包含动态内存页框的总数。
原创粉丝点击