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

实验八 理理解进程调度时机跟踪分析进程调度与进程切换的过程

进程调度

进度调度时机:

1、中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();

2、内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;

3、用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。

进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换(process switch)、任务切换(task switch)或上下文切换(context switch)。

挂起正在CPU上执行的进程,与中断时保存现场是不同的,中断前后是在同一个进程上下文中,只是由用户态转向内核态执行。

进程上下文

包含了进程执行需要的所有信息,包括:

1、用户地址空间:包括程序代码,数据,用户堆栈等

2、控制信息:进程描述符,内核堆栈等

3、硬件上下文(与中断保存硬件上下文的方法不同)

最一般的情况

正在运行的用户态进程X切换到运行用户态进程Y的过程

  • 正在运行的用户态进程X
  • 发生中断——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).SAVE_ALL //保存现场
  • 中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换
  • 标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)
  • restore_all //恢复现场
  • iret - pop cs:eip/ss:esp/eflags from kernel stack
  • 继续运行用户态进程Y

几种特殊情况

  • 通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;
  • 内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;
  • 创建子进程的系统调用在子进程中的执行起点及返回用户态,如fork;
  • 加载一个新的可执行程序后返回到用户态的情况,如execve;

schedule()函数

schedule()函数代码:

  1. static void __sched __schedule(void)
  2. {
  3. struct task_struct *prev, *next;
  4. unsigned long *switch_count;
  5. struct rq *rq;
  6. int cpu;
  7. need_resched:
  8. preempt_disable();
  9. cpu = smp_processor_id();
  10. rq = cpu_rq(cpu);
  11. rcu_note_context_switch(cpu);
  12. prev = rq->curr;
  13. schedule_debug(prev);
  14. if (sched_feat(HRTICK))
  15. hrtick_clear(rq);
  16. /*
  17. * Make sure that signal_pending_state()->signal_pending() below
  18. * can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)
  19. * done by the caller to avoid the race with signal_wake_up().
  20. */
  21. smp_mb__before_spinlock();
  22. raw_spin_lock_irq(&rq->lock);
  23. switch_count = &prev->nivcsw;
  24. if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
  25. if (unlikely(signal_pending_state(prev->state, prev))) {
  26. prev->state = TASK_RUNNING;
  27. } else {
  28. deactivate_task(rq, prev, DEQUEUE_SLEEP);
  29. prev->on_rq = 0;
  30. /*
  31. * If a worker went to sleep, notify and ask workqueue
  32. * whether it wants to wake up a task to maintain
  33. * concurrency.
  34. */
  35. if (prev->flags & PF_WQ_WORKER) {
  36. struct task_struct *to_wakeup;
  37. to_wakeup = wq_worker_sleeping(prev, cpu);
  38. if (to_wakeup)
  39. try_to_wake_up_local(to_wakeup);
  40. }
  41. }
  42. switch_count = &prev->nvcsw;
  43. }
  44. if (task_on_rq_queued(prev) || rq->skip_clock_update < 0)
  45. update_rq_clock(rq);
  46. next = pick_next_task(rq, prev);
  47. clear_tsk_need_resched(prev);
  48. clear_preempt_need_resched();
  49. rq->skip_clock_update = 0;
  50. if (likely(prev != next)) {
  51. rq->nr_switches++;
  52. rq->curr = next;
  53. ++*switch_count;
  54. context_switch(rq, prev, next); /* unlocks the rq */
  55. /*
  56. * The context switch have flipped the stack from under us
  57. * and restored the local variables which were saved when
  58. * this task called schedule() in the past. prev == current
  59. * is still correct, but it can be moved to another cpu/rq.
  60. */
  61. cpu = smp_processor_id();
  62. rq = cpu_rq(cpu);
  63. } else
  64. raw_spin_unlock_irq(&rq->lock);
  65. post_schedule(rq);
  66. sched_preempt_enable_no_resched();
  67. if (need_resched())
  68. goto need_resched;
  69. }

gdbb跟踪分析

和前几次实验一样编译内核,打开gdb:

使用gdb跟踪schedule()函数,设置断点:

(schedule(),context switch(),pick_next_task(),switch_to)



其中switch_to是宏定义,不能设置断点。

schedule()断点:



寻找下一个进程及判断上下文切换:

schedule 函数

  • 针对抢占的处理
  • raw_spin_lock_irq(&rq->lock);
  • 检查prev的状态,并且重设state的状态
  • next = pick_next_task(rq, prev); ////进程调度算法都封装这个函数内部
  • 更新就绪队列的时钟
  • context_switch(rq, prev, next); //进程上下文切换

schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换switch_to利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程

switch_to中的汇编代码

  1. #define switch_to(prev, next, last)
  2. do {
  3. unsigned long ebx, ecx, edx, esi, edi;
  4. asm volatile("pushfl\n\t" /* 保存当前进程的flags */
  5. "pushl %%ebp\n\t" /* 把当前进程的当前的ebp压入当前进程的栈 */
  6. "movl %%esp,%[prev_sp]\n\t" /*保存当前的esp到prev->thread.sp指向的内存中 */
  7. "movl %[next_sp],%%esp\n\t" /* 重置esp,把下个进程的next->thread.sp赋予esp */
  8. "movl $1f,%[prev_ip]\n\t" /*把1f放到[prev_ip]里,保存当前进程的EIP,当恢复prev进程时可从这里恢复*/
  9. "pushl %[next_ip]\n\t" /把next进程的起点,即ip的位置压到堆栈中,next_ip一般是$1f*/
  10. __switch_canary
  11. "jmp __switch_to\n"
  12. "1:\t"
  13. "popl %%ebp\n\t" /* 重置ebp */
  14. "popfl\n" /* 重置flags*/
  15. : [prev_sp] "=m" (prev->thread.sp),
  16. /* =m 表示把变量放入内存,即把 [prev_sp] 存储的变量放入内存,最后再写入prev->thread.sp */
  17. [prev_ip] "=m" (prev->thread.ip),
  18. "=a" (last),
  19. /*=a 表示把变量 last 放入 ax, eax = last */
  20. "=b" (ebx), "=c" (ecx), "=d" (edx),
  21. /* b 表示放入ebx, c 表示放入 ecx,d 表示放入 edx, S表示放入 si, D 表示放入 edi */
  22. "=S" (esi), "=D" (edi)
  23. __switch_canary_oparam
  24. : [next_sp] "m" (next->thread.sp), /* next->thread.sp 放入内存中的 [next_sp] */
  25. [next_ip] "m" (next->thread.ip),
  26. [prev] "a" (prev),
  27. [next] "d" (next)
  28. __switch_canary_iparam
  29. "memory");
  30. } while (0)

switch_to 从A进程切换到B进程的步骤如下:

  1. eax <== prev_A
  2. edx <== next_A

复制两个变量到寄存器([prev]"a" (prev);[next]"d" (next))

  1. pushfl /*将状态寄存器eflags压栈*/
  2. pushl %ebp

保存进程A的ebp和eflags(因为现在esp还在A的堆栈中,所以ebp和eflags被保存到A进程的内核堆栈中)

  1. movl%%esp, %[prev_sp]\n\t

保存当前esp到A进程内核描述符中(prev_A->thread.sp<== esp_A)

在调用switch_to时,prev是指向A进程自己的进程描述符的。

  1. movl %[next_sp], %%esp\n\t

从next(进程B)的描述符中取出之前从B切换出去时保存的esp_B(esp_B <==next_A->thread.sp)

在A进程中的next是指向B的进程描述符的。从这个时候开始,CPU当前执行的进程已经是B进程了,因为esp已经指向B的内核堆栈。但是,现在的ebp仍然指向A进程的内核堆栈,所以所有局部变量仍然是A中的局部变量,比如next实质上是%n(%ebp_A),也就是next_A,即指向B的进程描述符。

  1. movl $1f, %[prev_ip]\n\t

把标号为1的指令地址保存到A进程描述符的ip域(prev_A->thread.ip<== %1f)

当A进程下次从switch_to回来时,会从这条指令开始执行。具体方法要看后面被切换回来的B的下一条指令。:

  1. pushl %[next_ip]\n\t
  2. jmp switch_to\n

将返回地址保存到堆栈,然后调用switch_to()函数,switch_to()函数完成硬件上下文切换。

注意,如果之前B也被switch_to出去过,那么[next_ip]里存的就是下面这个1f的标号,但如果进程B刚刚被创建,之前没有被switch_to出去过,那么[next_ip]里存的将是ret_ftom_fork(参看copy_thread()函数)。

  1. popl %%ebp\n\t
  2. popfl\n

从switch_to()返回后继续从1:标号后面开始执行,修改ebp到B的内核堆栈,恢复B的eflags。如果从switch_to()返回后从这里继续运行,那么说明在此之前B肯定被switch_to调出过,因此此前肯定备份了ebp_B和flags_B,这里执行恢复操作。

此时ebp已经指向了B的内核堆栈,所以上面的prev,next等局部变量已经不是A进程堆栈中的了,而是B进程堆栈中的(B上次被切换出去之前也有这两个变量,所以代表着B堆栈中prev、next的值了),因为prev==%p(%ebp_B),而在B上次被切换出去之前,该位置保存的是B进程的描述符地址。如果这个时候就结束switch_to的话,在后面的代码中(即context_switch()函数中switch_to之后的代码)的prev变量是指向B进程的,因此,进程B就不知道是从哪个进程切换回来。而从context_switch()中switch_to之后的代码中,我们看到finish_task_switch(this_rq(),prev)中需要知道之前是从哪个进程切换过来的,因此,我们必须想办法保存A进程的描述符到B的堆栈中,这就是last的作用。

  1. "=a"(last)

将eax写入last,以在B的堆栈中保存正确的prev信息(即last_B <== %eax)

从context_switch()中看到的调用switch_to的方法是:switch_to(prev,next, prev),这里面的last实质上就是prev,因此在switch_to宏执行完之后,prev_B就是正确的A的进程描述符了。这里,last的作用相当于把进程A堆栈中的A进程描述符地址复制到了进程B的堆栈中。

至此,switch_to已经执行完成,A停止运行,而开始执行B。此后,可能在某一次调度中,进程A得到调度,就会出现switch_to(C,A)这样的调用,这时,A再次得到调度,得到调度后,A进程从context_switch()中switch_to后面的代码开始执行,这时候,它看到的prev_A将指向C的进程描述符。

2018-2019-1 20189221 《Linux内核原理与分析》第九周作业的更多相关文章

  1. 2019-2020-1 20199303<Linux内核原理与分析>第二周作业

    2019-2020-1 20199303第二周作业 1.汇编与寄存器的学习 寄存器是中央处理器内的组成部份.寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令.数据和位址.在中央处理器的控制部件中 ...

  2. 20169219 linux内核原理与分析第二周作业

    "linux内核分析"的第一讲主要讲了计算机的体系结构,和各寄存器之间对数据的处理过程. 通用寄存器 AX:累加器 BX:基地址寄存器 CX:计数寄存器 DX:数据寄存器 BP:堆 ...

  3. 2019-2020-1 20199314 <Linux内核原理与分析>第二周作业

    1.基础学习内容 1.1 冯诺依曼体系结构 计算机由控制器.运算器.存储器.输入设备.输出设备五部分组成. 1.1.1 冯诺依曼计算机特点 (1)采用存储程序方式,指令和数据不加区别混合存储在同一个存 ...

  4. Linux内核原理与分析-第一周作业

    本科期间,学校开设过linux相关的课程,当时的学习方式主要以课堂听授为主.虽然老师也提供了相关的学习教材跟参考材料,但是整体学下来感觉收获并不是太大,现在回想起来,主要还是由于自己课下没有及时动手实 ...

  5. 2019-2020-1 20199314 <Linux内核原理与分析>第一周作业

    前言 本周对实验楼的Linux基础入门进行了学习,目前学习到实验九完成到挑战二. 学习和实验内容 快速学习了Linux系统的发展历程及其简介,学习了下的变量.用户权限管理.文件打包及压缩.常用命令的和 ...

  6. Linux内核原理与分析-第二周作业

    写之前回看了一遍秒速五厘米:如果

  7. 2018-2019-1 20189221《Linux内核原理与分析》第一周作业

    Linux内核原理与分析 - 第一周作业 实验1 Linux系统简介 Linux历史 1991 年 10 月,Linus Torvalds想在自己的电脑上运行UNIX,可是 UNIX 的商业版本非常昂 ...

  8. 2020-2021-1 20209307 《Linux内核原理与分析》第九周作业

    这个作业属于哪个课程 <2020-2021-1Linux内核原理与分析)> 这个作业要求在哪里 <2020-2021-1Linux内核原理与分析第九周作业> 这个作业的目标 & ...

  9. 2018-2019-1 20189221 《Linux内核原理与分析》第八周作业

    2018-2019-1 20189221 <Linux内核原理与分析>第八周作业 实验七 编译链接过程 gcc –e –o hello.cpp hello.c / gcc -x cpp-o ...

随机推荐

  1. npm install 项目安装遇到问题

    npm cache clean/clear --force

  2. Java多线程系列——从菜鸟到入门

    持续更新系列. 参考自Java多线程系列目录(共43篇).<Java并发编程实战>.<实战Java高并发程序设计>.<Java并发编程的艺术>. 基础 Java多线 ...

  3. EntityFramework 动态构造排序 Func<IQueryable<T>, IOrderedQueryable<T>> Dynamic

    using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; us ...

  4. 资源查找器PathMatchingResourcePatternResolver的使用

    资源查找器PathMatchingResourcePatternResolver的使用 PathMatchingResourcePatternResolver是一个Ant通配符模式的Resource查 ...

  5. C语言 结构体(联合体)对齐规则

    /* 结构体(联合体)对齐规则 */ #include <stdio.h> #include <stdlib.h> #include <string.h> /* * ...

  6. WPS Office手机版调用接口代码指导帖之二 [复制链接]

    原文链接:http://bbs.wps.cn/thread-22349340-1-1.html 从V5.1版本开始,WPS移动版本提供了额外的功能,可以供第三方程序通过集成的方式调用“WPS移动版”打 ...

  7. 【FM】算法

    https://www.cnblogs.com/AndyJee/p/7879765.html

  8. GoLang之反射

    反射 反射(reflect) 所谓反射(reflect)就是能检查程序在运行时的状态. 使用反射的三条定律: 反射可以将“接口类型变量”转换为“反射类型对象”: 反射可以将“反射类型对象”转换为“接口 ...

  9. 织梦移动版页面点击下一篇获取不到id

    1.首先找到网站目录下面的/include/arc.archives.class.php文件 2.找到837行的如下内容 if ( defined('DEDEMOB') ) { $mlink = 'v ...

  10. MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. Commands that may modify the data set are disabled, because this instance is configured to report e

    早上来到公司,线上的项目报错: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionExcepti ...