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\n\t"  //把prev进程的flag保存到prev进程的内核堆栈中
                      "pushl %%ebp\n\t" //把prev进程的基址ebp保存到prev进程的内核堆栈中

                      "movl %%esp,%[prev_sp]\n\t"//把prev进程的内核栈esp保存到prev->thread.sp中
                      "movl %[next_sp],%%esp\n\t"//esp指向next进程的内核堆栈栈顶(next->thread.sp) 

                      "movl $1f,%[prev_ip]\n\t"//把"1:\t"地址赋给prev->thread.ip,当prev进程下次被switch_to切回来时,从"1:\t"处执行,即往后执行"popl %%ebp\n\t"和"popfl\n"
                      "pushl %[next_ip]\n\t"//把next->thread.ip压入next进程的内核堆栈栈顶
                      __switch_canary
                      "jmp __switch_to\n"//执行__switch_to()函数,完成硬件上下文切换
                      "1:\t"
                      "popl %%ebp\n\t"
                      "popfl\n"                         

                      /* 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:\t"地址(等到prev进程下次被switch_to切回来执行时,从"1:\t"处执行)。将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:\t"地址,即__switch_to函数执行结束返回时弹出的是"1:\t",eip指向"1:\t",执行"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:\t压栈,执行结束后eip指向"1:\t",这只对第一种情况适用,无法满足第二种情况的需要去执行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结构体管理缓存项和页I\O操作。一个文件可以有多个虚拟地址(被多个vm_area_struct标识)但是只能有一个物理地址(address_space数据结构)。
  • 每个address_space对象都有唯一的基树。基树是一个二叉树,只要指定了文件偏移量,就可以在基树中迅速检索到希望的数据。
  • 页高速缓存的数据比后台存储的数据更加新的时候,这些数据就叫脏数据。
  • linux 页高速缓存中的回写是由flusher 线程完成的,flusher线程在以下3种情况发生时触发回写操作。
  • 当空闲内存低于一个阀值时:空闲内存不足时,需要释放一部分缓存,由于只有不脏的页面才能被释放,所以要把脏页面都回写到磁盘,使其变成干净的页面。
  • 当脏页在内存中驻留时间超过一个阀值时:确保脏页面不会无限期的驻留在内存中,从而减少了数据丢失的风险。
  • 当用户进程调用 sync() 和 fsync() 系统调用时:给用户提供一种强制回写的方法,应对回写要求严格的场景。
时间: 2024-08-24 07:21:14

2017-2018-1 20179202《Linux内核原理与分析》第九周作业的相关文章

20169217 linux内核原理与分析 第四周作业

本次作业仍然分为两部分,第一部分为实验. 如实验楼的实验过程所述:使用实验楼的虚拟机打开shell,然后cd mykernel 您可以看到qemu窗口输出的内容的代码mymain.c和myinterrupt.c. 代码如下: cd LinuxKernel/linux-3.9.4  qemu -kernel arch/x86/boot/bzImage 实验截图如下: 通过观察我们会发现my_start_kernel和my_timer_handler快速的交替执行. 接下来查看mymain.c和my

《Linux内核原理与分析》教学进程

目录 2019-2020-1 <Linux内核原理与分析>教学进程 考核方案 第一周: 第二周: 第三周: 第四周: 第五周 第六周 第七周: 第八周 第九周 第十周 第十一周: 第十二周 第十三周 2019-2020-1 <Linux内核原理与分析>教学进程 考核方案 采取过程化考核,平时成绩占100分,成绩计算:30+30+15+25=100: 翻转课堂基础考核10次: 3*10 = 30 每次考试20-30道题目,考试成绩规格化成3分(比如总分30分就除以10) 翻转课堂测试

20169217 《Linux内核原理与分析》 课程总结

博客链接: 第一周作业 摘要:学习了实验楼linux基础入门课程. 第二周作业 摘要:实验楼实验一:反汇编一个简单的程序.书<linux内核设计与实现>:第1章,第2章,第18章内容. 第三周作业 摘要:自己对于为何要学习linux的感想. 第四周作业 摘要:实验二:分析精简内核源代码mymain.c和myinterrupt.c 书上第2章和第5章内容. 第五周作业 摘要:使用gdb跟踪调试内核从start_kernel到init进程启动 书上第4章和第6章内容. 第六周作业 摘要:使用库函数

2017-2018-1 20179202《Linux内核原理与分析》第八周作业

一 .可执行程序的装载 1. 预处理.编译.链接 gcc –e –o hello.cpp hello.c //预处理 gcc -x cpp-output -S -o hello.s hello.cpp //编译 gcc -x assembler -c hello.s -o hello.o-m32 //汇编 gcc -o hello hello.o //链接成可执行文件,使用共享库 用gcc -o hello.static hello.o -static静态编译出来的hello.static把C库

20169217 《Linux内核原理与分析》 第十一周作业

首先更新一下第一次实验关于堆栈的分析,这里我直接手写拍的图片. 这里再补充一点,第5步的call f 实际上等于 pushl %eip,movl f %eip,也就是将f的eip地址压栈,call g原理相同.leave等于movl %ebp,%esp,popl %ebp 也就是将ebp出栈,epb指向自己所存储值的栈,再将ebp的值付给esp,实现栈的释放.   接下来是实验: 2014年9月24日,Bash中发现了一个严重漏洞shellshock,该漏洞可用于许多系统,并且既可以远程也可以在

20169217《Linux内核原理与分析》第二周作业

通过第二周的学习,我想把我的博客分为两部分,第一部分是实验楼linux内核分析实验一的实验报告,第二部分是看书第1,2,18章的内容和时间情况. 现在先说实验一 实验内容:将一段c语言程序反汇编成汇编程序. c语言程序代码:应实验要求我把其中部分数值进行了修改. int g(int x) { return x+6; } int f(int x) { return g(x); } int main(void) { return f(9)+3; } 实验过程: 首先创建一个main.c文件 将刚刚修

2017-2018-1 20179209《Linux内核原理与分析》第八周作业

Linux内核如何装载和启动一个可执行程 一.实验 1.1理解编译链接的过程和ELF可执行文件格式. 1.1.1编译链接过程 能用图说明的问题,就少用文字描述: 1.1.2ELF可执行文件 ELF可执行文件中有三种主要的目标文件: 一个可重定位文件保存着代码和适当的数据,用来和其他的object文件一起创建一个可执行文件或者是一个共享文件.主要是.o文件. 一个可执行文件保存着一个用来执行的程勋:该文件指出了exec如何创建程序进程映像. 一个共享object文件保存着代码和合适地数据,用来被下

2017-2018-1 20179203 《Linux内核原理与分析》第八周作业

攥写人:李鹏举 学号:20179203 ( 原创作品转载请注明出处) ( 学习课程:<Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ) 一.实验要求: 1.理解编译链接的过程和ELF可执行文件格式,详细内容参考本周第一节: 2.编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接,编程练习动态链接库的这两种使用方式,详细内容参考本周第二节: 3.使用gdb跟踪分析一个

2017-2018-1 20179203 《Linux内核原理与分析》第九周作业

攥写人:李鹏举 学号:20179203 ( 原创作品转载请注明出处) ( 学习课程:<Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ) 一.实验要求: 1.理解Linux系统中进程调度的时机,可以在内核代码中搜索schedule()函数,看都是哪里调用了schedule(),判断我们课程内容中的总结是否准确: 2.使用gdb跟踪分析一个schedule()函数 ,验证您对Linux系统进程调度与进程切换过程的理