- 什么是进程优先级?
进程优先级实际上就是进程获取 CPU 资源分配的先后顺序, 就是指进程的优先权 (priority), 优先权高的进程有优先执行的权力.
- 为什么需要进程优先级?
CPU 资源是有限的, 一个 CPU 只能运行一个进程, 然而进程是可以有多个的, 所以需要存在进程优先级, 来确定进程获取 CPU 资源的先后顺序. 配置进程优先权对多任务环境的 linux 很有用, 可以改善系统性能.
一. 查看进程优先级
ps -l
在 Linux 操作系统中, 使用 ps -l
指令查看进程优先级 (只显示该终端下的进程, 若想显示所有终端下的进程, 需要加上 -a
选项).
UID: 执行进程的用户 ID.
PID: 进程 ID.
PPID: 父进程 ID.
PRI: 进程可被调度的优先级, 其值越小越早被调度.
NI: 进程的 nice 值.
PRI && NI
PRI
PRI 即进程的优先级, 也就是进程被 cpu 执行的先后顺序, PRI 的数值越小, 进程的优先级就越高.
NI
NI 表示进程的 nice 值, 其表示进程可被执行的优先级的修正数值.
PRI 与 NI 之间的关系为: PRI(new) = PRI(old) + nice.
在 Linux 操作系统当中, PRI(old)默认为 80, 即 PRI(new) = 80 + NI.
如此一来, 当将 nice 值设置为负值的时候, 进程的 PRI 数值将变小, 其优先级会变高, 越快被 CPU 执行. 所以, 在 Linux 下调整进程优先级, 就是调整进程的 nice 值, nice 数值的取值范围是 -20 至 19, 一共 40 个级别.
二. 修改进程优先级
top 指令修改进程优先级
top
命令就相当于 Windows 操作系统中的任务管理器, 它能够实时地显示系统当中进程的资源占用动态情况.
对 PID 为 26484 的进程的 nice 值进行修改.
使用 top
命令后按下 r
键,会要求你输入待调整 nice 值的进程的 PID.
键入 26484 后, 会要求你输入调整后的 nice 值.
输入 nice 值后键入 q
即可退出界面吗, 输入的 nice 值为 10, 此时再用 ps
命令查看进程的优先级信息, 即可发现进程的 NI 变成了 10, PRI 变成了 90, 符合公式 PRI(new) = 80 + NI.
提示: 若是想将进程的优先级调高, 需要使用
sudo
命令提升权限.
renice 指令修改进程优先级
renice 要修改的 nice 值 需要修改进程优先级的进程 PID
提示: 若是想使用
renice
命令将进程的优先级调高, 需要使用sudo
命令提升权限.
三. 进程调度
进程描述符 task_struct 中几个与进程优先级相关的字段
c
int prio, static_prio;
unsigned long rt_priority;
static_prio
表示静态优先级, rt_priority
表示实时优先级.
而 prio
表示动态优先级, 值是调度器最终使用的优先级数值,即调度器选择一个进程时作为判断标准的进程优先级.
c
#define MAX_USER_RT_PRIO 100
#define MAX_RT_PRIO MAX_USER_RT_PRIO
#define MAX_PRIO (MAX_RT_PRIO + 40)
prio
值的取值范围是 0 ~ MAX_PRIO - 1, 即 0 ~ 139(包括 0 和 139) , 根据调度策略的不同, prio
的取值范围又可以分为两个区间, 其中区间 0 ~ 99 的为实时进程, 区间 100 ~139 的为普通进程.
数据结构 runqueue
数据结构 runqueue
是 Linux 2.6 调度程序中最重要的数据结构, 系统中的每个 CPU 都有且只有一个运行队列.
如下为Linux 2.6.11版的内核中 runqueue的实现.
c
struct runqueue {
spinlock_t lock;
/*
* nr_running and cpu_load should be in the same cacheline because
* remote CPUs use both these fields when doing load calculation.
*/
unsigned long nr_running;
#ifdef CONFIG_SMP
unsigned long cpu_load;
#endif
unsigned long long nr_switches;
/*
* This is part of a global counter where only the total sum
* over all CPUs matters. A task can increase this counter on
* one CPU and if it got migrated afterwards it may decrease
* it on another CPU. Always updated under the runqueue lock:
*/
unsigned long nr_uninterruptible;
unsigned long expired_timestamp;
unsigned long long timestamp_last_tick;
task_t *curr, *idle;
struct mm_struct *prev_mm;
prio_array_t *active, *expired, arrays[2];
int best_expired_prio;
atomic_t nr_iowait;
#ifdef CONFIG_SMP
struct sched_domain *sd;
/* For active balancing */
int active_balance;
int push_cpu;
task_t *migration_thread;
struct list_head migration_queue;
#endif
#ifdef CONFIG_SCHEDSTATS
/* latency stats */
struct sched_info rq_sched_info;
/* sys_sched_yield() stats */
unsigned long yld_exp_empty;
unsigned long yld_act_empty;
unsigned long yld_both_empty;
unsigned long yld_cnt;
/* schedule() stats */
unsigned long sched_noswitch;
unsigned long sched_switch;
unsigned long sched_cnt;
unsigned long sched_goidle;
/* pull_task() stats */
unsigned long pt_gained[MAX_IDLE_TYPES];
unsigned long pt_lost[MAX_IDLE_TYPES];
/* active_load_balance() stats */
unsigned long alb_cnt;
unsigned long alb_lost;
unsigned long alb_gained;
unsigned long alb_failed;
/* try_to_wake_up() stats */
unsigned long ttwu_cnt;
unsigned long ttwu_attempts;
unsigned long ttwu_moved;
/* wake_up_new_task() stats */
unsigned long wunt_cnt;
unsigned long wunt_moved;
/* sched_migrate_task() stats */
unsigned long smt_cnt;
/* sched_balance_exec() stats */
unsigned long sbe_cnt;
#endif
};
runqueue
数据结构中最重要的字段是与由可运行进程组织起来的链表相关的字段, 运行队列的 arrays
字段是一个包含两个 prio_array_t
结构的数组, 每个 prio_array_t
结构都表示一个可运行进程的集合, 包含一个存放 140 个双向链表表头的结构体数组 (每个链表对应一个相应的进程优先级), 一个优先级位图 bitmap
和一个集合中所包含的进程数量的计数器 nr_active
.
c
typedef struct prio_array prio_array_t;
#define MAX_PRIO (MAX_RT_PRIO + 40)
#define BITMAP_SIZE ((((MAX_PRIO+1+7)/8)+sizeof(long)-1)/sizeof(long))
struct prio_array {
unsigned int nr_active;
unsigned long bitmap[BITMAP_SIZE]; // bitmap[5]
struct list_head queue[MAX_PRIO]; // queue[140]
};
调度过程如下:
- 从 0 下标开始遍历
queue[140]
. - 找到第一个非空队列, 该队列必定为进程优先级最高的队列.
- 拿出选中队列的第一个进程, 开始运行, 调度完成.
- 接着拿出选中队列的第二个进程进行调度, 直到被选中进程队列当中的所有进程都被调度.
- 继续向后遍历
queue[140]
, 寻找下一个非空队列.
为了提高查找非空队列的效率, 使用位图 bitmap[5]
, queue
数组当中一共有 140 个元素, 对应140 个进程优先级的队列, 可以用 5 × 32个比特位表示来表示相应优先级的进程队列是否为空, 这样一来便可以大大提高查找非空进程队列的效率.
活动进程与过期进程
即使具有较高优先级的进程获得了较大的 CPU 时间片, 也不应该使优先级较低的进程无法运行. 为了避免进程饥饿, 当一个进程用完它的时间片时, 它应该被还没有用完时间片的低优先级进程取代. 为了实现这种机制, 调度程序维持两个不相交的可运行进程的集合。
- 活动进程
这些进程还没有用完它们的时间片, 因此允许它们运行.
- 过期进程
这些可运行进程已经用完了它们的时间片, 并因此被禁止运行, 直到所有活动进程都过期.
c
prio_array_t *active, *expired, arrays[2];
如下图所示, runqueue
结构的 active
字段指向 arrays
中两个 prio_array_t
数据结构之一: 对应于包含活动进程 的可运行进程的集合. 相反, expired
字段指向数组中的另一个 prio_array_t
数据结构: 对应于包含过期进程的可运行进程的集合.
arrays
中两个数据结构的作用会发生周期性的变化: 活动进程突然变成过期进程, 而过期进程变为活动进程, 调度程序简单地交换运行队列的 active
和 expired
字段的内容以完成这种变化, 让过期进程变成活动进程, 活动进程变成过期进程, 就相当于又具有了一批新的活动进程, 如此循环进行即可完成对进程的调度.