分配内存区:
vmalloc是一个接口函数,内核代码使用它来分配在虚拟内存中连续但在物理内存中不一定连续的内存。因为用于vmalloc的内存页总是必须映射在内核地址空间中,因此使用ZONE_HIGHMEM内存域的页要优于其他内存域。这使得内核可以节省比较宝贵的低端内存域,而又不会带来而外的坏处。因此,vmalloc是内核出于自身的目的使用高端内存页得少数情况之一。
内核在管理虚拟内存中的vmalloc区域时,内核必须跟踪哪些子区域被使用、哪些是空闲的。为此定义了一个数据结构,将所有使用的部分保存在一个链表中。
struct vm_struct {
/* keep next,addr,size together to speedup lookups */
struct vm_struct *next;//使得内核可以将vmalloc区域中的所有子区域保存在一个单链表上
void *addr;//定义了分配的子区域在虚拟地址空间中的起始地址
unsigned long size;//表示该子区域的长度
unsigned long flags;//存储了与该内存区关联的标志集合,它只用于指定内存区类型(VM_MALLOC指定由vmalloc产生的子区域;VM_MAP用于表示将现存pages集合映射到连续的虚拟地址空间中;VM_IOREMAP表示将几乎随机的物理内存区域映射到vmalloc区域中,这是一个特定于体系结构的操作。)
struct page **pages;//pages是一个指针,指向page指针的数组,每个数组成员都表示一个映射到虚拟地址空间中物理内存页得page实例
unsigned int nr_pages;//指定pages中数组项的数目,即涉及的内存页数目
unsigned long phys_addr;//phys_addr仅当用ioremap映射了由物理地址描述的物理内存区域时才需要
};
void *vmalloc(unsigned long size)//size用于指定所需内存区的长度,单位是字节
{
return __vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL);
}
void *__vmalloc(unsigned long size, gfp_t gfp_mask, pgprot_t prot)
{
return __vmalloc_node(size, gfp_mask, prot, -1);
}
static void *__vmalloc_node(unsigned long size, gfp_t gfp_mask, pgprot_t prot,
int node)
{
struct vm_struct *area;
size = PAGE_ALIGN(size);//vmalloc分配的大小必须是页面的整数倍,这里将长度对齐到页面
if (!size || (size >> PAGE_SHIFT) > num_physpages)//分配长度为0,或者长度大于限制数,都返回失败
return NULL;
area = get_vm_area_node(size, VM_ALLOC, node, gfp_mask);//在vmalloc地址区间中找到合适的区域,这是通过遍历vmlist链表来实现的
if (!area)//vmalloc虚拟地址空间不足,可能是分配的长度太大,返回失败
return NULL;
return __vmalloc_area_node(area, gfp_mask, prot, node);
}
struct vm_struct *get_vm_area_node(unsigned long size, unsigned long flags,
int node, gfp_t gfp_mask)
{
return __get_vm_area_node(size, flags, VMALLOC_START, VMALLOC_END, node,
gfp_mask);
}
__get_vm_area_node源代码详细分析如下:
static struct vm_struct *__get_vm_area_node(unsigned long size, unsigned long flags,
unsigned long start, unsigned long end,
int node, gfp_t gfp_mask)
{
struct vm_struct **p, *tmp, *area;
unsigned long align = 1;
unsigned long addr;
BUG_ON(in_interrupt());//由于虚拟地址空间分配过程中可能由于地址空间紧张而导致进程被阻塞,因此vmalloc函数不能用于中断上下文,这里检查是否处于中断上下文,如果是则进入crash
if (flags & VM_IOREMAP) {//如果上层调用是通过ioremap进入的
int bit = fls(size);//根据长度计算它的置1的最高位
if (bit > IOREMAP_MAX_ORDER)//如果bit>24,即要分配的长度大于2^24,即16MB
bit = IOREMAP_MAX_ORDER;//以16M为边界对齐ioremap虚拟地址
else if (bit < PAGE_SHIFT)//小于1个页面
bit = PAGE_SHIFT;//计算边界用,至少以页面为边界
align = 1ul << bit;//设置对齐方式
}
addr = ALIGN(start, align);//起始地址以分配大小的边界对齐
size = PAGE_ALIGN(size);//分配长度至少以页面为单位
if (unlikely(!size))//参数不合法
return NULL;
area = kmalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);//分配一个vm_struct数据结构,它代表一段vmalloc地址空间
if (unlikely(!area))//分配vm_struct失败
return NULL;
/*
* We always allocate a guard page.
*/
size += PAGE_SIZE;//这里将长度增加一个页面,是为了在多个vmalloc地址空间之间,增加一段空洞,这些空洞没有分配实际的物理页面,也没有进行虚拟地址映射,如果有进程访问vmalloc分配的内存越界,则会触段错误
write_lock(&vmlist_lock);
for (p = &vmlist; (tmp = *p) != NULL ;p = &tmp->next) {//循环寻找链表,直到找到足够的空闲内存
if ((unsigned long)tmp->addr < addr) {//如果内存块起始地址小于对齐地址,则查找下一块
if((unsigned long)tmp->addr + tmp->size >= addr)//如果对齐地址在该内存块内
addr = ALIGN(tmp->size +
(unsigned long)tmp->addr, align);//从该内存块结束地址开始重新对齐
continue;
}
if ((size + addr) < addr)//翻转越界了,此处怎么理解?
goto out;
if (size + addr <= (unsigned long)tmp->addr)//该内存块起始地址之前存在足够大的空闲内存
goto found;
addr = ALIGN(tmp->size + (unsigned long)tmp->addr, align);//否则空闲内存不够就重新对齐
if (addr > end - size)//越界
goto out;
}
found://初始化area
area->next = *p;
*p = area;
area->flags = flags;
area->addr = (void *)addr;
area->size = size;
area->pages = NULL;
area->nr_pages = 0;
area->phys_addr = 0;
write_unlock(&vmlist_lock);
return area;
out:
write_unlock(&vmlist_lock);
kfree(area);
if (printk_ratelimit())
printk(KERN_WARNING "allocation failed: out of vmalloc space - use vmalloc=<size> to increase size.\n");
return NULL;
}
__vmalloc_area_node源代码详细分析如下:
void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
pgprot_t prot, int node)
{
struct page **pages;
unsigned int nr_pages, array_size, i;
nr_pages = (area->size - PAGE_SIZE) >> PAGE_SHIFT;//这里计算需要分配的页面数量,减去PAGE_SIZE是因为分配虚拟地址空间时多指定了一个空洞页面防止越界
array_size = (nr_pages * sizeof(struct page *));//分配的物理页面描述符要记录到一个临时数组中去,这里计算这个数据组的长度
area->nr_pages = nr_pages;//记录下该区域的物理页面数,释放时要使用
/* Please note that the recursion is strictly bounded. */
if (array_size > PAGE_SIZE) {//如果临时数组大于一个页面
pages = __vmalloc_node(array_size, gfp_mask | __GFP_ZERO,
PAGE_KERNEL, node);//通过vmalloc_node分配这个临时数组
area->flags |= VM_VPAGES;//该标志表示临时数组是通过vmalloc分配的,释放时要同时调用vfree释放这个临时数组
} else {//如果临时数组小于一个页面,则通过kmalloc从slab中分配这个数组
pages = kmalloc_node(array_size,
(gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO,
node);
}
area->pages = pages;//记录区域物理页面数组
if (!area->pages) {//分配临时数组失败,归还vm虚拟地址空间
remove_vm_area(area->addr);
kfree(area);
return NULL;
}
for (i = 0; i < area->nr_pages; i++) {//分配物理页面
if (node < 0)//没有显示指定分配页帧的结点
area->pages[i] = alloc_page(gfp_mask);//从当前结点分配页帧
else
area->pages[i] = alloc_pages_node(node, gfp_mask, 0);//从指定结点分配页帧
if (unlikely(!area->pages[i])) {//分配页面失败
/* Successfully allocated i pages, free them in __vunmap() */
area->nr_pages = i;//记录下该区域中成功分配的页面数,跳转到fail释放已经分配的页面
goto fail;
}
}
if (map_vm_area(area, prot, &pages))//进行虚实地址映射
goto fail;
return area->addr;
fail://分配物理内存失败,或者虚实映射失败
vfree(area->addr);//释放已经分配的页面并解除虚实映射
return NULL;
除了vmalloc之外,还有其他方法可以创建虚拟连续映射。这些都是基于上文讨论的__vmalloc函数或使用非常类似的机制。
1:vmalloc_32的工作方式与vmalloc相同,但会确保所使用的物理内存总是可以用普通32位指针寻址。如果某种体系结构的寻址能力超出基于字长计算的范围,那么这种保证就很重要。例如,在启用了PAE的IA-32系统上,就是如此。
2:vmap使用一个page数组作为起点,来创建虚拟连续内存区。与vmalloc相比,这函数所用的物理内存位置不是隐式分配的,而需要先行分配好,作为参数传递。此类映射可以通过vm_map实例中的VM_MAP标志辨别。
3:不同于上述所有的映射方法,ioremap是一个特定于处理器的函数,必须在所有体系结构上实现。它可以将取自物理地址空间、由系统总线用于I/O操作的一个内存块,映射到内核的地址空间中。
释放内存页:
有两个函数用于向内核释放内存,vfree用于释放vmalloc和vmalloc_32分配的内存区,而vunmap用于释放由vmap或ioremap创建的映射。这两个函数都会归结到__vunmap。
void vfree(void *addr)
{
BUG_ON(in_interrupt());//__vunmap函数不能在中断中被调用
__vunmap(addr, 1);//后一个参数1表示将释放的物理内存页返回给伙伴系统
}
void vunmap(void *addr)
{
BUG_ON(in_interrupt());//__vunmap函数不能在中断中被调用
__vunmap(addr, 0);//后一个参数0表示释放的物理内存页不返回给伙伴系统
}
__vunmap源代码详细分析如下:
static void __vunmap(void *addr, int deallocate_pages)//addr表示要释放的区域的起始地址,deallocate_pages指定了是否将与该区域相关的物理内存页返回给伙伴系统
{
struct vm_struct *area;
if (!addr)//传入的参数不合法,退出
return;
if ((PAGE_SIZE-1) & (unsigned long)addr) {//传入的地址没有页对齐,也不合法,因为vmalloc和vmap的映射地址都是页面对齐的
printk(KERN_ERR "Trying to vfree() bad address (%p)\n", addr);
WARN_ON(1);
return;
}
area = remove_vm_area(addr);//从vmlist中移除虚拟地址块
if (unlikely(!area)) {//如果虚拟地址块没有位于vmlist中,那么说明调用者传入了错误的地址参数,或者多次释放同一地址
printk(KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n",
addr);
WARN_ON(1);
return;
}
debug_check_no_locks_freed(addr, area->size);//调试相关的代码
if (deallocate_pages) {//调用者是vfree而不是vunmap,需要返还相关的物理内存给伙伴系统
int i;
for (i = 0; i < area->nr_pages; i++) {//遍历物理页面
BUG_ON(!area->pages[i]);
__free_page(area->pages[i]);//将页面归还给伙伴系统,以前的博客已经详细讲解
}
if (area->flags & VM_VPAGES)//存放页面的指针数组超过一页,是由vmalloc分配的
vfree(area->pages);// 通过vfree释放vmalloc分配的内存
else
kfree(area->pages);// 否则通过kfree将指针数组释放给slab管理器
}
kfree(area);//释放vm_struct描述符
return;
}
struct vm_struct *remove_vm_area(void *addr)//完成锁定之后调用__remove_vm_area
{
struct vm_struct *v;
write_lock(&vmlist_lock);
v = __remove_vm_area(addr);
write_unlock(&vmlist_lock);
return v;
}
__remove_vm_area源代码详细分析如下:
static struct vm_struct *__remove_vm_area(void *addr)
{
struct vm_struct **p, *tmp;
for (p = &vmlist ; (tmp = *p) != NULL ;p = &tmp->next) {//扫描vmlist,以找到相关项
if (tmp->addr == addr)
goto found;
}
return NULL;
found:
unmap_vm_area(tmp);//使用找到的vm_struct实例,从页表删除不再需要的项。它还会更新CPU高速缓存
*p = tmp->next;
/*
* Remove the guard page.
*/
tmp->size -= PAGE_SIZE;//还需减去两个vmalloc内存区之间的一个警戒页
return tmp;
}
我也知道有很多的细节都没有分析到位,但是我也没有办法,曾经想着把里面涉及到的每一个函数都分析到位,但是那样的话自己相当的痛苦,因为那样的结果就是很多天都没有办法前进一点,会让人相当的有挫败感,最后只能选择大概先都过一遍,因为自己是一个内核的初学者,而内核前后的关联又很大,也只能先过一遍,到后面我会重新回来看我写得博客,能增进一些分析就增进一些分析。如果您认为上面确实有很重要的地方我没有分析到,希望您指点。