zoukankan      html  css  js  c++  java
  • 2017-2018-1 20179202《Linux内核原理与分析》第九周作业

    进程的切换和系统的一般执行过程

    1.知识总结

    (1)进程调度的时机:

    • 中断处理过程直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule()。
    • 内核线程是一个特殊的进程,只有内核态没有用户态,可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度(内核线程可以直接访问内核函数,所以不会发生系统调用)。内核线程作为一类的特殊的进程可以主动调度,也可以被动调度。
    • 用户态进程无法实现主动调度,仅能在中断处理过程中进行调度(schedule是一个内核函数,不是一个系统调用)。

    (2)挂起正在CPU上执行的进程,与中断时保存现场不同。中断前后是在同一个进程上下文中,只是由用户态转向内核态执行。进程上下文包含了进程执行需要的所有信息:

    • 用户地址空间:包括程序代码,数据,用户堆栈等
    • 控制信息:进程描述符,内核堆栈等
    • 硬件上下文

    (3)schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,context_switch中的一个关键宏switch_to来进行关键上下文切换。

    (4)0到3G用户可以访问,3G以上只有内核态可以访问。所有进程3G以上都是完全共享的,比如进程X切换到进程Y,但是地址空间仍然是3G以上的部分,只是把进程描述符和其他的进程上下文切换了,只有在返回的时候才不同。哪一个进程都可以“招手”进入内核态,走了一段以后便可以返回到用户态,空车的时候就进入idle进程空转。

    2.关键代码分析

    (1)schedule

    asmlinkage__visible void __sched schedule(void)  
    {  
             struct task_struct *tsk = current;  
       
             sched_submit_work(tsk);  
             __schedule();  
    }  
    

    schedule()的尾部调用了__schedule(),__schedule()的关键代码next = pick_next_task(rq, prev);封装了进程调度算法,使用某种进程调度策略选择下一个进程。得到调度策略后用context_switch(rq, prev, next);实现进程上下文的切换。其中最关键的switch_to(prev,next, prev); 切换堆栈和寄存器的状态。

    (2)switch_to

    #define switch_to(prev, next, last) //prev指向当前进程,next指向被调度的进程                                   
    do {                                                                              
                                                            
             unsigned long ebx, ecx, edx, esi, edi;
                                      
             asm volatile("pushfl
    	"  //把prev进程的flag保存到prev进程的内核堆栈中
                          "pushl %%ebp
    	" //把prev进程的基址ebp保存到prev进程的内核堆栈中
               
                          "movl %%esp,%[prev_sp]
    	"//把prev进程的内核栈esp保存到prev->thread.sp中
                          "movl %[next_sp],%%esp
    	"//esp指向next进程的内核堆栈栈顶(next->thread.sp) 
                          
                          "movl $1f,%[prev_ip]
    	"//把"1:	"地址赋给prev->thread.ip,当prev进程下次被switch_to切回来时,从"1:	"处执行,即往后执行"popl %%ebp
    	"和"popfl
    "     
                          "pushl %[next_ip]
    	"//把next->thread.ip压入next进程的内核堆栈栈顶 
                          __switch_canary                                        
                          "jmp __switch_to
    "//执行__switch_to()函数,完成硬件上下文切换  
                          "1:	"                                                    
                          "popl %%ebp
    	"   
                          "popfl
    "                         
                                                                                    
                          /* output parameters */                                   
                          : [prev_sp] "=m"(prev->thread.sp),             
                            [prev_ip] "=m"(prev->thread.ip),              
                            "=a" (last),                                                
                                                                                       
                          /* clobbered output registers: */              
                            "=b" (ebx), "=c"(ecx), "=d" (edx),              
                            "=S" (esi), "=D"(edi)                            
                                                                                        
                           __switch_canary_oparam                                     
                                                                                        
                              /* input parameters: */                                  
                          : [next_sp]  "m" (next->thread.sp),              
                            [next_ip]  "m" (next->thread.ip),              
                                                                                       
                          /* regparm parameters for __switch_to():*/  
                          //jmp通过eax寄存器和edx寄存器传递参数
                            [prev]     "a" (prev),                                   
                            [next]     "d" (next)                                    
                                                                                         
                            __switch_canary_iparam                             
                                                                                        
                          : /* reloaded segment registers */                          
                         "memory");                                           
    } while (0)  
    
    

    [prev_sp] "=m"(prev->thread.sp),之前分析汇编的时候,看到的是使用标号(%0、%1、%2等)标记参数,为了更好的可读性,这里用字符串([prev_sp])来标记参数(prev->thread.sp)。

    首先保存prev进程的flags,ebp,用"movl %%esp,%[prev_sp]""movl %[next_sp],%%esp"完成内核堆栈的切换,使esp指向next进程的内核堆栈栈顶,然后把prev进程的thread.ip设置为"1: "地址(等到prev进程下次被switch_to切回来执行时,从"1: "处执行)。将next->thread.ip保存到next进程的内核堆栈栈顶,接下来执行jmp __switch_to(注意这里用的是jmp而不是call)完成硬件上下文切换,执行结束返回时弹出next进程内核堆栈的栈顶保存的next->thread.ip,eip指向此位置。分两种情况讨论一下:

    • 如果next进程之前被switch_to切出去过(可以理解为它之前也做过prev进程),next进程的内核堆栈上有被切出去是保存的的ebp和flags。由于执行过movl $1f,%[prev_ip],所以next->thread.ip是"1: "地址,即__switch_to函数执行结束返回时弹出的是"1: ",eip指向"1: ",执行"popl %%ebp""popfl" 恢复next进程的ebp和flag,next进程就可以执行了。
    • 如果next进程之前没有被switch_to出去过,那么next->thread.ip是ret_from_fork。__switch_to函数返回后执行的就是ret_from_fork。

    所以,如果使用call,会把call __switch_to的下一条1: 压栈,执行结束后eip指向"1: ",这只对第一种情况适用,无法满足第二种情况的需要去执行ret_from_fork。

    课本笔记

    • 用户空间中进程的内存叫做进程地址空间,也就是系统中每个用户空间进程所看到的内存。进程地址空间由可寻址的虚拟内存组成。
    • 内核使用内存描述符mm_struct结构体表示进程的地址空间。每个内存描述符都对应于进程地址空间中的唯一区间。所有的mm_struct结构体都通过自身的mmlist域链接在一个双向链表中。链表首元素是init_mm内存描述符,代表init进程的地址空间。
    • task_struct进程描述符中,mm域存放该进程使用的内存描述符。
    • 内核线程没有进程地址空间,也没有相关的内存描述符,内核线程对应的进程描述符中mm域也为空。内核线程直接使用前一个进程的内存描述符。
    • mm_struct中有vm_area_struct结构体,内存区域由它描述。内存区域在Linux内核中也被称作虚拟内存区域(VMAS)。vm_area_struct描述了指定地址空间内连续区间上的一个独立内存范围。内核将每个内存区域作为一个单独的内存对象管理,每个内存区域都拥有一致的属性。
    • vm_area_struct结构体中的vm_ops域指向域指定内存区域相关的操作函数表,内核使用表中的方法操作VMA。
    • 内核时常需要在某个内存区域上执行一些操作。find_vma在指定的地址空间中搜索一个vm_end大于addr的内存区域。find_vma_prev()和find_vma()工作方式相同,但返回的是第一个小于addr的VMA。find_vma_intersection()返回第一个和指定地址区间相交的VMA。
    • do_mmap()创建一个新的线性地址空间,即将一个地址区间加入到进程的地址空间中。do_munmap()函数从特定的进程地址空间中删除指定地址空间。
    • 地址转换(虚拟到物理)需要将虚拟地址分段,使每段虚地址都作为一个索引指向页表。页表项指向下一级别的页表或者指向最终的物理页面。linux中使用三级页表完成地址转换(顶级页表是页全局目录PGD,二级页表是中间页目录PMD,最后一级简称页表)。内存描述符的pgd域指向进程的页全局目录。

    • 翻译缓冲器(TLB)作为一个将虚拟地址映射到物理地址的硬件缓存,当请求访问一个虚拟地址时,处理器将首先检查TLB中是否缓存了该虚拟地址到物理地址的映射,如果找到了,物理地址就立刻返回,否则,就需要再通过页表搜索需要的物理地址。
    • 为了减少对磁盘I/O的操作,提高系统性能,Linux内核实现磁盘缓存的技术叫页高速缓存。即把磁盘中的数据缓存到物理内存中,把对磁盘的访问转换为对物理内存的访问。
    • 页高速缓存大小能动态调整。页高速缓存主要有读缓存、写缓存、缓存回收3种机制来保证读、写缓存以及释放缓存。
    • 页高速缓存的核心数据结构是address_space对象,它是一个嵌入在页所有者的索引节点对象中的数据结构。使用address_space结构体管理缓存项和页IO操作。一个文件可以有多个虚拟地址(被多个vm_area_struct标识)但是只能有一个物理地址(address_space数据结构)。
    • 每个address_space对象都有唯一的基树。基树是一个二叉树,只要指定了文件偏移量,就可以在基树中迅速检索到希望的数据。
    • 页高速缓存的数据比后台存储的数据更加新的时候,这些数据就叫脏数据。
    • linux 页高速缓存中的回写是由flusher 线程完成的,flusher线程在以下3种情况发生时触发回写操作。
      • 当空闲内存低于一个阀值时:空闲内存不足时,需要释放一部分缓存,由于只有不脏的页面才能被释放,所以要把脏页面都回写到磁盘,使其变成干净的页面。

      • 当脏页在内存中驻留时间超过一个阀值时:确保脏页面不会无限期的驻留在内存中,从而减少了数据丢失的风险。

      • 当用户进程调用 sync() 和 fsync() 系统调用时:给用户提供一种强制回写的方法,应对回写要求严格的场景。

  • 相关阅读:
    第4月第1天 makefile automake
    第3月30天 UIImage imageWithContentsOfFile卡顿 Can't add self as subview MPMoviePlayerControlle rcrash
    第3月第27天 uitableviewcell复用
    learning uboot fstype command
    learning uboot part command
    linux command dialog
    linux command curl and sha256sum implement download verification package
    learning shell script prompt to run with superuser privileges (4)
    learning shell get script absolute path (3)
    learning shell args handing key=value example (2)
  • 原文地址:https://www.cnblogs.com/Jspo/p/7896370.html
Copyright © 2011-2022 走看看