进程调度的时机

进程调度时机就是内核调用schedule函数的时机。当内核即将返回用户空间时,内核会检查need_resched标志是否设置。如果设置,则调用schedule函数,此时是从中断(或者异常、系统调用)处理程序返回用户空间的时间点作为一个固定的调度时间点。

除此之外,内核线程和中断处理程序中任何需要暂时中止执行当前执行路径的位置都可以直接调用schedule(),比如等待某个资源就绪。进程调度时机简单总结如下:

  • 用户进程通过特定的系统调用主动让出CPU。
  • 中断处理程序在内核返回用户态时进行调度。
  • 内核线程主动调用schedule函数让出CPU。
  • 中断处理程序主动调用schedule函数让出CPU。

schedule()在Linux内核4.15.13中实现如下:

/*
* __schedule() is the main scheduler function.
*
* The main means of driving the scheduler and thus entering this function are:
*
* 1. Explicit blocking: mutex, semaphore, waitqueue, etc.
*
* 2. TIF_NEED_RESCHED flag is checked on interrupt and userspace return
* paths. For example, see arch/x86/entry_64.S.
*
* To drive preemption between tasks, the scheduler sets the flag in timer
* interrupt handler scheduler_tick().
*
* 3. Wakeups don't really cause entry into schedule(). They add a
* task to the run-queue and that's it.
*
* Now, if the new task added to the run-queue preempts the current
* task, then the wakeup sets TIF_NEED_RESCHED and schedule() gets
* called on the nearest possible occasion:
*
* - If the kernel is preemptible (CONFIG_PREEMPT=y):
*
* - in syscall or exception context, at the next outmost
* preempt_enable(). (this might be as soon as the wake_up()'s
* spin_unlock()!)
*
* - in IRQ context, return from interrupt-handler to
* preemptible context
*
* - If the kernel is not preemptible (CONFIG_PREEMPT is not set)
* then at the next:
*
* - cond_resched() call
* - explicit schedule() call
* - return from syscall or exception to user-space
* - return from interrupt-handler to user-space
*
* WARNING: must be called with preemption disabled!
* 警告:只能在CPU不可抢占的时候被调用
*/
static void __sched notrace __schedule(bool preempt)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq_flags rf;
struct rq *rq;
int cpu; cpu = smp_processor_id();
rq = cpu_rq(cpu);
prev = rq->curr; schedule_debug(prev); if (sched_feat(HRTICK))
hrtick_clear(rq); local_irq_disable();
rcu_note_context_switch(preempt); /*
* Make sure that signal_pending_state()->signal_pending() below
* can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)
* done by the caller to avoid the race with signal_wake_up().
*/
rq_lock(rq, &rf);
smp_mb__after_spinlock(); /* Promote REQ to ACT */
rq->clock_update_flags <<= 1;
update_rq_clock(rq); switch_count = &prev->nivcsw;
if (!preempt && prev->state) {
if (unlikely(signal_pending_state(prev->state, prev))) {
prev->state = TASK_RUNNING;
} else {
deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);
prev->on_rq = 0; if (prev->in_iowait) {
atomic_inc(&rq->nr_iowait);
delayacct_blkio_start();
} /*
* If a worker went to sleep, notify and ask workqueue
* whether it wants to wake up a task to maintain
* concurrency.
*/
if (prev->flags & PF_WQ_WORKER) {
struct task_struct *to_wakeup; to_wakeup = wq_worker_sleeping(prev);
if (to_wakeup)
try_to_wake_up_local(to_wakeup, &rf);
}
}
switch_count = &prev->nvcsw;
} next = pick_next_task(rq, prev, &rf);
clear_tsk_need_resched(prev);
clear_preempt_need_resched(); if (likely(prev != next)) {
rq->nr_switches++;
rq->curr = next;
/*
* The membarrier system call requires each architecture
* to have a full memory barrier after updating
* rq->curr, before returning to user-space. For TSO
* (e.g. x86), the architecture must provide its own
* barrier in switch_mm(). For weakly ordered machines
* for which spin_unlock() acts as a full memory
* barrier, finish_lock_switch() in common code takes
* care of this barrier. For weakly ordered machines for
* which spin_unlock() acts as a RELEASE barrier (only
* arm64 and PowerPC), arm64 has a full barrier in
* switch_to(), and PowerPC has
* smp_mb__after_unlock_lock() before
* finish_lock_switch().
*/
++*switch_count; trace_sched_switch(preempt, prev, next);//任务切换 /* Also unlocks the rq: */
rq = context_switch(rq, prev, next, &rf);//上下文切换
} else {
rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
rq_unlock_irq(rq, &rf);
} balance_callback(rq);
}

那么在core.c中有哪些地方调用了schedule()呢?(也就是哪里可以发生进程调度),经过搜索发现以下几处:

  • asmlinkage __visible void __sched schedule_user(void);这个函数当set_need_resched()被调用或者被远程唤醒但IPI尚未到达。这是注释中给出的信息。
  • void __sched schedule_preempt_disabled(void);在禁用抢占的时候被调用。
  • static void do_sched_yield(void);将当前CPU声明给其他线程时执行此函数。
  • int __sched yield_to(struct task_struct *p, bool preempt);将当前CPU声明给同一进程组中的其他进程是调用此函数。
  • void io_schedule(void);源码中没有注释,不过可以推测是在当前进程准备进行IO时让出CPU。

其实linux中进程调度发生的时机和其他操作系统区别不大,无非是以下几种:

  1. 正在执行的进程执行完毕。
  2. 执行中的进程主动阻塞自己进入睡眠状态,或者调用了P原语因资源不足而被阻塞,或者调用了V原语激活了等待资源的队列。
  3. 进程IO准备就绪后被阻塞。
  4. 分时系统中时间片用完。
  5. 执行系统调用返回时发生调度。
  6. 就绪队列中有进程优先级高于当前进程时。

CGDB追踪调试schedule()

在自己系统中对menuOS进行调试,选择之前用过的fork系列的test.c。

分别在schedulecontext_switchswitch_topick_next_task处设置断点。由于switch_to是内嵌汇编代码所以无法跟踪调试,下面会单独分析。

另外在按s进行调试的过程中我们发现经常进入一个叫做update_curr的函数,该函数如下:

上下文(运行环境)的切换

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

进程上下文包含了进程执行需要的所有信息,包括用户地址空间(程序代码,数据,用户堆栈),控制信息(进程描述符,内核堆栈),硬件上下文,相关寄存器的值。

一般来说,CPU任何时刻都处于以下3中情况之中:

  • 运行于用户空间,执行用户进程上下文。
  • 运行于内和空间,处于进程,一般是内核线程的上下文。
  • 运行于内核空间,处于中断上下文。

进程上下文和中断上下文

上下文(congtext)简单来说就是一个环境。

用户空间的应用程序,通过系统调用,进入内核空间。这个时候用户空间的进程要传递很多变量、参数的值给内核,内核态运行的时候也要保存用户进程的一些寄存 器值、变量等。所谓的“进程上下文”,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈上的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。一个进程的上下文可以分为三个部分:

  • 用户级上下文:正文、数据、用户堆栈以及共享存储区。
  • 寄存器上下文:通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP)。
  • 系统级上下文:进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈。

当发生进程调度时,进行进程切换就是上下文切换(context switch).操作系统必须对上面提到的全部信息进行切换,新调度的进程才能运行。而系统调用进行的模式切换(mode switch)与进程切换比较起来,容易很多,而且节省时间,因为模式切换最主要的任务只是切换进程寄存器上下文的切换。

硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。所谓的“ 中断上下文”,就是硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。中断上下文,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被中断的进程环境)。中断时,内核不代表任何进程运行,它一般只访问系统空间,而不会访问进程空间,内核在中断上下文中执行时一般不会阻塞。

简单来说,中断发生以后,CPU跳到内核设置好的中断处理代码中去,由这部分内核代码来处理中断。这个处理过程中的上下文就是中断上下文。

Linux内核工作在进程上下文或者中断上下文。提供系统调用服务的内核代码代表发起系统调用的应用程序运行在进程上下文;另一方面,中断处理程序,异步运行在中断上下文。中断上下文和特定进程无关。

switch_to关键汇编代码分析

该部分定义在 /linux-3.18.6/arch/x86/include/asm/switch_to.h中,代码如下:


/*
* Saving eflags is important. It switches not only IOPL between tasks,
* it also protects other tasks from NT leaking through sysenter etc.
*/
#define switch_to(prev, next, last) \
do { \
/* \
* Context-switching clobbers all registers, so we clobber \
* them explicitly, via unused output variables. \
* (EAX and EBP is not listed because EBP is saved/restored \
* explicitly for wchan access and EAX is the return value of \
* __switch_to()) \
*/ \
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 \
//next_ip一般是$1f,新创建的进程则是ret_from_fork "jmp __switch_to\n" /* regparm call */ \
//jmp通过寄存器传递参数,比较直观;而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 (0)

在这段代码中有一些值得注意的地方,比如:

  • next_ip一般是$1f,对新创建的子进程是ret_from_fork。
  • jmp跳转到switch_to。这是jmp和ret的搭配。通常我们看到的是call和ret的搭配,call会自动压栈返回地址,ret会弹出返回地址。jmp不会压栈,ret会弹出当前栈顶,也就是$1f所在的位置。

经验

需要注意的是,在比较新版本的内核中,schedule()被调用只能是在CPU不可抢占的时候,需要检查的标志位也变成了TIF_NEED_RESCHED。具体在Linux系统中调度何时发生、怎样发生是一个比较复杂的问题。(我理解还比较模糊,在3.18.6这个版本的内核中甚至没有提到过CPU抢占和不可抢占这一说法,至少在core.c中没有。另外在3.18.6中用的都是_schedule()函数,而在4.15.13中用的都是schedule()函数,他们的写法有很大不同。不得不感叹于计算机行业的变化之快,这也是我们为什么需要做中学的原因之一。)

另外在这次的内核代码中,可以看到贴近硬件的编程人员的特别技能——likely和unlikely。

# define likely(x) __builtin_expect(!!(x), 1)
# define unlikely(x) __builtin_expect(!!(x), 0)

likely表示该表达式取1的可能性较大,unlikely表示该表达式取0的可能性更大。

从函数功能上讲这两个宏定义是一样的,但是在编译器编译时会把分支编译成有利于顺序执行的结构。

2018-2019-1 20189218《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. 2019-2020-1 20199329《Linux内核原理与分析》第十三周作业

    <Linux内核原理与分析>第十三周作业 一.本周内容概述 通过重现缓冲区溢出攻击来理解漏洞 二.本周学习内容 1.实验简介 注意:实验中命令在 xfce 终端中输入,前面有 $ 的内容为 ...

  10. 2019-2020-1 20199329《Linux内核原理与分析》第十二周作业

    <Linux内核原理与分析>第十二周作业 一.本周内容概述: 通过编程理解 Set-UID 的运行机制与安全问题 完成实验楼上的<SET-UID程序漏洞实验> 二.本周学习内容 ...

随机推荐

  1. HTML 标签大全及属性

    基本结构标签: <HTML>,表示该文件为HTML文件 <HEAD>,包含文件的标题,使用的脚本,样式定义等 <TITLE>---</TITLE>,包含 ...

  2. 洛谷P4424 寻宝游戏 [HNOI/AHOI2018]

    正解:思维题 解题报告: 传送门! 这题就是很思维题,,,想到辣实现麻油特别难,但难想到是真的TT 这题主要是要发现一个性质:&1无意义,&0相当于赋值为0,|1无意义,|1相当于赋值 ...

  3. 前端 html input标签 disable 属性

    该属性只要出现在标签中,表示禁用该控件 <!DOCTYPE html> <html lang="en"> <head> <meta cha ...

  4. 如何控制dedecms描述的长度?

    我们都知道调用dedecms的标题长度可以用titlelen='字符数',{dede:arclist titlelen='10'},表示标题长度为10个字符,也即是5个汉字.如果想要控制描述的调用长度 ...

  5. android 6.0之后动态获取权限

    Android 6.0 动态权限申请   1. 概述 Android 6.0 (API 23) 之前应用的权限在安装时全部授予,运行时应用不再需要询问用户.在 Android 6.0 或更高版本对权限 ...

  6. android 本地字符串存取

    存 // data 指定的文件名 SharedPreferences.Editor editor = getSharedPreferences("data",MODE_PRIVAT ...

  7. How to enable TLS 1.2 on Windows Server 2008 R2

    Problem How to enable TLS 1.2 on Windows Server 2008 R2? Resolution QuoVadis recommends enabling and ...

  8. boost--序列化库serialization

    序列化可以把对象转化成一个字节流存储或者传输,在需要时再回复成与原始状态一致的等价对象.C++标准没有定义这个功能.boost.serialization以库的形式提供了这个功能,非常强大,可以序列化 ...

  9. 做报表需要知道的基本的SQL语句

    为客户做报表需要操作数据库,基本的SQL是必须要掌握的,下面介绍做报表最常用的SQL语句.   方法/步骤   1 查询数据:编号表示查询顺序. (8) select (9) distinct (11 ...

  10. Zooming

    Zooming 是一款纯 javascript 图片缩放库,主要特点有: 不依赖其他库,纯 JavaScript 实现,支持移动设备: 流畅的动画: 可缩放高清图像: 易于集成和定制. 使用方法 1. ...