简介 #
本文主要描述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:
- CPU什么时候做出选择? 每隔多少秒,还是什么?
- CPU的选择策略具体是什么? 优先级?
- 怎么修改线程的调度策略 以及优先级?
Answer:
- 有多个时机会导致CPU做出选择,也叫做CPU进行调度:
- CPU tick。 在系统启动时,会利用硬件定时器定义一个clock event(详情见《Linux子系统 - time》),每当硬件定时器触发(或触发N次 CONFIG_HZ),CPU会选出下一个需要执行的任务,但不会立即发生调度
- 处理完任何一个中断后,如果是EL0级别的中断(用户空间被打断)则会触发一次调度,如果是EL1级别的中断(内核空间被打断)取决于CONFIG_PREEMPTION 触不触发调度。
- task主动出让,当某个task需要挂起,例如sleep,会触发调度。
- 唤醒,当有中断产生、或者释放信号量、释放互斥锁时,会调用一次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个属性:
-
调度策略(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线程
-
调度优先级(sched_priority)
-
调度nice值(sched_nice)
调度优先级越低代表优先级越高,只在RT调度器中有用:
调度nice值越高代表优先级越高,只在fair调度器中使用:
-
最终优先级值(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)
-
调度器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,¶m);
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(¤t_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, ¶m);
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:如何使用一个用户空间程序把网络卡死: #
- 编译一个实时优先级的while(1)程序,就上面这个 test
- 所有网络中断绑核到core0处理
- 设置不限制实时线程的CPU使用率:echo 1000000 > /proc/sys/kernel/sched_rt_runtime_us
- 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);