Linux进程调度笔记 一:Linux进程的四大要素 1:一段供进程执行的程序,该程序能够被多个进程执行。 2:独立的内核堆栈。 3:进程控制快(task_struct:有了这个数据结构,进程才能成为内核调度的一个基本单位接受内核的调度。同时,这个结构还记录着进程所占用的各项资源。 4:独立的存储空间:即拥有专有的用户空间,除了前面的内核空间还有用户空间。 线程:只有前三条,没有第四条。 内核线程:彻底没有用户空间。 用户线程:共享用户空间。 二:Linux进程分类: 1:交互式进程:这些进程常常和用户发生交互,因此花费一些时间等待用户的操做。当有输入时,进程必须很快的激活。一般,要求延迟在50-150毫秒。典型的交互式进程有:控制台命令,文本编辑器,图形应用程序。 2:批处理进程(Batch Process):不须要用户交互,通常在后台运行。因此不须要很是快的反应,他们常常被调度期限制。典型的批处理进程:编译器,数据库搜索引擎和科学计算。 3:实时进程:对调度有很是严格的要求,这种类型的进程不能被低优先级进程阻塞,而且在很短的时间内作出反应。典型的实时进程:音视频应用程序,机器人控制等。 批处理进程可能与I/O或者CPU有关,可是实时进程彻底经过Linux的调度算法识别。 其实交互式进程和批处理进程很难区别。 三:Linux进程优先级 1:静态优先级(priority): 被称为“静态”是由于它不随时间而改变,只能由用户进行修改。它指明了在被迫和其它进程竞争CPU以前该进程所应该被容许的时间片的最大值(20)。 每一个普通进程都一个静态优先级,内核为其分配的优先级数为:100(高优先级)-139(低优先级)。数值越大,优先级越低。新建立的进程通常继承父进程 的优先级,可是用户能够经过给nice()函数传递“nice value“或者setpriority()改变优先级。 2: 动态优先级(counter): counter 即系统为每一个进程运行而分配的时间片,Linux 兼用它来表示进程的动态优先级。只要进程拥有CPU,它就随着时间不断减少;当它为0 时,标记进程从新调度。它指明了在当前时间片中所剩余的时间量(最初为20) 事实上,在进程在调度的时候,调度器只察看动态优先级,其值为100-139。经过下面的公式能够根据静态优先计算出相应的动态优先级。 Dynamicy priority = max (100, min (static priority - bonus + 5, 139)) Bonus:0-10,比5小,下降动态优先级,反之,能够提升动态优先级。Bonus和进程的平均睡眠时间有关。 3: 实时优先级(rt_priority):值为1000。Linux把实时优先级与counter值相加做为实时进程的优先权值。较高权值的进程老是优先于较低权值的进程,若是一个进程不是实时进程, 其优先权就远小于1000,因此实时进程老是优先。 4:Base time quantum:是由静态优先级决定,当进程耗尽当前Base time quantum,kernel会从新分配一个Base time quantum给它。静态优先级和Base time quantum的关系为: (1) 当静态优先级小于120 Base time quantum(in millisecond)= (140 – static priority) * 20 (2) 当静态优先级大于等于120 Base time quantum(in millisecond)= (140 – static priority) * 5 四:Linux 进程的调度算法 1. 时间片轮转调度算法(round-robin):SCHED_RR,用于实时进程。系统使每一个进程依次地按时间片轮流执行的方式。 2. 优先权调度算法:SCHED_NORMAL,用于非实时进程。系统选择运行队列中优先级最高的进程运行。Linux 采用抢占式的优级算法,即系统中当前运行的进程永远是可运行进程中优先权最高的那个。 3. FIFO(先进先出) 调度算法:SCHED_FIFO,实时进程按调度策略分为两种。采用FIFO的实时进程必须是运行时间较短的进程,由于这种进程一旦得到CPU 就只有等到它运行完或因等待资源主动放弃CPU时其它进程才能得到运行机会。 五:Linux 进程的调度时机 1.进程状态转换时: 如进程终止,睡眠等; 2.可运行队列中增长新的进程时; 3.当前进程的时间片耗尽时; 4.进程从系统调用返回到用户态时; 5.内核处理完中断后,进程返回到用户态; 六:进程队列: 对队列都有初始化、添加、删除等功能。 1:运行队列:Linux系统为处于就绪态的进程的队列,只有在这个队列中的进程才有机会得到CPU。 2:等待队列:,Linux系统也为处于睡眠态的进程组建了一个队列。 七:调度使用的数据结构 1:runqueue Runqueu是调度器中很是重要的一个数据结构,每一个CPU都有本身的runqueue。 requeue Type Name Description spinlock_t lock 保护进程列表的自旋锁 unsigned long nr_running runqueue 列表中可运行进程数。 unsigned long cpu_load 基于runqueue平均进程数的CPU 加载因子 unsigned long nr_switches CPU运行的进程切换次数 unsigned long nr_uninterruptible 曾经在runqueue可是如今处于 TASK_UNINTERRUPTIBLE 状态的进程数 unsigned long expired_timestamp 老进程已经插入expired列表中的时间 unsigned long long timestamp_last_tick 最后一次时钟中断的Timestamp值 task_t * curr 当前运行进程描述符的指针 task_t * idle 进程描述符指针,指向当前CPU的swappe进程 struct mm_struct * prev_mm 在进程却换工程中,保存正被替换的进程的地址空间 prio_array_t * active 指向激活进程列表(arrays 中的一个) prio_array_t * expired 指向expired进程列表(arrays 中的一个) prio_array_t [2] arrays 激活和expired进程的2维数组,每一个prio_array_t表明一组可运行进程,140个双向列表,静态bitmap以及这组进程的counter. int best_expired_prio 在expired进程中最低的静态优先级 atomic_t nr_iowait 曾经在runqueue可是如今正在等待I/O操做完成的进程数 struct sched_domain * sd 指向当前CPU的基本调度域 int active_balance 标志一些进程将被从一个requeue转移到其余requeue队列 int push_cpu 没有使用 task_t * migration_thread 内核转移线程的进程描述符 struct list_head migration_queue 将被从requeue中转移的进程列表 九:调度使用的重要函数 调度须要一系列函数配合完成调度这一功能,其中最重要的以下: 调度重要函数 scheduler_tick 更新当前进程的time_slice。该函数有两种调用途径: 1:timer,调用频率为HZ,而且在关中断的状况下调用。 2:fork代码,当改变父进程的timeslice时。 try_to_wake_up 唤醒sleep进程。当进程不在可运行队列时,将其放在可运行队列。 recalc_task_prio 更新进程的动态优先级 schedule 选择一个进程运行 load_balance 保持多系统下runqueue平衡。检查当前CPU,保证一个域中runqueue平衡。 1:在进程却换前,scheduler作的事情 Schedule所做的事情是用某一个进程替换当前进程。 (1) 关闭内核抢占,初始化一些局部变量。 need_resched: preempt_disable( ); prev = current; rq = this_rq( ); 当前进程current被保存在prev,和当前CPU相关的runqueue的地址保存在rq中。 (2) 检查prev没有持有big kernel lock. if (prev->lock_depth >= 0) up(&kernel_sem); Schedule没有改变lock_depth的值,在prev唤醒本身执行的状况下,若是lock_depth的值不是负的,prev须要从新获取kernel_flag自旋锁。因此大内核锁在进程却换过程当中是自动释放的和自动获取的。 (3) 调用sched_clock( ),读取TSC,而且将TSC转换成纳秒,获得的timestamp保存在now中,而后Schedule计算prev使用的时间片。 now = sched_clock( ); run_time = now - prev->timestamp; if (run_time > 1000000000) run_time = 1000000000; (4) 在察看可运行进程的时候,schedule必须关闭当前CPU中断,而且获取自旋锁保护runqueue. spin_lock_irq(&rq->lock); (5) 为了识别当前进程是否已经终止,schedule检查PF_DEAD标志。 if (prev->flags & PF_DEAD) prev->state = EXIT_DEAD; (6) Schedule检查prev的状态,若是它是不可运行的,而且在内核态没有被抢占,那么从runqueue删除它。可是,若是prev有非阻塞等待信号 而且它的状态是TASK_INTERRUPTBLE,设置其状态为TASK_RUNNING,而且把它留在runqueue中。该动做和分配CPU给 prev不同,只是给prev一个从新选择执行的机会。 if (prev->state != TASK_RUNNING && !(preempt_count() & PREEMPT_ACTIVE)) { if (prev->state == TASK_INTERRUPTIBLE && signal_pending(prev)) prev->state = TASK_RUNNING; else { if (prev->state == TASK_UNINTERRUPTIBLE) rq->nr_uninterruptible++; deactivate_task(prev, rq); } } deactivate_task( )是从runqueue移除进程: rq->nr_running--; dequeue_task(p, p->array); p->array = NULL; (7) 检查runqueue中进程数, A:若是有多个可运行进程,调用dependent_sleeper( )函数。通常状况下,该函数当即返回0,可是若是内核支持超线程技术,该函数检查将被运行的进程是否有比已经运行在同一个物理CPU上一个逻辑CPU上的 兄弟进程的优先级低。若是是,schedule拒绝选择低优先级进程,而是执行swapper进程。 if (rq->nr_running) { if (dependent_sleeper(smp_processor_id( ), rq)) { next = rq->idle; goto switch_tasks; } } B:若是没有可运行进程,调用idle_balance( ),从其余runqueue队列中移动一些进程到当前runqueue,idle_balance( )和load_balance( )类似。 if (!rq->nr_running) { idle_balance(smp_processor_id( ), rq); if (!rq->nr_running) { next = rq->idle; rq->expired_timestamp = 0; wake_sleeping_dependent(smp_processor_id( ), rq); if (!rq->nr_running) goto switch_tasks; } } 若是idle_balance( )移动一些进程到当前runqueue失败,schedule( )调用wake_sleeping_dependent( )从新唤醒空闲CPU的可运行进程。 假设schedule( )已经决定runqueue中有可运行进程,那么它必须检查可运行进程中至少有一个进程是激活的。若是没有,交换runqueue中active 和expired域的内容,全部expired进程变成激活的,空数组准备接受之后expire的进程。 if (unlikely(!array->nr_active)) { /* * Switch the active and expired arrays. */ schedstat_inc(rq, sched_switch); rq->active = rq->expired; rq->expired = array; array = rq->active; rq->expired_timestamp = 0; rq->best_expired_prio = MAX_PRIO; } (8) 查找在active prio_array_t数组中的可运行进程。Schedule在active数组的位掩码中查找第一个非0位。当优先级列表不为0的时候,相应的位掩码 北设置,因此第一个不为0的位标示一个有最合适进程运行的列表。而后列表中第一个进程描述符被获取。 idx = sched_find_first_bit(array->bitmap); queue = array->queue + idx; next = list_entry(queue->next, task_t, run_list); 如今next指向将替换prev的进程描述符。 (9) 检查next->activated,它标示唤醒进程的状态。 (10) 若是next是一个普通进程,而且是从TASK_INTERRUPTIBLE 或者TASK_STOPPED状态唤醒。Scheduler在进程的平均睡眠时间上加从进程加入到runqueue开始的等待时间。 if (!rt_task(next) && next->activated > 0) { unsigned long long delta = now - next->timestamp; if (unlikely((long long)(now - next->timestamp) < 0)) delta = 0; if (next->activated == 1) delta = delta * (ON_RUNQUEUE_WEIGHT * 128 / 100) / 128; array = next->array; new_prio = recalc_task_prio(next, next->timestamp + delta); if (unlikely(next->prio != new_prio)) { dequeue_task(next, array); next->prio = new_prio; enqueue_task(next, array); } else requeue_task(next, array); } next->activated = 0; Scheduler区分被中断或者被延迟函数唤醒的进程与被系统调用服务程序或者内核线程唤醒的进程。前者,Scheduler加整个runqueue等待时间,后者只加一部分时间。 2:进程却换时,Scheduler作的事情: 如今,Scheduler已经肯定要运行的进程。 (1) 访问next的thread_info,它的地址保存在next进程描述符的顶部。 switch_tasks: if (next == rq->idle) schedstat_inc(rq, sched_goidle); prefetch(next) (2) 在替换prev前,执行一些管理工做 clear_tsk_need_resched(prev); rcu_qsctr_inc(task_cpu(prev)); clear_tsk_need_resched清除prev的TIF_NEED_RESCHED,该动做只发生在Scheduler是被间接调用的状况。 (3) 减小prev的平均睡眠时间到进程使用的cpu时间片。 prev->sleep_avg -= run_time; if ((long)prev->sleep_avg <= 0) prev->sleep_avg = 0; prev->timestamp = prev->last_ran = now; (4) 检查是否prev和next是同一个进程,若是为真,放弃进程却换,不然,执行(5) if (prev == next) { spin_unlock_irq(&rq->lock); goto finish_schedule; } (5) 真正的进程却换 next->timestamp = now; rq->nr_switches++; rq->curr = next; ++*switch_count; prepare_task_switch(rq, next); prev = context_switch(rq, prev, next); context_switch创建了next的地址空间,进程描述符的active_mm指向进程使用的地址空间描述符,而mm指向进程拥有的地址空间描 述符,一般两者是相同的。可是内核线程没有本身的地址空间,mm一直为NULL。若是next为内核线程,context_switch保证next使用 prev的地址空间。若是next是一个正常的进程,context_switch使用next的替换prev的地址空间。 struct mm_struct *mm = next->mm; struct mm_struct *oldmm = prev->active_mm; if (unlikely(!mm)) { next->active_mm = oldmm; atomic_inc(&oldmm->mm_count); enter_lazy_tlb(oldmm, next); } else switch_mm(oldmm, mm, next); 若是prev是一个内核线程或者正在退出的进程,context_switch在runqueue的prev_mm中保存prev使用的内存空间。 if (unlikely(!prev->mm)) { prev->active_mm = NULL; WARN_ON(rq->prev_mm); rq->prev_mm = oldmm; } 调用switch_to(prev, next, prev)进行prev和next的切换。(参见“进程间的切换“)。 3:进程切换后的工做 (1) finish_task_switch(): struct mm_struct *mm = rq->prev_mm; unsigned long prev_task_flags; rq->prev_mm = NULL; prev_task_flags = prev->flags; finish_arch_switch(prev); finish_lock_switch(rq, prev); if (mm) mmdrop(mm); if (unlikely(prev_task_flags & PF_DEAD)) put_task_struct(prev) 若是prev是内核线程,runqueue的prev_mm保存prev的内存空间描述符。Mmdrop减小内存空间的使用数,若是该数为0,该函数释放内存空间描述符,以及与之相关的页表和虚拟内存空间。 finish_task_switch()还释放runqueue的自选锁,开中断。 (2) 最后 prev = current; if (unlikely(reacquire_kernel_lock(prev) < 0)) goto need_resched_nonpreemptible; preempt_enable_no_resched(); if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) goto need_resched; schedule获取大内核块,从新使内核能够抢占,而且检查是否其余进程设置了当前进程的TIF_NEED_RESCHED,若是真,从新执行schedule,不然该程序结束。