技术改变命运
分类: LINUX
2016-08-24 10:21:28
地址为内核空间:
1,当地址为内核地址空间并且在内核中访问时,如果是非连续内存地址,将init_mm中对应的项复制到本进程对应的页表项做修正;
2,地址为内核空间时,检查页表的访问权限;
3,如果1,2没搞定,跳到非法访问处理(在后面详细分析这个);
地址为用户空间:
4,如果使用了保留位,打印信息,杀死当前进程;
5,如果在中断上下文中火临界区中时,直接跳到非法访问;
6,如果出错在内核空间中,查看异常表,进行相应的处理;
7,查找地址对应的vma,如果找不到,直接跳到非法访问处,如果找到正常,跳到good_area;
8,如果vma->start_address>address,可能是栈太小,对齐进行扩展;
9,good_area处,再次检查权限;
10,权限正确后分配新页框,页表等;
简单来说,讨论linux页表就是讨论linux进程的的页表:linux页表的创建与更新都包含于进程的创建与更新中。当前的linux内核采用的是写时复制方法,在创建一个linux进程时,完全复制父进程的页表,并且将父子进程的页表均置为写保护(即写地址的时候会产生缺页异常等)。那么父子进程谁向地址空间写数据时,产生缺页异常,分配新的页,并将两个页均置为可写,按照这种方式父子进程的地址空间渐渐变得不同。
按照上面的分析, 只需要讨论第一个进程页表初始化,进程创建时页表的拷贝,以及缺页异常时页表的更新即可。
1.init_task进程页表的初始化
init_task的地址空间是init_mm, init_mm在内核初始化的时候就赋值给了current->active_mm. init_mm的初始化页表是swapper_pg_dir,在mips架构中swapper_pg_dir初始化在函数pagetable_init中,初始化关系是
swapper_pg_dir -> invalide_pmd_table -> invalide_pte_table 或
swapper_pg_dir -> invalide_pte_table.
即在init_mm中,页表指向的全部是invalide_pte_table。
2.创建进程时页表的拷贝
进程创建一般调用的是do_fork函数,按照如下调用关系:
do_fork->copy_process->copy_mm->dup_mm->dup_mmap->copy_page_range
找到copy_page_range函数,这个函数便是负责页表的拷贝,函数核心代码如下:
874 do {
875 next = pgd_addr_end(addr, end);
876 if (pgd_none_or_clear_bad(src_pgd))
877 continue;
878 if (unlikely(copy_pud_range(dst_mm, src_mm, dst_pgd, src_pgd,
879 vma, addr, next))) {
880 ret = -ENOMEM;
881 break;
882 }
883 } while (dst_pgd++, src_pgd++, addr = next, addr != end);
copy_pud_range便是拷贝pud表,copy_pud_range调用copy_pmd_range, copy_pmd_range调用copy_pte_range,以此完成对三级页表的复制。需要注意的是在copy_pte_range调用的copy_one_pte中有如下代码:
694 if (is_cow_mapping(vm_flags)) {
695 ptep_set_wrprotect(src_mm, addr, src_pte);
696 pte = pte_wrprotect(pte);
697 }
这里便是判断如果采用的是写时复制,便将父子页均置为写保护,即会产生如下所示的缺页异常。
3.缺页异常时页表的更新
由页表的初始化可以看到,init_mm的页表全指向无效页表,然而普通的进程中不可能页表均指向无效项,因此肯定拥有一个不断扩充页表的机制,这个机制是通过缺页异常实现的。
以mips为例,mips的缺页异常最终会调用do_page_fault,do_page_fault调用handle_mm_fault,handle_mm_fault是公共代码,一般所有的缺页异常均会调用handle_mm_fault的核心代码如下:
3217 pud = pud_alloc(mm, pgd, address);
3218 if (!pud)
3219 return VM_FAULT_OOM;
3220 pmd = pmd_alloc(mm, pud, address);
3221 if (!pmd)
3222 return VM_FAULT_OOM;
3223 pte = pte_alloc_map(mm, pmd, address);
3224 if (!pte)
3225 return VM_FAULT_OOM;
其中pud_alloc代码如下:
1056 static inline pud_t *pud_alloc(struct mm_struct *mm, pgd_t *pgd, unsigned long address)
1057 {
1058 return (unlikely(pgd_none(*pgd)) && __pud_alloc(mm, pgd, address))?
1059 NULL: pud_offset(pgd, address);
1060 }
其中pgd_none用于判断pgd是否为invalide,如果是可调用__pud_alloc,如果不是获得其地址继续查。
pmd_alloc函数和pte_alloc_map函数类似。
因此可以看出,在缺页异常中,会按照地址一次查三张页表,如果页表为invalide,比如invalide_pmd_table或invalide_pte_table,则会分配一个新的页表项取代invalide的页表项。这便是页表扩充的机制。
需要注意的是handle_mm_fault最终会调用handle_pte_fault,在handle_pte_fault函数中有如下代码:
3171 if (flags & FAULT_FLAG_WRITE) {
3172 if (!pte_write(entry))
3173 return do_wp_page(mm, vma, address,
3174 pte, pmd, ptl, entry);
3175 entry = pte_mkdirty(entry);
3176 }
即在缺页异常中如果遇到写保护会调用do_wp_page,这里面会处理上面所说的写时复制中父子进程区分的问题。
如上三个部分便是linux页表的大体处理框架
三级页表结构示意图[zz]
图3.3 Linux的三级页表结构
Linux总是假定处理器有三级页表。每个页表通过所包含的下级页表的页面框号来访问。图3.3给出了虚拟地址是如何分割成多个域的,每个域提供了 某个指定页表的偏移。为了将虚拟地址转换成物理地址,处理器必须得到每个域的值。这个过程将持续三次直到对应于虚拟地址的物理页面框号被找到。最后再使用 虚拟地址中的最后一个域,得到了页面中数据的地址。
为了实现跨平台运行,Linux提供了一系列转换宏使得核心可以访问特定进程的页表。这样核心无需知道 页表入口的结构以及它们的排列方式。
这种策略相当成功,无论在具有三级页表结构的Alpha AXP还是两级页表的Intel X86处理器中,Linux总是使 用相同的页表操纵代码。
内容:
(1):从schedule()开始,几种不同类型的进程之间的调度选择;在相同类型的进程之间的调度选择算法
(2):从CPU的IP值的变化上,说明在switch_to宏执行后,执行分析
(3):堆栈发生切换位置,在切换堆栈前后,current_thread_info变化
(4):地址空间发生切换,解释地址空间的切换不会影响后续切换代码的执行
(5):current宏所代表的进程发生变化的源码位置
(6):任务状态段中关于内核堆栈的信息发生变化源码位置
1,从schedule()开始,说明几种不同类型的进程之间的调度选择;在相同类型的进程之间的调度选择算法。
在schedule()函数中,
首先禁止抢占,获取当前CPU,该CPU的执行队列,队列上正在执行的进程,以及该进程的交换计数信息并释放该进程占用的锁。之后,对禁止中断,更新运行队列时钟,该队列的自旋时钟加锁,后清除当前进程的thread_flag中TIF_NEED_RESCHED,
如果进程不在可运行状态,并且可被抢占,若进程处于非阻塞挂起,则将其改为可运行,否则调用deactivate_task()函数,并修改上下文交换次数。其中在deactive_task()函数中调用了denqueue_task()函数:
P进程调用属于自己调度类的dequeue_task()方法,将p从当前rp运行队列上移出。例如对于公平调度队列中的进程调用以下函数:
对p的所有实体除含有子实体的父进程外,从公平队列中移除。
如果运行队列上进程数是0,则先通过idle_balance函数从其他CPU上调度,进行负载均衡。
对当前运行的进程prev,通过调用它所属的类的put_prev_task方法,将当前进程放入运行队列的合适位置。下图展示过程,图为公平调度类的调度方法,之后对实时调度方法的说明(idle类方法为空):
与方法put_prev_entity()方法,将当前进程加入公平调度队列。因为如果该类是公平调度类,则调度一定会在公平调度队列中有一位置,更新当前实例的状态,并入队:
其中入队位置有该值决定entity_key:
对于采用实时调度的类,调用update_curr_rt函数,并置当前进程执行开始时间是0
下图为update_curr_tr函数,
计算delta_exec值为运行队列现在的时钟值与当前进程开始值。更改当前进程的状态,修改当前实时进程的总运行时间与开始时间,对实时调度队列中的实例更新时间。
之后,在运行队列上选择下一个进程中pick_next_task函数。
对于运行队列,如果队列中进程数与公平调度队列中的进程数相同,即没有实时进程时则在公平调度队列中选择进程:
对调度类中具有最高优先级的类赋值给class,调用该类的pick_next_task方法,根据不同调度类又分为:
对于实时进程则:
对于一个实例,如果它不在实时队列组中,则返回拥有这个实例的task_struct结构为next进程并修改执行开始时间为运行队列当时钟前值。
对于公平调度进程则:
返回公平调度队列上选择不在公平调度组中的task_struct。
对于idle,返回队列中的idle task_struct结构,在调度过程中,永远不会返回NULL,因为至少有idle进程的存在。
在队列中从不同类中,选择出了将要被调度的类后,如果选择的进程next与prev不同则,进行进程的上下文切换:
修改交换次数,将next至为当前进程,进行切换。
2,执行完switch_to后,又执行了battier函数,之后又执行finish_task_switch函数
另:
struct task_struct *__switch_to(struct task_struct *prev,struct task_struct *next);
将next->thread.esp中的数据存入esp寄存器中
在switch_to宏执行后,执行ret_from_fork()函数。
执行完这个函数之后,执行include/asm-x86/system.h 下的__switch_to函数()
再执行__unlazy_fpu()函数。
3,堆栈发生切换位置,在切换堆栈前后,current_thread_info变化
对于切换堆栈,在switch_to中查找修改堆栈指针代码即可即:
图中movel %[next_sp],%%esp 即为修改堆栈指针,指向next进程的堆栈。因为在内核态中,栈顶指针减去8K偏移(两页)便可得到thread_info位置,从而,在切换后current_thread_info内容为切换后的新进程的thread_info内容。
4,地址空间发生切换,解释地址空间的切换不会影响后续切换代码的执行
切换地址空间在context_switch函数的switch_mm方法,在switch_mm中,重新加载页表即修改cr3寄存器的值:
切换地址空间发生在切换堆栈之前,不会影响后续代码执行,因为进程的切换发生在内核态,内核态地址空间是共用的。没有修改堆栈指针及其他寄存器的值,即堆栈没有变,栈内值未发生改变。
5,current宏所代表的进程发生变化的源码位置
修改该CPU的current_task为next_p,即current宏发生了改变。
6,任务状态段中关于内核堆栈的信息发生变化源码位置
Tss段在_switch_to中被声明,并被赋值:
其中,esp0即为内核堆栈栈底指针