九. 内核的内存分配

前面已经准备好了内存池,这里就要正式实现内存的分配了。因为到目前为止,还没有用户进程,所以这里只实现内核中的动态内存分配。

内存分配的过程如下:
1. 在虚拟内存池中申请n个虚拟页
2. 在物理内存池中分配物理页
3. 在页表中添加虚拟地址与物理地址的映射关系

接下来就是一步步完成这三步

申请虚拟页

// 在虚拟内存池中申请pg_cnt个虚拟页
static void *vaddr_get(enum pool_flags pf, uint32_t pg_cnt)
{
    int vaddr_start = 0;
    int bit_idx_start = -1;
    uint32_t cnt = 0;

    if(pf == PF_KERNEL)
    {
        bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt);
        if(bit_idx_start == -1)
        {
            return NULL;
        }

        while (cnt < pg_cnt)
        {
            bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
        }

        vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
    }
    else
    {
        // 用户内存池
    }

    return (void *)vaddr_start;
}

该步只需要在在需要在虚拟内存池的位图结构中找到连续n个空闲的空间即可

虚拟内存池的结构如下

struct virtual_addr
{
    struct bitmap vaddr_bitmap;
    uint32_t vaddr_start;
};

kernel_vaddr是一个全局的虚拟内存池变量,它的初始化过程是在上一章完成的。

kernel_vaddr中的vaddr_start就是内核堆空间的起始地址,这个地址被设置为0xc0100000。因为在位图中,1bit实际代表1页大小的内存,所以这个地址的转换原理还是很简单的。申请到的空间的起始虚拟地址 就等于 堆空间的起始地址虚拟页的偏移量 * 页大小

分配物理页

// 在m_pool指向的物理内存池中分配一个物理页
static void *palloc(struct pool *m_pool)
{
    int bit_idx = bitmap_scan(&m_pool->pool_bitmap, 1);
    if(bit_idx == -1)
    {
        return NULL;
    }

    bitmap_set(&m_pool->pool_bitmap, bit_idx, 1);
    uint32_t page_phyaddr = bit_idx * PG_SIZE + m_pool->phy_addr_start;

    return (void*)page_phyaddr;
}

分配物理页的过程同分配虚拟页的过程差不多,只是这里是在物理内存池中进行分配。而且在分配的过程中,并不需要物理页是连续的,所以在这里一次只分配一个物理页。这样就可以做到虚拟地址连续,而物理地址不需要连续。

添加虚拟地址和物理地址的映射关系

在添加虚拟地址到物理地址映射关系的过程中,肯定要对页表或者页目录进行修改。因为这个对应关系都是写在页表中的,既然此时他们之间没有映射关系,那么就需要在页表中进行添加或者修改,是该虚拟地址能对应到物理地址上。

为了能够在页表中添加或修改数据,就需要访问到该虚拟地址对应的 页目录项地址(PDE)页表项地址(PTE) 通过PDE和PTE对页表进行修改

也就是说,找到该虚拟地址对应的PDE和PTE就成了这步的关键。

扫描二维码关注公众号,回复: 1031093 查看本文章

下面说一下处理器如何处理一个32位的虚拟地址,使其对应到物理地址上
1. 首先通过高10位的pde索引,找到页表的物理地址
2. 其次通过中间10位的pte索引,得到物理页的物理地址
3. 最后把低12位作为物理页的页内偏移,加上物理页的物理地址,即为最终的物理地址

通过这幅图来说明一下

虚拟地址到物理地址的映射

想要找到一个虚拟地址对应的PDE地址,那么首先要知道页目录表的地址,然后通过该虚拟地址的高10位,得到它相对于页目录表的偏移,便可以最终得到PDE的地址

mark

通过上面的图来说明一下,想要知道0x00c03123的PDE地址,这里假设页目录表的首地址为0xfffff000,0x00c03123的高十位为0x3,而页目录表中,每一个小方框的大小都为4字节,所以最终 PDE=0xfffff000 + 0x3 * 4

而当初在规划页表的时候,最后一个页目录项中存储的是页目录表的物理地址。当高20位全为1的时候访问到的就是最后一个页目录项,所以页目录表的物理地址也就为0xfffff000,代码如下

#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22)

// 得到虚拟地址对应的pde指针
uint32_t *pde_ptr(uint32_t vaddr)
{
    uint32_t *pde = (uint32_t*)(0xfffff000 + PDE_IDX(vaddr) * 4);

    return pde;
}

得到PTE的地址的过程就稍微复杂一点。

首先得知道页目录表中第0个页目录项所对应的页表的物理地址,这里假设是0xffc00000。

然后得知道它是哪张页表,也就是说是哪个页目录项所对应的页表,一个页目录项对应4KB大小的页表

最后根据该虚拟地址在页表中的偏移,也就是虚拟地址的中间10位,得到该PTE

同样通过0x00c03123来举例,它的高十位是0x3,中间十位是0x3

PTE = 0xffc00000 + 高十位 * 0x1000 + 中间十位 * 4

下面代码中的计算方式有点区别但是思路是一致的。

#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)

// 得到虚拟地址对应的pte指针
uint32_t *pte_ptr(uint32_t vaddr)
{
    uint32_t *pte = (uint32_t*)(0xffc00000 + ((vaddr & 0xffc00000) >> 10) + PTE_IDX(vaddr) * 4);
    // 0xffc00000 + 0x3 >> 10
    return pte;
}

这里放一张地址的映射关系图

mark

解决了最复杂的PTE和PDE的地址获取问题,下面添加虚拟地址到物理地址的映射关系就简单了

// 在页表中添加虚拟地址到物理地址的映射关系
static void page_table_add(void *_vaddr, void *_page_phyaddr)
{
    uint32_t vaddr = (uint32_t)_vaddr;
    uint32_t page_phyaddr = (uint32_t)_page_phyaddr;

    uint32_t *pde = pde_ptr((uint32_t)vaddr);
    uint32_t *pte = pte_ptr((uint32_t)vaddr);

    // 在页目录内判断目录项的P位,若为1,表示该表已存在
    if(*pde & 0x01)
    {
        // 创建页表的时候,pte不应该存在
        ASSERT(!(*pte & 0x01));

        if(!(*pte & 0x01))
        {
            *pte = page_phyaddr | PG_US_U | PG_RW_W | PG_P_1;
        }
    }
    else
    {// 页目录项不存在,此时先创建页目录项
        uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool);

        *pde = pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1;
        memset((void*)((int)pte & 0xfffff000), 0, PG_SIZE);

        ASSERT(!(*pte & 0x01));
        *pte = page_phyaddr | PG_US_U | PG_RW_W | PG_P_1;
    }
}

这里直接对pde或者pte内部的数据赋值就好了,赋值的数据需要根据pde和pte的结构来。直接上结构图

PDE与PTE的结构图

前二十位是物理地址的高20位,后面的则是一些访问属性。这里不再过多解释

内存分配接口函数

函数已经全部封装好了,接下来是对外接口的提供了

enum pool_flags
{
    PF_KERNEL=1,
    PF_USER
};


// 分配pg_cnt 个页空间
void *malloc_page(enum pool_flags pf, uint32_t pg_cnt)
{
    ASSERT(pg_cnt > 0 && pg_cnt < 3840);

    void *vaddr_start = vaddr_get(pf, pg_cnt);
    if(vaddr_start == NULL)
    {
        return NULL;
    }

    uint32_t vaddr = (uint32_t)vaddr_start;
    uint32_t cnt = pg_cnt;

    struct pool *mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;

    while (cnt-- > 0)
    {
        void *page_phyaddr = palloc(mem_pool);
        if(page_phyaddr == NULL)
        {// 此处分配失败需要释放已申请的虚拟页和物理页
            return NULL;
        }
        page_table_add((void*)vaddr, page_phyaddr);
        vaddr += PG_SIZE;
    }
    return vaddr_start;
}

// 在内核物理内存池中申请pg_cnt页内存
void *get_kernel_pages(uint32_t pg_cnt)
{
    void *vaddr = malloc_page(PF_KERNEL, pg_cnt);

    if(vaddr != NULL)
    {
        memset(vaddr,0, pg_cnt * PG_SIZE);
    }
    return vaddr;
}

接下来就在bochs中运行看看申请的空间有没有被写入页表中

这个是目前内核的内存布局信息,内核物理内存开始地址为0x200000。并且我们申请的内存开始地址是在0xc010000处,这也是内核堆空间的起始地址

内存布局信息

在main函数中我申请了三页的内存,这里也确实做了三页的内存映射。

页表信息

猜你喜欢

转载自blog.csdn.net/cw312644167/article/details/80112415
今日推荐