linux内核源码阅读之-内存管理1

422 阅读9分钟

本文以i386为例,内核代码版本:2.4.0

在操作系统中我们学过进程使用的虚拟地址,需要有一次地址映射才能转换为物理地址,这个转换过程是怎么样的呢?

1.页式管理结构

i386将虚拟地址分成了四段,分别代表PGD(页面目录),PMD(中间目录),PTE(页面表),offset(页内偏移),例如给一个32位的地址:

image.png

image.png

每个进程都有一个指向PGD表的指针,当需要将虚拟地址转换成物理地址时,先从cr3寄存器里边拿出来这个地址,也就得到了这个表,这个表有多少项呢?要根据虚拟地址的划分来算,如果虚拟地址划分中PGD是n位,那么PGD表就有2的n次方项,PMD,PTE同理。虚拟地址中PGD的值就是PGD表的下标,根据这个下标拿到一个指向PMD表的指针,然后根据虚拟地址中PMD的值拿到在PMD表中的下标,这个下标是指向一个PTE表的指针,根据此指针拿到PTE表,同理根据PTE值拿到一个地址,这个地址指向了实际的物理页面,此物理页面中偏移offset的地方就是虚拟地址对应的实际物理地址。

知道了整个过程,代码层面如何实现?本文讨论两层地址映射结构,也就是PMD部分在虚拟地址的划分中所占长度为0的情况。

几个重要的宏定义:

//include/asm-i386/pgtable-2level.h
#define PGDIR_SHIFT	22   //32位右移22位得到高10位,就是PGD表中的下标
#define PTRS_PER_PGD	1024   //PGD10位能找到1024个PGD表项
#define PMD_SHIFT	22  //PMD和PGD都是右移22位得到,PMD表只有一个项
#define PTRS_PER_PMD	1  //PMD表只有一项,
#define PTRS_PER_PTE	1024  //每个PTE(页表)有1024个条目,10位

32位地址意味着4G的虚拟地址空间,内核将这4G分位两段:0-3G为用户空间,3-4G位内核空间,但是物理内存总是从最低的0开始,所以对于内核来说地址映射是简单的线性映射,0xC0000000就是两者之间的偏移量。

//include/asm-i386/page.h
#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))

在进程切换的时候,需要将cr3设置成指向新进程的页面目录表(PGD),而改目录的起始地址在内核代码中是虚拟地址,但是cr3需要的是物理地址,这个时候就要用到__pa()了,如下:

//include/asm-i386/mmu_context.h
asm volatile("movl %0,%%cr3": :"r" (__pa(next->pgd)));

2.段选择子

在我们学习汇编的时候,一般说的指令寻址都是cs:ip, cs寄存器指向了代码段,ip指向了代码段的偏移,通过这种方式来进行指令寻址,但是经常查coredump时候看到的ip(32位eip,64位rip)中都是一个完整的地址,根据这个地址能直接定位到coredump时候得指令,而cs寄存器是一个很小的固定值,这是为什么?为什么rip保存的不是一个偏移地址? 我们先来看看按照段式寻址(cs:ip)这种方式是如何做的吧。

image.png 图上是一个段选择子(cs、ds等寄存器里),高13位代表了一个索引。80386中有个GDTR(全局段描述寄存器),这个寄存器里保存了一个地址,地址指向了内存中的一个数组,用段选择子的高13位作为下标在这个数组中取出一个值,这个值就是相应段(代码段、数据段等等)的基地址,对于指令寻址来说,再加上ip寄存器里的值就能找到指令的位置。

那么问题来了,为何和现在看到的不一样?很多操作系统的课本中说的是操作系统使用的段页式虚拟内存管理,这简直就是胡扯。实际上是很久很久以前,是只有分段的,但是内存不够时候,需要将一部分物理空间换出到磁盘上时候,段式结构中需要将整个段(通常很大,我们的线上模块代码段是以G为单位的)换出,效率很低。在i386之前,分段已经存在和很久,为了兼容,i386一律是对程序中的地址先分段,再将段分页,linux内核只要服从intel的选择,但是它绕过了这种方式。具体的做法是:将从段选择子->全局描述符表中拿到的基地址都这是为0,这样每个段的起始地址都是0,对于cs:ip这种指令寻址方式来说,ip寄存器里存放的就是实际的虚拟地址,不用和cs寄存器一起运算了。

3. 重要的数据结构

//放在这里备用,后边好查
//include/asm-i386/page.h
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)
#define PTE_MASK	PAGE_MASK

typedef struct { unsigned long pgprot; } pgprot_t;

#define pmd_val(x)	((x).pmd)
#define pgd_val(x)	((x).pgd)
#define pgprot_val(x)	((x).pgprot)

#define __pte(x) ((pte_t) { (x) } )
#define __pmd(x) ((pmd_t) { (x) } )
#define __pgd(x) ((pgd_t) { (x) } )
#define __pgprot(x)	((pgprot_t) { (x) } )

pte(页表项)作为指针其实只需要其高20位,就能定位到一个物理页面(4K,12位,用虚拟地址中的offset来定位),所以pte_t的低12位用于保存指向的物理页面的状态信息和访问权限。

//include/asm-i386/page.h
typedef struct { unsigned long pgprot; } pgprot_t;

//include/asm-i386/pgtable.h
#define _PAGE_PRESENT	0x001   //是否在内存中(swap)
#define _PAGE_RW	0x002   //读写标志控制
#define _PAGE_USER	0x004
#define _PAGE_PWT	0x008
#define _PAGE_PCD	0x010
#define _PAGE_ACCESSED	0x020
#define _PAGE_DIRTY	0x040
#define _PAGE_PSE	0x080	/* 4 MB (or 2MB) page, Pentium+, if present.. */
#define _PAGE_GLOBAL	0x100	/* Global TLB entry PPro+ */

#define _PAGE_PROTNONE	0x080	/* If not present */

内核中有一个全局mem_map指针,指向一个page数组,每个page代表一个物理页面,page这个结构后边会讲到。

set_pte()是一个宏,用来把一个表项的值设置到一个页面表项中:

//include/asm-i386/pgtable-2level.h
#define set_pte(pteptr, pteval) (*(pteptr) = pteval)

映射过程中,mmu先检查P标志位(_PAGE_PRESENT),表示映射的页面是否在内存中,只有p位1的时候才会完成映射,否则就会产生缺页异常。

page是代表物理页面的一个结构

typedef struct page {
	struct list_head list;
	struct address_space *mapping;
        // 页面内容来自文件时,index代表该页面在文件中的序号;
        // 页面被换出时,代表页面的去向
	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; /* non-NULL if kmapped */
	struct zone_struct *zone;
} mem_map_t;

系统中的每个物理页面都有一个page(mem_map_t)结构,系统初始化时候根据物理内存建立起来一个page结构输入mem_map,作为物理页面的仓库,每个page都代表一个物理页面,每个物理页面的page结构在这个数组里的下标就是该物理页面的序号。物理页面通常分为ZONE_DMA(硬件设备要求物理页连续,同时地址不能太高)和ZONE_NORMAL两个管理区,根据配置,还有可能存在ZONE_HIGHMEM(对应虚拟地址中的高端内存高端内存),每个管理区有一个zone_struct数据结构,在zone_struct数据结构中有一组空闲区间(free_area_t)队列,为什么是一组?因为往往需要成块的连续分配物理页面,所以分为连续1个,2个,4个...2的n次方个连续物理页面,n最大为10,也就是最大可以分配到连续2的10次方(1024)个连续页面,代码定义:

//include/linux/mmzone.h
typedef struct free_area_struct {
        //维持双向队列,嵌套在每个page中,把page串起来,物理页面的分配就是给这里串上物理页面
        //后续会讲
	struct list_head	free_list; 
	unsigned int		*map;
} free_area_t;

struct pglist_data;

typedef struct zone_struct {
	spinlock_t		lock;
        //该分区在所有物理页面(mem_map)中起始页面编号
	unsigned long		offset; 
	unsigned long		free_pages;
	unsigned long		inactive_clean_pages;
	unsigned long		inactive_dirty_pages;
	unsigned long		pages_min, pages_low, pages_high;

	/*
	 * free areas of different sizes
	 */
	struct list_head	inactive_clean_list;
	free_area_t		free_area[MAX_ORDER]; //空闲页面

	/*
	 * rarely used fields:
	 */
	char			*name;
	unsigned long		size;
	/*
	 * Discontig memory support fields.
	 */
	struct pglist_data	*zone_pgdat;
	unsigned long		zone_start_paddr;
	unsigned long		zone_start_mapnr;
	struct page		*zone_mem_map;
} zone_t;

由于NUMA架构的引入,需要在zone_struct上再包上一层pglist_data,之前的page结构数组也不再是全局的了,而是属于从属节点了。

image.png

NUMA架构

typedef struct pglist_data {
        //最多三个,每zone都有个指针指向所属pglist_data,只属于本节点
	zone_t node_zones[MAX_NR_ZONES]; 
        //NR_GFPINDEX=256,这个代表了分配策略,内存分配时候先在本节点分配,也就是上边这个数组,分配不到了再从全局的空间分配,分配之后先指定分配策略,也就是数组下标,然后去相应的zone里分配
	zonelist_t node_zonelists[NR_GFPINDEX];
	struct page *node_mem_map;  //当前node的物理页面
	unsigned long *valid_addr_bitmap;
	struct bootmem_data *bdata;
	unsigned long node_start_paddr;
	unsigned long node_start_mapnr;
	unsigned long node_size;
	int node_id;
	struct pglist_data *node_next; //多个node的pglist_data形成一条链表
} pg_data_t;

//这个是全局的
typedef struct zonelist_struct {
	zone_t * zones [MAX_NR_ZONES+1]; // NULL delimited
	int gfp_mask;
} zonelist_t;

image.png 其中pglist_data一个node一个

虚拟内存数据结构之间关系: image.png

task_strcut是每个进程的数据结构,每个结构上有个mm_struct指针,这就是虚拟内存相关数据结构的指针,这个结构上有个pgd,根据pgd能找到pmd表,根据虚拟地址里边的pmd和pte,offset字段可以定位到具体的物理内存页面中的偏移(offset,实际需要请求的地址),如果这个物理页面没有建立映射或者页面不在内存中(swap),就会产生缺页异常(注意:是异常不是中断)。这个结构体上还有一个mm指针,指向一个vm_area_struct,这是一个链表,通过vm_next指针把当前进程所有的vm_area_struct连接起来。

struct vm_area_struct {
	struct mm_struct * vm_mm;	/* VM area parameters */
	unsigned long vm_start;
	unsigned long vm_end;

	/* linked list of VM areas per task, sorted by address */
	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 */
	short vm_avl_height;
	struct vm_area_struct * vm_avl_left;
	struct vm_area_struct * vm_avl_right;

	/* For areas with an address space and backing store,
	 * one of the address_space->i_mmap{,shared} lists,
	 * for shm areas, the list of attaches, otherwise unused.
	 */
	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, *not* PAGE_CACHE_SIZE */
	struct file * vm_file;
	unsigned long vm_raend;
	void * vm_private_data;		/* was vm_pte (shared mem) */
};


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);
};

struct mm_struct {
	struct vm_area_struct * mmap;		/* list of VMAs */
	struct vm_area_struct * mmap_avl;	/* tree of VMAs */
	struct vm_area_struct * mmap_cache;	/* last find_vma result */
	pgd_t * pgd;  //页面目录表地址
	//之所以用原子是因为fork时候子进程会复用父进程的空间
        atomic_t mm_users;			/* How many users with user space? */
	atomic_t mm_count;			/* How many references to "struct mm_struct" (users count as 1) */
	int map_count;				/* number of VMAs */
	struct semaphore mmap_sem;
	spinlock_t page_table_lock;

	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;
};