Linux内核--内存管理

Linux内核--内存管理

Linux2.4内核以及高版本的Linux中,采用的是基本的页式管理机制,为什么这么说呢?因为在Linux内核启动的时候实际上已经让段式管理机制不起作用了。在i386机器中一般是不能跳过段式管理直接到页式管理的,Linux也不例外,所以在启动进程(线程)的时候将代码段数据段等进行如下设置(include/asm-i386/processor.h):

#define start_thread(regs, new_eip, new_esp) do {             \

       __asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0));    \

       set_fs(USER_DS);                             \

       regs->xds = __USER_DS;                                 \

       regs->xes = __USER_DS;                                 \

       regs->xss = __USER_DS;                                 \

       regs->xcs = __USER_CS;                                 \

       regs->eip = new_eip;                                 \

       regs->esp = new_esp;                                 \

} while (0)

也就是将代码段设置为__USER_CS,将数据段、堆栈段、附加段等设置为__USER_DS,在include/asm-i386-segment.h中如下定义各个段:

#define __KERNEL_CS              0x10

#define __KERNEL_DS              0x18

 

#define __USER_CS     0x23

#define __USER_DS     0x2B

也就是将内核代码段选择子定义为0x10,将内核数据段定义为0x18,将用户代码段定义为0x23,将用户数据段定义为0x2B。这几个段的描述符在arch/i386/kernel/head.S中定义:

ENTRY(gdt_table)

       .quad 0x0000000000000000 

       .quad 0x0000000000000000 

       .quad 0x00cf9a000000ffff    

       .quad 0x00cf92000000ffff    

       .quad 0x00cffa000000ffff    

       .quad 0x00cff2000000ffff    

       .quad 0x0000000000000000 

       .quad 0x0000000000000000 

可以看到,全局段描述符的前两项为空,未用,第三项为从0开始的4GB内核态代码段,第四项为从0开始的4GB内核态数据段,第五项为从0开始的4GB用户态代码段,第六项为从0开始的4GB用户态数据段。

Linux如此设置让每个进程的用户态和内核态的代码段、数据段一致,让段式管理实际上不起作用,而直接进行页式管理模块。

当然,我们在使用Linux的时候会有一些软件比如WINE来模拟Windows环境,运行部分Windows程序,这时候还是会用到段式管理的,这都不是重点,就不多说了。

以前说的页式存储管理是使用三层页表机制,即页目录表PGD,页表PT,和具体页内偏移。而在实际上Linux2.4内核中是在PGDPT之间加了一层PMD,但是在32位系统中,PMD实际上也是不起作用,只有在64位时候才起到其作用。所以在此真正说的还是按照以前的方式来说。

当配置内核选择32位系统的时候将CONFIG_X86_PAE设置为0,在include/asm-i386/pagetable.h中一个定义如下:

#ifndef __ASSEMBLY__

#if CONFIG_X86_PAE

# include <asm/pgtable-3level.h>

#else

# include <asm/pgtable-2level.h>

#endif

#endif

也就是说引用的是asm/pgtable-2level.h头文件,在此头文件中有定义如下:

#define PGDIR_SHIFT  22

#define PTRS_PER_PGD     1024

定义了一个32位线性地址从第bit22开始为页目录项,每个页目录表中有1024个页目录项。

Linux的页式管理中,操作系统实际上是给每个进程分配了4GB的虚拟存储空间。每个进程都有系统空间和用户空间。从0~3GB为用户程序空间,从3GB~4GB为系统空间,在此还是重点讲32位的系统。

进程的3GB自己的虚拟空间是私有的,而最高的1GB系统空间是公共的。如下:

Linux内核--内存管理(一、基础篇)


在实际物理内存中,内核空间总是从绝对地址0开始的,而且对于内核空间来说,映射就是简单的线性映射,不会出现虚拟地址A大于虚拟地址B但是实际映射到的物理地址A<B的情况。所以对于内核空间来说,虚拟地址和物理地址之差就是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))

__pa(x)就是将线性地址转化为物理地址,就是线性地址减去__PAGE_OFFSET__va(x)就是将物理地址转化为线性地址,就是物理地址加上__PAGE_OFFSET

同时在include/asm-i386/processor.h中有如下定义:

#define TASK_SIZE      (PAGE_OFFSET)

表明最大进程大小为3GB,并不包括系统空间。


Linux2.4内核中,内存管理是比较复杂的,所以数据结构也就比较复杂,当然这里说的管理还是指的页式管理。

最底层的数据结构当然就是最基本的页目录表和页表之类,定义在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;

其中,pte_t是页表项,pgd_t是页目录项,pmd_t是中间项(在此不用)。

紧随下面还定义了一个数据结构:

typedef struct unsigned long pgprot; pgprot_t;

pgprot_t是用来说明保护结构的,只用到低12位,因为像pte_tpgd_t的高20位存物理地址,低12位存储的是一些属性等,pgprot_t就是用来说明这些属性的,当把一个物理地址的低12位清0再与pgprot_t相“或”就得出pte_t或者pgd_t项的值。其中页表/页目录表中的低12位属性如下所示(include/asm-i386/pgtable.h)

#define _PAGE_PRESENT 0x001

#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

#define _PAGE_GLOBAL 0x100

 

#define _PAGE_PROTNONE 0x080

其中0x080位在此不用。

1、物理空间管理

在系统中,每个物理页面都有一个记录信息与之相对应,在内核中的数据结构就是page,定义于include/linux/mm.h中:

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;

在内存中,page使用list_head数据结构组成一个双向循环链表,list_head是一个通用的数据结构,贯穿在Linux的内核连接上,它只有两个指针定义如下:

struct list_head{

struct list_head *next,*prev;

};

page数据结构中mappingindex都和文件和内存交换有关,在后面会详解。next_hash是自身指针,将自身连接成一个链表。atomic_t实际上就是int型,count可以理解为目前正在使用的本块内存的程序数量(实际上是用于页面交换的计数,若页面为空闲则为0,分配就赋值1建立或恢复一次映射就加1,断开映射就减一 )。flags是标志页面的状态的标志。

page中有一个非常重要的struct zone_struct *zone,这是指向所属管理区的指针,在计算机中,整个内存区被分成ZONE_DMAZONE_NORMAL(或者还有ZONE_HIGHMEM,用于管理超过1GB的内存空间)。ZONE_DMA里面的页面是专供DMA使用的。DMA使用的内存区间是不需要经过内存映射的,而是外设直接访问内存地址,而且DMA要求地址连续并且地址数值较小,要单独加以管理。

在系统初始化的时候就根据系统内存大小建立了一个page类型的数组,每个page数据结构代表着一个页面,数组的下标就是实际物理内存的页面号。

对于每个管理区都有一个数据结构与之相对应,定义在include/linux/mmzone.h中:

typedef struct zone_struct {

 

spinlock_t lock;

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;

 

 

struct list_head inactive_clean_list;

free_area_t free_area[MAX_ORDER];

 

 

char *name;

unsigned long size;

 

struct pglist_data *zone_pgdat;

unsigned long zone_start_paddr;

unsigned long zone_start_mapnr;

struct page *zone_mem_map;

zone_t;

当初始化完成管理区建立完成以后,某个页面就永久属于某个管理区,不能更改。其中offset是指在mem_map中的起始页面号,从第几个物理页面开始。

zone_struct中有一个空闲区free_area[MAX_ORDER],它的定义也在mmzone.h中如下:

 

#define MAX_ORDER 10

 

typedef struct free_area_struct {

struct list_head free_list;

unsigned int *map;

free_area_t;

它是指内存中空闲区双向队列,在讲page数据结构的时候有一个struct list_head list就是链接到此种队列。但是为什么是“一组”队列而不是“一个”队列呢?因为在分配内存的时候往往需要分配“一块”内存,所以对内存也要按“块”进行管理,所以除了除了要有队列来保存离散的连续长度为1的页面以外,还要有队列来来保持连续长度为248一直到2^MAX_ORDER的队列,所以在zone_struct中要分配一个数组,数组中的每一项保存的是连续长度为2^n的内存页面。

zone_struct中还有一个数据结构:struct pglist_data *zone_pgdat,这个数据结构也是定义在mmzone.h中:

typedef struct pglist_data {

zone_t node_zones[MAX_NR_ZONES];

zonelist_t node_zonelists[NR_GFPINDEX];

struct page *node_mem_map;

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;

pg_data_t;

其中几个常数定义如下:

#define ZONE_DMA 0

#define ZONE_NORMAL 1

#define ZONE_HIGHMEM 2

#define MAX_NR_ZONES 3

 

#define NR_GFPINDEX 0x100

这个数据结构是用来描述“存储节点”的,什么叫存储节点?简单的说来,我们的内存都不是均匀的,包括了RAMROM、显卡RAM等等还有不同CPU模块上的内存等等,Linux使用存储节点来对内存进行划分,使同一存储节点内的内存均匀相同。

node_zones[]就是该节点的最多三个管理区。node_mem_map指向具体节点的page结构数组。zone_struct中的zone_pgdat指向它属于的存储节点(一个管理区只属于一个存储节点?我理解为可以有多个。因为存储节点链是连续的,所以只需要提供指针。所以一个管理区可以通过指针跨越多个存储节点)。node_next指针将存储节点链接成一个单链队列。

pglist_data中还有一种数据结构:zonelist_t,它也是定义在本文件中:

typedef struct zonelist_struct {

zone_t zones [MAX_NR_ZONES+1]; // NULL delimited

int gfp_mask;

zonelist_t;

它其实是一种策略数据结构,一个数据结构表示的是一种内存分配策略,分配内存的时候先从zones[0]管理区里面找符合条件的内存,找不到就从zones[1]里面找,以此类推。每一个数据结构表示一个寻找内存的顺序,每一个存储节点可以有0x100中这种寻找策略(顺序),所以在pglist_data中定义了一个数组:zonelist_t node_zonelists[NR_GFPINDEX]

以上说的都是物理空间管理,好了,来理清一下上面的数据结构我们用如下图来表示:

Linux内核--内存管理(二、数据结构说明)


 

2、虚拟空间管理

除了物理空间管理以外,Linux的一个强大之处还在于其虚拟空间的管理。前面也说过,Linux为每个进程分配了4GB的虚拟空间,其中,用户空间占了3GB。虽然当物理空间不连续的时候还是可以使用页表机制将其映射成连续的虚拟空间,但是实际上使用的时候虚拟空间也不一定是连续的。所以在Linux内核中还有一个数据结构来管理成块的虚拟空间(include/linux/mm.h):

struct vm_area_struct {

struct mm_struct vm_mm;

unsigned long vm_start;

unsigned long vm_end;

 

 

struct vm_area_struct *vm_next;

 

pgprot_t vm_page_prot;

unsigned long vm_flags;

 

 

short vm_avl_height;

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;

struct file vm_file;

unsigned long vm_raend;

void vm_private_data;

};

每一个vm_area_struct管理一块连续的虚拟空间(注意:不同进程的虚拟用户空间一般不互相影响),vm_startvm_end分别表示虚拟空间的开始和结束地址,注意这里的开始地址vm_start是包含在自身的管理区间内的,而vm_end是不包含在内的。

vm_next是用来连接统一进程的所有vm_area_struct虚拟链,是按照高低次序进行连接的。

当一个进程的虚存块划分地较少的时候可以使用链式连接查询,但是如果太多的话这样查询效率会降低,所以在块数较多的时候采用AVL树进行存储,也就是平衡二叉树。vm_area_struct中的vm_avl_heightvm_avl_leftvm_avl_right都是和AVL树有关。

vm_next_sharevm_pprev_sharevm_ops等都和磁盘文件以及内存换出等有关,在以后会说到。

vm_area_struct开头有一个定义:struct mm_struct *vm_mm这个是表明它属于哪个内存进程的内存管理。mm_struct的定义在include/linux/sched.h中:

struct mm_struct {

struct vm_area_struct mmap;

struct vm_area_struct mmap_avl;

struct vm_area_struct mmap_cache;

pgd_t pgd;

atomic_t mm_users;

atomic_t mm_count;

int map_count;

struct semaphore mmap_sem;

spinlock_t page_table_lock;

 

struct list_head mmlist;

 

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;

unsigned long swap_address;

 

 

mm_context_t context;

};

每个进程都有一个唯一的mm_struct数据结构,在进程的PCB中,有一个链接到其自己的mm_structmm_struct是整个用户空间的抽象,也是总的控制结构。其中:

mmap用来建立一个虚存空间的单链队列;

mmap_avl用来建立虚存空间的AVL树;

mmap_cache用来指向最近一次用到的虚存区间结构;

pgd用来指向该进程的页目录表;

一个进程的mm_struct可以为多个进程共享,mm_usermm_count就是用来计数的;

map_count用来记录进程有几个虚存空间;

mmap_sempage_table_lock是用来定义PV操作的信号量。

另外start_codeend_code等就是该进程的代码等起始、结束地址。

 

总的来说,一个进程的访问图如下:

Linux内核--内存管理(二、数据结构说明)

 

关于这些结构的处理方法以后再说。


以下所说皆是在内核中使用的方法:

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

其中,__pa(x)是在内核中给出线性地址求出物理地址的。

__va(x)是在内核中给出物理地址求出线性地址的。

 

page.h中有定义如下:

#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) )

都是用来求出实际值或者强制转换的。

 

include/asm-i386/pgtable-2level.h中有定义如下:

#define __mk_pte(page_nr,pgprot) __pte(((page_nr) << PAGE_SHIFT) pgprot_val(pgprot))

这个是给出页面号和属性来求出实际页面表项的。

 

include/asm-i386/pgtable.h中有定义如下:

#define mk_pte_phys(physpage, pgprot) __mk_pte((physpage) >> PAGE_SHIFT, pgprot)

这个是用来根据物理地址和属性求出实际页面表项的。

 

include/asm-i386/pgtable-2level.h中有如下定义:

#define set_pte(pteptr, pteval) (*(pteptr) pteval)

根据值设置页面表项。

 

pgtable-2level.h中还定义了一些检测页表的方法:

#define pte_none(x)  (!(x).pte_low)

用来检测页表项是否为空。

 

pgtable.h中定义了如下方法:

#define pte_present(x) ((x).pte_low (_PAGE_PRESENT _PAGE_PROTNONE))

用来表示是否存在,如果页面表项不为0但是存在位为0表示映射已经建立,但是页面不在内存中。

 

pgtable.h中还有如下定义:

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

分别表示是否读脏,是否已被访问和是否可写。

 

pgtable-2level.h中有定义如下:

#define pte_page(x) (mem_map+((unsigned long)(((x).pte_low >> PAGE_SHIFT))))

用来给出页表项求出对应的page项。

 

page.h中有如下定义:

#define virt_to_page(kaddr) (mem_map (__pa(kaddr) >> PAGE_SHIFT))

用来给出虚存地址(线性地址)求出对应的page项(page数据结构)。

 

以下为虚拟地址部分

mm/mmap.c中有定义如下:

struct vm_area_struct find_vma(struct mm_struct mm, unsigned long addr)

{

struct vm_area_struct *vma NULL;

 

if (mm) {

 

 

vma mm->mmap_cache;

if (!(vma && vma->vm_end addr && vma->vm_start <= addr)) {

if (!mm->mmap_avl) {

 

vma mm->mmap;

while (vma && vma->vm_end <= addr)

vma vma->vm_next;

else {

 

struct vm_area_struct tree mm->mmap_avl;

vma NULL;

for (;;) {

if (tree == vm_avl_empty)

break;

if (tree->vm_end addr) {

vma tree;

if (tree->vm_start <= addr)

break;

tree tree->vm_avl_left;

else

tree tree->vm_avl_right;

}

}

if (vma)

mm->mmap_cache vma;

}

}

return vma;

}

此函数有两个参数,一个是地址,一个是指向mm_struct结构的指针,返回的是此地址所在的vm_area_struct区域数据结构的指针。

 

在同一文件中还有定义如下:

void __insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vmp)

{

struct vm_area_struct **pprev;

struct file file;

 

if (!mm->mmap_avl) {

pprev &mm->mmap;

while (*pprev && (*pprev)->vm_start <= vmp->vm_start)

pprev &(*pprev)->vm_next;

else {

struct vm_area_struct *prev, *next;

avl_insert_neighbours(vmp, &mm->mmap_avl, &prev, &next);

pprev (prev &prev->vm_next &mm->mmap);

if (*pprev != next)

printk("insert_vm_struct: tree inconsistent with list\n");

}

vmp->vm_next *pprev;

*pprev vmp;

 

mm->map_count++;

if (mm->map_count >= AVL_MIN_MAP_COUNT && !mm->mmap_avl)

build_mmap_avl(mm);

 

file vmp->vm_file;

if (file) {

struct inode inode file->f_dentry->d_inode;

struct address_space *mapping inode->i_mapping;

struct vm_area_struct **head;

 

if (vmp->vm_flags VM_DENYWRITE)

atomic_dec(&inode->i_writecount);

 

head &mapping->i_mmap;

if (vmp->vm_flags VM_SHARED)

head &mapping->i_mmap_shared;

      

 

if((vmp->vm_next_share *head) != NULL)

(*head)->vm_pprev_share &vmp->vm_next_share;

*head vmp;

vmp->vm_pprev_share head;

}

}

 

void insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vmp)

{

lock_vma_mappings(vmp);

spin_lock(¤t->mm->page_table_lock);

__insert_vm_struct(mm, vmp);

spin_unlock(¤t->mm->page_table_lock);

unlock_vma_mappings(vmp);

}

用来往进程的mm_struct结构中新加入虚拟块vm_area_struct


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值