《Linux内核分析》第八周学习笔记 进程的切换和系统的一般执行过程

郭垚 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

【学习视频时间:1小时40分钟 实验时间:1小时35分钟 撰写博客时间:2小时40分钟】

【学习内容:进程切换、Linux系统的一般执行过程、Linux系统架构】

一、进程切换的关键代码switch_to分析

1.1 进程调度与进程调度的时机分析

  操作系统原理中介绍了大量进程调度算法,这些算法从实现的角度看仅仅是从运行队列中选择一个新进程,选择的过程中运用了不同的策略而已。 对于理解操作系统的工作机制,反而是进程的调度时机与进程的切换机制更为关键。

1. 进程调度的时机

schedule()函数实现调度:

  • 中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule()
  • 内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度
  • 用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度

2. 不同类型的进程有不同的调度需求

  • 第一种分类:

    • I/O-bound 频繁的进行I/O;通常会花费很多时间等待I/O操作的完成
    • CPU-bound 计算密集型;需要大量的CPU时间进行运算
  • 第二种分类:

    • 批处理进程
    • 实时进程
    • 交互式进程shell

【用户态进程只能被动调度,内核线程是只有内核态没有用户态的特殊进程。】

1.2 进程上下文切换相关代码分析

1. 为了控制进程的执行,内核必须有能力挂起正在CPU上执行的进程,并恢复以前挂起的某个进程的执行,这叫做进程切换、任务切换、上下文切换。

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

3. 进程上下文包含了进程执行需要的所有信息

  • 用户地址空间:包括程序代码,数据,用户堆栈等
  • 控制信息:进程描述符,内核堆栈等
  • 硬件上下文(注意中断也要保存硬件上下文只是保存的方法不同)

4. schedule()函数选择一个新的进程来运行,并调用context_ switch进行上下文的切换,这个宏调用switch_ to来进行关键上下文切换

  • next = pick_ next_ task(rq, prev);//进程调度算法都封装这个函数内部
  • context_ switch(rq, prev, next);//进程上下文切换
  • switch_ to利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程

#define switch_to(prev, next, last)                    do {
/*
34 * Context-switching clobbers all registers, so we clobber
35 * them explicitly, via unused output variables.
36 * (EAX and EBP is not listed because EBP is saved/restored
37 * explicitly for wchan access and EAX is the return value of
38 * __switch_to())
39 */
unsigned long ebx, ecx, edx, esi, edi; asm volatile("pushfl\n\t" /* save flags */
"pushl %%ebp\n\t" /* save EBP */
"movl %%esp,%[prev_sp]\n\t" /* save ESP */
"movl %[next_sp],%%esp\n\t" /* restore ESP */
"movl $1f,%[prev_ip]\n\t" /* save EIP */
"pushl %[next_ip]\n\t" /* restore EIP */
__switch_canary
"jmp __switch_to\n" /* regparm call */
"1:\t"
"popl %%ebp\n\t" /* restore EBP */
"popfl\n" /* restore flags */ /* 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(): */
[prev] "a" (prev),
[next] "d" (next) __switch_canary_iparam : /* reloaded segment registers */
"memory");
} while ()

5. schedule()函数代码分析

  • 创建一些局部变量

struct task_struct *prev, *next;//当前进程和一下个进程的进程结构体
unsigned long *switch_count;//进程切换次数
struct rq *rq;//就绪队列
int cpu;
  • 关闭内核抢占,初始化一部分变量

need_resched:
preempt_disable();//关闭内核抢占
cpu = smp_processor_id();
rq = cpu_rq(cpu);//与CPU相关的runqueue保存在rq中
rcu_note_context_switch(cpu);
prev = rq->curr;//将runqueue当前的值赋给prev
  • 选择next进程

next = pick_next_task(rq, prev);//挑选一个优先级最高的任务排进队列
clear_tsk_need_resched(prev);//清除prev的TIF_NEED_RESCHED标志。
clear_preempt_need_resched();
  • 完成进程的调度

next = pick_next_task(rq, prev);//挑选一个优先级最高的任务排进队列
clear_tsk_need_resched(prev);//清除prev的TIF_NEED_RESCHED标志。
clear_preempt_need_resched();

以上代码中context_switch(rq,prev,next)完成了从prev到next的进程上下文的切换。

6. 进程切换上下文的代码分析

  • schedule()函数选择一个新的进程来运行

next = pick_next_task(rq, prev);
clear_tsk_need_resched(prev);
clear_preempt_need_resched();
rq->skip_clock_update = ;
  • 通过context_switch完成进程上下文切换

2336context_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; arch_start_context_switch(prev); if (!mm) {
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count);
enter_lazy_tlb(oldmm, next);
} else
switch_mm(oldmm, mm, next); if (!prev->mm) {
prev->active_mm = NULL;
rq->prev_mm = oldmm;
} spin_release(&rq->lock.dep_map, , _THIS_IP_); context_tracking_task_switch(prev, next); switch_to(prev, next, prev); barrier(); finish_task_switch(this_rq(), prev);
}
  • switch_ to函数代码分析

  • 注意:

    • 下图42行保存当前进程的flags
    • 下图43行把当前进程的堆栈基址压栈
    • switch_ to完成寄存器的切换:先保存当前进程的寄存器,再进行堆栈切换(下图第44、45行)自此后所有的压栈都是在新进程的堆栈中了,再切换eip(下图46、56行),这样当前进程可以从新进程中恢复,还有其他必要的切换
    • next_ ip一般是$1f(对于新创建的进程来说就是ret_ from_ fork)
    • 下图47行表明下一进程栈顶是起点。next_ ip一般是$1f,对于新创建的子进程是ret_ from_fork
    • 下图49行jmp _ switch_ to是函数调用,通过寄存器传递参数;函数执行结束return的时候从下一条指令开始(即是新进程的开始)
    • next进程曾经是prev进程,nex执行完后执行的“下一个”其实是刚刚被切换的进程

  asm volatile("pushfl\n\t"       /* save    flags */
"pushl %%ebp\n\t" /* save EBP */
"movl %%esp,%[prev_sp]\n\t" /* save ESP */
"movl %[next_sp],%%esp\n\t" /* restore ESP */
"movl $1f,%[prev_ip]\n\t" /* save EIP */
"pushl %[next_ip]\n\t" /* restore EIP */
__switch_canary
"jmp __switch_to\n" /* regparm call */
"1:\t"
"popl %%ebp\n\t" /* restore EBP */
"popfl\n" /* restore flags */ /* 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(): */
[prev] "a" (prev),
[next] "d" (next) __switch_canary_iparam : /* reloaded segment registers */
"memory");
} while ()

二、Linux系统的一般执行过程

2.1 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

2.2 Linux系统的一般执行过程中的几个特殊情况

几种特殊情况:

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

2.3 内核与舞女

  1. 进程的地址空间一共有4G,其中0——3G是用户态可以访问,3G以上只有内核态可以访问
  2. 内核相当于出租车,可以为每一个“招手”的进程提供内核态到用户态的转换。
  3. 没有进程需要“承载”的时候,内核进入idle0号进程进行“空转”。当用户进程有需求时,内核发生中断,帮助用户进程完成请求,然后再返回到用户进程。就好像Taxi将用户载了一圈之后又把用户放下来。
  4. 3G以上的部分就是这样的“出租车”,是所有进程共享的,在内核态部分切换的时候就比较容易
  5. 内核是各种中断处理程序和内核线程的集合。

三、Linux系统架构和执行过程概览

3.1 Linux操作系统架构概览

1. 操作系统的基本概念及目的

2. 典型的Linux操作系统架构

3.2 最简单、最复杂的操作——执行ls命令

3.3 从CPU和内存的角度看Linux系统的执行

1. 执行gets()函数

2. 执行系统调用,陷入内核

3. 等待输入,CPU会调度其他进程执行,同时wait一个I/O中断

4. 输入ls,发I/O中断给CPU,中断处理程序进行现场保存、压栈等等

5. 中断处理程序发现X进程在等待这个I/O(此时X已经变成阻塞态),处理程序将X设置为WAKE_UP

6. 进程管理可能会把进程X设置为next进程,这样gets系统调用获得数据,再返回用户态堆栈

7. 从CPU执行指令的角度看:

8. 从内存角度看,所有的物理地址都会被映射到3G以上的地址空间。因为这部分对所有进程来说都是共享的:

四、实验

使用gdb跟踪分析一个schedule()函数 ,验证对Linux系统进程调度与进程切换过程的理解

跟踪调试schedule()函数的执行过程如下。由下图可知进程调度时,首先进入schedule()函数,将一个task_ struct结构体的指针tsk赋值为当前进程,然后调用sched_ submit_ work(tsk)。

进入sched_ submit_ work(tsk)函数查看它的工作:sched_ submit_ work主要作用是避免死锁。

由上图可知该函数时检测tsk->state是否为0 (runnable)若为运行态时则返回, tsk_ is_ pi_ blocked(tsk),检测tsk的死锁检测器是否为空,若非空的话就return。

进入schedule()函数,schedule()是切换进程的真正代码:

整个schedule的执行过程可以用下面的流程图表示:

总结

  本周视频主要讲解了进程切换的关键代码switch_ to分析、Linux系统的一般执行过程、Linux系统架构和执行过程。从中我了解到schedule()函数实现进程调度,context_ switch完成进程上下文切换,switch_ to完成寄存器的切换。在调度时机方面,内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度。而用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。

《Linux内核分析》第八周学习笔记的更多相关文章

  1. Linux 内核分析第八周学习笔记

    Linux 内核分析第八周学习笔记 zl + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-10 ...

  2. Linux内核分析——第八周学习笔记

    实验作业:进程调度时机跟踪分析进程调度与进程切换的过程 20135313吴子怡.北京电子科技学院 [第一部分]理解Linux系统中进程调度的时机 1.Linux的调度程序是一个叫schedule()的 ...

  3. Linux内核分析——第八周学习笔记20135308

    第八周 进程的切换和系统的一般执行过程 一.进程切换的关键代码switch_to分析 1.进程调度与进程调度的时机分析 (1)进程分类 第一种分类 I/O-bound:等待I/O CPU-bound: ...

  4. LINUX内核分析第八周学习总结——进程的切换和系统的一般执行过程

    LINUX内核分析第八周学习总结——进程的切换和系统的一般执行过程 张忻(原创作品转载请注明出处) <Linux内核分析>MOOC课程http://mooc.study.163.com/c ...

  5. LINUX内核分析第八周学习总结

    LINUX内核分析第八周学习总结 标签(空格分隔): 20135328陈都 陈都 原创作品转载请注明出处 <Linux内核分析>MOOC课程 http://mooc.study.163.c ...

  6. 《Linux内核分析》第一周学习笔记

    <Linux内核分析>第一周学习笔记 计算机是如何工作的 郭垚 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/c ...

  7. 《Linux内核分析》第二周学习笔记

    <Linux内核分析>第二周学习笔记 操作系统是如何工作的 郭垚 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/ ...

  8. linux内核分析第五周学习笔记

    linux内核分析第五周学习笔记 标签(空格分隔): 20135328陈都 陈都 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.co ...

  9. Linux内核分析第三周学习笔记

    linux内核分析第三周学习笔记 标签(空格分隔): 20135328陈都 陈都 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.co ...

  10. Linux内核分析第六周学习笔记——分析Linux内核创建一个新进程的过程

    Linux内核分析第六周学习笔记--分析Linux内核创建一个新进程的过程 zl + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/U ...

随机推荐

  1. MySQL内连接(INNER JOIN)

    MySQL INNER JOIN子句介绍 MySQL INNER JOIN子句将一个表中的行与其他表中的行进行匹配,并允许从两个表中查询包含列的行记录. INNER JOIN子句是SELECT语句的可 ...

  2. January 13th, 2018 Week 02nd Saturday

    Anyone who has no spiritual aspirations is an idiot. 任何没有精神追求的人都是愚昧无知的人. Today I went to a bookshop ...

  3. EasyUI设置选中复选框

    //设置选中 $('#isBind').prop('checked', true); //获取是否选中 var isChecked = $('#isBind').prop('checked'); if ...

  4. python2.7.15 + PyQt4部署

    安装python模块首先想到的就是 https://pypi.org/,但在上面下载的PyQt4的包却找不到安装的方法,于是找找试其它方法,下面介绍通过pip安装whl的安装方式 1.先打开 http ...

  5. MyCat读写分离、分库分表

    系统开发中,数据库是非常重要的一个点.除了程序的本身的优化,如:SQL语句优化.代码优化,数据库的处理本身优化也是非常重要的.主从.热备.分表分库等都是系统发展迟早会遇到的技术问题问题.Mycat是一 ...

  6. BZOJ1041:[HAOI2008]圆上的整点(数论)

    Description 求一个给定的圆(x^2+y^2=r^2),在圆周上有多少个点的坐标是整数. Input 只有一个正整数n,n<=2000 000 000 Output 整点个数 Samp ...

  7. 帝国CMS 列表模板页面 list.var 内容截取

    每天学习一点点 编程PDF电子书免费下载: http://www.shitanlife.com/code list.var 中没有好的办法,只能用程序代码来实现.将整个HTML以一个变量来拼接.如下: ...

  8. Ignatius and the Princess III(方案背包+搜索)

    就是问你,n这个数可以被多少种方案组成. 比如: 算是,方案+完全背包的模板题了. #include<iostream> #include<cstring> using nam ...

  9. sed插入,替换指定行的特定字符串,删除指定行首的#

    sed -i '$a service snmpd start' /etc/rc.local sed -i "41s:public:mykey:g" /etc/snmp/snmpd. ...

  10. MySQL(一)MySQL基础介绍

    最近的学习内容是数据库相关的一些知识,主要以MySQL为主,参考书籍——<MySQL必知必会> MySQL学习及下载地址:https://dev.mysql.com/ MySQL学习使用注 ...