Loading...
墨滴

公众号:offer多多

2021/07/29  阅读:68  主题:橙心

协程

60秒问答:什么是抢占式多任务?

阅读收益:

所谓进程调度,其实就是一个人在做 A 项目,在某个时刻,换成做 B 项目去了。

发生这种情况,主要有两种方式。

方式一:A 项目做着做着,发现里面有一条指令 sleep,也就是要休息一下,或者在等待某个 I/O 事件。 那没办法了,就要主动让出 CPU,然后可以开始做 B 项目。

方式二:A 项目做着做着,旷日持久,实在受不了了。

项目经理介入了,说这个项目 A 先停停,B 项目也要做一下,要不然 B 项目该投诉了。

问题

查看go1.4文档

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.

翻译:

  • 协程 现在已经是抢占调度。

  • 不回出现因为gcc和死循环造成调度上死锁或者延迟。

查看维基百科

协程是协作式多任务的,而线程典型是抢占式多任务的。 【单线程:优先级】

  • 这意味着协程提供并发性而非并行性。【多线程是多核】
  • 协程超过线程的好处是它们可以用于硬性实时的语境(在协程之间的切换不需要涉及任何系统调用或任何阻塞调用)

抢占式多任务处理是计算机操作系统中,一种实现多任务处理的方式, 相对于 协作式多任务处理而言。

协作式环境下,下一个进程被调度的前提是当前进程主动放弃时间片;

抢占式环境下,操作系统完全决定 进程调度方案,操作系统可以剥夺耗时长的进程的时间片,提供给其它进程。

查看 操作系统

17 | 调度(下):抢占式调度是如何发生的?

  • https://time.geekbang.org/column/article/93711 抢占式调度

场景: 最常见的现象就是一个进程执行时间太长了,是时候切换到另一个进程了。

总结了进程调度第一定律的核心函数 __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

对比libco

内核态的抢占时机

内核抢占实现(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

16 | 调度(中):主动调度是如何发生的?

https://time.geekbang.org/column/article/93396

进程上下文切换上下文切换主要干两件事情,

  • 一是切换进程空间,也即虚拟内存;
  • 二是切换寄存器和 CPU 上下文。
/*
 * 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);
}
  • switch_to

#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_RR、SCHED_DEADLINE 是实时进程的调度策略。

例如,SCHED_FIFO 就是交了相同钱的,先来先服务,但是有的加钱多,可以分配更高的优先级,也就是说,高优先级的进程可以抢占低优先级的进程,而相同优先级的进程,我们遵循先来先得。

  • 另外一种策略是,交了相同钱的,轮换着来,这就是 SCHED_RR 轮流调度算法,采用时间片,相同优先级的任务当用完时间片会被放到队列尾部,以保证公平性,而高优先级的任务也是可以抢占低优先级的任务。

  • 还有一种新的策略是 SCHED_DEADLINE,是按照任务的 deadline 进行调度的。当产生一个调度点的时候,DL 调度器总是选择其 deadline 距离当前时间点最近的那个任务,并调度它执行。

普通调度策略

  • 对于普通进程的调度策略有,SCHED_NORMAL、SCHED_BATCH、SCHED_IDLE。

  • SCHED_NORMAL 是普通的进程,就相当于咱们公司接的普通项目。

  • SCHED_BATCH 是后台进程,几乎不需要和前端进行交互。这有点像公司在接项目同时,开发一些可以复用的模块,作为公司的技术积累,从而使得在之后接新项目的时候,能够减少工作量。这类项目可以默默执行,不要影响需要交互的进程,可以降低它的优先级。

  • SCHED_IDLE 是特别空闲的时候才跑的进程,相当于咱们学习训练类的项目,比如咱们公司很长时间没有接到外在项目了,可以弄几个这样的项目练练手。

sched_class 有几种实现:

  • stop_sched_class 优先级最高的任务会使用这种策略,会中断所有其他线程,且不会被其他任务打断;

  • dl_sched_class 就对应上面的 deadline 调度策略;

  • rt_sched_class 就对应 RR 算法或者 FIFO 算法的调度策略,具体调度策略由进程的 task_struct->policy 指定;

  • fair_sched_class 就是普通进程的调度策略;

  • idle_sched_class 就是空闲进程的调度策略

  • 一个实时进程队列 rt_rq 和一个 CFS 运行队列 cfs_rq

这里给大家分享几个文章,大家对背后的机制明白后,这些代码就很好理解了,这些文章涉及linux调度器的发展历史,O(n), O(1)调度器,到cfs。

  1. http://www.wowotech.net/process_management/scheduler-history.html

  2. https://www.ibm.com/developerworks/cn/linux/l-cn-scheduler/index.html

  3. https://www.jianshu.com/p/673c9e4817a8

问:调度类是如何工作的?

  • for_each_class 循环,沿着上面的顺序,依次调用每个调度类的方法。

自定义:

GO TRACE 剖析 GO1.14 异步抢占式调度 https://www.freesion.com/article/83321440036/

  • 大神文章:深度解密Go语言之基于信号的抢占式调度

本文讲述了 Go 语言基于信号的异步抢占的全过程,一起来回顾下:

  1. M 注册一个 SIGURG 信号的处理函数:sighandler。
  2. sysmon 线程检测到执行时间过长的 goroutine、GC stw 时,会向相应的 M(或者说线程,每个线程对应一个 M)发送 SIGURG 信号。
  3. 收到信号后,内核执行 sighandler 函数,通过 pushCall 插入 asyncPreempt 函数调用。 回到当前 goroutine 执行 asyncPreempt 函数,通过 mcall 切到 g0 栈执行 gopreempt_m。
  4. 将当前 goroutine 插入到全局可运行队列,M 则继续寻找其他 goroutine 来运行。
  5. 被抢占的 goroutine 再次调度过来执行时,会继续原来的执行流。
  • goroutine 调度器原理 https://mp.weixin.qq.com/s?src=11&timestamp=1626260250&ver=3190&signature=ZSFplukBk6BkNzUMXTj2kY2w6QXPneRCvv1WgoayDIoFO8moIjc-2fQ2n21G0XMID3gTC9FRGrHdumlXrmIcESHz4UzOsmi0nN978FC8NcFIMvg2vC0WbI9OagQeM2v2&new=1

公众号:offer多多

2021/07/29  阅读:68  主题:橙心

作者介绍

公众号:offer多多