跳过正文

Linux kernel线程调度详解

·586 字·3 分钟
jiladahe1997
作者
jiladahe1997
To see the world as it is, and to love it

简介
#

本文主要描述Linux中的线程调度原理,从而可以从线程调度的角度对Linux的性能进行优化



创建线程/进程
#

创建线程
#

Linux创建线程有多种方式,不同方式的原理也不尽相同,这里介绍一种比较通用的方式:pthread_create 函数。

//https://github.com/bminor/glibc/blob/master/nptl/pthread_create.c#L621
__pthread_create_2_1 
    //https://github.com/bminor/glibc/blob/master/nptl/pthread_create.c#L297
    >create_thread 
        >__clone_internal (&args, &start_thread, pd);
            >__clone3
                //https://github.com/bminor/glibc/blob/master/sysdeps/unix/sysv/linux/aarch64/arch-syscall.h#L20
                >#define __NR_clone3 435

//kernel:
#define __NR_clone3 435
__SYSCALL(__NR_clone3, sys_clone3)

SYSCALL_DEFINE2(clone3, struct clone_args __user *, uargs, size_t, size)
{
    ...
	return kernel_clone(&kargs);
}


创建进程
#

//kernel fork(创建进程时,会调用fork)
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
	struct kernel_clone_args args = {
		.exit_signal = SIGCHLD,
	};

	return kernel_clone(&args);
#else
	/* can not support in nommu mode */
	return -EINVAL;
#endif
}

由此可以看出:创建线程或者进程调用到kernel的代码都是一样的,都是调用kernel_clone函数,从而创建出一个 struct task_struct 对象。




调度原理和过程
#

struct task_struct 对象创建完成后,即会加入到 struct rq (run queue)中,CPU根据一定的策略,从run queue中选取合适的 task_struct 执行。那么有两个问题:

Quesion:

  1. CPU什么时候做出选择? 每隔多少秒,还是什么?
  2. CPU的选择策略具体是什么? 优先级?
  3. 怎么修改线程的调度策略 以及优先级?


Answer:

  1. 有多个时机会导致CPU做出选择,也叫做CPU进行调度:
    1. CPU tick。 在系统启动时,会利用硬件定时器定义一个clock event(详情见《Linux子系统 - time》),每当硬件定时器触发(或触发N次 CONFIG_HZ),CPU会选出下一个需要执行的任务,但不会立即发生调度
    2. 处理完任何一个中断后,如果是EL0级别的中断(用户空间被打断)则会触发一次调度,如果是EL1级别的中断(内核空间被打断)取决于CONFIG_PREEMPTION 触不触发调度。
    3. task主动出让,当某个task需要挂起,例如sleep,会触发调度。
    4. 唤醒,当有中断产生、或者释放信号量、释放互斥锁时,会调用一次wakeup,触发cpu调度。

CPU tick触发流程:

tick_handle_periodic(开机过程中) 或者 tick_sched_handle(使能hrtimer后) 详情见《15Linux子系统 - time.md》 tick/jiffies/HZ章节
>update_process_times
 >scheduler_tick (trigger_load_balance多核负载均衡)
  >curr->sched_class->task_tick
   >task_tick_fair 或 task_tick_rt

主动出让触发流程:

msleep
>schedule_timeout_uninterruptible
 >schedule_timeout
  >schedule
   >__schedule_loop
    >__schedule
     >pick_next_task
      >__pick_next_task
       >

wakeup触发流程

wake_up_process
>try_to_wake_up
 >ttwu_do_wakeup
  >check_preempt_curr //RT > fair 优先找RT任务

进入中断处理完成后触发调度 el1内核线程时被中断,进入el1_irq

	.macro el1_interrupt_handler, handler:req
	enable_da_f

	mov	x0, sp
	bl	enter_el1_irq_or_nmi

	irq_handler	\handler

#ifdef CONFIG_PREEMPTION
	ldr	x24, [tsk, #TSK_TI_PREEMPT]	// get preempt count
alternative_if ARM64_HAS_IRQ_PRIO_MASKING
	/*
	 * DA_F were cleared at start of handling. If anything is set in DAIF,
	 * we come back from an NMI, so skip preemption
	 */
	mrs	x0, daif
	orr	x24, x24, x0
alternative_else_nop_endif
	cbnz	x24, 1f				// preempt count != 0 || NMI return path
	bl	arm64_preempt_schedule_irq	// irq en/disable is done inside

el0用户空间时被中断,进入el0_irq,ret_to_user

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)

SYM_CODE_START_LOCAL(ret_to_user)
    ...
    cbnz	x2, work_pending
    ...
SYM_CODE_END(ret_to_user)

work_pending:
	mov	x0, sp				// 'regs'
	mov	x1, x19
	bl	do_notify_resume
	ldr	x19, [tsk, #TSK_TI_FLAGS]	// re-check for single-step
	b	finish_ret_to_user

asmlinkage void do_notify_resume(struct pt_regs *regs,
				 unsigned long thread_flags)
    ...
    schedule();
    ...

调度函数
#

schedule __schedule




调度策略、优先级、调度器
#

Linux的每个 struct task_struct 对象都有4个属性:

  1. 调度策略(policy)

    用户层可见的调度策略有六种,但是到linux kernel后只有两种,见下面

    #define SCHED_NORMAL		0
    #define SCHED_FIFO		    1
    #define SCHED_RR		    2
    #define SCHED_BATCH		    3
    #define SCHED_IDLE		    5
    #define SCHED_DEADLINE		6
    

实时调度策略
为了使得某个进程/线程获得最优的延迟,即调度的最大优先级,我们需要使用 SCHED_FIFO 调度策略。 其在linux kernel内部的实现为:

pick_task_rt
>选出优先级最高的直接返回,没有CFS中占用时间判断

但是RT调度器中对所有RT线程的执行时间都做了限制:

task_tick_rt
>update_curr_rt
 >if (sched_rt_runtime(rt_rq) != RUNTIME_INF) {
			raw_spin_lock(&rt_rq->rt_runtime_lock);
			rt_rq->rt_time += delta_exec;
			exceeded = sched_rt_runtime_exceeded(rt_rq);
			if (exceeded)
//检查有没有超过最大限制,检查条件为运行时间 > rt_runtime(sysctl_sched_rt_runtime)
//默认为 950 ms,即1s内最多运行950ms的实时线程,另外50ms出让给CFS线程



  1. 调度优先级(sched_priority)

  2. 调度nice值(sched_nice)

    调度优先级越低代表优先级越高,只在RT调度器中有用:

    调度nice值越高代表优先级越高,只在fair调度器中使用:

  3. 最终优先级值(prio)

    prio值将上述的三个属性综合计算,最终内核只使用这个值来负责判断:

    • 0~100(MAX_USER_RT_PRIO) 代表使用RT调度器
    • 100~140 代表使用fair调度器
        if (dl_policy(policy))
            prio = MAX_DL_PRIO - 1;
        else if (rt_policy(policy))
            //根据优先级计算prio
            prio = MAX_RT_PRIO - 1 - rt_prio;
        else
            //根据nice值计算prio,默认是+20
            prio = NICE_TO_PRIO(nice)
    
  4. 调度器sched_class:

    调度器只有两种RT和fair,均用结构体struct sched_class表示。 当创建线程时,会根据prio值,算出:

    kernel_clone
    >copy_process
     >sched_fork
    
    if (dl_prio(p->prio))
        return -EAGAIN;
    else if (rt_prio(p->prio))
        p->sched_class = &rt_sched_class;
    else
        p->sched_class = &fair_sched_class;
    

修改线程调度策略和优先级:
#

对于内核线程,可以用 sched_setscheduler 函数进行修改。

对于用户线程,参考附录A代码。


可以使用 top 命令查看进程优先级,PR列代表的是prio值-100.

可以使用 ps -T -l PID 命令查看线程的优先级。(其中-T代表查看进程下的线程,-l代表查看优先级等信息)。根据参数不同,显示的优先级值也不同,具体见下图:




负载均衡
#

当有多个核时,linux会将所有的task平均分配到所有核上,这叫做负载均衡,即load balance。 linux的负载均衡原理如下:

>scheduler_tick (tick定时触发)
 >trigger_load_balance
  >SCHED_SOFTIRQ
   >run_rebalance_domains
    >rebalance_domains
     >load_balance(算法是寻找任务最少的cpu)
      >active_load_balance_cpu_stop
       >detach_one_task 并且 attach_one_task (在中断上下文,将task从一个rq队列更换到另外一个rq队列)



附录A:如何在用户空间修改线程优先级
#

方法1: 通过 chrt 命令

方法2: 通过pthread相关接口

用户空间设置调度优先级流程:

>SYSCALL_DEFINE3(sched_setscheduler
 >do_sched_setscheduler
  >sched_setscheduler
   >_sched_setscheduler



#include <sys/time.h>
#include <stdio.h>
#include <pthread.h>

void *thread(void *arg) {
    printf("start\n");
    int policy;
    struct sched_param param;
    pthread_getschedparam(pthread_self(),&policy,&param);
    printf("policy:%d\n",policy);
    if(policy == SCHED_OTHER)
        printf("SCHED_OTHER\n");
    if(policy == SCHED_RR);
        printf("SCHED_RR 1 \n");
    if(policy == SCHED_FIFO)
        printf("SCHED_FIFO\n");
    while(1) {
        struct timeval current_time;
        gettimeofday(&current_time, NULL);
        // printf("seconds : %ld\nmicro seconds : %ld",
        //     current_time.tv_sec, current_time.tv_usec);
    }
}

int main() {
    //启动一个线程,线程优先级调高
    pthread_t thid1;
    pthread_t thid2;
    void *ret;
    struct sched_param param;
    pthread_attr_t attr;
    pthread_attr_init (&attr);
    pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
    param.sched_priority = 12;
    pthread_attr_setschedparam (&attr, &param);
    pthread_attr_setinheritsched(&attr,PTHREAD_EXPLICIT_SCHED);//要使优先级其作用必须要有这句话

    if (pthread_create(&thid1, &attr, thread, "thread 1") != 0) {
        printf("pthread_create() error");
        return -1;
    }
    if (pthread_create(&thid2, &attr, thread, "thread 1") != 0) {
        printf("pthread_create() error");
        return -1;
    }
    if (pthread_join(thid1, &ret) != 0) {
        printf("pthread_create() error");
        return -1;
    }
    if (pthread_join(thid2, &ret) != 0) {
        printf("pthread_create() error");
        return -1;
    }



    return 0;
}

附录B:如何使用一个用户空间程序把网络卡死:
#

  1. 编译一个实时优先级的while(1)程序,就上面这个 test
  2. 所有网络中断绑核到core0处理
  3. 设置不限制实时线程的CPU使用率:echo 1000000 > /proc/sys/kernel/sched_rt_runtime_us
  4. taskset -c 0 ./test

附录C:内核线程绑核
#

    cpumask_t mask;

    cpumask_clear(&mask);
    cpumask_set_cpu(0, &mask);

    set_cpus_allowed_ptr(g_p_vcan_send_thread, &mask);