做者:世至其美
博客地址:hqber.com
转载须注明以上信息, 更多文章,请访问我的博客:hqber.comnode
最大限度地利用处理器时间,只要有能够执行的进程,那么就总会有进程正在执行。linux
按多任务系统分类算法
按进程分类c#
根据进程的价值和其对处理器的时间需求对进程进行分级。安全
Linux采用了两种优先级范围ide
分配给每一个可运行进程的处理器时间段。模块化
注意:如今操做系统对程序运行都采用了动态时间片计算的方式,而且引入了可配置的计算策略。Linux的“公平”调度算法自己并无采起时间片来达到公平调度。函数
Linux调度算法中,Linux调度器是以模块的方式提供的,这种模块化结构叫作调度器类。每一个调度器都有一个优先级,基础调度器(<kernel/sched/core.c>)会按照优先级顺序遍历调度类,拥有一个可执行进程的最高优先级的调度器类胜出。优化
一个针对普通进程的公平调度类。(SCHED_NORMAL)<kernel/sched/fair.c>Linux的CFS调度器没有规定时间片的大小,经过nice值做为权重调整处理器的使用比,nice值越小的进程被赋予高权重,高优先级,抢得更多的处理器时间使用比。最后经过计算(线程的处理器使用比*总处理器时间)获取每一个进程的处理器时间。抢占时机是当新进程消耗的处理器使用比小于当前的进程,则新进程马上投入运行,抢占当前进程。还设置了每一个进程得到的时间片底线(最小粒度),默认值是1ms,为了不可运行任务数量趋于无限,他们各自获取的处理器使用比和时间片都将趋于0,进程切换开销将是不可接受的。ui
特别关注如下四个方面:
调度器实体结构
CFS使用调度器的实体结构(源代码 | linux/sched.h | v4.19)追踪进程运行记帐,而后将实体结构体做为se的成员变量,嵌入在进程描述符struct task_struct内。
struct sched_entity { /* For load-balancing: */ struct load_weight load;// 权重,跟优先级有关 unsigned long runnable_weight;// 在全部可运行进程中所占的权重 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;// 上个周期中sum_exec_runtime u64 nr_migrations; struct sched_statistics statistics; // 如下省略了一些在特定宏条件下才会启用的变量 };
虚拟实时
vruntime变量存放进程的虚拟时间,单位为ns,和定时器节拍再也不相关。由于优先级相同的全部进程的虚拟运行时间是相同的,全部进程都将接收到相等的处理器份额。处理器只能是依次运行每一个进程,没法实现多任务运行。
所以,CFS使用vruntime变量来记录一个程序到底运行了多长时间以及还须要运行多久。update_curr函数是由系统定时器周期性调用的,不管进程在哪一种状态。
update_curr函数(源代码 | kernel/sched/fair.c | v4.19)
/* * 计算当前进程的执行时间,存放在delta_exec */ static void update_curr(struct cfs_rq *cfs_rq) { struct sched_entity *curr = cfs_rq->curr; u64 now = rq_clock_task(rq_of(cfs_rq)); u64 delta_exec; if (unlikely(!curr)) return; /*获取从最后一次修改负载后当前任务所占用的运行总时间 */ delta_exec = now - curr->exec_start; if (unlikely((s64)delta_exec <= 0)) return; //设置开始时间 curr->exec_start = now; //根据当前进程总数对运行时间进行加权计算 schedstat_set(curr->statistics.exec_max, max(delta_exec, curr->statistics.exec_max)); curr->sum_exec_runtime += delta_exec; schedstat_add(cfs_rq->exec_clock, delta_exec); curr->vruntime += calc_delta_fair(delta_exec, curr); update_min_vruntime(cfs_rq); if (entity_is_task(curr)) { struct task_struct *curtask = task_of(curr); trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime); cgroup_account_cputime(curtask, delta_exec); account_group_exec_runtime(curtask, delta_exec); } account_cfs_rq_runtime(cfs_rq, delta_exec); }
在进程选择方面,CFS调度算法核心是选择最小vruntime的任务,CFS是经过红黑树来组织可运行进程队列,并利用其迅速找到最小的vruntime值的进程。(红黑树最左侧的叶子节点)
挑选下一个Task
__pick_next_entity函数(源代码 | kernel/sched/fair.c | v4.19)
static struct sched_entity *__pick_next_entity(struct sched_entity *se) { //获取红黑树中vruntime值最小的可运行进程 struct rb_node *next = rb_next(&se->run_node); if (!next) return NULL; return rb_entry(next, struct sched_entity, run_node); }
若是没有可运行线程,CFS调度器会选择idle的线程执行。
向树中加入进程
当一个新的进程状态转换为可运行时,须要向可运行队列中插入一个新的节点。而这个过程本质上是向红黑树中插入新节点的过程。
这会发生在两种状况下:
enqueue_entity函数(源代码 | kernel/sched/fair.c | v4.19)
static void enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) { bool renorm = !(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_MIGRATED); bool curr = cfs_rq->curr == se; /* * 若是要加入的进程就是当前正在运行的进程,从新规范化vruntime */ if (renorm && curr) se->vruntime += cfs_rq->min_vruntime; //更新“当前任务”运行时的统计数据 update_curr(cfs_rq); /* * 若是不是当前正在运行的进程,也要恢复到当前的时间 */ if (renorm && !curr) se->vruntime += cfs_rq->min_vruntime; /* * 更新对应调度器实体的各类记录值 */ //更新同步实体和cfs_rq update_load_avg(cfs_rq, se, UPDATE_TG | DO_ATTACH); //将se加入group中 update_cfs_group(se); //从新分配权重占比,将其新的权重添加到cfs_rq-> load.weight enqueue_runnable_load_avg(cfs_rq, se); account_entity_enqueue(cfs_rq, se); if (flags & ENQUEUE_WAKEUP) place_entity(cfs_rq, se, 0); check_schedstat_required(); update_stats_enqueue(cfs_rq, se, flags); check_spread(cfs_rq, se); if (!curr) __enqueue_entity(cfs_rq, se);//将数据项插入红黑树 se->on_rq = 1; if (cfs_rq->nr_running == 1) { list_add_leaf_cfs_rq(cfs_rq); check_enqueue_throttle(cfs_rq); } }
__enqueue_entity函数(源代码 | kernel/sched/fair.c | v4.19)
/* * 将entity加入红黑树(rb-tree) */ static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) { struct rb_node **link = &cfs_rq->tasks_timeline.rb_root.rb_node; struct rb_node *parent = NULL; struct sched_entity *entry; bool leftmost = true; /* * 在红黑树中搜索合适的位置 */ while (*link) { parent = *link; entry = rb_entry(parent, struct sched_entity, run_node); /* * 具备相同键值的节点会被放在一块儿,键值是vruntime */ if (entity_before(se, entry)) { link = &parent->rb_left; } else { link = &parent->rb_right; leftmost = false; } } //向树中插入子节点 rb_link_node(&se->run_node, parent, link); //更新红黑树的自平衡相关属性,经过leftmost判断该节点是否为vruntime最小进程 rb_insert_color_cached(&se->run_node, &cfs_rq->tasks_timeline, leftmost); }
从树中删除进程
当进程堵塞(不可运行态)或者终止时(结束运行),需从红黑树中删除进程。
dequeue_entity函数(源代码| kernel/sched/fair.c | v4.19)
static void dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) { /* * 更新加载,保持entity和cfs_rq同步 */ //从cfs_rq->load.weight中减去entity的权重 update_curr(cfs_rq); //从cfs_rq->runnable_avg中减去entity负载 update_load_avg(cfs_rq, se, UPDATE_TG); //对于group的entity,更新其权重以反映其组的cfs_rq新份额 dequeue_runnable_load_avg(cfs_rq, se); update_stats_dequeue(cfs_rq, se, flags); clear_buddies(cfs_rq, se); //从树中进行删除节点 if (se != cfs_rq->curr) __dequeue_entity(cfs_rq, se); se->on_rq = 0; account_entity_dequeue(cfs_rq, se); /* * 从新规范化vruntime */ if (!(flags & DEQUEUE_SLEEP)) se->vruntime -= cfs_rq->min_vruntime; /* 返回剩余运行的时间 */ return_cfs_rq_runtime(cfs_rq); update_cfs_group(se); if ((flags & (DEQUEUE_SAVE | DEQUEUE_MOVE)) != DEQUEUE_SAVE) update_min_vruntime(cfs_rq); }
实际对树节点操做的工做由__dequeue_entity()实现的,
__dequeue_entity函数(源代码 | kernel/sched/fair.c | v4.19)
static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) { rb_erase_cached(&se->run_node, &cfs_rq->tasks_timeline); }
rb-tree中实现的树节点删除函数,[stackoverflow | Linux中的EXPORT_SYMBOL和c语言中的extern的区别](https://stackoverflow.com/questions/9836467/whats-meaning-of-export-symbol-in-linux-kernel-code#:~:text=EXPORT_SYMBOL()%20is%20a%20macro,kernel%20modules%20to%20access%20them.)
rb_erase_cached函数( 源代码 | lib/rbtree.c)
void rb_erase_cached(struct rb_node *node, struct rb_root_cached *root) { struct rb_node *rebalance; rebalance = __rb_erase_augmented(node, &root->rb_root, &root->rb_leftmost, &dummy_callbacks); if (rebalance) ____rb_erase_color(rebalance, &root->rb_root, dummy_rotate); } EXPORT_SYMBOL(rb_erase_cached);//EXPORT_SYMBOL只是一种相似于extern的机制,但它是可加载模块之间的参考,而不是文件
进程调度的统一入口是__schedule函数,它会选择一个最高优先级的调度类,每一个调度类都有本身的可运行队列,而后能够知道下一个运行的进程。
__schedule函数(源代码 | kernel/sched/core.c | v4.19 )
static void __sched notrace __schedule(bool preempt) { struct task_struct *prev, *next; unsigned long *switch_count; struct rq_flags rf; struct rq *rq; int cpu; cpu = smp_processor_id(); rq = cpu_rq(cpu); prev = rq->curr; schedule_debug(prev); if (sched_feat(HRTICK)) hrtick_clear(rq); local_irq_disable(); rcu_note_context_switch(preempt); /* * Make sure that signal_pending_state()->signal_pending() below * can't be reordered with __set_current_state(TASK_INTERRUPTIBLE) * done by the caller to avoid the race with signal_wake_up(). * * The membarrier system call requires a full memory barrier * after coming from user-space, before storing to rq->curr. */ rq_lock(rq, &rf); smp_mb__after_spinlock(); /* Promote REQ to ACT */ rq->clock_update_flags <<= 1; update_rq_clock(rq); switch_count = &prev->nivcsw; if (!preempt && prev->state) { if (unlikely(signal_pending_state(prev->state, prev))) { prev->state = TASK_RUNNING; } else { deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK); prev->on_rq = 0; if (prev->in_iowait) { atomic_inc(&rq->nr_iowait); delayacct_blkio_start(); } /* * If a worker went to sleep, notify and ask workqueue * whether it wants to wake up a task to maintain * concurrency. */ if (prev->flags & PF_WQ_WORKER) { struct task_struct *to_wakeup; to_wakeup = wq_worker_sleeping(prev); if (to_wakeup) try_to_wake_up_local(to_wakeup, &rf); } } switch_count = &prev->nvcsw; } //经过优先级获取最高优先级的调度类,而后从最高优先级的调度类中选择最高优先级的进程 next = pick_next_task(rq, prev, &rf); clear_tsk_need_resched(prev); clear_preempt_need_resched(); if (likely(prev != next)) { rq->nr_switches++; rq->curr = next; /* * The membarrier system call requires each architecture * to have a full memory barrier after updating * rq->curr, before returning to user-space. * * Here are the schemes providing that barrier on the * various architectures: * - mm ? switch_mm() : mmdrop() for x86, s390, sparc, PowerPC. * switch_mm() rely on membarrier_arch_switch_mm() on PowerPC. * - finish_lock_switch() for weakly-ordered * architectures where spin_unlock is a full barrier, * - switch_to() for arm64 (weakly-ordered, spin_unlock * is a RELEASE barrier), */ ++*switch_count; trace_sched_switch(preempt, prev, next); /* 上下文切换 */ rq = context_switch(rq, prev, next, &rf); } else { rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP); rq_unlock_irq(rq, &rf); } balance_callback(rq); }
pick_next_task主要功能是从发生调度的CPU运行队列中选择最高优先级的进程。
系统中的调度顺序为:实时进程→普通进程→空闲进程。(rt_sched_class → fair_sched_class → idle_sched_class )
pick_next_task函数(源代码 | kernel/sched/core.c | v4.19)
/* * 挑选最高优先级的任务 */ static inline struct task_struct * pick_next_task(struct rq *rq, struct task_struct *prev) { const struct sched_class *class = &fair_sched_class; struct task_struct *p; /* * 优化:若是当前全部要调度的进程都是普通进程,那么就直接采用普通进程的调度类(CFS) */ //就绪队列中的进程数是否与普通进程的就绪队列中的进程数是否相同 if (likely(prev->sched_class == class && rq->nr_running == rq->cfs.h_nr_running)) { p = fair_sched_class.pick_next_task(rq, prev); if (unlikely(p == RETRY_TASK)) goto again; /* 若是没有cfs调度类的进程处于就绪状态,也就是fair_sched_class->next == idle_sched_class(空闲进程), * 每一个CPU都有一个空闲调度类进程,永远不会阻塞。 */ if (unlikely(!p)) p = idle_sched_class.pick_next_task(rq, prev); return p; } // 若是是实时进程,则遍历调度类 again: for_each_class(class) { p = class->pick_next_task(rq, prev); if (p) { if (unlikely(p == RETRY_TASK)) goto again; return p; } } BUG(); /* the idle class will always have a runnable task */ }
休眠在Linux中有两种状态,一种是TASK_UNINTERRUPTIBLE的进程会忽略信号,另外一种是TASK_INTERRUPTIBLE的进程会在收到信号的时候被唤醒并响应。不过这两种状态的进程是处于同一个等待队列上的,等待事件,不能运行。
等待队列
等待队列的实现只是一个简单的链表,由等待某些事件发生的进程组成。wait_queue_head_t表示链表的头节点,加入了一个自旋锁来保持一致性(等待队列在中断时能够被随时修改)
wait_queue_head_t定义(源代码 | /include/linux/wait.h | v4.19)
struct __wait_queue_head { spinlock_t lock; struct list_head task_list; }; typedef struct __wait_queue_head wait_queue_head_t;
进入休眠的进程须要把本身加入到一个等待队列中,主要流程以下:
add_wait_queue函数(源代码 | /kernel/sched/wait.c | v4.19)
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait) { unsigned long flags; wait->flags &= ~WQ_FLAG_EXCLUSIVE; spin_lock_irqsave(&q->lock, flags); __add_wait_queue(q, wait); spin_unlock_irqrestore(&q->lock, flags); }
__add_wait_queue函数(源代码 | /include/linux/wait.h | v4.19)
static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new) { //将节点加入链表 list_add(&new->task_list, &head->task_list); }
prepare_to_wait函数(源代码 | /kernel/sched/wait.c | v4.19)
void prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state) { unsigned long flags; wait->flags &= ~WQ_FLAG_EXCLUSIVE; spin_lock_irqsave(&q->lock, flags); //等待队列为空,则将当前节点加入到链表 if (list_empty(&wait->task_list)) __add_wait_queue(q, wait); //设置进程当前状态 set_current_state(state); spin_unlock_irqrestore(&q->lock, flags); }
唤醒
唤醒操做主要经过调用wake_up函数,它会唤醒指定等待队列上的全部知足事件的进程,并将对应的进程标记为TASK_RUNNING状态,接着将进程加入红黑树中。具体调用过程以下:
wake_up函数(源代码 | /include/linux/wait.h | v4.19)
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
__wake_up函数(源代码 | /kernel/sched/wait.c | v4.19)
void __wake_up(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, void *key) { unsigned long flags; //互斥锁,保证原子操做 spin_lock_irqsave(&q->lock, flags); __wake_up_common(q, mode, nr_exclusive, 0, key); spin_unlock_irqrestore(&q->lock, flags); }
__wake_up_common函数(源代码 | /kernel/sched/wait.c | v4.19)
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, int wake_flags, void *key) { wait_queue_t *curr, *next; //遍历等待队列 list_for_each_entry_safe(curr, next, &q->task_list, task_list) { unsigned flags = curr->flags; if (curr->func(curr, mode, wake_flags, key) && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive) break; } }
从一个可执行进程切换到另外一个可执行的进程。
context_switch函数(源代码 | /kernel/sched/core.c | v4.19)
static __always_inline struct rq * context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next) { struct mm_struct *mm, *oldmm; prepare_task_switch(rq, prev, next); mm = next->mm; oldmm = prev->active_mm; /* * For paravirt, this is coupled with an exit in switch_to to * combine the page table reload and the switch backend into * one hypercall. */ arch_start_context_switch(prev); if (!mm) { next->active_mm = oldmm; atomic_inc(&oldmm->mm_count); enter_lazy_tlb(oldmm, next); } else switch_mm(oldmm, mm, next);//负责将虚拟内存从上一个进程映射切换到新进程 if (!prev->mm) { prev->active_mm = NULL; rq->prev_mm = oldmm; } /* * Since the runqueue lock will be released by the next * task (which is an invalid locking op but in the case * of the scheduler it's an obvious special-case), so we * do an early lockdep release here: */ lockdep_unpin_lock(&rq->lock); spin_release(&rq->lock.dep_map, 1, _THIS_IP_); /* 负责将上一个进程处理器状态切换到新进程的处理器状态, * 保存、恢复栈信息和寄存器信息,还有其余与体系相关的状态信息 */ switch_to(prev, next, prev); barrier(); return finish_task_switch(prev); }
内核即将返回用户空间的时候,若是need_resched标志位被设置,会致使schedule()被调用,此时就发生了用户抢占。意思是说,既然要从新进行调度,那么能够继续执行进入内核态以前的那个进程,也彻底能够从新选择另外一个进程来运行,因此若是设置了need_resched,内核就会选择一个更合适的进程投入运行。
简单来讲有如下两种状况会发生用户抢占:
Linux和其余大部分的Unix变体操做系统不一样的是,它支持完整的内核抢占。
不支持内核抢占的系统意味着:内核代码能够一直执行直到它完成为止,内核级的任务执行时没法从新调度,各个任务是以协做方式工做的,并不存在抢占的可能性。
在Linux中,只要从新调度是安全的,内核就能够在任什么时候间抢占正在执行的任务,这个安全是指,只要没有持有锁,就能够进行抢占。
为了支持内核抢占,Linux作出了以下的变更:
除了响应中断后返回,还有一种状况会发生内核抢占,那就是内核中的进程因为阻塞等缘由显式地调用schedule()来进行显式地内核抢占。固然,这个进程显式地调用schedule()调度进程,就意味着它明白本身是能够安全地被抢占的,所以咱们不用任何额外的逻辑去检查安全性问题。
下面罗列可能的内核抢占状况:
Linux提供了两种实时调度策略: SCHED_FIFO和SCHED_RR。 而普通的、非实时的调度策略是SCHED_NORMAL。实时进程(SCHED_FIFO和SCHED_RR)比普通进程(SCHED_NORMAL)优先级高,能够进行抢占。
这两种实时调度算法实现的都是静态优先级。内核不为实时进程计算动态优先级。这能保证给定优先级别的实时进程总能抢占优先级比它低的进程。Linux实时调度算法是软实时工做方式,尽可能使进程
SCHED_FIFO实现了一种简单的、先入先出的调度算法, 它不使用时间片。
SCHED_FIFO的进程不基于时间片,一旦处于可执行状态,就会一直执行,直到它本身阻塞或者显式地释放处理器为止。只有较高优先级的SCHED_FIFO或者SCHED_RR任务才能抢占SCHED_FIFO任务。 只要有SCHED_FIFO级进程在执行,其余级别较低的进程就只能等待它结束后才有机会执行,除非它主动让出处理器才会退出。
SCHED_RR是一种带有时间片的SCHED_FIFO。
当SCHED_RR任务耗尽它的时间片,在同一优先级的其余实时进程被轮流调度。时间片只能用来从新调度同一优先级的进程。对于SCHED_FIFO进程,高优先级老是马上抢占低优先级,可是低优先级进程决不能抢占SCHED_RR任务,即便它的时间片耗尽。
Linux提供了一个系统调用族,用于管理与调度程序的相关参数。这些系统调用能够用来操做和处理进程优先级、调度策略及处理器绑定,同时还提供了显式地将处理器交给其余进程的机制。
系统调用 | 描述 |
---|---|
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() | 暂时让出处理器 |
做者:世至其美
博客地址:hqber.com
转载须注明以上信息, 更多文章,请访问我的博客:hqber.com