首页 > 系统相关 >Linux中内核线程可以被抢占吗?

Linux中内核线程可以被抢占吗?

时间:2023-07-21 17:45:05浏览次数:27  
标签:抢占 rq PREEMPT 线程 内核 Linux curr CONFIG

1 背景

  说起抢占,需要关注服务器上Linux内核中的CONFIG_PREEMPT_xxx采用的何种模式,下面是几个比较常见系统的配置方式

  • 例如REHL以及centos7使用的是CONFIG_PREEMPT_VOLUNTARY
  • 又例如SLES以及龙蜥OS使用的是CONFIG_PREEMPT_NONE

  咱们这里要分析的就是在CONFIG_PREEMPT_VOLUNTARY或者CONFIG_PREEMPT_NONE的情况下,如果OS中有一个内核线程一直死循环运行,可以被其他高优先级内核线程(worker,甚至是softlockup的watchdog线程)抢占吗?

2 分析

  要回答上面的问题首先要知道抢占是如何发生的,下面就以centos为例进行分析。

  在Linux内核中抢占的发生分为2步:

  • 标记抢占
  • 执行抢占

2.1 标记抢占

  标记抢占是指为当前任务标记"TIF_NEED_RESCHED"标志,有了这个标志后,系统会在某个合适的时间点"执行抢占",抢占当前任务。

  系统中有多个时机来标志抢占,一般情况下有如下时机来进行标记。

2.1.1 周期性时钟中断

  系统周期性的产生时钟中断,在时钟中断中会对CPU上的当前任务进行时间片统计;不同的调度类有不同的统计方式,他们都是用回调函数sched_class->task_tick来实施。

void scheduler_tick(void)       
{       
        int cpu = smp_processor_id();
        struct rq *rq = cpu_rq(cpu);
        struct task_struct *curr = rq->curr;
        ......
        curr->sched_class->task_tick(rq, curr, 0);
        ......
}

  以fair class调度类为例,sched_class->task_tick()最终会调用task_tick_fair()来实施,这个流程中会检查当前任务ra->curr的时间片是否超限,如果超限则为curr标记TIF_NEED_RESCHED:

static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
        ......
        ideal_runtime = sched_slice(cfs_rq, curr);
        delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
        if (delta_exec > ideal_runtime) {  //如果超限
                resched_curr(rq_of(cfs_rq));    //则标记抢占
        ......
}

2.1.2 任务唤醒和创建

  任务唤醒和创建也是一个比较常见的发生抢占的时机。试想一下,如果有唤醒的任务或者新建的任务加入到就绪队列,它们的"优先级"可能要高于current;此时需要提供一个时机让"新"任务可以有机会优先得到运行。

  新创建任务的情况:

void wake_up_new_task(struct task_struct *p)
{
    ......
    activate_task(rq, p, ENQUEUE_NOCLOCK);    //新任务加入就绪队列
    ......
    check_preempt_curr(rq, p, WF_FORK);        //检查是否要抢占curr,如果条件满足则标记current需要抢占

  再来看看任务唤醒的情况:

/* 唤醒的调用流程
try_to_wake_up()-->
    ttwu_queue(p, cpu, wake_flags)-->
        ttwu_do_activate(rq, p, wake_flags, &rf)-->
            ttwu_do_wakeup(rq, p, wake_flags, rf)
*/

static void ttwu_do_wakeup(struct rq *rq, struct task_struct *p, int wake_flags,
                           struct rq_flags *rf)
{
        check_preempt_curr(rq, p, wake_flags);    //检查是否需要抢占,如果条件满足则标记当前任务需要抢占
        ......
}

  上面两种场景种,都要调用check_preempt_curr函数,这个函数会根据不同的调度class来做决策是否需要抢占当前任务(class相同,class不同的场景都会分开考虑);对于fair class而言调用的是check_preempt_wakeup。

2.1.3 其他场景

  除了上述场景外,还有其他场景可能导致curr被抢占,例如任务发生迁移(load balance,numa balance),任务调度策略发生变化(任务的优先级改变)等情况。这些情况都有一个共同特点,当前CPU上的运行策略可能发生改变,即CPU上可能有优先级更高的任务产生,导致current任务需要尽快腾出CPU资源给其他任务运行。此时就会是一个标记抢占的时机,待到一个合适的时间点执行真正的抢占。

2.2 执行抢占

  在2.1中咱们讨论了任务标记了"TIF_NEED_RESCHED"标志,表示这个任务在合适的时机放弃CPU资源,被其他任务抢占。这个执行抢占的时机有用户态抢占和内核态抢占两类,下面依次分析。

2.2.1 用户态抢占的情况

  用户态抢占是指current任务从内核态退出到用户态时发生抢占。为什么这里是一个执行抢占的时机呢?

  因为current被标记抢占一定是发生在内核态,所以在从内核态退出到用户态时是一个绝佳的执行抢占时机。咱们从代码层面看看这个用户态抢占是如何发生的。这里以用户态被中断打断的场景为例进行讲解。

2.2.1.1 arm64架构情形

  阶段1:中断处理函数汇编部分

/*el0_irq是用户态发生中断的中断处理函数 */
SYM_CODE_START_LOCAL_NOALIGN(el0_irq)
        kernel_entry 0
el0_irq_naked:
        el0_interrupt_handler handle_arch_irq
        b       ret_to_user   //返回用户态
SYM_CODE_END(el0_irq)

/* go on */
ret_to_user:
        disable_daif
        ldr     x1, [tsk, #TSK_TI_FLAGS]
        and     x2, x1, #_TIF_WORK_MASK
        cbnz    x2, work_pending      //这里会去检查是否需要抢占
finish_ret_to_user:
        enable_step_tsk x1, x2
#ifdef CONFIG_GCC_PLUGIN_STACKLEAK
        bl      stackleak_erase
#endif
        kernel_exit 0
ENDPROC(ret_to_user)

/* 有需要处理的work,例如:执行抢占 */
work_pending:
        mov     x0, sp                          // 'regs'
        bl      do_notify_resume    //最终是这个函数里面执行抢占
        .....

  阶段2:中断处理函数返回用户态的检查

asmlinkage void do_notify_resume(struct pt_regs *regs,
                                 unsigned long thread_flags)
{
        ......
        do {
                /* Check valid user FS if needed */
                addr_limit_user_check();

                if (thread_flags & _TIF_NEED_RESCHED) {  //检查抢占标志
                        /* Unmask Debug and SError for the next task */
                        local_daif_restore(DAIF_PROCCTX_NOIRQ);

                        schedule();  //调度,切换
                } else {

2.2.1..2 x86_64架构情形

  阶段1:中断处理函数中汇编部分

common_interrupt:
        addq    $-0x80, (%rsp)                  /* Adjust vector to [-256, -1] range */
        call    interrupt_entry
        UNWIND_HINT_REGS indirect=1
        call    do_IRQ  /* rdi points to pt_regs */   //调用中断处理函数
        /* 0(%rsp): old RSP */
ret_from_intr:
        DISABLE_INTERRUPTS(CLBR_ANY)
        TRACE_IRQS_OFF

        LEAVE_IRQ_STACK

        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

  阶段2:中断处理函数返回用户态的处理

__visible inline void prepare_exit_to_usermode(struct pt_regs *regs)
{
       ......
        cached_flags = READ_ONCE(ti->flags);

        if (unlikely(cached_flags & EXIT_TO_USERMODE_LOOP_FLAGS))  //这里的标志是多种组合,_TIF_NEED_RESCHED是其中之一
                exit_to_usermode_loop(regs, cached_flags);
        ......  
}

static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags)
{
        while (true) {
                /* We have work to do. */
                local_irq_enable();

                if (cached_flags & _TIF_NEED_RESCHED)  //如果需要抢占则放弃CPU资源
                        schedule();
       ......}
}

2.2.2 内核态抢占的情况

  在2.2.1中我们通过中断直接返回用户态来说明用户态抢占的情形。这里我们再以中断返回内核态为例来说明内核抢占的情况。

  在开讲之前,说明一下中断返回内核态是一种什么样的情形,例如:程序A在用户态调用调用系统调用syscall_X然后陷入内核态执行这个系统调用,就在syscall_X在正在内核态执行的过程中突然被一个中断打断,CPU执行中断处理函数;待中断处理函数处理完毕、退出中断,此时退出到前面系统调用在内核态执行的上下文---这时的上下文是在内核态,在这个上下文发生抢占就是内核态抢占。

  需要说明的是,要有内核态的抢占发生,内核必须要有CONFIG_PREEMPT=y配置;对于REHL&centos、SLES&龙蜥来说内核使能的分别是CONFIG_PREEMPT_VOLUNTARY和CONFIG_PREEMPT_NONE,这些OS都无法在内核抢占。因此在这些系统中如果一个内核线程一直运行,不主动放弃CPU资源的情况下是无法被其他任务抢占的,即使优先级最高的stop class任务也不行。

  下面我们看看内核抢占的情形。

2.2.2.1 arm64架构情形

el1_irq:
        ......
        irq_handler      //中断处理函数

/* 只有使能了CONFIG_PREEMPT才有机会发生内核抢占 */
#ifdef CONFIG_PREEMPT
        ldr     w24, [tsk, #TSK_TI_PREEMPT]     // get preempt count
        cbnz    w24, 1f                         // preempt count != 0
        ldr     x0, [tsk, #TSK_TI_FLAGS]        // get flags
        tbz     x0, #TIF_NEED_RESCHED, 1f       // needs rescheduling?
        bl      el1_preempt    //内核抢占检查函数,就不展开了
1:
#endif

2.2.2.1 x86_64架构情形  

ret_from_intr:
       ......
       /* 检查中断发生的上下文 */
        testb   $3, CS(%rsp)
        jz      retint_kernel    /* 中断发生在内核态上下文 */



retint_kernel:
/* 和arm64一样,也是只有在内核使能了CONFIG_PREEMPT配置才会发生抢占 */
#ifdef CONFIG_PREEMPT
        .......
0:      cmpl    $0, PER_CPU_VAR(__preempt_count)
        jnz     1f
        call    preempt_schedule_irq    /* 内核态抢占 */
        jmp     0b
1:
#endif

3 结论

  REHL、centos、SLES一级龙蜥等操作系统内核态不会被其他内核线程抢占,这些系统只有在返回用户态时才会发生抢占。因此内核中的softlockup检测机制就是利用这一特点,在内核中为每个CPU创建一个优先级最高的任务watdog/N,N是cpu编号;内核期望这个watchdog线程会定期更新时间戳,一旦这个时间戳未按时更新,说明watchdog线程没有及时得到调度;由于watchdog线程的优先级很高,正常情况下watchdog线程是一定是有机会得到运行的,除非内核长时间没有达到执行抢占的时机,例如长时间在内核态运行不退出到用户态。

 

  题外话再扩展一下CONFIG_PREEMPT_VOLUNTARY和CONFIG_PREEMPT_NONE的区别:

  REHL以及centos7使用的是CONFIG_PREEMPT_VOLUNTARY,这种情况下,内核态可以通过调用might_sleep()中调用一次schedule()来主动放弃CPU触发抢占,但是仍然不能被动抢占

  SLES以及龙蜥OS使用的是CONFIG_PREEMPT_NONE,表示不允许在内核态发生抢占。

 

标签:抢占,rq,PREEMPT,线程,内核,Linux,curr,CONFIG
From: https://www.cnblogs.com/liuhailong0112/p/17561327.html

相关文章

  • 多线程下,C++如何调用Python脚本的方法
    视频教程:多线程场景下,用C++调用Python脚本的方法Git:https://github.com/JasonLiThirty/C-andPython接口函数Python3.6提供给C/C++接口函数,基本都是定义pylifecycle.h,pythonrun.h,ceval.h中。Py_Initialize()和Py_Finalize()C++应用程序调用Python脚本之前,必须先调用Py_I......
  • Linux常用命令
    Linux命令一切都是一个文件。(存储形式)系统中拥有小型,单一用途的程序。当遇到复杂任务,通过不同功能用途的程序组合起来完成。(大化小,小化了)避免令人困惑的用户界面。(统统用命令)连配置文件都存储在文本中,方便增、删、改、查。不在乎后缀名,文件名与文件类型不相关。......
  • linux 中printf命令终端输出变量值
     001、直接输出变量[root@PC1test03]#ls[root@PC1test03]#num=100##测试变量值[root@PC1test03]#printf$num##输出变量100[root@PC1test03]# 002、[root@PC1test03]#ls[root@PC1test03]#num=100[root@PC1test03]#printf$num100......
  • Linux精品书籍下载
    Linux精品书籍Linux命令行第2版出版日期2019年3月5日502页4.7星1740评(2023-7-21)带你从第一次敲击终端键盘,到在最流行的Linuxshell(或命令行)Bash中编写完整的程序。在学习的过程中,你将学到几代经验丰富、善于躲避鼠标的大师们传授下来的永恒技能:文件导航、环境配置、......
  • linux基础之守护进程
    一.守护进程(Daemon)1.关于守护进程守护进程,顾名思义,也就是专门守护一个进程的进程。守护进程的职责就是专门确保被指定的进程的运行。守护进程也称精灵进程(Daemon),是运行在后台的一种特殊进程。它独立于控制终端,并且周期性的执行某种任务或等待处理某些发生的事件。守护进程是一种......
  • 如何设置线程数
    如何设置线程数并不是一个只要一个公式就是可以推导出来的,这实际需要经过试验测量,虽然在《java并发编程实战》中给了一个如下一个公式:           N(线程数)=N(cpu总数)*U(cpu的利用率)*(1+W(等待时间)/C(计算时间)) 而实际还要考虑内存等方面 参考:  http://ifeve.com/ho......
  • java线程运行越久获得时间片越少
    Java线程运行越久获得时间片越少1.介绍在Java中,线程是一种独立执行的代码片段,它可以并发执行和共享内存。每个线程都有自己的执行路径,并且可以与其他线程同时运行。在多线程的情况下,操作系统通过分配时间片来控制每个线程的执行时间。时间片是操作系统中用于调度进程和线程的一......
  • java线程休眠三秒钟
    如何在Java中实现线程休眠三秒钟简介在Java中,我们可以使用Thread.sleep()方法来实现线程的休眠。该方法可以使当前线程暂停执行一段指定的时间,以毫秒为单位。在本文中,我将向您展示如何使用Thread.sleep()方法在Java中实现线程休眠三秒钟。步骤以下是实现线程休眠三秒钟的步骤:......
  • (转)GUI为什么不设计为多线程
    在我们这批新人转正评审的时候,我师父问了我的小伙伴一个问题:为什么一些更新界面的方法只能在主线程中调用?师父没有问我这个问题,让知其然但不知其所以然的我有种侥幸逃过一难的心情。我想如果回答那是因为Android GUI库是单线程消息机制的,更新界面的操作必须放到主线程中执行,那师父......
  • (转)Swing 线程之SwingUtilities.invokeLater()
     现在我们要做一个简单的界面。包括一个进度条、一个输入框、开始和停止按钮。需要实现的功能是:当点击开始按钮,则更新进度条,并且在输入框内把完成的百分比输出(这里只做例子,没有真正去做某个工作)。 代码1: 1.importjava.awt.FlowLayout;2.importjava.awt.event.Actio......