find_vma_intersection()在mm.h文件中,实现如下:
//判断进程的地址空间是否与给定的地址区间相交叉
/* Look up the first VMA which intersects the interval start_addr..end_addr-1, NULL if none. Assume start_addr < end_addr. */ static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr) { struct vm_area_struct * vma = find_vma(mm,start_addr); if (vma && end_addr <= vma->vm_start) vma = NULL; return vma; }
那为什么brk中find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE)调用中,newbrk为什么要加上PAGE_SIZE呢?这是因为newbrk 与oldbrk已经是经过页框对齐后的地址:如下
newbrk = PAGE_ALIGN(brk);
oldbrk = PAGE_ALIGN(mm->brk);
而且,每个vma的起始地址跟长度都是与页框对齐的(参考ULK3).注意到find_vma_intersection()判断是否交替的时候带有一个’=’.也就是判断newbrk的下一个页框是否在进程的线性区中。接着往下看,经过判断之后,就会进入到do_brk():
/* * this is really a simplified "do_mmap". it only handles * anonymous maps. eventually we may be able to do some * brk-specific accounting here. */ unsigned long do_brk(unsigned long addr, unsigned long len) { struct mm_struct * mm = current->mm; struct vm_area_struct * vma, * prev; unsigned long flags; struct rb_node ** rb_link, * rb_parent; pgoff_t pgoff = addr >> PAGE_SHIFT; int error; //长度按页框对齐,不过在我们这个流程来说,这个步骤是没必要的 //因为start 与 end都与页框对齐,end – start肯定也是与页框对齐的 len = PAGE_ALIGN(len); if (!len) return addr; /*检查给定的地址是否能够进行安全的地址映射*/ error = security_file_mmap(NULL, 0, 0, 0, addr, 1); if (error) return error; flags = VM_DATA_DEFAULT_FLAGS | VM_ACCOUNT | mm->def_flags; error = get_unmapped_area(NULL, addr, len, 0, MAP_FIXED); if (error & ~PAGE_MASK) return error; /* * mlock MCL_FUTURE? */ //VM_LOCKED: 页被锁住不能被交换出去 if (mm->def_flags & VM_LOCKED) { unsigned long locked, lock_limit; locked = len >> PAGE_SHIFT; locked += mm->locked_vm; lock_limit = rlimit(RLIMIT_MEMLOCK); lock_limit >>= PAGE_SHIFT; if (locked > lock_limit && !capable(CAP_IPC_LOCK)) return -EAGAIN; } /* * mm->mmap_sem is required to protect against another thread * changing the mappings in case we sleep. */ verify_mm_writelocked(mm); /* * Clear old maps. this also does some error checking for us */ / *find_vma_prepare函数扫描当前进程地址空间的vm_area_struct结构所形成的红黑树,试图找到结束地址高于addr的第一个区间;如果找到了一个虚拟区,说明addr所在的虚拟区已经在使用,也就是已经有映射存在,因此要调用do_munmap()把这个老的虚拟区从进程地址空间中撤销,如果撤销不成功,就返回一个负数;如果撤销成功,就继续查找,直到在红黑树中找不到addr所在的虚拟区,并继续下面的检查*/ munmap_back: vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent); if (vma && vma->vm_start < addr + len) { if (do_munmap(mm, addr, len)) return -ENOMEM; goto munmap_back; } /* Check against address space limits *after* clearing old maps... */ /*检查是否要对此虚拟区间进行扩充*/ if (!may_expand_vm(mm, len >> PAGE_SHIFT)) return -ENOMEM; if (mm->map_count > sysctl_max_map_count) return -ENOMEM; //判断系统是否有足够的内存 if (security_vm_enough_memory(len >> PAGE_SHIFT)) return -ENOMEM; /* Can we just expand an old private anonymous mapping? */ //判断是否可以合并, 如果可以合并,就将基合并为一个VMA区 /*如果是匿名映射(file为空),并且这个虚拟区是非共享的,则可以把这个虚拟区和*与它紧挨的前一个虚拟区进行合并;虚拟区的合并是由vma_merge()函数实现的。如*果合并成功,则转out处,请看后面out处的代码*/ vma = vma_merge(mm, prev, addr, addr + len, flags, NULL, NULL, pgoff, NULL); if (vma) goto out; /* * create a vma struct for an anonymous mapping */ //不可以合并,新建一个VMA /*分配映射虚拟区空间*/ vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL); if (!vma) { vm_unacct_memory(len >> PAGE_SHIFT); return -ENOMEM; } //设值VMA的值 INIT_LIST_HEAD(&vma->anon_vma_chain); vma->vm_mm = mm; vma->vm_start = addr; vma->vm_end = addr + len; vma->vm_pgoff = pgoff; vma->vm_flags = flags; vma->vm_page_prot = vm_get_page_prot(flags); //将新分配的VMA插入到进程的VMA链表 /*把新建的虚拟区插入到进程的地址空间*/ vma_link(mm, vma, prev, rb_link, rb_parent); out: /*增加进程地址空间长度*/ mm->total_vm += len >> PAGE_SHIFT; //如果定义了LOCKED。就为其分配内存 if (flags & VM_LOCKED) { /*虚拟区间加锁状态下完成虚拟页面到物理页面的映射并完成文件到物理内存的真正调入 */ if (!mlock_vma_pages_range(vma, addr, addr + len)) mm->locked_vm += (len >> PAGE_SHIFT); } return addr; }
/** * __mlock_vma_pages_range() - mlock a range of pages in the vma. * @vma: target vma * @start: start address * @end: end address * * This takes care of making the pages present too. * * return 0 on success, negative error code on error. * * vma->vm_mm->mmap_sem must be held for at least read. */ static long __mlock_vma_pages_range(struct vm_area_struct *vma, unsigned long start, unsigned long end) { struct mm_struct *mm = vma->vm_mm; unsigned long addr = start; struct page *pages[16]; /* 16 gives a reasonable batch */ int nr_pages = (end - start) / PAGE_SIZE; int ret = 0; int gup_flags; VM_BUG_ON(start & ~PAGE_MASK); VM_BUG_ON(end & ~PAGE_MASK); VM_BUG_ON(start < vma->vm_start); VM_BUG_ON(end > vma->vm_end); VM_BUG_ON(!rwsem_is_locked(&mm->mmap_sem)); gup_flags = FOLL_TOUCH | FOLL_GET; if (vma->vm_flags & VM_WRITE) gup_flags |= FOLL_WRITE; /* We don't try to access the guard page of a stack vma */ if (stack_guard_page(vma, start)) { addr += PAGE_SIZE; nr_pages--; } while (nr_pages > 0) { int i; cond_resched(); /* * get_user_pages makes pages present if we are * setting mlock. and this extra reference count will * disable migration of this page. However, page may * still be truncated out from under us. */ ret = __get_user_pages(current, mm, addr, min_t(int, nr_pages, ARRAY_SIZE(pages)), gup_flags, pages, NULL); /* * This can happen for, e.g., VM_NONLINEAR regions before * a page has been allocated and mapped at a given offset, * or for addresses that map beyond end of a file. * We'll mlock the pages if/when they get faulted in. */ if (ret < 0) break; lru_add_drain(); /* push cached pages to LRU */ for (i = 0; i < ret; i++) { struct page *page = pages[i]; if (page->mapping) { /* * That preliminary check is mainly to avoid * the pointless overhead of lock_page on the * ZERO_PAGE: which might bounce very badly if * there is contention. However, we're still * dirtying its cacheline with get/put_page: * we'll add another __get_user_pages flag to * avoid it if that case turns out to matter. */ lock_page(page); /* * Because we lock page here and migration is * blocked by the elevated reference, we need * only check for file-cache page truncation. */ if (page->mapping) mlock_vma_page(page); unlock_page(page); } put_page(page); /* ref from get_user_pages() */ } addr += ret * PAGE_SIZE; nr_pages -= ret; ret = 0; } return ret; /* 0 or negative error code */ }
回顾整个函数,整理出流程图如下:
在分析2.6.35.2的同时,我们小组也对另外的内核版本源码进行了简单的对比,发现一些异同。
在旧版本(2.4和2.6.26)中,系统调用应用的函数是sys_brk(),而在新版本2.6.27开始,sys_brk()这个函数已经不存在了,在2.6.35中,已形演变为:SYSCALL_DEFINE1(brk, unsigned long, brk)。其中SYSCALL_DEFINE1只是一个宏定义,这样做的目的是为了统一代码格式,封装性变得更好一些,便于以后源代码的拓展。
在旧版本中,Split_vma
新版本:__split_vma
旧:unmap_vma_list(),调用remove_vm_struct()
新:直接remove_vma_list(),精简了代码,代码重构了。
旧:vma->vm_end - vma->vm_start) >> PAGE_SHIFT;以一行语句的形式展现,代码只限于在本函数内使用。
新:vma_pages()函数,是对vma调整的封装,便于其它函数调用,应用范围变大。
旧:clear_page_tables()
新:free_pgd_range(),做了一些封装。
旧:make_pages_present,
新:mlock_vma_pages_range。
2.4内核的流程图:
在本次分析报告中,我们小组通过从各个方面查找资料,了解Linux操作系统中内存管理的过程。在研读代码的过程中,上网查阅了相关老版本的资料,发现新版本与老版本的源代码上,有了较多的不同之处。在分析过程中我使用了Windows下的工具Source Insight 3.5,锻炼了我们分析大型软件代码的能力。通过与同组同学的合作,加强了我们的团体合作能力。
通过对Linux源代码的阅读,让我们对C语言有了全新的认识,深感标准C的高级编程较难。代码中C语言的各种灵活运用让我们大开眼界,在源码中,大量使用了static关键字用来修饰函数和结构体定义,来限制函数或变量的作用域,以及高级宏定义的使用。函数指针的使用,函数之间的调用,使用较多。同时对于goto语句我们也有了全新的认识。在分析实验中虽然我们没有怎么编写代码但对Linux源码的阅读也是对我们编程能力的一次大的提升。
通过对Linux系统调用代码的分析,看到大型软件的编程习惯,体会到编程之美,小组成员决定在以后的编程过程中养成良好的编程习惯,同时着手研究开源软件项目,阅读更多优秀的代码,来增长见识和水平。
[1]毛德操,胡希明;Linux内核源代码情景分析;浙江大学出版社
[2]www.kerneltravel.com 内核之旅网站
[3] http://blog.chinaunix.net ChinaUnix社区
[1]Windows下的代码阅读工具 Source Insight3.5
[2]Microsoft Visio 2007 流程图绘制工具
[3]Microsoft Visual Stdio 2010 代码查看工具