Linux 调度:进程调度时机

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 进程的调度时机

发生进程调度的本质,是因为系统资源的限制:如果系统只有 8 个 CPU 核,那么同一时间只能有 8 个进程同时并行。那么在 Linux 系统下,什么时候会发生调度?或者说什么时候会发生进程切换?让我们来深入下 Linux 内核代码实现的细节。本文基于 Linux 4.14 源码进行分析。

所有进程的调度,最终都会经过 kernel/sched/core.c 中的 __schedule(),查找内核源码中所有对 __schedule() 的调用,可以找出所有的调度时机点。笔者将所有调度点按调度切换过程是否立即执行,将它们分为两类

  • 延迟调度
    调度切换过程不会立即发生,而是先设置进程的 _TIF_NEED_RESCHED 标志,然后在某个延迟调度点,检查 _TIF_NEED_RESCHED 标志是否有设置:如果有,则会调用 __schedule() 执行实际的进程调度切换过程。也就是说,延迟调度是一个两步走的过程。
  • 即时调度
    调度切换过程会立即执行,即立即调用 __schedule()

2.1 延迟调度

前面有说过,延迟调度是一个分两步走的过程:

1. 通过设置进程的 _TIF_NEED_RESCHED 标志,发起调度请求。
2. 在延迟调度点检测进程的 _TIF_NEED_RESCHED 标志:
   如果有设置,则调用 __schedule() 执行进程调度切换。

2.1.1 延迟调度第 1 步:发起调度请求

本节讨论延迟调度第 1 步,即设置进程_TIF_NEED_RESCHED 标志、发起调度请求的各种场景。

2.1.1.1 进程创建

系统创建进程时,会试图唤醒新进程执行。不管调用 kernel_thread() 接口创建内核线程,还是调用 fork(), vfork(), clone() 接口创建用户态进程,最终都会调用 _do_fork()

/* kernel/fork.c */

long _do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr,
	      unsigned long tls)
{
	struct task_struct *p;

	...
	p = copy_process(clone_flags, stack_start, stack_size,
			 child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
	...
	if (!IS_ERR(p)) {
		...
		wake_up_new_task(p); /* 唤醒 新创建的 子进程 参与调度 */
		...
	} else {
		...
	}
	...
}
/* kernel/sched/core.c */

void wake_up_new_task(struct task_struct *p)
{
	...
	p->state = TASK_RUNNING; /* 将新进程标记为 TASK_RUNNING 状态 */
	...
	/* 检测新进程是否要抢占当前进程: 可能触发调度(即设置进程的 _TIF_NEED_RESCHED 标志位) */
	check_preempt_curr(rq, p, WF_FORK);
	...
}

void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)
{
	const struct sched_class *class;

	if (p->sched_class == rq->curr->sched_class) { /* 使用 相同调度类 进程 的 抢占检测 */
		/*
		 * 如果抢占条件成立,则调用 resched_curr() 设置 当前进程 
		 * 的 _TIF_NEED_RESCHED 发起调度请求,实际的调度发生调度检
		 * 测点(系统调用返回、中断处理返回 等等情形)。
		 */
		rq->curr->sched_class->check_preempt_curr(rq, p, flags);
	} else { /* 使用 不同调度类 进程 的 抢占检测: 高优先级调度类进程 抢占 低优先级类进程 */
		for_each_class(class) { /* 由 高优先级调度类 往 低优先级调度类 遍历 */
			/*
			 * 如果进程 @p 的调度类优先级 比 当前进程 的 调度类优先级
			 * 要低,结束检测过程:
			 * 低优先级类进程 不允许 抢占 高优先级类进程。
			 */
			if (class == rq->curr->sched_class)
				break;
			if (class == p->sched_class) { /* 高优先级调度类进程 [无条件抢占] 低优先级类进程 */
				resched_curr(rq); /* 发起调度请求 */
				break;
			}
		}
	}

	...
}

/* 设置进程的 _TIF_NEED_RESCHED 标志,发起调度请求 */
void resched_curr(struct rq *rq)
{
	struct task_struct *curr = rq->curr;
	int cpu;

	...

	cpu = cpu_of(rq);

	if (cpu == smp_processor_id()) { /* 在当前 CPU 上发起调度请求 */
		set_tsk_need_resched(curr); /* 设置 _TIF_NEED_RESCHED 标记,发起调度请求 */
		...
		return;
	}
	
	if (set_nr_and_not_polling(curr))
		smp_send_reschedule(cpu); /* 在非当前 CPU 上请求调度 */
	else
		trace_sched_wake_idle_without_ipi(cpu);
}
/* include/linux/sched.h */

static inline void set_tsk_need_resched(struct task_struct *tsk)
{
	set_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
}

从代码分析看出,新进程创建时的调度,是一种延迟调度,进程的调度切换过程不会立即发生。

2.1.1.2 周期性调度

系统时钟中断按 HZ 频率发生,在时钟中断处理过程中,触发周期性调度:

tick_periodic()
	update_process_times()
		scheduler_tick()
/* kernel/sched/core.c */

void scheduler_tick(void)
{
	int cpu = smp_processor_id();
	struct rq *rq = cpu_rq(cpu);
	struct task_struct *curr = rq->curr;
	struct rq_flags rf;

	...
	/*
	 * STOP: task_tick_stop()
	 * DL  : task_tick_dl()
         * RT  : task_tick_rt()
         * CFS : task_tick_fair()
         */
	curr->sched_class->task_tick(rq, curr, 0);
	...
}

这里以 CFS 调度器为例,简要分析下 CFS 调度器周期性调度的逻辑:

/* kernel/sched/fair.c */

static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
{
	struct cfs_rq *cfs_rq;
	struct sched_entity *se = &curr->se;

	for_each_sched_entity(se) {
		cfs_rq = cfs_rq_of(se);
		entity_tick(cfs_rq, se, queued);
	}

	...
}

static void
entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
	/*
	 * o 更新当前进程的运行时间:真实 + 虚拟 时间
	 * o 更新运行队列上进程的最小虚拟运行时间 min_vruntime 
	 */
	update_curr(cfs_rq);

	...

	/*
	 * 运行队列上进程数大于 1 个,随着系统运行,有可能会
	 * 发生抢占,进行调度抢占检测处理。
	 */
	if (cfs_rq->nr_running > 1)
		check_preempt_tick(cfs_rq, curr);
}

static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
	unsigned long ideal_runtime, delta_exec;
	struct sched_entity *se;
	s64 delta;

	/* 计算一个 调度周期 内,  进程 @curr 在运行队列 @cfs_rq 上的【理论真实运行时间】 */
	ideal_runtime = sched_slice(cfs_rq, curr);
	/* 计算进程 @curr 在 本次调度周期内 的 真实运行时间 */
	delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
	if (delta_exec > ideal_runtime) { /* 如果 进程 @curr 的 调度周期 内的 时间份额 已经耗完, */
		/* 
		 * 为保证 调度周期其它进程也得到执行,耗完 本次调度周期
		 * 内 时间份额 的 进程应暂停执行,于是发起重新调度请求。
		 */
		resched_curr(rq_of(cfs_rq));
		...
		return;
	}

	/*
	 * 确保 进程 的 最短运行 时间: 
	 * 进程一次在 CPU 上的 运行时间 不小于 调度粒度 时间
	 */
	if (delta_exec < sysctl_sched_min_granularity)
		return;
	/*
	 * 挑选下一个可运行的进程: 
	 * 即运行队列中 @cfs_rq 中 vruntime 最小的进程。
	 */
	se = __pick_first_entity(cfs_rq);
	/*
	 * 计算运行队列 @cfs_rq 【当前运行进程】 和 
	 * 【挑选的下一个可运行进程】 之间 的 虚拟运行时间差值,
	 * 用该差值来决定 【挑选的下一个可运行进程】 是否抢占。
	 */
	delta = curr->vruntime - se->vruntime;

	/* 当前进程 仍然是 虚拟运行时间最小 的 进程, 则 不发生抢占, 继续 运行 当前进程 */
	if (delta < 0)
		return;
 
	if (delta > ideal_runtime)
		resched_curr(rq_of(cfs_rq)); /* 设置 _TIF_NEED_RESCHED 标志,发起调度请求 */
}
2.1.1.3 进程唤醒

进程等待的事件到达超时时间到期锁持有者释放锁等情形下,将进入进程唤醒过程。进程唤醒过程最终都通过 wake_up() 系列接口来达成:

/* include/linux/wait.h */

#define wake_up(x)   __wake_up(x, TASK_NORMAL, 1, NULL)
/* kernel/sched/wait.c */

__wake_up()
	__wake_up_common_lock()
		__wake_up_common()

static int __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode,
			int nr_exclusive, int wake_flags, void *key,
			wait_queue_entry_t *bookmark)
{
	wait_queue_entry_t *curr, *next;

	if (bookmark && (bookmark->flags & WQ_FLAG_BOOKMARK)) {
		...
	} else
		curr = list_first_entry(&wq_head->head, wait_queue_entry_t, entry);
	
	...

	list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {
		...
		ret = curr->func(curr, mode, wake_flags, key); /* 如 pollwake() */
		...
	}
}

接下来的唤醒过程,按不同的睡眠场景而各有不同,这里以进程因 poll() 陷入睡眠为例,分析其唤醒过程:

pollwake()
	__pollwake()
		default_wake_function()
			try_to_wake_up()

剩余的唤醒过程,不管因什么场景下进入睡眠,都汇聚到到 try_to_wake_up(),我们重点关注调度请求的发起过程

/* kernel/sched/core.c */

try_to_wake_up()
	ttwu_queue()
		ttwu_do_activate()
			ttwu_do_wakeup()
				check_preempt_curr()

看看,执行流程又走到前面分析过的 check_preempt_curr() 的函数,该函数检测唤醒抢占的可能,如果被唤醒进程符合对当前进程发起抢占的条件,则设置 _TIF_NEED_RESCHED 标志,发起抢占调度请求;之后在延迟调度触发点,做实际的进程调度切换工作。

2.1.1.4 CPU 亲和性设置

在设置进程的 CPU 亲和性时,也可能会发起调度请求:

__set_cpus_allowed_ptr()
	...
	if (task_running(rq, p) || p->state == TASK_WAKING) {
		...
		stop_one_cpu(cpu_of(rq), migration_cpu_stop, &arg);
		...
	} else if (task_on_rq_queued(p)) {
		rq = move_queued_task(rq, &rf, p, dest_cpu);
	}
	...

migration_cpu_stop() / move_queued_task()
	...
	check_preempt_curr() /* 可能发起调度请求(即设置 _TIF_NEED_RESCHED 标志)*/
	...
2.1.1.5 CPU 热插拔:启停

CPU 核启用时,可能将一些进程迁移到它上面执行;CPU 停止时,将其上的进程迁移到别的 CPU 上执行。本文对 CPU 核启停触发调用的代码细节不做展开,感兴趣的读者可查阅相关资料。

2.1.1.6 CPU 负载均衡

进程在 CPU 核间进行负载均衡时,也可能触发调度。本文对这些代码细节不做展开,感兴趣的读者可查阅相关资料。

2.1.1.7 NUMA 节点间迁移和均衡

进程在 NUMA 节点间迁移、或做负载均衡时,也可能触发调度。本文对这些代码细节不做展开,感兴趣的读者可查阅相关资料。

2.1.1.8 用户修改调度参数

在用户修改进程调度参数时,也会引发进程调度。Linux 内核向用户提供如下接口修改调度参数:

int setpriority(int which, id_t who, int prio);
int nice(int inc);

int sched_setparam(pid_t pid, const struct sched_param *param);
int sched_setattr(pid_t pid, struct sched_attr *attr,
                   unsigned int flags);
int sched_setscheduler(pid_t pid, int policy,
						const struct sched_param *param);
2.1.1.4.1 setpriority() / nice()
sys_setpriority()
	set_one_prio()
		set_user_nice()

sys_nice()
	set_user_nice(current, nice)

void set_user_nice(struct task_struct *p, long nice)
{
	...
	if (queued) {
		enqueue_task(rq, p, ENQUEUE_RESTORE | ENQUEUE_NOCLOCK);
		/*
		 * If the task increased its priority or is running and
		 * lowered its priority, then reschedule its CPU:
		 */
		if (delta < 0 || (delta > 0 && task_running(rq, p)))
			resched_curr(rq); /* 发起调度请求 */
	}
	...
}
2.1.1.4.2 sched_setparam() / sched_setattr() / sched_setscheduler()
sys_sched_setparam()
	do_sched_setscheduler(pid, SETPARAM_POLICY, param)
		sched_setscheduler(p, policy, &lparam)
			_sched_setscheduler(p, policy, param, true)

static int _sched_setscheduler(struct task_struct *p, int policy,
			       const struct sched_param *param, bool check)
{
	struct sched_attr attr = {
		.sched_policy   = policy, // 修改调度策略
		.sched_priority = param->sched_priority, // 修改调度优先级
		.sched_nice	= PRIO_TO_NICE(p->static_prio), // 修改 nice 值
	};

	...

	return __sched_setscheduler(p, &attr, check, true);
}

__sched_setscheduler()
	check_class_changed(rq, p, prev_class, oldprio)

static inline void check_class_changed(struct rq *rq, struct task_struct *p,
				       const struct sched_class *prev_class,
				       int oldprio)
{
	if (prev_class != p->sched_class) {
		/*
		 * 进程 从 前一调度类别 切出:
		 * STOP: NULL
		 * DL  : switched_from_dl()
	 	 * RT  : switched_from_rt()
		 * CFS : switched_from_fair()
		 */
		if (prev_class->switched_from)
			prev_class->switched_from(rq, p);

		/*
		 * 进程 切入 下一调度类别:
		 * STOP: switched_to_stop()
	 	 * DL  : switched_to_dl()
	 	 * RT  : switched_to_rt()
		 * CFS : switched_to_fair()
		 */
		p->sched_class->switched_to(rq, p);
	} else if (oldprio != p->prio || dl_task(p))
		/*
		 * 进程 优先级 变换:
		 * STOP: prio_changed_stop()
		 * DL  : prio_changed_dl()
		 * RT  : prio_changed_rt()
		 * CFS : prio_changed_fair()
		 */
		p->sched_class->prio_changed(rq, p, oldprio);
}

不管是因为优先级的变化引起调度类别的切换,还是只是仅仅是同一调度类的优先级的变化,在合适的条件下,都会调用 resched_curr() 发起调度请求。

sched_setattr()sched_setscheduler()sched_setparam() 触发调度的流程类似:

sys_sched_setattr()
	__sched_setscheduler(p, attr, true, true)
		// 参考 sched_setparam() 调用流程
sys_sched_setscheduler()
	do_sched_setscheduler(pid, policy, param)
		// 参考 sched_setparam() 调用流程

2.1.2 延迟调度第 2 步:检测调度请求并执行调度切换

本节讨论延迟调度第 2 步,即检测进程的 _TIF_NEED_RESCHED 标志、执行进程调度切换的各种场景。

2.1.2.1 中断异常处理返回

这里以 ARM 架构的中断处理过程为例,来说明中断处理过程退出时发生的延迟调度中断异常既可以发生在内核态,也可以发生在用户态。我们分别从内核态用户态来进行分析调度发生的过程。

2.1.2.1.1 内核态 中断 处理结束时的 调度
/* arch/arm/kernel/entry-armv.S */

	.align 5
__irq_svc:
	svc_entry
	irq_handler /* 处理 内核态 中断 */

	/* 检查进程是否设置了 _TIF_NEED_RESCHED,发起调度请求 */
#ifdef CONFIG_PREEMPT
	ldr r8, [tsk, #TI_PREEMPT]  @ get preempt count
	ldr r0, [tsk, #TI_FLAGS]  @ get flags
	teq r8, #0    @ if preempt count != 0
	movne r0, #0    @ force flags to 0
	tst r0, #_TIF_NEED_RESCHED /* 检查是否有调度需求(通过检查 _TIF_NEED_RESCHED 标记) */
	blne svc_preempt /* 中断处理结束后,发起 内核态 抢占 */
#endif

	svc_exit r5, irq = 1   @ return from exception
 UNWIND(.fnend  )
ENDPROC(__irq_svc)

	.ltorg

#ifdef CONFIG_PREEMPT
svc_preempt:
	mov r8, lr
	/* 发起 内核态 抢占调度 */
1:      bl preempt_schedule_irq  @ irq en/disable is done inside
	ldr r0, [tsk, #TI_FLAGS]  @ get new tasks TI_FLAGS
	tst r0, #_TIF_NEED_RESCHED
	reteq r8    @ go again
	b 1b
#endif

从上面的代码分析可以看到,内核态中断处理结束时的发生调度,要满足两个条件:

. 开启了内核态抢占,即开启了内核配置项 CONFIG_PREEMPT
. 被中断进程设置了 _TIF_NEED_RESCHED,发起了调度请求

这里的调度过程是立即发生的,但发生调度的前提之一是,被中断进程设置了 _TIF_NEED_RESCHED 标志,所以这可以说是延迟调度的一种情形。对进程设置 _TIF_NEED_RESCHED 标志的场景,后面的分析会有讨论。

2.1.2.1.2 用户态 中断、异常 处理结束时的 调度
/* arch/arm/kernel/entry-armv.S */

	.align 5
__irq_usr:
	...
	irq_handler /* 处理 用户态 中断 */
	get_thread_info tsk
	mov why, #0
	b ret_to_user_from_irq /* 从中断返回用户态空间 */
 UNWIND(.fnend  )
ENDPROC(__irq_usr)
/* arch/arm/kernel/entry-common.S */

ENTRY(ret_to_user)
ret_slow_syscall:
	disable_irq_notrace   @ disable interrupts
ENTRY(ret_to_user_from_irq)
	...
	ldr r1, [tsk, #TI_FLAGS]
	tst r1, #_TIF_WORK_MASK
	bne slow_work_pending
no_work_pending:
	...
ENDPROC(ret_to_user_from_irq)
ENDPROC(ret_to_user)

	...
/* 有挂起的工作要做,先做完挂起的工作(如处理进程调度),再返回用户空间 */
slow_work_pending:
	mov r0, sp    @ 'regs'
	mov r2, why    @ 'syscall'
	bl do_work_pending
	...
/* arch/arm/kernel/signal.c */

asmlinkage int
do_work_pending(struct pt_regs *regs, unsigned int thread_flags, int syscall)
{
	/*
	 * The assembly code enters us with IRQs off, but it hasn't
	 * informed the tracing code of that for efficiency reasons.
	 * Update the trace code with the current status.
	 */
	trace_hardirqs_off();
	do {
		if (likely(thread_flags & _TIF_NEED_RESCHED)) { /* 检查是否设置了 _TIF_NEED_RESCHED 标志位 */
			schedule(); /* 执行调度:schedule() -> __schedule(false) */
		} else {
			...
		}
		...
		thread_flags = current_thread_info()->flags;
	} while (thread_flags & _TIF_WORK_MASK);
	return 0;
}

事实上,不光用户态中断会触发可能的进程调度(如果设置了 _TIF_NEED_RESCHED 标志位的话),一些异常也会触发可能的进程调度:

/* arch/arm/kernel/entry-armv.S */

	.align 5
__und_usr: /* 用户模式未定义指令异常入口 */
	...
	badr r9, ret_from_exception
	...

...

	.align 5
__pabt_usr:
	...
	/* fall through */
/*
 * This is the return code to user mode for abort handlers
 */
ENTRY(ret_from_exception)
	...
	// ret_to_user 的定义见前面的代码分析,最终根据是否设置了 _TIF_NEED_RESCHED,
	// 确定是否触发调度过程,即调用 __schedule() 
	b ret_to_user
	...
ENDPROC(__pabt_usr)
ENDPROC(ret_from_exception)
2.1.2.2 系统调用 返回 (用户态) 时 的 调度
/* arch/arm/kernel/entry-common.S */

	.align 5
ENTRY(vector_swi) // 系统调用内核入口
	...
	/* 调用系统调用接口 */
	invoke_syscall tbl, scno, r10, __ret_fast_syscall // 系统调用 return 返回到 __ret_fast_syscall 标号处
	...
	
ret_fast_syscall:
__ret_fast_syscall:
	// 禁用中断
	disable_irq_notrace   @ disable interrupts
	...
	// r1 = thread_info::flags 
	ldr r1, [tsk, #TI_FLAGS]  @ re-check for syscall tracing
	tst r1, #_TIF_SYSCALL_WORK | _TIF_WORK_MASK // 检查是否有挂起的工作要做
	/* 
	 * 检查到有挂起的工作要做,先跳转到 fast_work_pending   
	 * 做完挂起的工作,然后再返回用户空间。
	 */
	bne fast_work_pending
	...

/* Ok, we need to do extra processing, enter the slow path. */
fast_work_pending:
	...
slow_work_pending:
	mov r0, sp    @ 'regs'
	mov r2, why    @ 'syscall'
	bl do_work_pending
	...

最终调用了 do_work_pending()处理调度请求等工作,这在前面已经分析过了,这里就不再赘述。

2.1.2.3 使能抢占时 的 调度
/* include/linux/preempt.h */

#ifdef CONFIG_PREEMPT
#define preempt_enable() \
do { \
	barrier(); \
	if (unlikely(preempt_count_dec_and_test())) \
		__preempt_schedule(); \
} while (0)

#define preempt_enable_notrace() \
do { \
	barrier(); \
	if (unlikely(__preempt_count_dec_and_test())) \
		__preempt_schedule_notrace(); \
} while (0)

...

#else /* !CONFIG_PREEMPT */

#define preempt_enable() \
do { \
	barrier(); \
	preempt_count_dec(); \
} while (0)

#define preempt_enable_notrace() \
do { \
	barrier(); \
	__preempt_count_dec(); \
} while (0)

#endif /* CONFIG_PREEMPT */
/* include/asm-generic/premmpt.h */

#ifdef CONFIG_PREEMPT
extern asmlinkage void preempt_schedule(void);
#define __preempt_schedule() preempt_schedule()
extern asmlinkage void preempt_schedule_notrace(void);
#define __preempt_schedule_notrace() preempt_schedule_notrace()
#endif /* CONFIG_PREEMPT */
/* kernel/sched/core.c */

#ifdef CONFIG_PREEMPT
asmlinkage __visible void __sched notrace preempt_schedule(void)
{
	/*
	 * If there is a non-zero preempt_count or interrupts are disabled,
	 * we do not want to preempt the current task. Just return..
	 */
	if (likely(!preemptible()))
		return;

	preempt_schedule_common();
}
...

static void __sched notrace preempt_schedule_common(void)
{
	do {
		...
		__schedule(true); /* 抢占调度 */
		...
	} while (need_resched());
}

asmlinkage __visible void __sched notrace preempt_schedule_notrace(void)
{
	...
	do {
		...
		__schedule(true); /* 抢占调度 */
		...
	} while (need_resched()); /* 检测 _TIF_NEED_RESCHED,处理 更多的 调度请求 */
}
/* include/linux/sched.h */

static __always_inline bool need_resched(void)
{
	return unlikely(tif_need_resched());
}
/* include/linux/thread_info.h */

#define tif_need_resched() test_thread_flag(TIF_NEED_RESCHED)
/* arch/arm/include/asm/thread_info.h */

#define TIF_NEED_RESCHED 1 /* rescheduling necessary */

#define _TIF_NEED_RESCHED (1 << TIF_NEED_RESCHED)

从代码分析了解到,使能抢占式的调度,发生在内核态,且只有在开启了内核抢占(即使能了配置 CONFIG_PREEMPT)时才会发生。另外,值得注意的是,系统中一些接口封装了对 preempt_enable() 的调用,因此它们也成了延迟调度点,如 spin_unlock()

spin_unlock()
	raw_spin_unlock()
		_raw_spin_unlock()
			__raw_spin_unlock()
				spin_release(&lock->dep_map, 1, _RET_IP_);
				do_raw_spin_unlock(lock);
				preempt_enable();
2.1.2.4 主动插入延迟调度点

在很耗时的代码片段中,调用 cond_resched() 主动插入延迟调度检测点,触发可能的进程调度,以降低系统的响应延迟。如内存压缩场景:

static unsigned long do_shrink_slab(struct shrink_control *shrinkctl,
				    struct shrinker *shrinker,
				    unsigned long nr_scanned,
				    unsigned long nr_eligible)
{
	...
	while (total_scan >= batch_size ||
	       total_scan >= freeable) {
		...
		cond_resched(); /* 内存压缩可能是比较耗时的,主动在循环每一轮进行一次延迟调度 */
	}
	...
}
/* include/linux/sched.h */

#define cond_resched() ({			\
	___might_sleep(__FILE__, __LINE__, 0);	\
	_cond_resched();			\
})
#ifndef CONFIG_PREEMPT
int __sched _cond_resched(void)
{
	if (should_resched(0)) { /* 检测延迟调度请求 */
		preempt_schedule_common(); /* 执行进程调度切换 */
		return 1;
	}
	return 0;
}
EXPORT_SYMBOL(_cond_resched);
#endif
/* include/asm-generic/preempt.h */

static __always_inline bool should_resched(int preempt_offset)
{
	return unlikely(preempt_count() == preempt_offset &&
			tif_need_resched());
}

2.2 即时调度

本节讨论调用 __schedule() 立即执行进程调度切换的即时调度的各种场景。

2.2.1 进程退出

/* kernel/exit.c */

void __noreturn do_exit(long code)
{
	...
	do_task_dead();
}
/* kernel/sched/core.c */

void __noreturn do_task_dead(void)
{
	...
	__schedule(false); /* 当前进程退出,挑选新进程执行,调度过程立即执行 */
	...
}

从代码分析看出,进程退出时的调度是一种调度切换过程立即发生即时调度。发生的场景,如程序调用 exit() 主动退出,或因为某些异常被动退出情形(典型的如 segment fault)。

2.2.2 进程主动放弃 CPU 的情形

进程主动主动放弃 CPU的情形,大概可以分为以下两种:

o 进程进入睡眠
o 进程放弃 CPU
2.2.2.1 进程进入睡眠

进程进入睡眠,意味着一段时间内放弃在 CPU 上执行,那必然要调度一个新的进程来执行,也就是会进程睡眠会导致进程切换调度。

2.2.2.1.1 调用睡眠函数

先看内核态的睡眠函数调用:

/* kernel/time/timer.c */

void msleep(unsigned int msecs)
{
	unsigned long timeout = msecs_to_jiffies(msecs) + 1;

	while (timeout)
		timeout = schedule_timeout_uninterruptible(timeout);
}

signed long __sched schedule_timeout_uninterruptible(signed long timeout)
{
	__set_current_state(TASK_UNINTERRUPTIBLE);
	return schedule_timeout(timeout);
}

signed long __sched schedule_timeout(signed long timeout)
{
	struct timer_list timer;
	unsigned long expire;
	
	...

	expire = timeout + jiffies;

	/* 超时后 通过 process_timeout() 唤醒 */
	setup_timer_on_stack(&timer, process_timeout, (unsigned long)current);
	__mod_timer(&timer, expire, false);
	schedule(); /* 调度出去:调度切换过程立即执行 */
	del_singleshot_timer_sync(&timer);

	/* Remove the timer from the object tracker */
	destroy_timer_on_stack(&timer);

	timeout = expire - jiffies;

out:
	return timeout < 0 ? 0 : timeout;
}

再看用户态睡眠函数 sleep() 调用,其最终实现系统调用 clock_nanosleep()

clock_nanosleep (CLOCK_REALTIME, 0, requested_time, remaining)

而系统调用 clock_nanosleep() 的实现为高精度定时器:

// 启用 CONFIG_POSIX_TIMERS 的情形

/* kernel/time/posix-timers.c */

SYSCALL_DEFINE4(clock_nanosleep, const clockid_t, which_clock, int, flags,
		const struct timespec __user *, rqtp,
		struct timespec __user *, rmtp)
{
	const struct k_clock *kc = clockid_to_kclock(which_clock);
	...

	...
	return kc->nsleep(which_clock, flags, &t); /* common_nsleep() */
}

static int common_nsleep(const clockid_t which_clock, int flags,
			 const struct timespec64 *rqtp)
{
	return hrtimer_nanosleep(rqtp, flags & TIMER_ABSTIME ?
				 HRTIMER_MODE_ABS : HRTIMER_MODE_REL,
				 which_clock);
}
/* kernel/time/hrtimer.c */

hrtimer_nanosleep()
	do_nanosleep()

static int __sched do_nanosleep(struct hrtimer_sleeper *t, enum hrtimer_mode mode)
{
	...
	
	hrtimer_init_sleeper(t, current); /* 配置超时唤醒接口 */

	do {
		set_current_state(TASK_INTERRUPTIBLE); /* 进入 可中断睡眠态 */
		hrtimer_start_expires(&t->timer, mode); /* 启动定时器 */

		if (likely(t->task))
			freezable_schedule(); /* 进行调度 */
		
		...
	} while (t->task && !signal_pending(current));

	...
}

void hrtimer_init_sleeper(struct hrtimer_sleeper *sl, struct task_struct *task)
{
	sl->timer.function = hrtimer_wakeup; /* 设置 睡眠 超时唤醒接口 hrtimer_wakeup() */
	sl->task = task;
}

freezable_schedule()
	schedule()

/* 睡眠 超时唤醒回调接口 */
static enum hrtimer_restart hrtimer_wakeup(struct hrtimer *timer)
{
	struct hrtimer_sleeper *t =
		container_of(timer, struct hrtimer_sleeper, timer);
	struct task_struct *task = t->task;

	t->task = NULL;
	if (task)
		wake_up_process(task); /* 唤醒进程 */

	return HRTIMER_NORESTART;
}
2.2.2.1.2 等待特定事件

进程等待特定事件到来时,也可能进入睡眠,从而引发进程调度。进程通过接口 wait_event() 等待特定事件到来:

/* include/linux/wait.h */

#define wait_event(wq_head, condition)      \
do {          \
	might_sleep();        \
	if (condition)        \
		break;        \
	__wait_event(wq_head, condition);     \
} while (0)

#define __wait_event(wq_head, condition)     \
	(void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0, \
			    schedule())

#define ___wait_event(wq_head, condition, state, exclusive, ret, cmd)		\
({										\
	__label__ __out;							\
	struct wait_queue_entry __wq_entry;					\
	long __ret = ret; /* explicit shadow */					\
										\
	init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0);	\
	for (;;) {								\
		long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state);\
										\
		if (condition)							\
			break;							\
										\
		if (___wait_is_interruptible(state) && __int) {			\
			__ret = __int;						\
			goto __out;						\
		}								\
										\
		cmd; /* schedule(); */						\
	}									\
	finish_wait(&wq_head, &__wq_entry);					\
__out: __ret;									\
})

可以看到,wait_event() 在等待特定事件时,进行了进程调度切换,而其自身则进入不可中断睡眠态(TASK_UNINTERRUPTIBLE),直到特定事件到来时被唤醒。

2.2.2.1.3 锁竞争

进程在试图获取锁时,如果失败,也会导致进入睡眠,从而引发进程调度:

mutex_lock()
	__mutex_lock_slowpath()
		__mutex_lock(lock, TASK_UNINTERRUPTIBLE, 0, NULL, _RET_IP_)
			__mutex_lock_common()
				...
				preempt_disable();
				/* 成功获取到锁的情形,立马就返回了 */
				...
				/* 没有成功获取锁的情形,调度其它进程执行,而进程自身进入睡眠 等待 锁持有者 释放锁 后 被唤醒 */
				set_current_state(state);
				for (;;) {
					...
					schedule_preempt_disabled(); /* 发起调度 */
					...
				}
/* kernel/sched/core.c */

void __sched schedule_preempt_disabled(void)
{
	sched_preempt_enable_no_resched(); /* 当前抢占处于禁用状态,要能调度,先得启用抢占 */
	schedule(); /* 发起调度 */
	preempt_disable(); /* 平衡 进入函数时 的 调度抢占使能计数 */
}

2.2.3 进程放弃 CPU

进程用户态通过 sched_yield() 系统调用主动放弃 CPU,而内核态通过调用 yield() 接口:

/* kernel/sched/core.c */

void __sched yield(void)
{
	set_current_state(TASK_RUNNING);
	sys_sched_yield();
}

SYSCALL_DEFINE0(sched_yield)
{
	...
	
	/*
	 * STOP: yield_task_stop()
	 * DL  : yield_task_dl()
	 * RT  : yield_task_rt()
	 * CFS : yield_task_fair() => 标记为主动放弃 CPU 的进程,指示 CFS 挑选下次执行进程时,尽量不要去选它
	 */
	current->sched_class->yield_task(rq);

	...

	schedule(); /* 发起调度 */

	return 0;
}

sched_yield()进程睡眠有相似的地方,都是主动发起调度放弃 CPU,但它们不同的是:

o sched_yield()
  进程试图放弃 CPU,但下次挑选执行的进程还可能会是它,因此有可能继续执行,且进程一直
  处于【可运行状态】。
o 进程睡眠
  进程睡眠一段时间后被唤醒,也就是说一定会有一段事件得不到执行,且期间处于【睡眠状态】。
  • 16
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值