理解进程调度时机

进程调度时机

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

schedule()函数分析

schedule()函数原型位于linux-3.18.6/kernel/sched/core.c中,其中主要的关键函数有pick_next_task(),这个函数调用之后,会根据某种进程调度策略选择出下一个运行的进程。紧接着是context_switch(),这是进程上下文切换函数,它的函数实现如下:

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;
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, 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);
}

在context_switch()函数中,最为重要的是switch_to()函数,它主要是由内联汇编实现,功能是完成进程切换。

#define switch_to(prev, next, last)
do {
unsigned long ebx, ecx, edx, esi, edi;
asm volatile("pushfl\n\t" /* 保存当前进程的flags */
"pushl %%ebp\n\t" /* 把当前进程的当前的ebp压入当前进程的栈 */
"movl %%esp,%[prev_sp]\n\t" /*保存当前的esp到prev->thread.sp指向的内存中 */
"movl %[next_sp],%%esp\n\t" /* 重置esp,把下个进程的next->thread.sp赋予esp */
"movl $1f,%[prev_ip]\n\t" /*把1:的代码在内存中存储的地址保存到prev->thread.ip中*/
"pushl %[next_ip]\n\t" /*重置eip */
__switch_canary
"jmp __switch_to\n" /*跳转到switch_to函数*/
"1:\t"
"popl %%ebp\n\t" /* 重置ebp */
"popfl\n" /* 重置flags*/ : [prev_sp] "=m" (prev->thread.sp),
[prev_ip] "=m" (prev->thread.ip),
"=a" (last),
"=b" (ebx), "=c" (ecx), "=d" (edx),
"=S" (esi), "=D" (edi) __switch_canary_oparam : [next_sp] "m" (next->thread.sp),
[next_ip] "m" (next->thread.ip),
[prev] "a" (prev),
[next] "d" (next)
__switch_canary_iparam
"memory");
} while (0)

switch_to()是A进程到B进程的过渡,我们可以认为在switch_to()这个点上,A进程被切出,B进程被切入。进入 switch_to()的宏里面之后,首先pushfl和pushl ebp肯定仍然属于进程 A,之后把esp指向了B的堆栈,从此时开始的指令流都属于跟B进程有关的了。但是,这个时候B进程还没有完全准备好,因为 ebp和硬件上下文等内容还没有切换成B的,剩下的部分宏代码就是完成这些事情。对于 A 进程它始终没有感觉到自己被打断过,它认为自己一直是不间断执行的。switch_to()函数,除了改变了A进程中的prev变量外,对A没有其它任何影响。在系统中任何进程看到的都是这个样子,所有进程都认为自己在不间断的独立运行。

gdb跟踪fork命令

一想到schedule()函数,我想到的就是fork命令,正好上上节课在MenOS中新增加了fork命令,所以就用gdb跟踪之前的fork命令吧。

断点设置:

b schedule
b context_switch
b pick_next_task

本来想在switch_to也设置一个断点,但设置了好几遍都没成功,起初还在想为什么,后来发现switch_to并不是单独函数,而是定义在宏中的一个宏。所以设置不了断点,然后尝试在它附近的函数设置断点,发现都无果,说明在context_switch中的“函数”都是写在宏中的,那果断放弃。

schedule()断点:



pick_next_task()断点:



context_switch()断点:

总结

进程切换的一般过程:一个用户态进程A,发生中断,save cs:eip/esp/eflags等到内核堆栈;SAVE_ALL保存现场。中断处理过程中直接调用或者中断返回前调用schedule,其中switch_to中的汇编代码做了关键的部分。切换了内核堆栈,从当前进程进入到下一个进程。从标号1开始就开始运行下一个进程(这个进程必须是曾经通过这个过程被切换出去的,比如新进程就不包含在内);恢复下一个进程的现场,pop出eip,esp。继续运行进程切换前用户态正在跑的程序。

特殊情况:

  • 通过中断处理过程中的调度,用户态进程与内核进程之间互相切换,与一般情形类似;
  • 内核进程程主动调用 schedule 函数,只有进程上下文的切换,没有中断上下文切换;
  • 创建子进程的系统调用在子进程中的执行起点及返回用户态,如:fork;
  • 加载一个新的可执行程序后返回到用户态的情况,如:execve。

课本内容

mm_struct 与内核线程

当一个进程被调度时,该进程的mm域指向的地址空间被装载到内存,进程描述符中的active_mm域会被更新,指向新的地址空间。内核线程没有地址空间,所以mm域为NULL。于是,当一个内核线程被调度时,内核发现它的mm域为NULL,就会保留前一个进程的地址空间,随后内核更新内核线程对应的进程描述符中的active_mm域,使其指向前一个进程的内存描述符,所以在需要时,内核线程便可以使用前一个进程的页表。因为内核线程不防问用户空间的内存,所以它们仅仅使用地址空间中和内核内存相关的信息,这些信息的含义和普通进程完全相同。

页表

Linux中使用三级页表完成地址转换。利用多级页表能够节约地址转换需占用的存放空间。

  • 顶级页表是全局目录(PGD),它包含一个pgd_t类型数组,多数体系结构中pgd_t类型等同于无符号长整型类型。PGD中的表项指向二级页目录中的表项:PMD。
  • 二级页表是中间爱你页目录(PMD),它是个pmd_t类型数组,其中的表项指向PTE中的表项。
  • 最后一级的页表简称页表,其中包含了pte_t类型的页表项,该页表项指向物理页面。

高速缓存(cache)存在的意义(回顾)

  • 访问磁盘速度要远远低于内存访问的速度,因此,从内存访问数据比从磁盘访问速度更快。
  • 数据一旦被访问,就很有可能在短期再次被访问,这种在短时间内集中访问同一片数据的原理称作临时局部原理。

基树

因为在任何页I/O操作内核前都要检查页是否已经在页高速缓存中了,所以这种频繁进行的检查必须是迅速高效的,否则搜索和检查页高速缓存的开销肯能抵消页高速缓存带来的好处。页高速缓存通过两个参数address_space对象加一个偏移量进行搜索。每个address_space对象都有唯一的基树,它保存在page_tree结构体中。基树是一个二叉树,只要指定了文件偏移量,就可以在基树中迅速检索到希望的页。页面高速缓存的搜索函数find_get_page()要调用radix_tree_lookup(),该函数会在指定的基树中搜索指定的页面。

PS:学了这么多关于Linux系统实现的内容,越来越觉得数据结构的重要性,起初感觉数据结构这门课很“废”,可能是我代码写得太少。现在看来,数据结构真的是门艺术。

多线程可以避免拥塞

单个线程可能会堵塞在某个队列的处理上,不能使所有磁盘都处于饱和的工作状态,原因在于磁盘的吞吐量是非常有限的。正是因为磁盘的吞吐量很有限,所以如果只有唯一线程执行页回写操作,那么这个线程很容易苦苦等待对一个磁盘的操作。为了避免这种情况发生,内核需要多个回写线程并发执行,这样单个设备队列的拥塞救护会成为系统的瓶颈了。

2017-2018-1 20179209《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. 2017-2018-1 20179209《Linux内核原理与分析》第七周作业

    一.实验 1.1task_struct数据结构 Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息.它定义在linux-3.18.6 ...

  10. 20169212《Linux内核原理与分析》课程总结

    20169212<Linux内核原理与分析>课程总结 每周作业链接汇总 第一周作业:完成linux基础入门实验,了解一些基础的命令操作. 第二周作业:学习MOOC课程--计算机是如何工作的 ...

随机推荐

  1. MySQL时间增加、字符串拼接

    MySQL时间增加.字符串拼接 SELECT DATE_ADD(startTime,  INTERVAL 10 SECOND); CONCAT(string1,string2,…)

  2. eCos系统无法正确链接到在C++源文件里自己定义的cyg_user_start函数的问题和解决的方法

    在C++源文件里定义cyg_user_start函数前,将其声明成C函数.就可以解决这个问题. eCos官网:http://ecos.sourceware.org eCos中文技术网:http://w ...

  3. EffectiveJava(21)策略模式多种方式实现字符串比较

    **调用对象上的方法通常是执行该对象上的某项操作**. 如果一个对象的方法执行其他对象的操作,一个类仅仅导出这个方法(它的实例相当于一个指向该方法的指针),这样的实例被称为函数对象 例如: /** * ...

  4. 用MyEclipse2016 CI版创建一个SpringBoot程序

    之前先要在Eclipse里安装STS,步骤如下: 1.点击菜单Help->Install from Catalog 2.在弹出的对话框中点击Popular选项卡,在STS旁边点Install按钮 ...

  5. Android学习(九) SharedPreferences

    一.SharedPreferences:一种清醒的存储方式,基于XML存储key-value键值对方式的数据. SharedPreferences对象本身只能获取数据,而不能存储和修改数据,存储修改只 ...

  6. Oracle基础(三) 表空间

    数据库的存储结构 数据库主要用于存储和检索相关的信息,Oracle数据库包含逻辑结构和物理结构. 物理结构是指现实存储单元,由一组文件组成如数据文件.日志文件.控制文件. 数据文件:用于存储数据的文件 ...

  7. MySQL 系列教程(二) 你不知道的数据库操作

    本章内容: 查看\创建\使用\删除 数据库 用户管理及授权实战 局域网远程连接法 查看\创建\使用\删除\清空\修改 数据库表(是否可空,默认值,主键,自增,外键) 表内容的增删改查 where条件. ...

  8. Oracle 为表空间增加数据文件

    dba权限检查下 select tablespace_name, file_id, file_name, ),) total_space from dba_data_files order by ta ...

  9. preloadjs实现网页资源预加载

    <!doctype html> <html lang="zh"> <head> <title>PreloadJS的基础使用</ ...

  10. 如何进入到Docker容器内部

    启动Docker容器后,对应的服务(例如tomcat启动)也通过dockerfile文件命令运行起来了,这个时候如何进行容器内部观察容器的运行状态. 1.docker attach 这个命令在创建一个 ...