lab2物理内存管理实现

0、虚拟内存的分配(malloc,free)

从操作系统角度来看,进程分配内存有2种方式,分别由2个系统调用完成:brk和mmap(不考虑共享内存)。

  1.    brk是将数据段(.data)的最高地址指针_edata往高地址推
    
  2.    mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。
    

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

malloc,free的实现原理:

该部分出处:http://blog.163.com/xychenbaihu@yeah/blog/static/132229655201210975312473/
小于128K内存分配:
malloc小于128k的内存,使用brk分配内存,将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系),如下图(32位系统):
在这里插入图片描述

  1.    进程启动的时候,其(虚拟)内存空间的初始布局如图1所示
    

其中,mmap内存映射文件是在堆和栈的中间(例如libc-2.2.93.so,其它数据文件等),为了简单起见,省略了内存映射文件。
_edata指针(glibc里面定义)指向数据段的最高地址。
2. 进程调用A=malloc(30K)以后,内存空间如图2
malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配。
你可能会问:只要把_edata+30K就完成内存分配了?
事实是这样的,_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的。
3. 进程调用B=malloc(40K)以后,内存空间如图3。

大于128K内存分配
malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0),如下图:
在这里插入图片描述
4. 进程调用C=malloc(200K)以后,内存空间如图4:
5. 默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。这样子做主要是因为:brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,这就是内存碎片产生的原因,什么时候紧缩看下面),而mmap分配的内存可以单独释放。
当然,还有其它的好处,也有坏处,再具体下去,有兴趣的同学可以去看glibc里面malloc的代码了。
6. 进程调用D=malloc(100K)以后,内存空间如图5;
7. 进程调用free©以后,C对应的虚拟内存和物理内存一起释放,如图6所示。
8. 进程调用free(B)以后,如图7所示:
B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,如果往回推,那么D这块内存怎么办呢?当然,B这块内存,是可以重用的,如果这个时候再来一个40K的请求,那么malloc很可能就把B这块内存返回回去了。
9. 进程调用free(D)以后,如图8所示
B和D连接起来,变成一块140K的空闲内存。
10. 内存紧缩操作(trim)
默认情况下:当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩,变成图9所示。
在这里插入图片描述

一、物理内存页的分配

物理内存页分配算法:
最先匹配(First-fit):找到的第一个大于我要分配的大小的空闲块
最佳匹配(Best-fit):找到那个比我大,但是又是大的最小的那个
最差匹配(Worst-fit):找到那个比我大,但是又是大的最多的那个
这里实现的是最先匹配(First-fit)

本次实验主要完成ucore内核对物理内存的管理工作。
内存管理相关的总体控制函数是pmm_init函数(kern_init调用pmm_init),它完成的主要工作包括:
1、初始化物理内存页管理器框架pmm_manager;
2、建立空闲的page链表,这样就可以分配以页(4KB)为单位的空闲内存了;
3、检查物理内存页分配算法;
4、为确保切换到分页机制后,代码能够正常执行,先建立一个临时二级页表;
5、建立一一映射关系的二级页表;
6、使能分页机制;
7、重新设置全局段描述符表;
8、取消临时二级页表;
9、检查页表建立是否正确;
10、通过自映射机制完成页表的打印输出

1、初始化物理内存页管理器框架pmm_manager;

struct pmm_manager {
            const char *name; //物理内存页管理器的名字
            void (*init)(void); //初始化内存管理器
            void (*init_memmap)(struct Page *base, size_t n); //初始化管理空闲内存页的数据结构
            struct Page *(*alloc_pages)(size_t n); //分配n个物理内存页
            void (*free_pages)(struct Page *base, size_t n); //释放n个物理内存页
            size_t (*nr_free_pages)(void); //返回当前剩余的空闲页数
            void (*check)(void); //用于检测分配/释放实现是否正确的辅助函数
};

2、建立空闲的page链表
物理页(按4KB对齐,且大小为4KB的物理内存单元),每个物理页可以用一个 Page数据结构来表示。

struct Page {
    int ref;        // 这页被页表的引用记数
    uint32_t flags; // flags用到了两个bit表示页目前具有的两种属性。bit 0表示此页是否被保留(reserved),如果是被保留的页,则bit 0会设置为1,且不能放到空闲页链表中,即这样的页不是空闲页,不能动态分配与释放。比如目前内核代码占用的空间就属于这样“被保留”的页。bit 1表示此页是否是free的,如果设置为1,表示这页是free的,可以被分配;如果设置为0,表示这页已经被分配出去了,不能被再二次分配。
    unsigned int property;//property用来记录某连续内存空闲块的大小(即地址连续的空闲页的个数)。这里需要注意的是用到此成员变量的这个Page比较特殊,是这个连续内存空闲块地址最小的一页(即头一页, Head Page)。
    list_entry_t page_link;// page_link是便于把多个连续内存空闲块链接在一起的双向链表指针。这里需要注意的是用到此成员变量的这个Page比较特殊,是这个连续内存空闲块地址最小的一页(即头一页, Head Page)。连续内存空闲块利用这个页的成员变量page_link来链接比它地址小和大的其他连续内存空闲块。
};

在初始情况下,也许这个物理内存的空闲物理页都是连续的,这样就形成了一个大的连续内存空闲块。但随着物理页的分配与释放,这个大的连续内存空闲块会分裂为一系列地址不连续的多个小连续内存空闲块,且每个连续内存空闲块内部的物理页是连续的。那么为了有效地管理这些小连续内存空闲块。所有的连续内存空闲块可用一个双向链表管理起来,便于分配和释放,为此定义了一个free_area_t数据结构,包含了一个list_entry结构的双向链表指针和记录当前空闲页的个数的无符号整型变量nr_free。其中的链表指针指向了空闲的物理页。

/* free_area_t - maintains a doubly linked list to record free (unused) pages */
typedef struct {
            list_entry_t free_list;                                // the list header
            unsigned int nr_free;                                 // # of free pages in this free list
} free_area_t;

在本实验中只实现了最简单的内存页分配算法(first-fit)。

3、物理内存页分配算法
default_init_memmap需要根据page_init函数中传递过来的参数(某个连续地址的空闲块的起始页,页个数)来建立一个连续内存空闲块的双向链表。这里有一个假定page_init函数是按地址从小到大的顺序传来的连续内存空闲块的。链表头是free_area.free_list,链表项是Page数据结构的base->page_link。这样我们就依靠Page数据结构中的成员变量page_link形成了连续内存空闲块列表。
default_init_memmap函数将根据每个物理页帧的情况来建立空闲页链表,且空闲页块应该是根据地址高低形成一个有序链表。
空闲块是按照地址顺序排序
在这里插入图片描述
firstfit需要从空闲链表头开始查找最小的地址,通过list_next找到下一个空闲块元素,通过le2page宏可以由链表元素获得对应的Page指针p。通过p->property可以了解此空闲块的大小。如果>=n,这就找到了!如果<n,则list_next,继续查找。直到list_next== &free_list,这表示找完了一遍了。找到后,就要从新组织空闲块,然后把找到的page返回。
在这里插入图片描述
在这里插入图片描述
default_free_pages函数的实现其实是default_alloc_pages的逆过程,还需要考虑空闲块的合并问题。

在这里插入图片描述
页式管理的实现
x86 体系结构将内存地址分成三种:逻辑地址(也称虚地址)、线性地址和物理地址。逻辑地址即是程序指令中使用的地址,物理地址是实际访问内存的地址。逻辑地址通过段式管理的地址映射可以得到线性地址,线性地址通过页式管理的地址映射得到物理地址。在 ucore 中段式管理只起到了一个过渡作用,它将逻辑地址不加转换直接映射成线性地址
ucore 的页式管理通过一个二级的页表实现。一级页表的起始物理地址存放在 cr3 寄存器中
在二级页表结构中,页目录表占4KB空间,可通过alloc_page函数获得一个空闲物理页作为页目录表

lab1中的分段管理机制
分段机制涉及4个关键内容:逻辑地址、段描述符(描述段的属性)、段描述符表(包含多个段描述符的“数组”)、段选择子(段寄存器,用于定位段描述符表中表项的索引)。转换逻辑地址到物理地址分以下两步:
[1] 分段地址转换:CPU把逻辑地址(由段选择子selector和段偏移offset组成)中的段选择子的内容作为段描述符表的索引,找到表中对应的段描述符,然后把段描述符中保存的段基址加上段偏移值,形成线性地址(Linear Address)。如果不启动分页存储管理机制,则线性地址等于物理地址。 [2] 分页地址转换,这一步中把线性地址转换为物理地址。

lab2的内存管理机制
最终的段页式映射关系: virt addr = linear addr = phy addr + 0xC0000000
lab1中虚拟地址、线性地址以及物理地址之间的映射关系如下:virt addr = linear addr = phy addr

二、伙伴系统的实现(解决外部内存碎片问题)

Linux采用伙伴系统解决外部碎片的问题,采用slab解决内部碎片的问题
上述first_fit页分配算法会产生内存碎片(外部碎片),每次分配一次就留下一个小分区。
如果内存都分配给进程了,这时候又有新的内存需要分配内存,完整连续的内存是不够了,但是还有一些小的内存碎片。这时候考虑碎片整理的方法
碎片整理:
1、紧凑
通过移动分配给进程的内存,将这些内存移动到一起,从而将内存碎片合并起来。
2、分区对换
把等待状态的进程的内存给抢占了,然后将等待状态的进程所占内存转移到外存去。

伙伴系统
原理:
在这里插入图片描述
分配内存:
1.寻找大小合适的内存块(大于等于所需大小并且最接近2的幂,比如需要27,实际分配32)
1.如果找到了,分配给应用程序。
2.如果没找到,分出合适的内存块。
1.对半分离出高于所需大小的空闲内存块
2.如果分到最低限度,分配这个大小。
3.回溯到步骤1(寻找合适大小的块)
4.重复该步骤直到一个合适的块

释放内存:
1.释放该内存块
1.寻找相邻的块,看其是否释放了。
2.如果相邻块也释放了,合并这两个块,重复上述步骤直到遇上未释放的相邻块,或者达到最高上限(即所有内存都释放了)。

其优点是快速搜索合并(O(logN)时间复杂度)以及低外部碎片(最佳适配best-fit);其缺点是内部碎片,因为按2的幂划分块,如果碰上66单位大小,那么必须划分128单位大小的块。

实现:

伙伴分配器的数据结构:

struct buddy2 {
  unsigned size;//size表明管理内存的总单元数目
  unsigned longest[1];//longest就是二叉树的节点标记,表明所对应的内存块的空闲单位
};

初始化:

struct buddy2* buddy2_new( int size ) {
  struct buddy2* self;
  unsigned node_size;
  int i;
  if (size < 1 || !IS_POWER_OF_2(size))
    return NULL;
  self = (struct buddy2*)ALLOC( 2 * size * sizeof(unsigned));//整个分配器的大小就是满二叉树节点数目,即所需管理内存单元数目的2倍。
  self->size = size;
  node_size = size * 2;
  for (i = 0; i < 2 * size - 1; ++i) {
    if (IS_POWER_OF_2(i+1))
      node_size /= 2;
    self->longest[i] = node_size;
  }
  return self;
}

整个分配器的大小就是满二叉树节点数目,即所需管理内存单元数目的2倍。一个节点对应4个字节,longest记录了节点所对应的的内存块大小。
在这里插入图片描述
内存分配:

int buddy2_alloc(struct buddy2* self, int size) {//入参是分配器指针和需要分配的大小,返回值是内存块索引。
  unsigned index = 0;
  unsigned node_size;
  unsigned offset = 0;
  if (self==NULL)
    return -1;
  if (size <= 0)
    size = 1;
  else if (!IS_POWER_OF_2(size))
    size = fixsize(size);//将size调整到2的幂大小
  if (self->longest[index] < size)
    return -1;
  for(node_size = self->size; node_size != size; node_size /= 2 ) {//进行适配搜索,深度优先遍历
    if (self->longest[LEFT_LEAF(index)] >= size)//当前块的一半比需要的大,就往下一层找。当前块的一半比需要的小的时候就停,表示找到了
      index = LEFT_LEAF(index);
    else
      index = RIGHT_LEAF(index);
  }
  self->longest[index] = 0;//当找到对应节点后,将其longest标记为0,即分离适配的块出来
  offset = (index + 1) * node_size - self->size;//转换为内存块索引offset返回
  while (index) {//在函数返回之前需要回溯,因为小块内存被占用,大块就不能分配了,比如下标[8]标记为0分离出来,那么其			  父节点下标[0]、[1]、[3]也需要相应大小的分离。将它们的longest进行折扣计算,取左右子树较大值,下标[3]取4,下标[1]取8,下标[0]取16,表明其对应的最大空闲值。
    index = PARENT(index);
    self->longest[index] =
      MAX(self->longest[LEFT_LEAF(index)], self->longest[RIGHT_LEAF(index)]);
  }
  return offset;
}

深度优先搜索的改进:
应该尽量使用更小的内存块而不是把一个大块拆小,只要把for循环的内容改一下即可:当左子树管理的大小和右子树管理的大小都大于所要分配的size时,选两者之间较小的一个。

将索引转换为内存块索引offset返回:
该二叉树是数组,从下标[0]开始,每level层的第一个左子树下标是2^level-1。
假设总大小为16,31个节点,5层。现在需要将下标转换成索引offset(即alloc出来的地址),有如下映射关系:[index]->offset
size = 16:[0]->0
size = 8:[1]->0;[2]->8
size = 4:[3]->0;[4]->4;[5]->8;[6]->12
size = 2:略
size = 1:略
可建立如下公式:offset = (index + 1) * size – 16,直观上看图就可理解。

内存释放:

void buddy2_free(struct buddy2* self, int offset) {
  unsigned node_size, index = 0;
  unsigned left_longest, right_longest;
  assert(self && offset >= 0 && offset < size);
  node_size = 1;
  index = offset + self->size - 1;
  for (; self->longest[index] ; index = PARENT(index)) {
    node_size *= 2;
    if (index == 0)//从最后的节点开始一直往上找到longest为0的节点,即当初分配块所适配的大小和位置。
      return;
  }
  self->longest[index] = node_size;//将longest恢复到原来满状态的值
  while (index) {//继续向上回溯,检查是否存在合并的块,依据就是左右子树longest的值相加是否等于原空闲块满状态的大小,如果能够合并,就将父节点longest标记为相加的和
    index = PARENT(index);
    node_size *= 2;
    left_longest = self->longest[LEFT_LEAF(index)];
    right_longest = self->longest[RIGHT_LEAF(index)];
    if (left_longest + right_longest == node_size)
      self->longest[index] = node_size;
    else
      self->longest[index] = MAX(left_longest, right_longest);
  }
}

三、段页式管理内存映射机制

在这里插入图片描述
x86 体系结构将内存地址分成三种:逻辑地址(也称虚地址)、线性地址和物理地址。逻辑地址即是程序指令中使用的地址,物理地址是实际访问内存的地址。逻辑地址通过段式管理的地址映射可以得到线性地址,线性地址通过页式管理的地址映射得到物理地址。
首先是段映射机制,逻辑地址中包括段选择子和段偏移,段选择子作为全局描述符表(GDT)的索引找到对应段描述符,段描述符中包含段基址,段基址加上段偏移就是所找线性地址
然后是二级页表映射机制,线性地址由页目录表偏移,页表偏移,和物理内存偏移组成,首先从CR3中获取页目录表的基址,页目录表基址加上页目录表偏移得到页表的基址,然后找到该页表,页表基址加上页表偏移得到实际物理内存页基址,物理内存页基址加上物理内存偏移得到最终要访问的物理地址。

四、段页式管理内存映射关系建立

1、段表及段映射的建立
全局描述符表(GDT) 全局描述符表是一个保存多个段描述符的“数组”,其起始地址保存在全局描述符表寄存器GDTR中。
段描述符中会设定该内容到底属于哪个特权级,代码段寄存器cs中的CPL字段(2位)指示了cpu当前的特权级,cs的低两位是0表示运行在内核态,cs低两位是3表示运行在用户态
段映射采用的是对等映射,就是将逻辑地址不加转换直接映射成线性地址。通过建立全局段描述符表,让每个段的基址为0,从而确定了对等映射关系。
2、内核二级页表及映射关系的建立(重要)
页表结构
页表起始地址存放在CR3寄存器中(页表其实是个大数组)
在这里插入图片描述
页表内除了存储前面提过的帧号,还存储了一些页表项标志位:
存在位,记录该页号是否有对应的帧;
修改位,记录页面的内容是否修改了;
引用位,记录是否有对该页面的引用。

页目录项中存放它所指向的页表的起始地址
页表项中存放它对应的物理页的起始地址
有了起始地址,再加上EIP对应的偏移,就形成最终的物理地址
例如多级页表的访问过程:
在这里插入图片描述
内核二级页表映射关系的建立

boot_pgdir = boot_alloc_page();//获取指向页目录表的指针,boot_pgdir实际上就是我们的一级页表,分配一页作为我们的页目录表
memset(boot_pgdir, 0, PGSIZE);//
boot_cr3 = PADDR(boot_pgdir);//一级页表的起始物理地址存放在 cr3 寄存器中

内存中分配一页空闲页作为页目录表,将该页的起始地址放到cr3寄存器中
在这里插入图片描述
根据内核虚拟地址,在我刚建立的页目录表中寻找其对应的页目录项(即对应二级页表的起始地址)
若页目录表中没有对应的页目录项,表明映射关系未建立,需要在内存中分配一页空闲页作为二级页表,再根据该内核虚拟地址,继续在二级页表中查找其对应的二级页表项(即对应的物理页起始地址),
若二级页表中不存在对应的二级页表项,则将该内核虚拟地址实际的物理地址填充到该二级页表项中

boot_map_segment(boot_pgdir, KERNBASE, KMEMSIZE, 0, PTE_W);//调用boot_map_segment函数进一步建立一一映射关系,具体处理过程以页为单位进行设置,即linear addr = phy addr + 0xC0000000

//la线性地址,pa物理地址
static void boot_map_segment(pde_t *pgdir, uintptr_t la, size_t size, uintptr_t pa, uint32_t perm) {
    assert(PGOFF(la) == PGOFF(pa));
    size_t n = ROUNDUP(size + PGOFF(la), PGSIZE) / PGSIZE;
    la = ROUNDDOWN(la, PGSIZE);
    pa = ROUNDDOWN(pa, PGSIZE);
    for (; n > 0; n --, la += PGSIZE, pa += PGSIZE) {
        pte_t *ptep = get_pte(pgdir, la, 1);//调用get_pte,为0-size的虚拟地址建立物理地址映射(需要新分配页)
        assert(ptep != NULL);
        *ptep = pa | PTE_P | perm;
    }
}

PTE_U:位3,表示用户态的软件可以读取对应地址的物理内存页内容
PTE_W:位2,表示物理内存页内容可写
PTE_P:位1,表示物理内存页存在
get_pte

    //pde_t为一级页表的表项,pte_t为二级页表的表项,pgdir是一级页表本身,给出页表起始地址。
    pde_t *pdep = &pgdir[PDX(la)];// (1) find page directory entry查找页目录项
    if (!(*pdep & PTE_P)) {// (2) check if entry is not present页目录项不存在,则新分配页
        struct Page *page;
        if (!create || (page = alloc_page()) == NULL) {// (3) check if creating is needed, then alloc page for page table
            return NULL;
        }
        set_page_ref(page, 1);// (4) set page reference
        uintptr_t pa = page2pa(page);// (5) get linear address of page
        memset(KADDR(pa), 0, PGSIZE);// (6) clear page content using memset
        *pdep = pa | PTE_U | PTE_W | PTE_P;// (7) set page directory entry's permission
    }
    return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)];// (8) return page table entry

目前我们只有boot_pgdir一个页表,但是引入进程的概念之后每个进程都会有自己的页表。有可能根本就没有对应的二级页表的情况,所以二级页表不必要一开始就分配,而是等到需要的时候再添加对应的二级页表。如果在查找二级页表项时,发现对应的二级页表不存在,则需要根据create参数的值来处理是否创建新的二级页表。如果create参数为0,则get_pte返回NULL;如果create参数不为0,则get_pte需要申请一个新的物理页(通过alloc_page来实现,可在mm/pmm.h中找到它的定义),再在一级页表中添加页目录项指向表示二级页表的新物理页。注意,新申请的页必须全部设定为零,因为这个页所代表的虚拟地址都没有被映射。

4、建立临时二级页表
第一个阶段(开启保护模式,创建启动段表)是bootloader阶段,其虚拟地址、线性地址以及物理地址之间的映射关系与lab1的一样,即:
lab2 stage 1: virt addr = linear addr = phy addr
第二个阶段(创建初始页目录表,开启分页模式)编译好的ucore自带了一个设置好的页目录表和相应的页表,将0~4M的线性地址一一映射到物理地址。
第三个阶段(完善段表和页表)将页目录表项补充完成(从04M扩充到0KMEMSIZE)

5、建立一一映射关系的二级页表
在二级页表结构中,页目录表占4KB空间,可通过alloc_page函数获得一个空闲物理页作为页目录表
将页目录表项补充完成(从04M扩充到0KMEMSIZE):
补充流程:在这里插入图片描述
6、使能分页机制
7、重新设置全局段描述符表

更新了段映射机制,使用了一个新的段表。这个新段表除了包括内核态的代码段和数据段描述符,还包括用户态的代码段和数据段描述符以及TSS(段)的描述符。
8、取消临时二级页表
9、检查页表建立是否正确
10、通过自映射机制完成页表的打印输出

五、页目录表自映射机制(觉得困难)

内核虚拟内存布局:
在这里插入图片描述
每个页表的大小为4KB,每个页表项大小为4B,表明每个页表有1024(即1K)个页表项,而每个页表项可以映射一个地址空间页(4KB),所以每个页表项可以映射4KB大小的地址空间,则每个页表可以映射4M的地址空间。
所以,正常情况下,要想映射4G的虚拟地址,我需要有1024(1K)个页表,即1M个页表项,对应1个页目录表,所以我需要1024个页表,1个页目录表。这些页表和页目录表所占空间大小为(4M+4K)
而页目录表自映射,指的我用4M空间就可以完成,不需要4M+4K
在这里插入图片描述
我的页目录表和页表放在连续的4M虚拟地址空间内(0xF8000000——0xFAC00000),这样会有一张页表的内容与页目录表的内容完全相同,节省了页目录表的空间。
4M的连续虚拟空间中,第一个页表映射整个虚拟空间的第一个4M空间,第二个页表映射第二个……这样,会有一个页表映射他所处的这4M空间,这样这个特殊的页表就会将这4M空间内的所有页表都映射到了,这个特殊的页表就起到了页目录表的功能
这个特殊的页表中每一个页表项映射一个4k的页表,所以会有某个页表项映射到它自身,这就是页目录表的自映射。

页目录自映射是虚拟空间内的映射,与虚拟地址与物理地址的映射无关!!!
虚拟空间内的映射指的是:例如,任何0—4M的内核虚拟地址,都可以通过我第一个4KB的页表,来找到对应的实际物理地址。
只要确定了页表所在的4MB空间的位置,就可以确定出页目录的地址。(确定页表所在的4M空间属于整个虚拟内存的第几个4M,然后就确定出页目录位于1024个页表的第几个,就确定出页目录的地址了)

windows采用了页目录表自映射
节省4K空间,方便用来维护页表。操作虚拟地址对应页表项非常方便。采用页目录自映射的方式,用户进程页表也在用户空间拥有一个自映射页目录,有助于实现用户进程的统一内存布局。

linux的页表没有实现自映射
linux连续地址空间如此宝贵,浪费当作页表linux觉得这不值得,页表连续的话就要先预留4m页面,然后页表没有完全分配前会造成很多空洞,很少有进程能完全用完4m的页表

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值