内存寻址——分页

Linux中的分页

分页单元把线性地址转换为物理地址,为了效率起见,线性地址被分成以固定长度为单位的组,成为页。这样页内部连续的线性地址被映射到连续的物理地址中。这样,内核就可以指定一个页的物理地址和其存取权限,而不用指定页所包含的全部线性地址的存取权限。
分页单元把所有的RAM分成固定长度的页框(有时也叫做物理页)。每个页框包含一个页,也就是说一个页框的长度与一个页的长度一致。页框是主存的一部分,因此也是一个存储区域,区分一页和一个页框很重要的,前者只是一个数据块,可以存放在任何页框或者磁盘中。

Linux采用了一种同时适用于32位和64位系统的普通分页模型。从2.6.11版本开始,采用了四级分页模型(如图5.1):页全局目录(Page Global Directory),页上级目录(Page Upper Directory),页中间目录(Page Middle Directory),页表(Page Table)。
在这里插入图片描述
对于没有启用物理地址扩展的32位系统,两级页表已经足够了。使用页全局目录和页表就可以了,内核为页上级目录和页中间目录保留了一个位置,通过把它们的目录项数设置为1,并把这两个目录项映射到页全局目录的一个适当的目录项。这样就兼容了32位的系统和64位的系统。
对于32位的没有开启PAE的系统,页全局目录占用GLOBAL DIR占用10位,UPPER DIR和MIDDLE DIR占用0位,TABLE占用10位,OFFSET占用12位,页全局目录存储在一个4K字节的页中,页全局目录表共有1K个表项,每个表项为4个字节,线性地址最高的10位(22-31)用来产生第一级表索引,由该索引得到的表项中的内容定位了二级表中的一个表的地址,即下级页表所在的内存块号。
第二级表称为页表,存储在一个4K字节页中,它包含了1K字节的表项,每个表项包含了一个页的物理地址。二级页表由线性地址的中间10位(12-21)位进行索引,定位页表表项,获得页的物理地址。页物理地址的高20位与线性地址的低12位形成最后的物理地址。

对于启用了物理扩展的32位系统使用三级页表。Linux的页全局目录对应80x86的页目录指针表(PDPT),取消了页上级目录,页中间目录对应80x86的页目录,Linux的页表对应80x86的页表。
cr3寄存器的高20位保存当前页全局目录的物理地址,每一个进程都有它自己的页全局目录和自己的页表集。当发生进程切换的时候,Linux会把cr3控制寄存器的内容保存在前一个执行进程的描述符中,然后把下一个要执行进程的描述符的值装入cr3寄存器中。

页目录和页表格式

在这里插入图片描述
上图就是页目录项和页表项的格式。可以看出,由于页表或者页的物理地址都是4KB对齐的(低12位全是零),所以上图中只保留了物理基地址的高20位(bit[31:12])。低12位可以安排其他用途。
【P】:存在位。为1表示页表或者页位于内存中。否则,表示不在内存中,必须先予以创建或者从磁盘调入内存后方可使用。
【R/W】:读写标志。为1表示页面可以被读写,为0表示只读。当处理器运行在0、1、2特权级时,此位不起作用。页目录中的这个位对其所映射的所有页面起作用。
【U/S】:用户/超级用户标志。为1时,允许所有特权级别的程序访问;为0时,仅允许特权级为0、1、2的程序访问。页目录中的这个位对其所映射的所有页面起作用。
【PWT】:Page级的Write-Through标志位。为1时使用Write-Through的Cache类型;为0时使用Write-Back的Cache类型。当CR0.CD=1时(Cache被Disable掉),此标志被忽略。对于我们的实验,此位清零。
【PCD】:Page级的Cache Disable标志位。为1时,物理页面是不能被Cache的;为0时允许Cache。当CR0.CD=1时,此标志被忽略。对于我们的实验,此位清零。
【A】:访问位。该位由处理器固件设置,用来指示此表项所指向的页是否已被访问(读或写),一旦置位,处理器从不清这个标志位。这个位可以被操作系统用来监视页的使用频率。
【D】:脏位。该位由处理器固件设置,用来指示此表项所指向的页是否写过数据。
【PS】:Page Size位。为0时,页的大小是4KB;为1时,页的大小是4MB(for normal 32-bit addressing )或者2MB(if extended physical addressing is enabled).
【G】:全局位。如果页是全局的,那么它将在高速缓存中一直保存。当CR4.PGE=1时,可以设置此位为1,指示Page是全局Page,在CR3被更新时,TLB内的全局Page不会被刷新。
【AVL】:被处理器忽略,软件可以使用。

内核页表初始化

内核维持着一组自己使用的页表,驻留在主内核全局目录中,主内核页全局目录的最高目录项部分作为参考模型,为系统中每个普通进程对应的页全局目录项提供参考模型(进程的内核态从oxc0000000-----oxffffffff的线性地址!)
内核页表的初始化化分为2个阶段:
第一阶段:临时内核页表初始化
初始化临时内核页表是在startup_32汇编语言函数中完成的。在ULK所述中,假设内核能容纳于RAM的前8MB空间,然后对RAM的前8MB进行恒等映射(例如用户地址0x00003000映射物理地址0x00003000,0xc0003000映射到物理地址0x00003000),来初始化临时页全局目录swapper_pg_dir和相应的页表。映射8MB只需要填充swapper_pg_dir中第0项,1项,768项和769项。前两项是给用户线性地址映射,后两项给内核线性地址映射。用页全局目录里的两项就能对8MB映射的理由是2×1024(页表有1024项)×4K(一页的大小)=8M。实际上初始化内核页表来对RAM的前8MB映射不是个硬性的规定。这取决于你的内核的配置(我认为大多数情况下是对8MB映射)。在startup_32中可以看到,对多少内存进行映射是通过pg0动态判断的。
 临时页表在pg0变量处开始存放,紧接在内核未初始化的数据段。(_end符号后面)。通过查看/boot/System.map,可以找到swapper_pg_dir的地址,例如0xc047d000 (抄了某位前辈的数据,由于不知道原始作者是谁,就先在这声明下。这个数据是编译时产生的所以,因机器而异,后面的数据同此处)。pg0的线性地址为0xc04f4000。之后我们进入代码:

movl $(pg0 - __PAGE_OFFSET), %edi  
    /* move pg0 - __PAGE_OFFSET = c04f4000-c0000000=4f4000 => edi 
       相当于第一个页表项的初始物理地址        */
    movl $(swapper_pg_dir - __PAGE_OFFSET), %edx  
    /* c047d000 - c0000000 = 47d000 => edx 
           相当于第一个页目录项的初始物理地址         */
    movl $0x007, %eax            
    /* 0x007 = PRESENT+RW+USER 
           这里为什么要加7呢,因为无论是页表项还是页目录项的前20位是物理地址,后12位为状态字
           这里的加7实际上就是将后三位也就是PRESENT,RW,USER三位置为    */
10:
    leal 0x007(%edi),%ecx            
    /* Create PDE entry 
       由于这里是设置页目录项的,所以将页表项的第一项物理地址+7
       如果带入我们之前的值的话,ecx = 4f4007即页表项的第一项的物理地址,
       加上PRESENT,RW,USER三个标志位    */
    movl %ecx,(%edx)            
    /* Store identity PDE entry 
       将4f4007存储到edx中 即swapper_pg_dir[0]也就是47d000这个地址    */
    movl %ecx,page_pde_offset(%edx)        
    /* Store kernel PDE entry 
       将4f4007存储到内核的页表空间中 即swapper_pg_dir[0x300]也就是c047d000这个地址*/
    addl $4,%edx
    /* swapper_pg_dir指针向下移动一个entry,也就是4个字节*/
    movl $1024, %ecx
    /* 初始化计数器,循环1024次,也就是1024个页表*/
11:
    stosl
    /* 把eax中的内容放入edi指向的物理地址中,然后edi+4。
       edi指向的地址为pg0的地址,之前eax赋值为7,所以pg0的第一项也就是地址4f4000的值为00000007*/
    addl $0x1000,%eax
    /* 移动指针到下一个页的地址,因为一个页的大小为4096,即0x1000个字节 */
    loop 11b
    /* 经过循环将pg0的1024个表项都进行赋值 */
    /* End condition: we must map up to and including INIT_MAP_BEYOND_END */
    /* bytes beyond the end of our own page tables; the +0x007 is the attribute bits */
    leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
    /* INIT_MAP_BEYOND_END = 128k,文件中定义的。也就是目前pg0的指针+128k内核空间+7是否被全部映射(<ebp),
       如果没有呗全部映射,则跳到10,再映射4MB空间。*/
    cmpl %ebp,%eax
    jb 10b
    movl %edi,(init_pg_tables_end - __PAGE_OFFSET)
/* 如果映射完成,则将pg0末尾地址赋值给init_pg_tables_end*/

注意代码中的子针变量都减去一个线性偏移_PAGE_OFFSET。这是因为在编译内核时。这些变量引用的是启用分页后线性空间的地址。所以为得到实际的物理地址应减去这个线性偏移量。

第二阶段:初始化最终内核页表
注意此时分页机制已启动。初始化使得从oxc0000000开始的线性地址转化为从0开始的物理地址
初始化发生在paging_init()函数中,而该函数位于setup_arch()中,而setup_arch()在start_kernel函数中被调用。
paging_init()函数执行步骤:
1:调用pagetable_init建立内核页表;
2:把swapper_pg_dir的物理地址写道cr3寄存器中;
3:根据CPU能力及编译内核时的配置,正确的设置cr4的PAE标志位;
4:调用__flush_tlb_all使得TLB(转换后援缓冲器)的所用项无效。
5:调用kmap_init初始化临时内核映射。
6:调用zone_sizes_init初始化节点内存信息,其中就包括分配mem_map内存
我们详细分析每一个步骤
首先是pagetable_init的调用栈

pagetable_init
pgd_t *pgd_base = swapper_pg_dir;
//初始化物理内存前896MB的页全局目录项、页表项和页
kernel_physical_mapping_init(pgd_base);
//得到线性地址0xc0000000在页全局目录中的索引
        pgd_idx = pgd_index(PAGE_OFFSET);
        pgd = pgd_base + pgd_idx;
        pfn = 0;
        for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++)
//初始化页中间目录,在32位系统中,页中间目录就是页全局目录
            pmd = one_md_table_init(pgd);
                pud = pud_offset(pgd, 0);
                pmd_table = pmd_offset(pud, 0);
//当总共内存大于896MB的时候,max_low_pfn=896MB>>12,也就是内核直接映射的区域,剩下的128MB用于临时内核映射、永久内核映射和非连续内存区
            if (pfn >= max_low_pfn)
                continue;
            for (pmd_idx = 0; pmd_idx < PTRS_PER_PMD && pfn < max_low_pfn; pmd++, pmd_idx++)
//得到页框号对应的虚拟地址
                unsigned int address = pfn * PAGE_SIZE + PAGE_OFFSET;
//初始化页表
                pte = one_page_table_init(pmd);
                    if (pmd_none(*pmd)) //!pmd_val(x)
//利用bootmem申请一块页大小的内存,此时诸如伙伴系统、slab内存分配器都没有初始化,只有内存管理器bootmem分配器在系统初始化的时候进行内存管理与分配
                        pte_t *page_table = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE);
                            __alloc_bootmem((x), PAGE_SIZE, 0)
                                pg_data_t *pgdat = pgdat_list;  //struct pglist_data contig_page_data = { .bdata = &contig_bootmem_data };
                                for_each_pgdat(pgdat)
                                    if ((ptr = __alloc_bootmem_core(pgdat->bdata, size, align, goal)))
                                        return(ptr);   
//设置也中间目录项的页表物理地址,每一个目录项的高20位用来存放页表物理地址的高20位,剩下的12位用来存放一些标志
                        set_pmd(pmd, __pmd(__pa(page_table) | _PAGE_TABLE));
                        return page_table;
                for (pte_ofs = 0; pte_ofs < PTRS_PER_PTE && pfn < max_low_pfn; pte++, pfn++, pte_ofs++)
//设置页表项的值,为页框物理地址和标志的组合,因为内核直接映射区域为前896MB,所谓直接映射就是物理地址+PAGE_SIZE就是虚拟地址,所以可以根据物理页框号得到虚拟地址
                    if (is_kernel_text(address))    
                        set_pte(pte, pfn_pte(pfn, PAGE_KERNEL_EXEC));
                    else
                        set_pte(pte, pfn_pte(pfn, PAGE_KERNEL));
            return pmd_table;
	vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK; //最低固定映射地址的低一字节,即3G+896M
//初始化临时内核映射的页全局目录项、页表项
	page_table_range_init(vaddr, 0, pgd_base);
		vaddr = start;
		pgd_idx = pgd_index(vaddr);
		pmd_idx = pmd_index(vaddr);
		pgd = pgd_base + pgd_idx;
		for ( ; (pgd_idx < PTRS_PER_PGD) && (vaddr != end); pgd++, pgd_idx++)
			if (pgd_none(*pgd)) 
				one_md_table_init(pgd);
			pud = pud_offset(pgd, vaddr);
			pmd = pmd_offset(pud, vaddr);
			for (; (pmd_idx < PTRS_PER_PMD) && (vaddr != end); pmd++, pmd_idx++)
				if (pmd_none(*pmd)) 
					one_page_table_init(pmd);
						pte_t *page_table = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE);
						set_pmd(pmd, __pmd(__pa(page_table) | _PAGE_TABLE));
						return page_table; //返回线性地址 
				vaddr += PMD_SIZE;
//初始化永久内核映射的页全局目录项、页表项
	permanent_kmaps_init(pgd_base)
		vaddr = PKMAP_BASE;  
		page_table_range_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base);
			vaddr = start;
			pgd_idx = pgd_index(vaddr);  //根据虚拟地址找到页全局项的索引
			pmd_idx = pmd_index(vaddr);
			pgd = pgd_base + pgd_idx;  
			
			for ( ; (pgd_idx < PTRS_PER_PGD) && (vaddr != end); pgd++, pgd_idx++)
				pud = pud_offset(pgd, vaddr);
				pmd = pmd_offset(pud, vaddr);
		
		pgd = swapper_pg_dir + pgd_index(vaddr);
		pud = pud_offset(pgd, vaddr);
		pmd = pmd_offset(pud, vaddr);
		pte = pte_offset_kernel(pmd, vaddr);
		pkmap_page_table = pte;	  //指向永久内核映射的第一个页表项

从上面的调用栈可以看到pagetable_init初始化前物理内存前896MB的页全局目录项、页表项和页,同时初始化内核永久映射和临时映射的页全局目录项、页表项。

kmap_init的调用栈如下

kmap_init
    kmap_vstart = __fix_to_virt(FIX_KMAP_BEGIN);
    kmap_pte = kmap_get_fixmap_pte(kmap_vstart);
        pte_offset_kernel(pmd_offset(pud_offset(pgd_offset_k(vaddr), vaddr), (vaddr)), (vaddr))

kmap_ini的目的是获得临时内核映射的第一个页表项,并赋值给kmap_pte

zone_sizes_init();
    unsigned long zones_size[MAX_NR_ZONES] = {0, 0, 0};
    max_dma = virt_to_phys((char *)MAX_DMA_ADDRESS) >> PAGE_SHIFT;  //(PAGE_OFFSET+0x1000000)  
	low = max_low_pfn;  //低端内存区(直接映射空间区的内存)的最大可用页帧号(896MB>>12)
	high = highend_pfn;    //可以假设为1G>>PAGE_SHIFT

	zones_size[ZONE_DMA] = max_dma;
	zones_size[ZONE_NORMAL] = low - max_dma;
	zones_size[ZONE_HIGHMEM] = high - low;   

    free_area_init(zones_size); 
        free_area_init_node(0, &contig_page_data, zones_size, __pa(PAGE_OFFSET) >> PAGE_SHIFT, NULL);
            /*
            void __init free_area_init_node(int nid, struct pglist_data *pgdat,
                    unsigned long *zones_size, unsigned long node_start_pfn,
                    unsigned long *zholes_size)
            */
            //对于PC机, 内存中只有一个node
            pgdat->node_id = nid;//0
            pgdat->node_start_pfn = node_start_pfn; //内核地址的开始page序号  为0
            calculate_zone_totalpages(pgdat, zones_size, zholes_size);
                for (i = 0; i < MAX_NR_ZONES; i++)
                    totalpages += zones_size[i];         
                pgdat->node_spanned_pages = totalpages;    
                realtotalpages = totalpages;
                if (zholes_size)
                    for (i = 0; i < MAX_NR_ZONES; i++)
                        realtotalpages -= zholes_size[i]; //减去空洞
                pgdat->node_present_pages = realtotalpages;    
//如果没有给mem_map分配内存,就给mem_map分配内存,mem_map的大小是由 node_spanned_pages决定的 ,mem_map保存了前1G物理内存所有的页描述符  
            if (!pfn_to_page(node_start_pfn))
                node_alloc_mem_map(pgdat); 
                    size = (pgdat->node_spanned_pages + 1) * sizeof(struct page);
                    pgdat->node_mem_map = alloc_bootmem_node(pgdat, size); //分配大块的内存
                    mem_map = contig_page_data.node_mem_map;  //赋值给mem_map
            free_area_init_core(pgdat, zones_size, zholes_size);            
                int cpu, nid = pgdat->node_id;
                unsigned long zone_start_pfn = pgdat->node_start_pfn;  
                init_waitqueue_head(&pgdat->kswapd_wait);
                for (j = 0; j < MAX_NR_ZONES; j++)
                    struct zone *zone = pgdat->node_zones + j;
                    zone_table[NODEZONE(nid, j)] = zone;
                    /*
                        #define NODEZONE(node, zone)	((node << ZONES_SHIFT) | zone)
                        #define ZONES_SHIFT		2
                    */
                    realsize = size = zones_size[j];
                    if (zholes_size)
                        realsize -= zholes_size[j];            
            
                    zone->spanned_pages = size;
                    zone->present_pages = realsize;
                    zone->name = zone_names[j];
                    spin_lock_init(&zone->lock);
                    spin_lock_init(&zone->lru_lock);
                    zone->zone_pgdat = pgdat;
                    zone->free_pages = 0;

                    zone->temp_priority = zone->prev_priority = DEF_PRIORITY; 

                    batch = zone->present_pages / 1024;
                    
                    for (cpu = 0; cpu < NR_CPUS; cpu++) 
                        struct per_cpu_pages *pcp;
                        pcp = &zone->pageset[cpu].pcp[0];	/* hot */
                        pcp->count = 0;
                        pcp->low = 2 * batch;
                        pcp->high = 6 * batch;
                        pcp->batch = 1 * batch;
                        INIT_LIST_HEAD(&pcp->list);

                        pcp = &zone->pageset[cpu].pcp[1];	/* cold */
                        pcp->count = 0;
                        pcp->low = 0;
                        pcp->high = 2 * batch;
                        pcp->batch = 1 * batch;
                        INIT_LIST_HEAD(&pcp->list);                        
                    
                    zone->zone_mem_map = pfn_to_page(zone_start_pfn);
                    zone->zone_start_pfn = zone_start_pfn;
                    
                    memmap_init(size, nid, j, zone_start_pfn);
                        memmap_init_zone((size), (nid), (zone), (start_pfn))
                            struct page *start = pfn_to_page(start_pfn);
                            for (page = start; page < (start + size); page++)
//设置页描述符的flags字段,由flags字段可以确定当前页属于哪一个区域
                                set_page_zone(page, NODEZONE(nid, zone)); 
                                    page->flags &= ~(~0UL << NODEZONE_SHIFT);
                                    page->flags |= nodezone_num << NODEZONE_SHIFT;  
                                set_page_count(page, 0);
                                    atomic_set(&(p)->_count, v - 1)
                                reset_page_mapcount(page);
                                SetPageReserved(page);
                                    set_bit(PG_reserved, &(page)->flags)
                                INIT_LIST_HEAD(&page->lru);
                                start_pfn++;
                    zone_start_pfn += size;
                    zone_init_free_lists(pgdat, zone, zone->spanned_pages);
                        for (order = 0; order < MAX_ORDER ; order++) {
                            INIT_LIST_HEAD(&zone->free_area[order].free_list);
                            zone->free_area[order].nr_free = 0;               

zone_sizes_init的主要作用就是申请mem_map的内存,并设置mem_map中所有页描述符的flags字段,其可以指定当前page属于哪一个区域

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值