Linux 进程优先级

1. 前言

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

2. 进程优先级概述

进程的优先级值越小,表示进程的优先级越高系统将为其分配更多的 CPU 资源

2.1 普通进程的优先级

普通进程,在目前绝大多数 Linux 系统上,是指通过 CFS(Completely Fair Scheduler) 调度类别 fair_sched_class 的进程。

普通进程的优先级,其数值位于半开半闭区间为 [100, 140)

/* include/linux/sched/prio.h */

#define MAX_NICE	19
#define MIN_NICE	-20
#define NICE_WIDTH	(MAX_NICE - MIN_NICE + 1) /* 19 - (-20) + 1 = 40 */

...

/*
 * Priority of a process goes from 0..MAX_PRIO-1, valid RT
 * priority is 0..MAX_RT_PRIO-1, and SCHED_NORMAL/SCHED_BATCH
 * tasks are in the range MAX_RT_PRIO..MAX_PRIO-1. Priority
 * values are inverted: lower p->prio value means higher priority.
 *
 * The MAX_USER_RT_PRIO value allows the actual maximum
 * RT priority to be separate from the value exported to
 * user-space.  This allows kernel threads to set their
 * priority to a value higher than any user task. Note:
 * MAX_RT_PRIO must not be smaller than MAX_USER_RT_PRIO.
 */

#define MAX_USER_RT_PRIO	100
#define MAX_RT_PRIO		MAX_USER_RT_PRIO /* 100 */

#define MAX_PRIO		(MAX_RT_PRIO + NICE_WIDTH) /* 100 + 40 = 140 */
#define DEFAULT_PRIO		(MAX_RT_PRIO + NICE_WIDTH / 2) /* 100 + 40 / 2 = 120 */

对于普通进程的优先级,读者更熟悉的叫法可能是 nice 值,其数值区间 [-20, 19],普通进程 nice 值和优先级的相互转化公式如下:

/* include/linux/sched/prio.h */

/*
 * Convert user-nice values [ -20 ... 0 ... 19 ]
 * to static priority [ MAX_RT_PRIO..MAX_PRIO-1 ],
 * and back.
 */
#define NICE_TO_PRIO(nice)	((nice) + DEFAULT_PRIO) /* nice => prio: [-20, 19] => [100, 139] */
#define PRIO_TO_NICE(prio)	((prio) - DEFAULT_PRIO) /* prio => nice: [100, 139] => [-20, 19] */

2.2 实时进程的优先级

对于大多数普通进程,通过 CFS 调度器能满足业务需求;但对于一些对响应速度有要求的、时间敏感的进程,使用 CFS 调度器可能无法满足需求,为满足这类业务需求,Linux 在系统中了加入实时调度器类别 rt_sched_class

实时调度器调度的进程,其优先级数值位于区间 [0, 99],下面的代码注释非常明确地说明这一点:

/*
 * Priority of a process goes from 0..MAX_PRIO-1, valid RT
 * priority is 0..MAX_RT_PRIO-1, and SCHED_NORMAL/SCHED_BATCH
 * tasks are in the range MAX_RT_PRIO..MAX_PRIO-1. Priority
 * values are inverted: lower p->prio value means higher priority.
 *
 * The MAX_USER_RT_PRIO value allows the actual maximum
 * RT priority to be separate from the value exported to
 * user-space.  This allows kernel threads to set their
 * priority to a value higher than any user task. Note:
 * MAX_RT_PRIO must not be smaller than MAX_USER_RT_PRIO.
 */

#define MAX_USER_RT_PRIO	100
#define MAX_RT_PRIO		MAX_USER_RT_PRIO /* 100 */

3. 进程优先级初始化

Linux 内核通过下面的数据结构来管理进程的各种优先级:

struct task_struct {
	...
	int				prio;
	int				static_prio;
	int				normal_prio;
	unsigned int			rt_priority;
	...
};

上面各数据域的作用在后面将一一加以说明。

3.1 系统中第一个静态创建进程的优先级

/* include/linux/init_task.h */

#define INIT_TASK(tsk)	\
{									\
	... \
	.prio		= MAX_PRIO-20,	/* 120 */				\
	.static_prio	= MAX_PRIO-20,	/* 120 */			\
	.normal_prio	= MAX_PRIO-20,	/* 120 */			\
	.policy		= SCHED_NORMAL,					\
	... \
}

可以看到,系统中第一个进程 init_task 的优先级为 MAX_PRIO-20,即 120,也即缺省普通进程的优先级 DEFAULT_PRIO

3.2 动态创建进程的优先级初始化

除了系统中第一个进程 init_task 外,其它进程都是通过动态方式创建的。动态创建进程的优先级,在创建过程中初始化:

/* kernel/fork.c */

kernel_thread() / clone() / fork() / vfork()
	_do_fork()
		copy_process()
			...
			/* 复制 当前进程 的 优先级 到 新进程 @p */
			p = dup_task_struct(current, node);
			...
			/* Perform scheduler related setup. Assign this task to a CPU. */
			/* 设定 新进程 @p 的 优先级 */
			retval = sched_fork(clone_flags, p);
			...

static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
{
	struct task_struct *tsk;
	...

	tsk = alloc_task_struct_node(node); /* 分配进程结构体 task_struct */
	...
	err = arch_dup_task_struct(tsk, orig); /* 复制旧进程数据到新进程,也包括优先级 */
	...
	return tsk;
	...
}

/* kernel/sched/core.c */

int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
	...
	/*
	 * Make sure we do not leak PI boosting priority to the child.
	 */
	/* 
	 * 用当前进程的 current->normal_prio 重新设定新进程的 p->prio :
	 * 避免当前进程因为某些原因(如为解决优先级反转的优先级继承场景)而 
	 * 临时提升的 优先级传递给子进程。
	 * 
	 * prio 是一个可能会动态变化的优先级。
	 */
	p->prio = current->normal_prio;
	
	...
	
	/* 按优先级来决定进程的调度器类型 */
	if (dl_prio(p->prio)) {
		put_cpu();
		return -EAGAIN;
	} else if (rt_prio(p->prio)) {
		p->sched_class = &rt_sched_class;
	} else {
		p->sched_class = &fair_sched_class;
	}
	...
}

4. 进程优先级的动态修改

进程的优先级,在其创建时进行初始化,从章节 4. 分析中了解到,进程的初始优先级,继承自发起进程创建的当前进程;在进程运行过程中,可以通过系统接口动态改变进程的优先级。动态修改进程优先级的接口分为两类,分别用于普通进程优先修改实时进程优先级修改。

4.1 普通进程的优先级修改

普通进程优先级 范围为 [100, 139],对应 nice 区间为 [-20, 19]普通进程优先级修改的核心接口为 set_user_nice(),按 nice 值来修改普通进程的优先级:

/* kernel/sched/core.c */

void set_user_nice(struct task_struct *p, long nice)
{
	...

	/*
	 * The RT priorities are set via sched_setscheduler(), but we still
	 * allow the 'normal' nice value to be set - but as expected
	 * it wont have any effect on scheduling until the task is
	 * SCHED_DEADLINE, SCHED_FIFO or SCHED_RR:
	 */
	/*
	 * 设置 DL,RT 类进程静态优先级(p->static_prio)。
	 * RT 优先级是通过 sched_setscheduler() 设置的,但仍然允许设置进程的
	 * nice 值。在进程使用 SCHED_DEADLINE、SCHED_FIFO 或 SCHED_RR 实时
	 * 调度策略期间,它不会对调度产生任何影响;只有进程切换为普通进程的调度
	 * 策略时才会对调度产生影响。
	 */
	if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
		p->static_prio = NICE_TO_PRIO(nice);
		goto out_unlock;
	}
	...

	/*
	 * 对普通进程,根据新 nice 值,重新设定进程的: 
	 * . 优先级
	 * . 运行时间权重
	 */
	p->static_prio = NICE_TO_PRIO(nice); /* 普通进程的 nice 值转为静态优先级 p->static_prio */
	set_load_weight(p); /* 设置 普通进程 的 运行时间权重 */
	...
	p->prio = effective_prio(p);
	...
out_unlock:
	...
}

/*
 * Calculate the current priority, i.e. the priority
 * taken into account by the scheduler. This value might
 * be boosted by RT tasks, or might be boosted by
 * interactivity modifiers. Will be RT if the task got
 * RT-boosted. If not then it returns p->normal_prio.
 */
static int effective_prio(struct task_struct *p)
{
	p->normal_prio = normal_prio(p);
	/*
	 * If we are RT tasks or we were boosted to RT priority,
	 * keep the priority unchanged. Otherwise, update priority
	 * to the normal priority:
	 */
	if (!rt_prio(p->prio)) /* 普通进程 */
		return p->normal_prio;
	return p->prio; /* RT(Real Time), DL(Deadline) 进程 */
}

/*
 * Calculate the expected normal priority: i.e. priority
 * without taking RT-inheritance into account. Might be
 * boosted by interactivity modifiers. Changes upon fork,
 * setprio syscalls, and whenever the interactivity
 * estimator recalculates.
 */
/* DL, RT, 普通 进程 优先级 归一化处理 */
static inline int normal_prio(struct task_struct *p)
{
	int prio;

	if (task_has_dl_policy(p)) /* DL(Deadline) 进程 */
		prio = MAX_DL_PRIO-1; /* DL(Deadline) 进程的优先级值为 -1 */
	else if (task_has_rt_policy(p)) /* RT(Real Time) 进程 */
		prio = MAX_RT_PRIO-1 - p->rt_priority;
	else /* 普通进程 */
		prio = __normal_prio(p);
	return prio;
}

/* 普通进程的优先级和 nice 值一一对应: [-20,19] => [100,139] */
static inline int __normal_prio(struct task_struct *p)
{
	return p->static_prio;
}

普通内核线程调用 set_user_nice() 修改优先级示例:

/* mm/kmemleak.c */

/* kmemleak 扫描线程修改其优先级 */
static int kmemleak_scan_thread(void *arg)
{
	...
	set_user_nice(current, 10);
	...
}

用户空间进程可通过系统调用接口 nice(), setpriority() 来修改优先级:

/* kernel/sched/core.c */
SYSCALL_DEFINE1(nice, int, increment)
{
	...
	set_user_nice(current, nice);
	...
}

/* kernel/sys.c */
SYSCALL_DEFINE3(setpriority, int, which, int, who, int, niceval)
{
	...
	switch (which) {
	case PRIO_PROCESS:
		if (who)
			p = find_task_by_vpid(who);
		else
			p = current;
		if (p)
			error = set_one_prio(p, niceval, error);
		break;
	...
	}
	...
}

static int set_one_prio(struct task_struct *p, int niceval, int error)
{
	...
	set_user_nice(p, niceval);
	...
}

Linux 系统还提供了命令行工具 nice, renice 来修改、查询普通进程的 nice 值。

4.2 实时进程的优先级修改

实时进程,包括 RT(Real Time)DL(Deadline) 进程。对于 DL 进程,其优先级固定为 -1;对于 RT 进程,其优先级范围为 [0, 99]。实时进程优先级的修改核心接口为 __sched_setscheduler()

static int __sched_setscheduler(struct task_struct *p,
				const struct sched_attr *attr,
				bool user, bool pi)
{
	/* DL 进程的优先级固定为 -1 (即 MAX_DL_PRIO - 1,MAX_DL_PRIO 为 0) */
	int newprio = dl_policy(attr->sched_policy) ? MAX_DL_PRIO - 1 :
		      MAX_RT_PRIO - 1 - attr->sched_priority;
	...

	...
	rq = task_rq_lock(p, &rf);
	...
	__setscheduler(rq, p, attr, pi);
	...
}

/* Actually do priority change: must hold pi & rq lock. */
static void __setscheduler(struct rq *rq, struct task_struct *p,
			   const struct sched_attr *attr, bool keep_boost)
{
	__setscheduler_params(p, attr);

	/*
	 * Keep a potential priority boosting if called from
	 * sched_setscheduler().
	 */
	p->prio = normal_prio(p);
	if (keep_boost)
		p->prio = rt_effective_prio(p, p->prio);

	/* 按优先级选择调度器算法 */
	if (dl_prio(p->prio))
		p->sched_class = &dl_sched_class;
	else if (rt_prio(p->prio))
		p->sched_class = &rt_sched_class;
	else
		p->sched_class = &fair_sched_class;
}

static void __setscheduler_params(struct task_struct *p,
		const struct sched_attr *attr)
{
	int policy = attr->sched_policy;

	...
	if (dl_policy(policy)) /* DL 进程 */
		__setparam_dl(p, attr);
	else if (fair_policy(policy)) /* 普通进程 */
		p->static_prio = NICE_TO_PRIO(attr->sched_nice); /* nice 值转静态优先级 */

	/*
	 * __sched_setscheduler() ensures attr->sched_priority == 0 when
	 * !rt_policy. Always setting this ensures that things like
	 * getparam()/getattr() don't report silly values for !rt tasks.
	 */
	p->rt_priority = attr->sched_priority; /* 记录设置的 RT 进程优先级 */
	p->normal_prio = normal_prio(p);
	set_load_weight(p);
}

内核空间修改 RT 进程优先级示例:

/* softlockup 内核线程提升为 RT 实时进程,提高被调度的概率 */

/* kernel/watchdog.c */

static void watchdog_enable(unsigned int cpu)
{
	...
	watchdog_set_prio(SCHED_FIFO, MAX_RT_PRIO - 1); /* 设置为 RT 调度类 的 调度策略 SCHED_FIFO */
}

static void watchdog_set_prio(unsigned int policy, unsigned int prio)
{
	struct sched_param param = { .sched_priority = prio };

	sched_setscheduler(current, policy, &param);
}

用户空间修改 RT 进程优先级示例,如音频播放场景:

#include <sched.h>

struct sched_param sched_param;

sched_getparam(0, &sched_param);
sched_param.sched_priority = sched_get_priority_max(SCHED_RR);
sched_setscheduler(0, SCHED_RR, &sched_param);

Linux 用户空间提供命令 chrt 修改实时进程的调度策略和优先级。

5. 进程优先级反转

假定有高、中、低三个优先级的 3 个进程,高、低优先级的两个进程共享了某个资源,且通过锁来互斥访问该资源。在某一时刻,低优先级的获取了资源锁后,被中优先级的进程给抢占了,这样中优先级的进程,比高优先级的进程获得了更多的 CPU 时间,发生了进程优先级反转

解决进程优先级反转基本思路是:进入临界区时,临时提升低优先级进程的优先级,这样避免低优先级进程在临界区时被中优先级的进程抢占。要将低优先级进程优先级临时提升到什么程度,不同的解决方案给出了不同的答案。常见的方案有:Priority inheritancePriority ceiling protocolRandom boosting

当然,既然进程优先级反转是由阻塞引起的,那么还可以通过禁用中断抢占避免阻塞来解决。

Linux 社区公版代码的 CONFIG_RT_MUTEXES 配置功能用来解决优先级反转问题,其核心功能实现在 kernel/locking/rtmutex.ckernel/sched/core.c 中。另外,在 kernel/futex.c 也穿插有相关代码。kernel/sched/core.c 中的函数 rt_mutex_adjust_prio() 就是临时提升进程优先级的核心接口。

对于优先级反转问题,除 Linux 社区公版代码外,各个厂家也私下有各自的解决方案。

对用户空间,可通过 glibc 提供的接口 pthread_mutexattr_setprotocol() 来设置优先级反转处理协议方案。

6. 进程优先级小结

struct task_struct {
	...
	int    prio;
	int    static_prio;
	int    normal_prio;
	unsigned int   rt_priority;
	...
};
  • prio
    存储进程运行时优先级,也称为动态优先级。在大多数时候和 normal_prio 相同,在少数优先级临时提升场景不同于 normal_prio ,如前面提到优先级反转场景临时。
  • static_prio
    存储普通进程 nice 值对应的优先级,也称为静态优先级,对 RT、DL 进程没有意义。取值区间为 [100, 139]
  • normal_prio
    存储进程优先级经 normal_prio() 归一化处理后的值。
    o 对于 DL(Deadline) 进程,normal_prio 的值固定为 -1,表示 DL 进程的优先级。
    o 对于 RT(Real Time) 进程,存储的是将 rt_priority 的值经公式 MAX_RT_PRIO - 1 - rt_priority 转换后的优先级值。取值区间为 [0, 99]
    o 对于普通进程,存储的是将 nice 值转换后的优先级值 static_prio 。普通进程的 normal_priostatic_prio 值相同。取值区间为 [100, 139]
    normal_prio() 的所谓归一化处理,是指将各类型进程的优先级,包括DL 进程RT 进程普通进程统一到同一数轴上来,其区间范围为 [-1, 139]-1 用于 DL 进程,[0, 99] 用于 RT 进程,[100, 139] 用于普通进程
  • rt_priority
    存储用户空间设置的 RT 进程的优先级,取值区间范围为 [0, 99]。对用户空间而言,数值越大,表示 RT 优先级越高,为什么?normal_prio() 对 RT 进程优先级归一化处理代码片段 prio = MAX_RT_PRIO-1 - p->rt_priority; 揭示了这一点。

用户空间可以通过 topps 等工具观察进程优先级。

posted @ 2025-04-07 11:28  JiMoKuangXiangQu  阅读(137)  评论(0)    收藏  举报