《Linux内核分析》

第八章 可执行程序工作原理进程的切换和系统的一般执行过程

8.1 知识点

进程调度的时机

  • ntel定义的中断类型主要有以下几种

    • 硬中断(Interrupt)
    • 软中断/异常(Exception)
      • 故障(Fault)
      • 退出(Abort)
      • 陷阱(Trap)
  • schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换
    • next = pick_next_task(rq, prev);//进程调度算法都封装这个函数内部
    • context_switch(rq, prev, next);//进程上下文切换
    • switch_to利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程
  • Linux系统的一般执行过程
    • 最一般的情况:正在运行的用户态进程X切换到运行用户态进程Y的过程

      • 1.正在运行的用户态进程X
      • 2.发生中断——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).
      • 3.SAVE_ALL //保存现场
      • 4.中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换
      • 5.标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)
      • 6.restore_all //恢复现场
      • 7.iret - pop cs:eip/ss:esp/eflags from kernel stack
      • 8.继续运行用户态进程Y
  • 几种特殊情况
    • 通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;
    • 内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;
    • 创建子进程的系统调用在子进程中的执行起点及返回用户态,如fork;
    • 加载一个新的可执行程序后返回到用户态的情况,如execve;
    • ch_to利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程



8.2 核心代码分析

context_switch代码

static inline void context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next)
{
struct mm_struct *mm, *oldmm; prepare_task_switch(rq, prev, next); mm = next->mm;
oldmm = prev->active_mm;
/*
* For paravirt, this is coupled with an exit in switch_to to
* combine the page table reload and the switch backend into
* one hypercall.
*/
arch_start_context_switch(prev); if (!mm) { //如果被切换进来的进程的mm为空切换,内核线程mm为空
next->active_mm = oldmm; //将共享切换出去的进程的active_mm
atomic_inc(&oldmm->mm_count); //有一个进程共享,所有引用计数加一
enter_lazy_tlb(oldmm, next); //普通mm不为空,则调用switch_mm切换地址空间
} else
switch_mm(oldmm, mm, next); if (!prev->mm) {
prev->active_mm = NULL;
rq->prev_mm = oldmm;
}
/*
* Since the runqueue lock will be released by the next
* task (which is an invalid locking op but in the case
* of the scheduler it's an obvious special-case), so we
* do an early lockdep release here:
*/
spin_release(&rq->lock.dep_map, 1, _THIS_IP_); context_tracking_task_switch(prev, next);
// 这里切换寄存器状态和栈
switch_to(prev, next, prev); barrier();
/*
* this_rq must be evaluated again because prev may have moved
* CPUs since it called schedule(), thus the 'rq' on its stack
* frame will be invalid.
*/
finish_task_switch(this_rq(), prev);
}

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"//保存ESP
"movl %[next_sp],%%esp\n\t"//更新ESP,将下一栈顶保存到ESP中 "movl $1f,%[prev_ip]\n\t"//保存当前进程EIP*
"pushl %[next_ip]\n\t"//把next进程起点压入next进程的内核堆栈栈顶
__switch_canary
"jmp __switch_to\n"//prev进程中设置next进程堆栈
//jmp不同于call,是通过寄存器传递参数,而不是通过堆栈传递参数,所以ret时弹出的是之前压入栈顶的next进程起点
//wancheng EIP的切换
"1:\t"
"popl %%ebp\n\t"
"popfl\n" /* output parameters */
: [prev_sp] "=m"(prev->thread.sp), //保存prev进程的esp
[prev_ip] "=m"(prev->thread.ip), //保存prev进程的eip
"=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进程内核堆栈栈顶地址,即esp
[next_ip] "m" (next->thread.ip), //next进程的原eip /* regparm parameters for __switch_to():*/
//jmp通过eax寄存器和edx寄存器传递参数
[prev] "a" (prev),
[next] "d" (next) __switch_canary_iparam : /* 重新加载段寄存器
"memory");
} while (0)

8.3 实验

  • 克隆menu,编译内核,启动gdb







  • 在schedule(),context_switch(),pick_next_task()打入断点

  • 按c执行,停在schedule函数处

  • 按c继续执行到pick_next_task断点处

  • 按c继续执行到context_switch断点处,用来实现进程的切换。

总结

一次一般的进程切换过程,其中必须完成的关键操作是:切换地址空间、切换内核堆栈、切换内核控制流程,加上一些必要的寄存器保存和恢复。这里,除去地址空间的切换,其他操作要强调“内核”一词。这是因为,这些操作并非针对用户代码,切换完成后,也没有立即跑到next的用户空间中执行。用户上下文的保存和恢复是通过中断和异常机制,在内核态和用户态相互切换时才发生的。schedule()是内核和其他部分用于调用进程调度器的入口,选择哪个进程可以运行,何时将其投入运行。就如switch_to中的方法,通过压栈出栈交换prev_ip和next_ip。然后返回,从而完成进程调度。而用哪个作为下来的进程,则通过优先级的算法和进程调度算法来决定。

最新文章

  1. JavaScript判断移动端及pc端访问不同的网站
  2. 怎么在excel中快速查找重复记录
  3. GROUP与HAVING的使用
  4. android 中退出程序的两种方式
  5. bookshelf
  6. Gradle里配置jetty实现静态资源的热部署
  7. [转]Oracle 调用存储过程并显示结果集 Oracle.DataAccess.Client OracleDbType.RefCursor
  8. leetcode problem (2-4)
  9. 开发自定义View
  10. iOS加密个人见解
  11. 基于SOAP的xml网络交互心得
  12. 基于visual Studio2013解决C语言竞赛题之1089牛虎过河
  13. 括号匹配(C++ Stack)
  14. BAT54C 二极管是如何工作的?
  15. Raft算法,从学习到忘记
  16. java线程池01-ThreadPoolExecutor构造方法参数的使用规则
  17. 新手创建Vue项目
  18. Linux系统的启动过程
  19. Springboot整合Ehcache缓存
  20. display: table-cell的实用应用

热门文章

  1. Windows程序卡顿、无响应问题定位
  2. Pytest系列(2) - assert断言详细使用
  3. Python库的安装方式
  4. Mob之社会化分享集成ShareSDK
  5. Ubuntu16.04下LAMP环境的安装与配置
  6. 空间复杂度(Space Complexity)
  7. css指示箭头两种实现方法
  8. .Net 微服务架构技术栈的那些事
  9. VUE开发之异常篇
  10. c期末笔记(1)