当给用户态进程分配内存是时:
①、进程对动态内存的请求被认为是不紧迫的。例如,当进程的可执行文件被装入时,进程并不一定立即对所有的代码页进行访问。类似的,当进程调用malloc()以获得请求的动态内存时,并不意味着进程很快就会访问所有获得的内存。因此,一般来说,内核总是尽量推迟给用户态进程分配动态内存。
②、由于用户进程是不可信的,因此,内核必须能随时准备捕获用户态进程引起的寻址错误。
当用户态进程请求动态内存时,并没有获得请求的页框,而仅仅获得对一个新的线性地址区间的使用权,而这一线性地址就成为进程地址空间的一部分。这一区间叫做线性区。
1、进程的地址空间
进程的地址空间是由允许进程使用的全部线性地址组成。
线性区是由起始线性地址、长度和一些访问权限来描述的。为了效率起见,起始地址和线性区的长度都必须是4096的倍数,以便每个线性区所识别的数据完全填满分配给它的页框。进程会下以下情况获得新的线性区:
①、当用户在控制台输入一条命令,shell进程创建一个新的进程去执行这条命令。一个全新的地址空间分配给了新进程。
②、正在运行的进程可能决定装入一个完全不同的程序。在这种情况下,进程标示符不变,可是在装入这个程序以前所使用的线性区却被释放,并且有一组新的线性区被分配给这个进程。
③、正在运行的进程可能对一个文件执行“内存映射”。这时,内核给这个进程分配一个新的线性区来映射这个文件。
④、进程可能持续向他的用户态堆栈增加数据,知道映射这个堆栈的线性区用完为止,内核也许会决定扩展这个线性区的大小。
⑤、进程可能创建IPC共享线性区。
⑥、进程可能通过类似malloc()扩展自己的动态区。结果是,内核可能决定扩展给这个堆所分配的线性地址。
缺页异常处理程序要根据引发异常的线性地址,判断地址是否属于进程的地址空间,然后判断这个地址是由编程错误引发的无效线性地址还是因为这个地址存在但是其页框还没有分配。
2、内存描述符
mm_struct。
所有的内存描述符存放在一个双向链表中。每个描述符在mmlist字段存放链表相邻元素的地址。链表的第一个元素时init_mm的mmlist字段,init_mm是初始化阶段进程0所使用的内存描述符。mmlist_lock自旋锁保护多处理器系统对链表的同时访问。
mm_users字段存放共享mm_struct数据结构的轻量级进程的个数。mm_count字段是内存描述符的主使用计数器,在mm_users次使用计数器中的所有用户再mm_count中只作为一个单位。如果把内存描述符暂时借给一个内核线程,那么内核就增加mm_count。
mm_alloc()函数用来获得一个新的内存描述符。由于这些描述符被保存在slab分配器高速缓存中,因此,mm_alloc()调用kmem_cache_alloc()来初始化新的内存描述符,并把mm_users和mm_count都值为1;
mm_put()函数递减内存描述符的mm_users字段。如果字段为0,这个函数就释放局部描述符表、线性区描述符以及有内存描述符所引用的页表,并调用mmdrop();后一个函数把mm_count字段-1,如果mm_count=0,就释放mm_struct结构。
2.1 内核线程的内存描述符
内核线程不用线性区,内存描述符的很多字段对内核线程没有意义。
因为大雨TASK_SIZE线性地址的相应页表项都应总是相同的,因此,一个内核线程到底使用什么样的页表集根本没有关系。为了避免无用的TLB和高速缓存刷新,内核线程使用一组最近运行的普通进程的页表。
进程描述符中的mm字段进程所拥有的内存描述符,mm_active字段指向进程运行时所使用的内存描述符。对于普通进程而言,这两个字段存放相同的值。但是,内核线程mm字段为NULL,当内核线程得以运行时,它的active_mm字段被初始化为前一个运行进程的active_mm值。
只要处于内核态的一个进程为“高端”线性地址修改了页表项,那么,它就也应当更新系统中所有进程页表集中的相应项。事实上,一旦内核态的一个进程进行了设置,那么,映射应该对内核态的其他所有进程都有效。触及所有进程的页表集合是相当费时的操作,因此,linux采用一种延迟的方式。
每当一个高端地址被重新映射时(一般是通过vmalloc()或vfree()),内核就更新被定位在swapper_pg_dir主内核页全局目录中的常规页表集合。这个页全局目录由主内存描述符的pgd字段所指向,而主内存描述符存放于init_mm变量。
3 、线性区
每个线性区描述符表示一个线性地址区间。vm_start字段包含区间第一个线性地址,vm_end字段包含区间之外的第一个线性地址。vm_end-vm_start表示线性区长度。vm_mm字段指向拥有这个区间的进程的mm_struct内存描述符。进程所拥有的线性区从来不重叠,并且内核尽力把新分配的线性区与紧邻的现有的线性区进行合并。如果进程的地址空间删除一个线性地址区间,内核就要调整受影响的线性区大小。有些情况下,调整大小迫使一个线性区被分成两个更小的部分。
vm_ops字段指向vm_operations_struct数据结构,该结构中存放的是线性区的方法。
3.1 线性区数据结构
3.2 线性区访问权限
与线性区相关的标志存放在vm_area_struct的vm_flags字段中。与线性区相关的访问权限必须被复制到相应的所有表项中,以便由分页单元直接执行检查。页访问权限表示何种类型的访问应该产生一个缺页异常。然而,并不能把线性区的访问权限直接转换成页保护单位。
为了做到在“写时复制”技术中适当的推迟页框的分配,只要相应的页不是由多个进程所共享,那么这种页框都是写保护的。
4、线性区的处理
4.1查找给定地址的最邻近区:find_vma():
4.2 查找与一个给定的地址区间相重叠的线性区:find_vma_intersection();
4.3 查找一个空闲的地址区间:get_unmapped_area();
4.4 向内存描述符链表中插入一个线性区:insert_vm_struct();
5、分配线性地址区间
6、释放线性地址区间
7、缺页异常处理程序
缺页异常处理程序必须区分以下两种情况:由编程错误引起的异常,以及由引用属于进程地址空间但还尚未分配物理页框的页所引起的异常。
线性区描述符可以让缺页异常处理程序非常有效的完成它的工作。do_page_fault()函数是缺页中断服务程序,它把引起缺页的线性地址与当前进程的线性区相比较,从而能够选择适当的方式处理这个异常。
do_page_fault()函数接收以下参数:
①、pt_regs结构的地址regs,该结构包含当异常发生时的微处理器寄存器的寄存器的值。
②、3位的error_code,当异常发生时由控制单元压入栈。0位表示页是否存在,1位表示异常是由读和执行访问引起还是由写引起,2位表示异常发生时处理器处于内核态还是用户态。
do_page_fault()第一步操作是读取引起缺页的线性地址。当异常发生时,CPU控制单元把这个值存放在cr2寄存器中:
asm("movl %%cr2,%0":"=r" (address));
if(regs->eflags&0x00020200)
local_irq_enable();
tak = current;
①、do_page_fault()首先检查引起缺页的线性地址是否属于地4个GB:
info.si_code = SEGV_MAPERR;
//如果属于第四个GB
if(address>=TASK_SIZE){
if(!(error_code&5))//满足添加的error=010,即表示处理器处于内核态,且访问的页不存在
goto vmalloc_fault;
goto bad_error_nosemaphore;
}
②、然后在线性地址不属于第四个GB的情况下,缺页处理程序检查异常发生时是否内核正在执行一些关键例程或正在运行内核线程:
if(in_atomic()||!tsk->mm)
goto bad_area_nosemaphore;//内核线程不会使用小于TASK_SIZE的地址;中断处理程序等也不会,因为用户态的线性地址可能没有分配对应的页框,重新分配会产生延迟,而这些情况下延迟是禁止的。
③、如果不满足上述判断条件,即异常没有发生在中断、可延迟、临界区或内核线程中。函数再检查进程所拥有的线性区以决定引起缺页的线性地址是否包含在进程的地址空间中,为此,函数必须获得进程的mmap_sem读写信号量:
if(!down_read_trylock(&tsk->mm->mmap_sem)){//无法获得信号量
if((error_code&4)=0)/**error_code最高位为0,即处于内核态*/&&!search_exception_table(regs->eip)/**是否正在使用作为系统调用参数传递给内核的线性地址*/)
goto bad_area_nosemaphore;
down_read(&tsk->mm->mmap_sem);//获取信号量,可能会等待
}
④、函数能执行到此,应该已经获取了mmap_sem信号量。do_page_fault()开始搜索错误线性地址所在的线性区:
vma = find_vma(tsk->mm,address);//找到在mm中address之后的第一个线性区
if(!vma)//说明address之后没有线性区,这个地址肯定是无效的
goto bad_area;
if(vma->vm_start<=address)//address之后的第一个线性区包含address
goto good_area;
⑤、如果以上都不满足,函数确定address没有包含在任何线性区中。函数还必须执行进一步的检查,由于这个错误地址可能是由push或pusha指令在进程的用户态堆栈上的操作引起的。
栈是如何映射到线性区上的:每个向低地址扩展的栈所在的区,它的VM_GROWSDOWN标志被设置,这样,当vm_start字段的值可能被减小的时候,而vm_end字段的值保持不变。这种线性区的边界包括但不严格限定用户态堆栈当前的大小。这种细微的差别基于以下原因:
线性区的大小是4KB的倍数,而栈的大小是任意的;分配给一个线性区的页框在这个线性区被删除之前用于不能释放。尤其是,一个栈所在线性区的vm_start字段的值只能减小,永远也不能增加。
所以,当进程填满分配给它的堆栈的最后一个页框后,进程如何引起一个缺页异常——push引用了线性区以外的一个地址。
if(!(vma->vm_flags&VM_GROWSDOWN))
goto bad_area;
if((error_code&4)&&address+32<regs->esp)//检查address是否小于regs->esp栈指针,判断address是否在栈中:如果堆栈填满,则address应该不在栈中。因为几个与栈相关的汇编指令只有在访问内存之后才执行减esp寄存器的操作,所以允许进程有32字节的后备区间。如果是内核访问用户态的栈,则不需要检查address是否小于regs->esp栈指针,因为内核是可信任的。
goto bad_area;
if(expand_stack(vma,address))
goto bad_area;
goto good_area;
7.1 处理地址空间以外的错误地址
如果address不属于进程的地址空间,那么do_page_fault()函数继续执行bad_area标记处的语句。如果错误发生在用户态,则发送一个SIGSEGV信号给current进程,并结束函数。
如果错误发生在内核态则说明:异常的引起是由于把某个线性地址作为系统调用的参数传递给内核或者异常是由一个真正的内核缺陷引起。
no_context:
if((fixup=search_exception_table(regs->eip))!=0){
regs->eip = fixup;//代码跳到fixup指定的处理函数中,典型操作是向当前进程发送SIGSEGV信号,或用一个适当的错误码终止系统调用处理程序。
return;
}
如果是因为内核缺陷,函数把CPU寄存器和内核态堆栈的全部转储打印到控制台,并输出到一个系统消息缓冲区,然后调用do_exit()杀死当前进程。
7.2 处理地址空间内的错误地址
如果addr地址属于进程的地址空间,则do_page_fault()转到good_area标记处执行:
good_area:
info.si_code = SEGV_ACCERR;
write = 0;
if(error_code&2){//写
if(!(vma->vm_flags&VM_WRITE))
goto bad_area;
wirte++;
}else
if((error_code&1)||!(vma->vm_flags&(VM_READ|VM_EXEC)))
goto bad_area;
如果这个线性区的访问权限与引起异常的访问类型相匹配,则调用handle_mm_fault()函数分配一个新的页框:
survive:
ret = handle_mm_fault(tsk->mm,vma,address,write);
if(ret==VM_FAULT_MINOR||ret==VM_FAULT_MAJOR){//次缺页或主缺页
if(ret==VM_FAULT_MINOR) tsk->min_flt++;
else tsk->maj_flt++;
up_read(&tsk->mm->mmap_sem);
return;
}
if(ret==VM_FAULT_SIGBUS)
//向进程发送SIGBUS信号
if(ret==VM_FAULT_OOM)
//执行out_of_memory(),内核通常杀死当前进程
handle_mm_fault()函数分析:
参数:mm异常发生时正在CPU上运行的进程的内存描述符
vma指向引起异常的线性地址所在线性区的描述符
address引起异常地址
write_access 是否是试图写
这个函数首先检查用来映射address的页中间目录和页表是否存在。即使address属于进程地址空间,相应的页表也可能还没有被分配。
pgd=pgd_offset(mm,address);
spin_lock(&mm->page_table_lock);
pud = pud_alloc(mm,pgd,address);
if(pud)
pmd = pmd_alloc(mm,pud,address);
if(pmd){
pte = pte_alloc_map(mm,pmd,address);
if(pte)
return handle_pte_fault(mm,vma,address,write_access,pte,pmd);
}
}
spin_unlock(&mm->page_table_lock);
return VM_FALUT_OOM;
handle_pte_fault()函数检查address地址所对应的页表项,并决定是通过请求调页还是写时复制来分配一个新的页框。
7.3 请求调页
请求调页技术背后原因:进程开始运行的时候并不访问其地址空间中的全部地址;事实上,有一部分地址也许永远不被进程使用。此外,程序的局部性原理保证了再程序执行的每个阶段,真正引用的进程页只有一小部分,因此临时用不着的页所在的页框可以由其他进程来使用。
被访问的页可能不在主存中,其原因或是进程从没有访问过该页,或者是内核已经回收了相应的页框。在这两种情况下,缺页处理程序必须为进程分配新的页框。
当页从未被访问或页线性地址映射磁盘文件时则调用do_no_page()函数。该函数通过检查vma线性区描述符的nopage字段来确定这个页是否映射到一个磁盘文件,如果页被映射到一个文件,nopage字段就指向一个函数,该函数把所缺页从磁盘装入RAM。
当nopage==NULL时,do_no_page()调用do_anonymous_page()函数获得一个新的页框:
if(write_access)
pte_unmap(page_table);
spin_unlock(&mm->page_table_lock);
page = alloc_page(GFP_HIGHUSER|__GFP_ZERO);
spin_lock(&mm->page_table_lock);
page_table = pte_offset_map(pmd,addr)
mm->rss++;
entry = maybe_mkwrite(pte_mkdirty(mk_pte(page,vma->vm_page_prot)),vma);
lru_cache_add_active(page);
SetPageReferenced(page);
set_pte(page_table,entry);
pte_unmap(page_table);
spin_unlock(&mm->page_table_lock);
return VM_FAULT_MINOR;
}
当处理读访问时,页的内容是无关紧要的,因为进程第一次对他访问。给进程一个填充为0的页更安全。linux给其分配一个现有的0页,零页在内核初始化是被静态分配。由于这个页标记为不可写,因此如果进程试图写这个页,则写时复制机制被激活。
7.4 写时复制
第一代Unix系统实现了一种傻瓜式的进程创建:当发出fork()系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程。这种行为非常耗时,因为需要:
①、为子进程的页表分配页框
②、为子进程的页分配页框
③、初始化子进程页表
④、把父进程的页复制到子进程相对应的页中
这种创建地址空间的方法涉及许多内存访问,消耗许多内存周期,并且完全破坏了高速缓存中的内存。在大多数情况下,这样做常常是毫无意义的,因为许多进程通过装入一个新的程序开始他们的执行,这样就完全丢弃了所继承的地址空间。
现在的Unix内核采用一种更为有效的方法,称之为写时复制(COW):父进程和子进程共享页框而不是复制页框。然而,只要页框被共享,被共享的页框就不能修改。无论父进程还是子进程何时试图写一个共享的页框,就产生一个异常,这时内核就把这个页复制到一个新的页框中并标记为可写。原来的页框仍然是写保护的:当其他进程试图写入时,内核检查写进程是否是这个页框的唯一主人,如果是,就把这个页框标记为对这个进程是可写的。
页描述符的_count字段用于跟踪共享相应页框的进程数目。只要进程释放一个页框或者在他上面执行写时复制,它的_count字段就减少;只有当_count变为-1时,这个页框才被释放。
当handle_pte_fault()确定缺页异常是由访问内存中现有的一个页而引起时,它执行以下指令:
if(pte_present(entry)){
if(write_access){
if(!pte_write(entry))
return do_wp_page(mm,vma,address,pte,pmd,entry);
entry = pte_mkdirty(entry);
}
entry = pte_mkyong(entry);
set_pte(pte,entry);
flush_tlb_page(vma,address);
pte_unmap(pte);
spin_unlock(&mm->page_table_lock);
return VM_FAULT_MINOR;
}
如果访问权限是写允许的而页框是写保护的,总是需要调用do_wp_page()函数。
do_wp_page()函数首先获取与缺页异常相关的页框描述符(缺页表项对应的页框)。接下来,函数确定页的复制是否真正需要。如果仅有一个进程拥有这个页,那么,写时复制就不必应用,且该进程应当*的写该页。当写时复制不进行时,就把该页框标记为可写,以免试图写时引起进一步的缺页异常:
set_pte(page_table,maybe_mkwrite(pte_mkyong(pte_mkdirty(pte)),vma));
flush_tlb_page(vma,address);
pte_unmap(page_table);
spin_unlock(&mm->page_table_lock);
return VM_FALUT_MINOR;
如果两个或多个进程通过写时复制共享页框,那么函数就把旧页框(old_page)的内容复制到新分配的页框(new_page)中。为了避免竞争条件,在开始复制操作前调用get_page()把old_page的使用计数加1:
old_page = pte_page(pte);
pte_unmap(page_table);
get_page(old_page);
spin_unlock(&mm->page_table_lock);
if(old_page==virt_to_page(empty_zero_page))
new_page = alloc_page(GFP_HIGHUSER | __GFP_ZERO);
}else{
new_page = alloc_page(GFP_HIGHUSER);
vfrom = kmap_atomic(old_page,KM_USER0);//建立临时内核映射
vto = kmap_atomic(new_page,KM_USER1);
copy_page(vto,vfrom);
kunmap_atomic(vfrom,KM_USER0);//撤销临时内核映射
kunmap_atomic(vto,KM_USER0);
}
如果旧页框是零页,就在分配新的页框时把新页框填充为0.否则使用copy_page()宏复制页框的内容。
因为页框的分配可能阻塞进程,因此,函数检查自从函数开始执行以来是否已经修改了页表项(pte和*page_table具有不同的值)。如果是,新的页框被释放,old_page的使用计数-1(取消为避免竞争而做的+1),函数结束。
然后,新的页框的物理地址最终被写进页表项,且使相应的TLB寄存器无效:
spin_lock(&mm->page_table_lock);
entry = maybe_mkwrite(pte_mkdirty(mk_pte(new_page,vma->vm_page_prot)),vma);
set_pte(page_table,entry);
flush_tlb_page(vma,address);//刷新指定进程中单个页表项相关的TLB表项
lru_cache_add_active(new_page);//把页框插入到欲交换相关的数据结构中
pte_unmap(page_table);
spin_unlock(&mm->page_table_lock);
最后,do_wp_page()把old_page的使用计数器减少两次。第一次减少是取消复制页框内容之前进行的安全性增加;第二次的减少是反映当前进程不在拥有页框这一事实。
7.5 处理非连续内存区访问
内核在更新非连续内存区对应的页表项时是非常懒惰的。事实上,vmalloc()和vfree()函数只是把自己限制在更新主内核页表。
一旦内核初始化阶段结束,任何进程或内核进程便都不直接使用主内核页表。因此,内核态进程对非连续内存区第一次访问时,当把线性地址转换为物理地址时,CPU的内存管理单元遇到空的页表项并产生一个缺页。缺页异常处理程序认识这种特殊情况,因为异常发生在内核态且产生缺页的线性得之大于TASK_SIZE。因此,do_page_fault()检查相应的主内核页表项:
vmalloc_fault:
asm("movl %%cr3,%0":"=r"(pgd_paddr));//将当前进程页全局目录的物理地址赋给局部变量pgd_paddr
pgd = pgd_index(address) +(pgd_t *)__va(pgd_paddr);//把当前进程页全局目的线性地址赋值给pgd
pgd_k = init_mm.pgd + pgd_index(address); //主内核页全局目录线性地址赋值给pgd_k
if(!pgd_present(*pgd_k))
goto no_context;
pud = pud_offset(pgd,address);
pud_k = pud_offset(pgd_k,address);
if(!pud_present(*pud_k))
goto no_context;
pmd = pmd_offset(pud,address);
pmd_k = pmd_offset(pud_k,address);
if(!pmd_present(*pmd_k))
goto no_context;
set_pmd(pmd,*pmd_k);
pte_k = pte_offset_kernel(pmd_k,address);
if(!pte_present(*pte_k))
goto no_context;
return;
如果产生缺页的线性地址所对应的主内核页全局目录为空,goto no_context;否则,函数检查与错误线性地址相对应的主内核页上级目录和主内核页中间目录是否为空,goto no_context;否则,就把主内核目录项赋值到进程页中间目录的相应项中。
8、创建和删除进程的地址空间
当创建一个新的进程时内核调用copy_mm()函数。这个函数通过建立新进程的所有页表和内存描述符来创建进程的地址空间。
通常,每个进程都有自己的地址空间,但是轻量级进程可以通过调用clone()函数(设置了VM_CLONE)来创建。这些轻量级进程共享同一地址空间,也就是说,允许他们对同一组页进行寻址。
Linux实现轻量级进程很简单,即不复制父进程的地址空间。创建轻量级的进程比创建普通进程相应要快得多,而且只要父进程和子进程谨慎协调他们的访问,就可以认为页的共享是有益的。
如果通过clone()系统调用已经创建了新进程,VM_CLONE被设置,则copy_mm()函数把父进程地址空间直接赋给子进程:
if(clone_flags&CLONE_VM){
atomic_inc(¤t->mm->mm_users);
spin_unlock_wait(¤t->mm->page_table_lock);//如果其他CPU持有进程页表自旋锁,就调用spin_unlock_wait()函数保证在释放锁之前,缺页处理程序不会结束。
tsk->mm = current->mm;
tak->active_mm = current->mm;
return 0;
}
如果没有设置CLONE_VM标志,copy_mm()函数就必须创建一个新的地址空间。这个函数分配一个新的内存描述符,把他的地址存放在新进程描述符的mm字段中,并把current->mm的内容复制到tsk->mm中,然后改变新进程描述符的一些字段:
tsk->mm = kmem_cache_alloc(mm_cachep,SLAB_KERNEL);
memcpy(tsk->mm,current->mm,sizeof(*tsk->mm));
atomic_set(&tsk->mm->mm_users,1);
atomic_set(&tsk->mm->mm_count,1);
init_rwsem(&tsk->mm->mmap_sem);
tsk->mm->core_waiters= 0;
tsk->mm->page_table_lock = SPIN_LOCK_UNLOCKED;
tsk->mm->ioctx_list_lock = RW_LOCK_UNLOCKED;
tsk->mm->ioctx_list=NULL;
tsk->mm->default_kioctx=INIT_KIOCTX(tsk->mm->default_kioctx,*tsk->mm);
tsk->mm->free_area_cache=(TASK_SIZE/3+0xfff)&0xfffff000;
tsk->mm->pgd = pgd_alloc(tsk->mm);
tsk->mm->def_flags=0;
然后调用依赖于体系结构的init_new_context()函数。最后调用dup_map()函数既复制父进程的线性区,也复制父进程的页表。dup_mmap()函数把新内存描述符tsk->mm插入到内存描述符的全局链表中,然后,从current->mm->mmap所指向的线性区开始扫描父进程的线性区链表。它复制遇到的每个vm_area_struct线性区描述符,并把复制品插入到子进程的线性区链表和红黑树中。
在插入一个新的线性区描述符之后,如果需要的话,dup_mmap()立即调用copy_page_range()创建必要的页表来映射这个线性区所包含的一组页,并且初始化新的页表项。尤其是,与私有的、可读写的页对应的任一页框都标记为对父子进程是只读的,以便这种页框能用写时复制机制进行处理。
当进程结束时,内核调用exit_mm()函数释放进程的地址空间:
mm_release(tsk,tsk->mm);
if(!(mm=tsk->mm))
return ;
down_read(&mm->mmap_sem);
如果正在被终止的进程不是内核线程,exit_mm()函数就必须释放内存描述符合所有相关的数据结构。首先,它金叉mm->core_waiters标志是否设置:是则把内存的所有内容卸载到一个转储文件中。为了避免转储文件的混乱,函数利用mm->core_done和mm->core_startup_done补充原语使共享同一个内存描述符mm的轻量级进程的执行串行化。
接下来,函数递增内存描述符的主使用计数器,重新设置进程描述符的mm字段,并使处理器处于懒惰TLB模式:
atomic_inc(&mm->mm_count)
spin_lock(tsk->alloc_lock);
tsk->mm = NULL:
up_read(&mm->map_sem);
enter_lazy_tlb(mm,current);
spin_unlock(tsk->aloc_lock);
mmput(mm);
最后调用mmput()函数释放局部描述符表、线性区描述符和页表。因为exit_mm()已经递增了主使用计数器,所以并不释放内存描述符本身。当要把正在被终止的进程从本地CPU撤销时,将由finish_task_switch()函数释放内存描述符。
9、堆的管理
堆用于满足进程的动态内存请求。内存描述符的start_brk与brk字段分别限定了这个区的开始地址和结束地址。
进程可以使用以下API来请求和释放动态内存:
malloc(size);请求size字节的动态内存。成功,返回第一个字节线性地址
calloc(n,size);请求含有n个大小为size的元素的一个数组。
realloc(ptr,size);改变签名的malloc()和calloc()分配的内存区字段的大小。
free(addr);释放有malloc()或calloc()分配的起始地址为addr的线性区。
brk(addr);直接修改堆的大小。addr指定current->mm->brk的新值,返回线性地址新的结束值。
sbrk(incr);类似brk(),incr参数指定是增加还是减少以字节为单位的堆大小。
brk()首先检查addr参数是否位于进程代码所在的线性区。如果是,立即返回,因为堆不能与代码区重叠:
mm=current->mm;
down_write(&mm->mmap_sem);
if(addr<mm->end_code){
out:
up_write(&mm->mmap_sem);
return mm->brk;
}
由于brk()系统调用作用域某一线性区,它分配和释放完整的页。因此,该函数把addr的值调整为PAGE_SIZE的倍数,然后把调整的结果与内存描述符的brk字段比较:
newbrk = (addr + PAGE_SIZE)&PAGE_MASK;
oldbrk = (mm->brk + PAGE_SIZE)&PAGE_MASK;
if(old==newbrk)
mm->brk = addr;
goto out;
}
如果进程请求缩小堆,则sys_brk()调用do_munmap()函数完成任务。
if(addr<=mm->brk){
if(!do_munmap(mm,newbrk,oldbrk-newbrk))
mm->brk=addr;
goto out;
}
如果进程请求扩大堆,则sys_brk()首先检查是否允许进程这样做。
rlim = current->signal->rlim[RLIMIT_DATA].rlim_cur;
if(rlim<RLIM_INFINITY&&addr-mm->start_data>rlim)
goto out;
然后函数检查扩大后的堆是否和进程的其他线性区重叠,如果是则out
if(find_vma_intersection(mm,oldbrk,newbrk+PAGE_SIZE))
goto out;
如果一切顺利,调用do_brk()函数
if(do_brk(oldbrk,new-oldbrk)==oldbrk)
mm->brk = addr;