进程调度
调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间。
调度程序没有太复杂的原理,最大限度地利用处理器时间的原则是,只要有可以执行的进程,那么就总会有进程正在执行。
一、多任务
多任务系统可以划分为两类:非抢占式多任务和抢占式多任务。
Linux提供了抢占式的多任务模式,在此模式下,有调度程序来决定什么时候停止一个进程的运行,以便其他进程能够得到执行机会。
这个强制挂起的动作就叫抢占。进程在被抢占之前,能够运行的时间是预先设置好的,叫进程的时间片。
二、Linux 的进程调度
最终在2.6.23中确定了Linux的调度算法,称为“完全公平调度算法”,简称CFS。
三、策略
I/O消耗型和处理器消耗型进程
进程可以被分为I/O消耗型和处理器消耗型。
调度策略通常要在两个矛盾的目标中间寻找平衡:进程响应速度(响应时间短)和最大系统利用率(高吞吐量)。
3.1 进程优先级
调度算法中最基本的一类就是基于优先级的调度。通常做法是优先级高的进程先运行,低的后运行,相同优先级的进程按轮转方式进行调度。
Linux采用了两种不同优先级范围,第一种是用nice值,从-20到+19,默认为0。nice值越大,优先级越低
第二种范围是实时优先级,其值是可配置的。范围是0到99(包括0和99),实时优先级越高,数值越大。
nice和实时优先级是两个互不相交的范畴。
3.2 时间片
时间片是一个数值,它表明进程再被抢占前所能持续运行的时间。过长或过短的设置时间片都不合适,会增加系统开销。
很多操作系统默认10ms,但Linux的CFS调度系统不是直接分配时间片给进程,而是按nice值比例分配进程时间片。
而Linux的抢占也是需要根据时间片的,如果消耗的使用比比当前进程小,则新进程立刻投入运行,抢占当前进程。否则,退出其运行。
四、Linux调度算法
Linux调度器是以模块方式提供的,目的是可以允许不同类型的进程可以针对性的选择调度算法。这种模块化结构被称为调度器类。
任何进程所获得的处理器时间是由它自己和其他所有可运行进程nice值的相对差值决定的。
五、Linux调度的实现
CFS相关代码位于文件kernel/sched_fair.c中,特别的有四个组成部分:
时间记账、进程选择、调度器入口、睡眠和唤醒
5.1 时间记账
所有调度器都必须对进程运行时间做记账,当一个进程时间片减少到0时,就会进行抢占。
5.1.1调度器实时结构
CFS必须维护每个进程运行的时间记账,因为它需要确保公平分配处理器运行时间。在文件<linux/sched.h>的struct_sched_entity中,来追踪进程运行记账:
struct sched_entity { struct load_weight load; /* for load-balancing 负荷权重,决定进程在CPU上运行时间和被调度次数*/ struct rb_node run_node; /* 树节点,用于在红黑树上组织排序 */ struct list_head group_node; /* 实体所在的进程组 */ unsigned int on_rq; /* 是否在就绪队列上 */ u64 exec_start; /* 调度器上次更新这个实例的时间(实际时间) */ u64 sum_exec_runtime; /* 自进程启动起来,运行的总时间(实际时间) */ u64 vruntime; /* 该调度实体运行的虚拟时间 */ u64 prev_sum_exec_runtime; /* 进程在上次被撤销cpu时,运行的总时间(实际时间) */ u64 nr_migrations; /* 此调度实体中进程移到其他CPU组的数量 */ #ifdef CONFIG_SCHEDSTATS struct sched_statistics statistics; /* 用于统计一些数据 */ #endif #ifdef CONFIG_FAIR_GROUP_SCHED struct sched_entity *parent; /* 父亲调度实体指针 */ /* rq on which this entity is (to be) queued: */ struct cfs_rq *cfs_rq; /* 实体所处红黑树运行队列 */ /* rq "owned" by this entity/group: */ struct cfs_rq *my_q; /* 实体的红黑树运行队列,如果NULL表明其是一个进程 */ #endif };
调度器实体作为一个se的成员变量,嵌入在struct task_struct内。
5.1.2 虚拟实时
vruntime存放进程运行的虚拟时间,该运行时间是进过了所有可运行进程总数标准化的。
定义在kernel/sched_fair.c中的update_curr()函数实现了该记账功能。
static void update_curr(struct cfs_rq *cfs_rq) { struct sched_entity *curr = cfs_rq->curr; u64 now = rq_of(cfs_rq)->clock_task; unsigned long delta_exec; if (unlikely(!curr)) return; /* * Get the amount of time the current task was running * since the last time we changed load (this cannot * overflow on 32 bits): */ /* 计算当前执行时间,存放在变量delta_exec */ delta_exec = (unsigned long)(now - curr->exec_start); if (!delta_exec) return; /* 传递给__update_curr */ __update_curr(cfs_rq, curr, delta_exec); curr->exec_start = now; if (entity_is_task(curr)) { struct task_struct *curtask = task_of(curr); trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime); cpuacct_charge(curtask, delta_exec); account_group_exec_runtime(curtask, delta_exec); } } static inline void __update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr, unsigned long delta_exec) { unsigned long delta_exec_weighted; /* 根据当前可运行进程总数对运行时间进行加权计算 */ schedstat_set(curr->statistics.exec_max, max((u64)delta_exec, curr->statistics.exec_max)); curr->sum_exec_runtime += delta_exec; schedstat_add(cfs_rq, exec_clock, delta_exec); delta_exec_weighted = calc_delta_fair(delta_exec, curr); /* 权重值与当前运行进程的vruntime相加 */ curr->vruntime += delta_exec_weighted; update_min_vruntime(cfs_rq); #if defined CONFIG_SMP && defined CONFIG_FAIR_GROUP_SCHED cfs_rq->load_unacc_exec_time += delta_exec; #endif }
update_curr()由系统定时器周期调用,无论进程处于可运行态,还是阻塞处于不可运行态。
以此,vruntime可以准确测量给定进程的运行时间。
5.2 进程选择
当CFS需要选择下一个运行进程时,它会挑一个具有最小vruntime的进程。
这就是CFS调度算法的核心:选择具有最小vruntime的任务。
5.2.1 挑选下一个任务
CFS的进程选择算法可简单总结为“运行rbtree树中最左边叶子节点所代表的那个进程。
实现这一过程的函数是__pick_next_entity(),定义在文件kernel/sched_fair.c中
static struct sched_entity *__pick_next_entity(struct sched_entity *se) { struct rb_node *next = rb_next(&se->run_node); if (!next) return NULL; return rb_entry(next, struct sched_entity, run_node); } /* 函数本省不会遍历找到最左叶子节点,该值已经被缓存在rb_leftmost字段中 * 如果返回值是NULL,表明没有最左叶子节点 */
5.2.2 向书中加入进程
CFS将进程加入rbtree,缓存最左叶子节点。发生在进程变为可运行状态或者是通过fork()调用第一次创建进程时。
enqueue_entity()函数实现了这一目的,在文件kernel/sched_fair.c中:
static void enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) { /* * 通过调用update_curr(),在更新min_vruntime之前先更新规范化的vruntime */ if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING)) se->vruntime += cfs_rq->min_vruntime; /* * 更新"当前任务"的运行时统计数据 */ update_curr(cfs_rq); update_cfs_load(cfs_rq, 0); account_entity_enqueue(cfs_rq, se); update_cfs_shares(cfs_rq); if (flags & ENQUEUE_WAKEUP) { place_entity(cfs_rq, se, 0); enqueue_sleeper(cfs_rq, se); } update_stats_enqueue(cfs_rq, se); check_spread(cfs_rq, se); if (se != cfs_rq->curr) __enqueue_entity(cfs_rq, se); se->on_rq = 1; if (cfs_rq->nr_running == 1) list_add_leaf_cfs_rq(cfs_rq); }
该函数更新运行时间和其他一些统计数据,把数据项真正插入到红黑树中:
/* 把一个调度实体插入红黑树中 */ static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) { struct rb_node **link = &cfs_rq->tasks_timeline.rb_node; struct rb_node *parent = NULL; struct sched_entity *entry; s64 key = entity_key(cfs_rq, se); int leftmost = 1; /* 在红黑树中查找合适的位置 */ while (*link) { parent = *link; entry = rb_entry(parent, struct sched_entity, run_node); /* 我们并不关心冲突,具有相同键值的结点呆在一起 */ if (key < entity_key(cfs_rq, entry)) { link = &parent->rb_left; } else { link = &parent->rb_right; leftmost = 0; /* 右分支说明不是最小 */ } } /* 维护一个缓存,其中存放树最左叶子节点(也就是最常使用的) */ if (leftmost) cfs_rq->rb_leftmost = &se->run_node; rb_link_node(&se->run_node, parent, link); /* 插入新结点 */ rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline); /* 着色 */ }
5.2.3 从树中删除进程
CFS删除动作发生在进程堵塞(变为不可运行状态)或者终止(结束运行)时。
static void dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) { /* 更新“当前任务”的运行时统计数据 */ update_curr(cfs_rq); update_stats_dequeue(cfs_rq, se); if (flags & DEQUEUE_SLEEP) { #ifdef CONFIG_SCHEDSTATS if (entity_is_task(se)) { struct task_struct *tsk = task_of(se); if (tsk->state & TASK_INTERRUPTIBLE) se->statistics.sleep_start = rq_of(cfs_rq)->clock; if (tsk->state & TASK_UNINTERRUPTIBLE) se->statistics.block_start = rq_of(cfs_rq)->clock; } #endif } clear_buddies(cfs_rq, se); if (se != cfs_rq->curr) __dequeue_entity(cfs_rq, se); /* 实际的删除工作 */ se->on_rq = 0; update_cfs_load(cfs_rq, 0); account_entity_dequeue(cfs_rq, se); /* 在更新min_vruntime之后对调度实体进行规范化,因为更新可以指向"->curr"项 * 我们需要在规范化的位置反映这一变化 */ if (!(flags & DEQUEUE_SLEEP)) se->vruntime -= cfs_rq->min_vruntime; update_min_vruntime(cfs_rq); update_cfs_shares(cfs_rq); } static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) { if (cfs_rq->rb_leftmost == &se->run_node) { struct rb_node *next_node; next_node = rb_next(&se->run_node); cfs_rq->rb_leftmost = next_node; } /* rb_erase可以完成所有工作 */ rb_erase(&se->run_node, &cfs_rq->tasks_timeline); }
5.3 调度器入口
进程调度的入口函数是schedule(),在文件kernel/sched.c中。是内核用于调度进程调度器的入口:
选择哪个进程可以运行,何时将其投入运行。schedule可以找到最高优先级的调度类,让它找到下一个该运行的进程。
schedule()最重要 的事情是调用pick_next_task(),它会按优先级从高到低依次检查后,调用优先级最高的进程:
/* 挑选最高优先级的任务 */ static inline struct task_struct * pick_next_task(struct rq *rq) { const struct sched_class *class; struct task_struct *p; /* 优化:我们知道如果所有任务都在公平类中 * 那么我们就可以直接调用那个函数 * 所有可运行进程数量等于CFS类对应的可运行进程数 */ if (likely(rq->nr_running == rq->cfs.nr_running)) { p = fair_sched_class.pick_next_task(rq); if (likely(p)) return p; } /* 从最高优先级类开始,遍历每个调度类 */ for_each_class(class) { p = class->pick_next_task(rq); /* 返回下一个可运行进程的指针,没有返回NULL */ if (p) return p; } BUG(); /* the idle class will always have a runnable task */ }
5.4 睡眠和唤醒
休眠(被阻塞)的进程处于一个特殊的不可执行状态。
进程把自己标记成休眠状态,从可执行红黑树中移出,放入等待队列,然后调用schedule()选择和执行一个其他进程。
唤醒则刚好相反:进程被设置为可执行状态,然后再从等待队列中移到可执行红黑树中。
5.4.1 等待队列
休眠通过等待队列处理。等待队列是某些事件发生的进程组组成的简单链表。
wake_queue_head_t 是等待队列的结构,DECLARE_WAITQUEUE()静态创建,init_waitqueue_head()动态创建。
内核中进行休眠的推荐操作相对复杂了一点:
/* 'q'是我们希望休眠的等待队列 */ DEFINE_WAIT(wait); add_wait_queue(q, &wait); while(!condition) { /* 'condition' 是我们在等待的事件 */ prepare_to_wait(&q, &wait, TASK_INTERRUPTIBLE); if(signal_pending(current)) /* 处理信号 */ schedule(); } finish_wait(&q, &wait); /* 1)调用宏DEFINE_WAIT()创建一个等待队列的项 * 2)调用add_wait_queue()把自己加入到队列中。该队列会在进程等待的天骄满足时唤醒它, * 当然我们必须在其他地方撰写相关代码,在事件发生时,对等待队列执行wake_up()操作。 * 3)调用prepare_to_wait()方法将进程的状态变更为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE。 * 而且该函数如果有必要的话会将进程加回到等待队列,这是在接下来的循环遍历中所需要的 * 4)如果状态被设置为TASK_INTERRUPTIBLE,则信号唤醒进程。这就是所谓的伪唤醒 * (唤醒不是因为事件的发生),因此检查并处理信号 * 5)当进程被唤醒的时候,它会再次检查条件是否为真。如果是,它就退出循环,如果不是,它 * 再次调用schedule()并一直重复这步操作。 * 6)当条件满足后,进程将自己设置为TASK_RUNNING并调用finish_wait()方法把自己移出等待队列 */
函数inotify_read(),位于文件fs/notify/inotify/inotify_user.c中。
/* 等待队列的一个典型用法 */ static ssize_t inotify_read(struct file *file, char __user *buf, size_t count, loff_t *pos) { struct fsnotify_group *group; struct fsnotify_event *kevent; char __user *start; int ret; DEFINE_WAIT(wait); start = buf; group = file->private_data; while (1) { prepare_to_wait(&group->notification_waitq, &wait, TASK_INTERRUPTIBLE); mutex_lock(&group->notification_mutex); kevent = get_one_event(group, count); mutex_unlock(&group->notification_mutex); pr_debug("%s: group=%p kevent=%p\n", __func__, group, kevent); if (kevent) { ret = PTR_ERR(kevent); if (IS_ERR(kevent)) break; ret = copy_event_to_user(group, kevent, buf); fsnotify_put_event(kevent); if (ret < 0) break; buf += ret; count -= ret; continue; } ret = -EAGAIN; if (file->f_flags & O_NONBLOCK) break; ret = -EINTR; if (signal_pending(current)) break; if (start != buf) break; schedule(); } finish_wait(&group->notification_waitq, &wait); if (start != buf && ret != -EFAULT) ret = buf - start; return ret; }
5.4.2 唤醒
唤醒操作通过wake_up()进行,它会唤醒指定的等待队列上的所有进程。
try_wake_up()函数负责将进程设置为TASK_RUNNING状态
enqueue_task()将进程放入红黑树中,如果被唤醒的进程优先级比当前的高,还要设置need_resched标志。
六、抢占和上下文切换
上下文切换:从一个可执行进程切换到另一个可执行进程。在kernel/sched.c中的context_switch()函数负责处理。
每当新进程被选出来投入运行时,schedule()就会调用该函数,它完成两个工作:
- 调用switch_mm(),把虚拟内存从上一个进程映射给新进程。在<asm/mmu_context.h>中
- 调用switch_to(),把上一个进程的处理器状态切换到新进程中,在<asm/system.h>中
在进程调度中,need_resched标志和schedule()函数是非常重要的组成。
6.1 用户抢占
用户抢占:内核返回用户空间时,如果need_resched被置位,会导致schedule()被调用。
用户抢占产生的情况:
- 从系统调用返回用户空间时
- 从中断处理程序返回用户空间时
6.2 内核抢占
为了适应抢占,每个进程thread_info引入 preempt_count计数器。计数器初始为0,每当使用锁的时候数值加1,释放减1。当数值为0时,内核可以抢占。
内核抢占会发生在:
- 中断处理程序正在执行,且返回内核空间之前。
- 内核代码再一次具有可抢占性的时候。
- 如果内核中的任务显示地调用schedule()
- 如果内核中的任务阻塞(同样会调用schedule())
七、实时调度策略
Linux提供两种调度策略:SCHED_FIFO和SCHED_RR。而普通的、非实时的调度使用SCHED_NORMAL策略。
SCHED_FIFO:实现了一种简单的、先入先出的调度算法。
SCHED_RR:在耗尽事先分配给它的时间后就不能再继续执行了。
八、与调度相关的系统调用
与调度相关的系统调用函数
nice() /* 设置进程nice值 */ sched_setscheduler() /* 设置进程的调度策略 */ sched_getscheduler() /* 获取进程的调度策略 */ sched_setparam() /* 设置进程的实时优先级 */ sched_getparam() /* 获取进程的实时优先级 */ sched_get_priority_max() /* 获取实时优先级的最小值 */ sched_get_priority_min() /* 获取实时优先级的最小值 */ sched_rr_get_interval() /* 获取进程的时间片值 */ sched_setaffinity() /* 设置进程的处理器的亲和力 */ sched_getaffinity() /* 获取进程的处理器的亲和力 */ sched_yield() /* 暂时让出处理器 */
8.1 与调度策略和优先级相关的系统调用
sched_setscheduler()和sched_getscheduler()分别用于设置和获取进程的调度策略和实时优先级。
最重要的工作在于读取或改写进程tast_struct的policy和rt_priority的值。
sched_get_priority_max()和sched_get_priority_min()用于返回给定调度策略的最大和最小优先级。
8.2 与处理器帮顶有关的系统调用
Linux调度程序提供强制的处理器绑定机制。
8.3 放弃处理器时间
通过sched_yield()系统调用,提供給了一种让进程显式地将处理器时间让给其他等待执行进程的机制。