所谓进程调度,其实就是一个人在做 A 项目,在某个时刻,换成做 B 项目去了。
发生这种情况,主要有两种方式。
方式一:A 项目做着做着,发现里面有一条指令 sleep,也就是要休息一下,或者在等待某个 I/O 事件。那没办法了,就要主动让出 CPU,然后可以开始做 B 项目。
方式二:A 项目做着做着,旷日持久,实在受不了了。
项目经理介入了,说这个项目 A 先停停,B 项目也要做一下,要不然 B 项目该投诉了。
2020年2月25日 https://golang.org/doc/go1.14 Go语言发布了1.14版本。
新版本的runtime实现了一个令人欣喜的特性,那就是实现了真正意义上的抢占式调度
Goroutines are now asynchronously preemptible.
As a result, loops without function calls no longer potentially deadlock the scheduler or significantly delay garbage collection.
翻译:
协程是协作式多任务的,而线程典型是抢占式多任务的。【单线程:优先级】
抢占式多任务处理是计算机操作系统中,一种实现多任务处理的方式, 相对于 协作式多任务处理而言。
协作式环境下,下一个进程被调度的前提是当前进程主动放弃时间片;
抢占式环境下,操作系统完全决定 进程调度方案,操作系统可以剥夺耗时长的进程的时间片,提供给其它进程。
场景:最常见的现象就是一个进程执行时间太长了,是时候切换到另一个进程了。
总结了进程调度第一定律的核心函数 __schedule 的执行过程
对于用户态的进程来讲,从系统调用中返回的那个时刻,是一个被抢占的时机。
static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags)
{
while (true) {
/* We have work to do. */
local_irq_enable();
//如果被打了 _TIF_NEED_RESCHED,调用 schedule 进行调度,
if (cached_flags & _TIF_NEED_RESCHED)
schedule();
......
}
}
在 arch/x86/entry/entry_64.S 中有中断的处理过程
common_interrupt:
ASM_CLAC
addq $-0x80, (%rsp)
interrupt do_IRQ
ret_from_intr:
popq %rsp
testb $3, CS(%rsp)
jz retint_kernel
/* Interrupt came from user space */
GLOBAL(retint_user)
mov %rsp,%rdi
call prepare_exit_to_usermode
TRACE_IRQS_IRETQ
SWAPGS
jmp restore_regs_and_iret
/* Returning to kernel space */
retint_kernel:
#ifdef CONFIG_PREEMPT
bt $9, EFLAGS(%rsp)
jnc 1f
0: cmpl $0, PER_CPU_VAR(__preempt_count)
jnz 1f
call preempt_schedule_irq
jmp 0b
内核抢占实现(preempt) http://blog.chinaunix.net/uid-12461657-id-3353217.html?spm=a2c6h.12873639.0.0.33ba6468hSLfWL
对内核态的执行中,被抢占的时机一般发生在 preempt_enable() 中。在内核态的执行中,有的操作是不能被中断的,所以在进行这些操作之前,
总是先调用 preempt_disable() 关闭抢占, 当再次打开的时候,就是一次内核态代码被抢占的机会。
preempt_enable() 开启抢占
preempt_disable() 禁止抢占
内核中每个进程数据结构里有一个计数器preempt_count
抢占的开启与禁止,操作当前进程的preempt_count
内核在进行进程调度的时候,只要prempt_count为0,内核就可以进行抢占。
struct thread_info {
struct task_struct *task; /* main task structure */
............//省略
int cpu; /* cpu we're on */
int preempt_count; /* 0 => preemptable, <0 => BUG */
};
#define preempt_enable() \
do { \
preempt_enable_no_resched(); \
barrier(); \
preempt_check_resched(); \
} while (0)
#define preempt_disable() \
do { \
inc_preempt_count(); \
barrier(); \
} while (0
https://time.geekbang.org/column/article/93396
进程上下文切换上下文切换主要干两件事情,
/*
* context_switch - switch to the new MM and the new thread's register state.
*/
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next, struct rq_flags *rf)
{
struct mm_struct *mm, *oldmm;
......
mm = next->mm;
oldmm = prev->active_mm;
......
switch_mm_irqs_off(oldmm, mm, next);
......
/* Here we just switch the register state and the stack. */
它就是寄存器和栈的切换,它调用到了 __switch_to_asm。
这是一段汇编代码,主要用于栈的切换。
switch_to(prev, next, prev);
barrier();
return finish_task_switch(prev);
}
#define switch_to(prev, next, last) \
do { \
prepare_switch_to(prev, next); \
\
((last) = __switch_to_asm((prev), (next))); \
} while (0)
# 查看当前进程的调度策略
$ chrt -p 31636
pid 31636 的当前调度策略:SCHED_OTHER
pid 31636 的当前调度优先级:0
# 修改31636进程的调度策略为SCHED_FIFO,优先级为10
$ chrt -f -p 10 31636
$ chrt -p 31636
pid 31636 的当前调度策略:SCHED_FIFO
pid 31636 的当前调度优先级:10
- 调度策略与调度类
- 进程包括两类: 实时进程(优先级高); 普通进程
- 两种进程调度策略不同: task_struct->policy 指明采用哪种调度策略(有6种策略)
- 优先级配合调度策略, 实时进程(0-99); 普通进程(100-139)
- 实时调度策略, 高优先级可抢占低优先级进程
- FIFO: 相同优先级进程先来先得
- RR: 轮流调度策略, 采用时间片轮流调度相同优先级进程
- Deadline: 在调度时, 选择 deadline 最近的进程
- 普通调度策略
- normal: 普通进程
- batch: 后台进程, 可以降低优先级
- idle: 空闲时才运行
- 调度类: task_struct 中 * sched_class 指向封装了调度策略执行逻辑的类(有5种)
- stop: 优先级最高. 将中断其他所有进程, 且不能被打断
- dl: 实现 deadline 调度策略
- rt: RR 或 FIFO, 具体策略由 task_struct->policy 指定
- fair: 普通进程调度
- idle: 空闲进程调度
- 普通进程的 fair 完全公平调度算法 CFS(Linux 实现)
- 记录进程运行时间( vruntime 虚拟运行时间)
- 优先调度 vruntime 小的进程
- 按照比例累计 vruntime, 使之考虑进优先级关系
- 调度队列和调度实体
- CFS 中需要对 vruntime 排序找最小, 不断查询更新, 因此利用红黑树实现调度队列
- task_struct 中有 实时, deadline 和 cfs 三个调度实体, cfs 调度实体即红黑树节点
- 每个 CPU 都有 rq 结构体, 里面有 dl_rq, rt_rq 和 cfs_rq 三个调度队列以及其他信息; 队列描述该 CPU 所运行的所有进程
- 先在 rt_rq 中找进程运行, 若没有再到 cfs_rq 中找; cfs_rq 中 rb_root 指向红黑树根节点, rb_leftmost指向最左节点
- 调度类如何工作
- 调度类中有一个成员指向下一个调度类(按优先级顺序串起来)
- 找下一个运行任务时, 按 stop-dl-rt-fair-idle 依次调用调度类, 不同调度类操作不同调度队列
例如,SCHED_FIFO 就是交了相同钱的,先来先服务,但是有的加钱多,可以分配更高的优先级,也就是说,高优先级的进程可以抢占低优先级的进程,而相同优先级的进程,我们遵循先来先得。
sched_class 有几种实现:
这里给大家分享几个文章,大家对背后的机制明白后,这些代码就很好理解了,这些文章涉及linux调度器的发展历史,O(n), O(1)调度器,到cfs。
GO TRACE 剖析 GO1.14 异步抢占式调度 https://www.freesion.com/article/83321440036/
本文讲述了 Go 语言基于信号的异步抢占的全过程,一起来回顾下: