现在的位置: 首页 > 综合 > 正文

非连续内存区

2018年04月11日 ⁄ 综合 ⁄ 共 8334字 ⁄ 字号 评论关闭

从前面的博文中我们已经知道,把一块存放slab结构的内存区映射到一组连续的物理页是最好的选择,这样会充分利用高速缓存并获得较低的平均访问时间。

不过,上面的方式主要是针对那些使用非常频繁的内核数据结构——如task_struct、inode来设计的。如果对内存区的请求不是很频繁,那么,通过连续的线性地址,而不是物理地址来访问非连续的物理页框这样一种分配模式就会很有意义了。

这种模式的主要优点是避免了外碎片,而缺点是必须打乱内核页表。此外,非连续内存区的大小必须是4096 的倍数。Linux 在几个方面使用非连续内存区:为活动的交换区分配数据结构,为模块分配空间,或者给某些I/O 驱动程序分配缓冲区等。此外,非连续内存区还提供了另一种使用高端内存页框的方法(参见前面的“高端内存映射 ”博文)。

1 非连续内存区的线性地址

要查找线性地址的一个空闲区,我们可以从PAGE_OFFSET开始查找(通常为0xc0000000,即第4 个GB 的起始地址)。下图让我们回忆了如何使用第4个GB 的线性地址:

回忆一下:

(1)内存区的开始部分包含的是对前896MB RAM 进行映射的线性地址。直接映射的物理内存末尾所对应的线性地址保存在high_memory全局变量中。当物理内存小于896MB,则线性地址 0xc0000000以后的896MB与其一一对应;当物理内存大于896MB而小于4GB时,只直接映射前896MB的地址到0xc0000000以后的线性空间,然后把线性空间的其他部分与896MB和4GB物理空间映射起来,称为动态重映射,这是本博的重点;当物理内存大于4GB,则需要考虑PAE 的情况,其他的东东没什么区别,我们不做过多的回忆了。

(2)内核的页表由内核页全局目录变量swapper_pg_dir维护;pagetable_init()建立内核页表项。

(3)内存区的结尾部分包含的是固定映射的线性地址,主要用于存放一些常量线性地址,具体查看“高端内存映射 ”博文。

(4)从PKMAP_BASE 开始,我们查找用于高端内存页框的永久内核映射的线性地址,具体查看“高端内存映射 ”博文。

(5)其余的线性地址可以用于非连续内存区。在物理内存映射的末尾与第一个内存区之间插入一个大小为8MB(宏VMALLOC_OFFSET)的安全区,目的是为了“捕获”对内存的越界访问。出于同样的理由,插入其他4KB 大小的安全区来隔离非连续的内存区。

本博,就来详细讨论(5)——非连续内存区,为非连续内存区保留的线性地址空间的起始地址由VMALLOC_START宏定义,而末尾地址由VMALLOC_END 宏定义。

2 非连续内存区的描述符

每个非连续内存区都对应着一个类型为vm_struct 的描述符:
struct vm_struct {
    void            *addr;
    unsigned long        size;
    unsigned long        flags;
    struct page        **pages;
    unsigned int        nr_pages;
    unsigned long        phys_addr;
    struct vm_struct    *next;
};

介绍下它的字段:

void *    addr    内存区内第一个内存单元的线性地址(首址)
unsigned long    size    内存区的大小加4096(内存区之间的安全区间的大小)
unsigned long    flags    非连续内存区映射的内存的类型
struct page **    pages    指向nr_pages数组的指针,该数组由指向页描述符的指针组成
unsigned int    nr_pages    内存区填充的页的个数
unsigned long    phys_addr    该字段设为0,除非内存已被创建来映射一个硬件设备的I/O 共享内存
struct vm_struct *    next    指向下一个vm_struct结构的指针

通过next字段,这些描述符被插入到一个简单的链表中,链表第一个元素的地址存放在vmlist变量中。对这个链表的访问依靠vmlist_lock读/ 写自旋锁来保护。

flags字段标识了非连续区映射的内存的类型:
(1)VM_ALLOC 表示使用vmalloc()得到的页;
(2)VM_MAP 表示使用vmap()映射的已经被分配的页;
(3)VM_IOREMAP 表示使用ioremap()映射的硬件设备的板上内存。

get_vm_area()函数在线性地址VMALLOC_START 和VMALLOC_END 之间查找一个空闲区域。该函数使用两个参数:将被创建的内存区的字节大小(size)和指定空闲区类型(参见上面)的标志(flag)。步骤执行如下:

1.调用kmalloc()为vm_struct 类型的新描述符获得一个内存区。
2.为写得到vmlist_lock 锁,并扫描类型为vm_struct 的描述符链表来查找线性地址一个空闲区域,至少覆盖size + 4096 个地址(4096 是内存区之间的安全区间大小)。
3.如果存在这样一个区间,函数就初始化描述符的字段,释放vmlist_lock 锁,并以返回这个非连续内存区的起始地址而结束。
4.否则,get_vm_area()释放先前得到的描述符,释放vmlist_lock,然后返回NULL。

3 分配非连续内存区

vmalloc()函数给内核分配一个非连续内存区。参数size表示所请求内存区的大小。如果这个函数能够满足请求,就返回新内存区的起始地址;否则,返回一个NULL 指针(mm\ vmalloc.c):

void * vmalloc(unsigned long size)
{
    struct vm_struct *area;
    struct page **pages;
    unsigned int array_size, i;
    size = (size + PAGE_SIZE - 1) & PAGE_MASK;
    area = get_vm_area(size, VM_ALLOC);
    if (!area)
        return NULL;
    area->nr_pages = size >> PAGE_SHIFT;
    array_size = (area->nr_pages * sizeof(struct page *));
    area->pages = pages = kmalloc(array_size, GFP_KERNEL);

    if (!area->pages) {
        remove_vm_area(area->addr);
        kfree(area);
        return NULL;
    }
    memset(area->pages, 0, array_size);
    for (i=0; i<area->nr_pages; i++) {
        area->pages[i] = alloc_page(GFP_KERNEL|_ _GFP_HIGHMEM);

        if (!area->pages[i]) {
            area->nr_pages = i;
    fail:   vfree(area->addr);
            return NULL;
       }
    }
    if (map_vm_area(area, _ _pgprot(0x63), &pages) )
        goto fail;
    return area->addr;
}

函数首先将参数size 设为4096(页框大小)的整数倍。然后,vmalloc()调用get_vm_area()来创建一个新的描述符,并返回分配给这个内存区的线性地址。描述符的flags 字段被初始化为VM_ALLOC标志,该标志意味着通过使用vmalloc()函数,非连续页框将被映射到一个线性地址区间。然后vmalloc()函数调用kmalloc()来请求一组连续页框,这组连续页框足够包含一个页描述符指针数组。调用memset()函数来将所有这些指针设为NULL。接着重复调用alloc_page()函数,每一次为区间中nr_pages个页的每一个分配一个页框,并把对应页描述符的地址存放在
area->pages数组中。注意,必须使用area->pages 数组是因为页框可能属于ZONE_HIGHMEM内存管理区,所以此时它们不必被映射到一个线性地址上。

简要介绍一下memset(area->pages, 0, array_size)的实现函数:
static inline void * __memset_generic(void * s, char c,size_t count)
{
int d0, d1;
__asm__ __volatile__(
    "rep\n\t"
    "stosb"
    : "=&c" (d0), "=&D" (d1)
    :"a" (c),"1" (s),"0" (count)
    :"memory");
return s;
}

现在到了棘手的部分。直到这里,已经得到了一个新的连续线性地址区间,并且已经分配了一组非连续页框来映射这些线性地址。最后至关重要的步骤是修改内核使用的页表项 ,以此表明分配给非连续内存区的每个页框现在对应着一个线性地址,这个线性地址被包含在vmalloc()产生的非连续线性地址区间中。这就是map_vm_area()所要做的,下面来详细说说:

map_vm_area()函数使用以下3 个参数:

area:指向内存区的vm_struct 描述符的指针。
prot:已分配页框的保护位。它总是被置为0x63,对应着Present、Accessed、Read/Write 及Dirty。
pages:指向一个指针数组的变量的地址,该指针数组的指针指向页描述符(因此,structpage *** 被当作数据类型使用!)。

函数首先把内存区的开始和末尾的线性地址分别分配给局部变量address和end:
address = area->addr;
end = address + (area->size - PAGE_SIZE);

请记住,area->size存放的是内存区的实际地址加上4KB 内存之间的安全区间。然后函数使用pgd_offset_k宏来得到在主内核页全局目录中的目录项,该项对应于内存区起始线性地址,然后获得内核页表自旋锁:
pgd = pgd_offset_k(address);
spin_lock(&init_mm.page_table_lock);

然后,函数执行下列循环:
int ret = 0;
for (i = pgd_index(address); i < pgd_index(end-1); i++) {
    pud_t *pud = pud_alloc(&init_mm, pgd, address);
    ret = -ENOMEM;
    if (!pud)
        break;
    next = (address + PGDIR_SIZE) & PGDIR_MASK;
    if (next < address || next > end)
        next = end;
    if (map_area_pud(pud, address, next, prot, pages))
        break;
    address = next;
    pgd++;
    ret = 0;
}
spin_unlock(&init_mm.page_table_lock);
flush_cache_vmap((unsigned long)area->addr, end);
return ret;

每次循环都首先调用pud_alloc()来为新内存区创建一个页上级目录,并把它的物理地址写入内核页全局目录的合适表项。然后调用 alloc_area_pud()为新的页上级目录分配所有相关的页表。接下来,把常量230(在PAE被激活的情况下,否则为222)与address 的当前值相加(230 就是一个页上级目录所跨越的线性地址范围的大小),最后增加指向页全局目录的指针pgd。

循环结束的条件是:指向非连续内存区的所有页表项全被建立。

map_area_pud()函数为页上级目录所指向的所有页表执行一个类似的循环:
do {
    pmd_t * pmd = pmd_alloc(&init_mm, pud, address);
    if (!pmd)
        return -ENOMEM;
    if (map_area_pmd(pmd, address, end-address, prot, pages))
        return -ENOMEM;
    address = (address + PUD_SIZE) & PUD_MASK;
    pud++;
} while (address < end);

map_area_pmd()函数为页中间目录所指向的所有页表执行一个类似的循环:
do {
    pte_t * pte = pte_alloc_kernel(&init_mm, pmd, address);
    if (!pte)
        return -ENOMEM;
    if (map_area_pte(pte, address, end-address, prot, pages))
        return -ENOMEM;
    address = (address + PMD_SIZE) & PMD_MASK;
    pmd++;
} while (address < end);

pte_alloc_kernel()函数分配一个新的页表,并更新页中间目录中相应的目录项。接下来,map_area_pte()为页表中相应的表项分配所有的页框。address值增加222(222 就是一个页表所跨越的线性地址区间的大小),并且循环反复执行。

map_area_pte()的主循环为:
do {
    struct page * page = **pages;
    set_pte(pte, mk_pte(page, prot));
    address += PAGE_SIZE;
    pte++;
    (*pages)++;
} while (address < end);

将被映射的页框的页描述符地址page 是从地址pages 处的变量指向的数组项读得的。通过set_pte和mk_pte宏,把新页框的物理地址写进页表。把常量4096(即一个页框的长度)加到address上之后,循环又重复执行。

注意,map_vm_area()并不触及当前进程的页表。因此,当内核态的进程访问非连续内存区时,缺页发生,因为该内存区所对应的进程页表中的表项为空。然而,缺页处理程序要检查这个缺页线性地址是否在主内核页表中(也就是init_mm.pgd页全局目录和它的子页表)。一旦处理程序发现一个主内核页表含有这个线性地址的非空项,就把它的值拷贝到相应的进程页表项中,并恢复进程的正常执行。这种机制将在“缺页异常处理程序”博文描述。

除了vmalloc()函数之外,非连续内存区还能由vmalloc_32()函数分配,该函数与vmalloc()很相似,但是它只从ZONE_NORMAL和ZONE_DMA内存管理区中分配页框。

Linux 2.6 还特别提供了了一个vmap()函数,它将映射非连续内存区中已经分配的页框:本质上,该函数接收一组指向页描述符的指针作为参数,调用 get_vm_area()得到一个新vm_struct描述符,然后调用map_vm_area()来映射页框。因此该函数与vmalloc()相似,但是它不分配页框。

4 释放非连续内存区

vfree()函数释放vmalloc()或vmalloc_32()创建的非连续内存区,而vunmap()函数释放vmap()创建的内存区。两个函数都使用同一个参数 —— 将要释放的内存区的起始线性地址address;它们都依赖于__vunmap()函数来做实质性的工作。

__vunmap()函数接收两个参数:将要释放的内存区的起始地址的地址addr,以及标志deallocate_pages,如果被映射到内存区内的页框应当被释放到分区页框分配器(调用vfree())中,那么这个标志被置位,否则被清除(vunmap()被调用)。该函数执行以下操作:
1.    调用remove_vm_area()函数得到vm_struct 描述符的地址area,并清除非连续内存区中的线性地址对应的内核的页表项。
2.    如果deallocate_pages 被置位,函数扫描指向页描述符的area->pages指针数组;对于数组的每一个元素,调用__free_page()函数释放页框到分区页框分配器。此外,执行kfree(area->pages)来释放数组本身。
3.    调用kfree(area)来释放vm_struct 描述符。

remove_vm_area()函数执行如下循环:
write_lock(&vmlist_lock);
for (p = &vmlist ; (tmp = *p) ; p = &tmp->next) {
    if (tmp->addr == addr) {
        unmap_vm_area(tmp);
        *p = tmp->next;
        break;
    }
}
write_unlock(&vmlist_lock);
return tmp;

内存区本身通过调用unmap_vm_area()来释放。这个函数接收单个参数,即指向内存区的vm_struct描述符的指针area。它执行下列循环以进行map_vm_area()的反向操作:
address = area->addr;
end = address + area->size;
pgd = pgd_offset_k(address);
for (i = pgd_index(address); i <= pgd_index(end-1); i++) {
    next = (address + PGDIR_SIZE) & PGDIR_MASK;
    if (next <= address || next > end)
        next = end;
    unmap_area_pud(pgd, address, next - address);
    address = next;
    pgd++;
}

unmap_area_pud()依次在循环中执行map_area_pud()的反操作:
do {
    unmap_area_pmd(pud, address, end-address);
    address = (address + PUD_SIZE) & PUD_MASK;
    pud++;
} while (address && (address < end));

unmap_area_pmd()函数在循环中执行map_area_pmd()的反操作:
do {
    unmap_area_pte(pmd, address, end-address);
    address = (address + PMD_SIZE) & PMD_MASK;
    pmd++;
} while (address < end);

最后,unmap_area_pte()在循环中执行map_area_pte()的反操作:
do {
    pte_t page = ptep_get_and_clear(pte);
    address += PAGE_SIZE;
    pte++;
    if (!pte_none(page) && !pte_present(page))
        printk("Whee... Swapped out page in kernel page table\n");
} while (address < end);

在每次循环过程中,ptep_get_and_clear 宏将pte 指向的页表项设为0。

与vmalloc()一样,内核修改主内核页全局目录和它的子页表中的相应项,但是映射第4 个GB 的进程页表的项保持不变。这是在情理之中的,因为内核永远也不会收回扎根于主内核页全局目录中的页上级目录、页中间目录和页表。

例如,假定内核态的进程访问一个随后要释放的非连续内存区。进程的页全局目录项等于主内核页全局目录中的相应项,由于“缺页异常处理程序”博文中所描述的机制,这些目录项指向相同的页上级目录、页中间目录和页表。unmap_area_pte()函数只清除页表中的项(不回收页表本身)。进程对已释放非连续内存区的进一步访问必将由于空的页表项而触发缺页异常。但是,缺页处理程序会认为这样的访问是一个错误,因为主内核页表不包含有效的表项。

抱歉!评论已关闭.