UNIX v6源代码分析调试之三:单步调试系统代码 main函数之 kvmalloc

      kvmalloc();      // kernel page table

     kvmalloc函数初始化内核的内存分页页表。关于虚拟内存,线性地址,内存分页,内存分段等等在操作系统原理的书籍中都有详细说明,我这里就不啰嗦了。

      从代码实现的角度来理解和分析内存分页管理。kvmalloc的函数如下:

// Allocate one page table for the machine for the kernel address
// space for scheduler processes.
void
kvmalloc(void)
{
  kpgdir = setupkvm();
  switchkvm();
}

    这里分为2步,第一步是建立内核使用的内存分页表项(使用二级目录),第二步是通过设置cr3控制寄存器,来设置分页表的表头地址。第二步比较简单,通过 asm volatile("movl %0,%%cr3" : : "r" (val)); 设置即可。

    但是在这2步之前,需要预先设置好cr0控制寄存器,开启分页功能。否则仅仅设置cr3寄存器是徒劳的。  cr0是在哪里设置的呢?答案是在main函数之前,entry.s文件里面开启了分页功能,开启代码如下:

  # Turn on paging.
  movl    %cr0, %eax
  orl     $(CR0_PG|CR0_WP), %eax
  movl    %eax, %cr0

    具体设置值的定义和意义可以参考Intel汇编手册<<Intel® 64 and IA-32 Architectures Software Developer's Manual>>文档。

     重点是setupkvm函数,实现如下:

// This table defines the kernel's mappings, which are present in
// every process's page table.
static struct kmap {
  void *virt;
  uint phys_start;
  uint phys_end;
  int perm;
} kmap[] = {
 { (void*)KERNBASE, 0,             EXTMEM,    PTE_W}, // I/O space
 { (void*)KERNLINK, V2P(KERNLINK), V2P(data), 0},     // kern text+rodata
 { (void*)data,     V2P(data),     PHYSTOP,   PTE_W}, // kern data+memory
 { (void*)DEVSPACE, DEVSPACE,      0,         PTE_W}, // more devices
};

// Set up kernel part of a page table.
pde_t*
setupkvm(void)
{
  pde_t *pgdir;
  struct kmap *k;

  if((pgdir = (pde_t*)kalloc()) == 0)
    return 0;
  memset(pgdir, 0, PGSIZE);
  if (p2v(PHYSTOP) > (void*)DEVSPACE)
    panic("PHYSTOP too high");
  for(k = kmap; k < &kmap[NELEM(kmap)]; k++)
    if(mappages(pgdir, k->virt, k->phys_end - k->phys_start, 
                (uint)k->phys_start, k->perm) < 0)
      return 0;
  return pgdir;
}

      setupkvm函数分为这几步:

      1 分配页目录内存: pgdir = (pde_t*)kalloc(),kalloc固定分配4k的内存,那么页目录的大小为4k(4096字节),每个页目录指向一个页表。32位机器中,地址为4字节,那么最多有1k的页表。

     2 初始化内存,同时判断是否超出范围。

     3 对内核中各个不同用途的内存(定义了4个不同用途的内存块),调用mappages函数初始化页表。

     接着分析 mappages 函数, 注释添加在原有代码里面,如下:

// Create PTEs for virtual addresses starting at va that refer to
// physical addresses starting at pa. va and size might not
// be page-aligned.
static int
mappages(pde_t *pgdir, void *va, uint size, uint pa, int perm)
{
  char *a, *last;
  pte_t *pte;
  
  a = (char*)PGROUNDDOWN((uint)va);  //4096字节对齐,也就是低3位置0
  last = (char*)PGROUNDDOWN(((uint)va) + size - 1); //4096字节对齐,也就是低3位置0

  for(;;){
    if((pte = walkpgdir(pgdir, a, 1)) == 0)   //通过虚拟地址的页目录偏移、页表偏移计算出虚拟地址的页项地址 
      return -1;
    if(*pte & PTE_P)
      panic("remap");
    *pte = pa | perm | PTE_P;   //设置地址和相关的flag
    if(a == last)
      break;  //需要计算页表项的地址段都计算完成了
    a += PGSIZE; //虚拟地址往后移动一页(4k为一页),计算下一页的页表项
    pa += PGSIZE;
  }
  return 0;
}

    到这里,代码分析完成。不过感觉这个二级目录的页表并没有展示的很清楚。这里列出部分内存数据来进一步说明。以I/O space的地址段为例。

{ (void*)KERNBASE, 0,             EXTMEM,    PTE_W}, // I/O space

   

   I/O地址空间:虚拟地址为0x80000000-0x800ff000,物理地址为0x0-0x100000。页目录的头地址为0x803ff000,大小为4k字节。我们来看看建立的二级目录表是什么样子的。

   首先,对于逻辑地址,计算出页目录的偏移。pde = &pgdir[PDX(va)];  0x80000000-0x800ff000计算出PDX为0x200,每个目录占用4字节,在页目录的偏移为0x800。如下图:

   

     偏移0x800处,值为0x003FE007,   这个值是这样计算出来的 *pde = v2p(pgtab) | PTE_P | PTE_W | PTE_U;  最低字节的07是3个内存标准位按位或得到。0x003FE000是页目录0x800偏移地址指向的页表地址,这个页表地址是物理地址。虚拟地址应该为0x8003FE000,该页表的内容即指向物理地址 物理地址为 0x000000-0x100000,具体值如下:

    

       这些值怎么理解呢,其实是通过 *pte = pa | perm | PTE_P; 这条语句去设置的页项。每4个字节都是一个页表项。比如第一个,0x00000003, 最低位的03就是内存标准位,最低3位置0后即为页表指向的物理地址。比如0x000000,0x00001000 ,0x00002000,可以看到每个表项的大小和间隔都是4k,4096个字节。

     注意,这个page页的大小在UNIX v6里面定义为4K,其它操作系统就不一定了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值