前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >60秒问答:抢占式多任务处理 vs 协作式多任务处理

60秒问答:抢占式多任务处理 vs 协作式多任务处理

作者头像
程序员小王
发布2021-07-22 10:55:39
1.7K0
发布2021-07-22 10:55:39
举报
文章被收录于专栏:架构说架构说

阅读收益:

所谓进程调度,其实就是一个人在做 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 的执行过程

用户态的抢占时机:

对于用户态的进程来讲,从系统调用中返回的那个时刻,是一个被抢占的时机。

代码语言:javascript
复制

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() 关闭抢占, 当再次打开的时候,就是一次内核态代码被抢占的机会。

  • 内核关闭抢占 是为了防止死锁
代码语言:javascript
复制
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 上下文。
代码语言:javascript
复制
/*
 * 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
代码语言:javascript
复制

#define switch_to(prev, next, last)          \
do {                  \
  prepare_switch_to(prev, next);          \
                  \
  ((last) = __switch_to_asm((prev), (next)));      \
} while (0)

调度

代码语言:javascript
复制
# 查看当前进程的调度策略
$ 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
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-07-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Offer多多 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 阅读收益:
  • 问题
  • 查看go1.4文档
  • 查看维基百科
  • 查看 操作系统
    • 17 | 调度(下):抢占式调度是如何发生的?
      • 用户态的抢占时机:
      • 对比libco
        • 内核态的抢占时机
        • 16 | 调度(中):主动调度是如何发生的?
        • 调度
          • 实时调度策略
            • 普通调度策略
              • 问:调度类是如何工作的?
              • 自定义:
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档