[linux]进程(五)——进程调度

14,进程调度:

进程调度概念:
进程调度程序决定哪个进程投入运行,何时运行以及运行多长时间。
linux是抢占式多任务操作系统,
linux在2.6.23内核中采用的是“完全公平调度算法”简称CFS

进程调度前提:
cpu一个处理器在同一时刻只能运行一个进程
进程响应快,后台吞吐量大,避免进程饥饿等

linux进程调度的一些策略:
决定什么时候以什么样的方式选择一个新进程运行这组规则称为调度策略。
(1)进程可以分为两种:IO消耗型和处理器消耗型,通常IO消耗型的优先级大于处理器消耗型
(2)进程优先级
(3)时间片,时间片表明了进程被抢占前所能运行的持续的时间

内核态线程(轻量级进程)和用户态进程如何区别调度:

linux下的内核进程称为轻量级进程,每个进程下仅对应一个用户线程;
用户级同一进程下的多个线程,被一一映射到具有相同ID的多个内核进程下。由于具有相同的ID,所以,这些进程可以共享文件和内存等资源。也就是说,LINUX中,ID相同的进程间调度切换时,不需要切换上下文。

 linux用户态一个进程里的多个线程是如何调度的

用户线程之间的调度由在用户空间实现的线程库实现。

进程调度的基本单位:

调度的基本单位是task_struct结构体~,

LINUX下面线程 是 lightweight process就是轻量级的进程。 一句话线程是共享了部分资源(地址空间、文件句柄、信号量等等)的进程。所以线程也按照进程的调度方式来进行调度。

linux进程的调度算法
linux允许多个不同的调度算法并存,每个调度算法都有自己可以负责的一些进程,对于普通进程的调度算法是CFS,实时进程也有自己的调度类
CFS算法:CFS完全放弃了时间片,而是分配给进程一个处理器比重,通过nice值来计算进程获得处理器的使用权重,
CFS为每个进程都引入了获得时间片的底线,称为最小粒度,默认值是1ms
实时调度算法和策略
linux提供了两种实时调度策略,SCHED_FIFO和SCHED_RR,这两种调度算法类总比SCHED_NORMAL被先执行,SCHED_FIFO调度的进程除非被
优先级更高的进程抢占,否则一直运行到自己主动退出。SCHED_FIFO是不带时间片的,而SCHED_RR是带时间片的,SCHED_RR里的进程执行
完分配给它的时间片就不再运行了~

linux下调度的实现

cfs不再说什么优先级,而统一改称权值,不同的权值的进程在相同真实时钟流逝中其虚拟时钟走的不同,也就是其vruntime的增量不同
CFS算法,虚拟运行时间:CFS通过每个进程的虚拟运行时间(vruntime)来衡量哪个进程最值得被调度。CFS中的就绪队列是一棵以vruntime为键值的红黑树,
虚拟时间越小的进程越靠近整个红黑树的最左端。因此,调度器每次选择位于红黑树最左端的那个进程,该进程的vruntime最小
static const int prio_to_weight[40] = {
 /* -20 */     88761,     71755,     56483,     46273,     36291,
 /* -15 */     29154,     23254,     18705,     14949,     11916,
 /* -10 */      9548,      7620,      6100,      4904,      3906,
 /*  -5 */      3121,      2501,      1991,      1586,      1277,
 /*   0 */      1024,       820,       655,       526,       423,
 /*   5 */       335,       272,       215,       172,       137,
 /*  10 */       110,        87,        70,        56,        45,
 /*  15 */        36,        29,        23,        18,        15,
};
一次调度间隔的虚拟运行时间 = 实际运行时间 * (NICE_0_LOAD / 权重) 
其中,NICE_0_LOAD是nice为0时的权重。也就是说,nice值为0的进程实际运行时间和虚拟运行时间相同。
权重越大,虚拟运行时间越小,进程获得调度的概率就会更大。

那么被调用的进程会运行多久了:一个进程被调度的最大一直运行时间=(系统调度周期)*(本进程的权值/本运行队列的总权值)

cfs如何保证一段时间内每个进程都能得到执行?答:内核有一个调度延迟的概念,即保证每个进程在一段时间内都能被运行一次的时间间隔,通过/proc/sys/kernel/sched_latency_ns控制,默认为20ms,第二个控制参数sched_nr_latency表示一个调度延迟周期内处理的最大的活动进程数目。

当一个系统调度周期结束后,系统的虚拟时间归一化~都变为一样~


真实时钟步伐


进程1(权值1)虚拟时钟步伐


进程2(权值2)虚拟时钟步伐


进程3(权值3)虚拟时钟步伐


1/6


2/6


0


0


2/6


2/6


1/6


0


3/6


2/6


1/6


1/9


4/6


2/6


1/6


2/9(vruntime依然最小,依然是进程3运行)


5/6


2/6


2/6


2/9


6/6


2/6


2/6


3/9


7/6


4/6


2/6


3/9


8/6


4/6


3/6


3/9


9/6


4/6


3/6


4/9


10/6


4/6


3/6


5/9


11/6


4/6


4/6


5/9


12/6


4/6


4/6


6/9


13/6


6/6


4/6


6/9


14/6


6/6


5/6


6/9


15/6


6/6


5/6


7/9


16/6


6/6


5/6


8/9


17/6


6/6


6/6


8/9


18/6


6/6


6/6


9/9

图表1.cfs原理实例表(黑体为运行路径)

当一个进程新加入到队列中应该如何决定其虚拟运行时间?

:在place_entity()函数,这个函数有一个initial参数,这是为了区别两种不同情况下的vruntime调整,一种情况是新创建进程的情况,它的initial值为1.另外一种情况是唤醒一个进程时的vruntime调整.,在第一种情况下,新创建的进程都以cfs_rq->min_vruntime都为一个基础,在其后的一段时间后得到调度.这段时间的大小为sched_vslice(),它其实就是将sched_slice()转换为虚拟运行时间,在第二种情况下,是调整唤醒进程的vruntime值,这种情况比较复杂.首先,为什么要对唤醒进程的时间进行调整呢?我们来考虑一个这样的问题:假设进程A睡眠了较长时间,以后被唤醒,如果没有调整其vruntime值,就会使得其vruntime远小于cfs_rq->min_vruntime.这样的后果就是,在其后相当长的一段时间内,进程A都会独占CPU,造成了其它进程不能及时得到响应.那怎么调整呢?有以下两个情况:
1):如果进程睡眠的时间很短,也就是se->vruntime仍然大于cfs_rq->min_vruntime.这种情况下,不需要对se->vruntime进程调整.
2):如果进程睡眠时间较长,那当然要对睡眠进程的时间做一个补偿,因此,就将cfs_rq->min_vruntime适当减一个差值做为进程的vruntime值,当然,这种情况下,不能使调整之后的值比之前的vruntime还要小

pick_next_task_fair()函数用于从cfs_rq中选择下一个应该被调度的进程,红黑树中最左边的节点虚拟时间比当前进程虚拟时间小就一定会被调度?调度粒度

调度周期保存在sysctl_sched_wakeup_granularity中,最小为4ms?

进程调度涉及的相关数据结构

 

(1)runqueue

每个CPU都有一个属于自己的就绪队列
每个 CPU 的就绪队列按时间片是否用完分为两部分,分别通过 active 指针和 expired 指针访问,active 指向时间片没用完、当前可被调度的就绪进程,
expired 指向时间片已用完的就绪进程。每一类就绪进程都用一个 struct prio_array 的结构表示:

[cpp] view plaincopy

  1. struct prio_array {
  2. int nr_active;      /* 本进程组中的进程数 */
  3. struct list_head queue[MAX_PRIO];
  4. /* 以优先级为索引的 HASH 表,见下 */
  5. unsigned long bitmap[BITMAP_SIZE];
  6. /* 加速以上 HASH 表访问的位图,见下 */
  7. };

每个cpu的就绪队列的结构体是struct rq,如下所示:

[cpp] view plaincopy

  1. struct rq{
  2. unsigned long_nr_running;
  3. struct cfs_rq cfs;   //CFS调度器
  4. struct rt_rq rt;
  5. ‘‘‘‘‘
  6. };
  7. struct cfs_rq {
  8. struct load_weight load;
  9. unsigned long nr_running;
  10. u64 min_vruntime;           //记录了队列上所有进程的最小运行虚拟时间
  11. struct rb_root tasks_timeline;
  12. struct rb_node *rb_leftmost;       //红黑树最左边的节点,即即将被调度的进程
  13. struct sched_entity *curr;

(2)进程描述符中与调度相关的几个结构体

[cpp] view plaincopy

  1. struct task_struct {
  2. ...
  3. int prio, static_prio, normal_prio;
  4. unsigned int rt_priority;
  5. struct list_head run_list;
  6. const struct sched_class *sched_class;     //该进程所属于的调度器类
  7. struct sched_entity se;             //可调度实体,调度器可以根据此来操作各个进程
  8. unsigned int policy;
  9. cpumask_t cpus_allowed;         //用来限制进程在哪个cpu运行
  10. unsigned int time_slice;
  11. ...

进程调度涉及的具体代码:
调度器的主入口函数是schedule()函数,在schedule()里会去挑选优先级最高的一个调度类,每个调度类都有一个自己的一个可运行队列,
然后由调度类去挑选谁是下一个运行的进程。schedule()函数里会调用的是pick_next_task()函数,
进程的睡眠和唤醒
进程的睡眠过程为:进程把自己设置为休眠状态,从可执行的红黑树移出,放入等待队列,然后调用schedule()函数选择和执行下一个进程,
进程的唤醒过程和进程的睡眠过程正好相反。
等待队列:
等待队列就是等待一些事情发生的进程组成的链表,进程休眠就是将自己放入等待队列

typedef  struct __wait_queue_head  wait_queue_head_t;
linux内核使用等待队列的步骤
1,声明一个等待队列头:
wait_queue_head_t read_wq;
2,通过init_waitqueue_head()来动态创建一个等待队列
init_waitqueue_head(read_wq);
3,在内核代码中等待某个条件满足从而唤醒等待队列
int ret = wait_event_interruptible(read_wq,rx_done);
4,在其它地方唤醒等待队列

wake_up(&read_wq);

唤醒方式:wake_up (唤醒时要检测条件是否为真,如果还为假则继续睡眠,唤醒前一定要把条件变为真)

等待队列内核代码的实现

linux内个用wake_queue_head_t来代表等待队列,如下所示:

[cpp] view plaincopy

  1. struct  __wait_queue_head {
  2. wq_lock_t  lock;
  3. struct  list_head  task_list;
  4. };
  5. typedef  struct __wait_queue_head  wait_queue_head_t;

等待队列链表中的元素类型为wait_queue_t,我们可以称之为等待队列项:

[cpp] view plaincopy

  1. struct __wait_queue {
  2. unsigned int flags;
  3. #define WQ_FLAG_EXCLUSIVE        0x01  //独占等待标志,一次只唤醒一个进程
  4. void *private;        //private成员用来存放已经被初始化的current进程
  5. wait_queue_func_t func;       //默认的唤醒函数,默认等待队列项名字为_wait,默认唤醒函数为autoremove_wake_function(),该函数最终调用到try_to_wakeup()函数
  6. struct list_head task_list;
  7. };
  8. #define __wait_event_interruptible(wq, condition, ret)
  9. do {
  10. DEFINE_WAIT(__wait);                                                                                                                          \
  11. for (;;) {
  12. prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE    //如果当前等待队列头为空则将当前进程_wait加入等待队列,并将当前进程状态设置为TASK_INTERRUPTIBLE
  13. if (condition)
  14. break;
  15. if (!signal_pending(current))  //检查当前进程是否有信号处理,返回不为0表示有信号需要处理。
  16. schedule();
  17. continue;                //for (;;) 循环的作用是让进程被唤醒后再一次去检查一下condition是否满足。
  18. 主要是为了防止等待队列上的多个进程被同时唤醒后有可能其他进程已经抢先资源占有过去造成资源又变为不可用,因此最好再判断一下。
  19. }                                                                \
  20. ret = -ERESTARTSYS               //如果返回值-ERESTARTSYS,并且当前调度的信号具备-ERESTARTSYS                                                                         //属性,系统就会在用户信号函数返回之后再执行该系统调用。</span></p>
  21. break;                                                               \
  22. }                                                                         \
  23. finish_wait(&wq, &__wait);                                         \
  24. } while (0)
  25. 68 prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
  26. 69 {
  27. 70         unsigned long flags;
  28. 71
  29. 72         wait->flags &= ~WQ_FLAG_EXCLUSIVE;
  30. 73         spin_lock_irqsave(&q->lock, flags);
  31. 74         if (list_empty(&wait->task_list))
  32. 75                 __add_wait_queue(q, wait);
  33. 76         set_current_state(state);  //将当前进程的状态设置为TASK_INTERRUPTIBLE状态
  34. 77         spin_unlock_irqrestore(&q->lock, flags);
  35. 78 }
  36. 130 static inline void __add_wait_queue_tail(wait_queue_head_t *head,
  37. 131                                                 wait_queue_t *new)
  38. 132 {
  39. 133         list_add_tail(&new->task_list, &head->task_list);
  40. 134 }

[cpp] view plaincopy

  1. <span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255); color: rgb(0, 0, 102);">wake_up()的作用是针对当前等待队列里的等待队列项调用其处理函数,也就是</span><span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255); color: rgb(0, 0, 102);">autoremove_wake_function(),该函数最终调用到try_to_wakeup()函数。</span>

等待队列的运用场景:

当山层给底层下命令读写磁盘数据,由于flash速度没有内存快,可能阻塞

adb_read,adb server端在等待adb client端数据到来的时候

其它:

/proc/sched_debug 包含了调度器当前状态的所有信息

系统何时会调用schedule()函数

答:一:进程从中断,异常,系统调用返回用户态的时候,系统调用 do_fork()函数

二:定时中端,当前进程的时间片用完,  do_timer()函数

三:进程状态切换的时候,进程终止,进程唤醒等,进程调用sleep() exit()等函数,唤醒进程 wake_up_process()函数

四:改变进程的调度策略:setscheduler()函数

五:系统调用礼让:sys_sched_yield()函数

时间: 2024-12-16 20:35:32

[linux]进程(五)——进程调度的相关文章

[linux] 进程五状态模型

运行态:该进程正在执行:就绪态:进程做好了准备,只要有机会就开始执行:阻塞态:进程在某些事件发生前不能执行,如I/O 操作完成:新建态:刚刚创建的进程,操作系统还没有把它加入到可执行进程组中.通常是进程控制块已经创建但还没有加载到内存中的新进程:退出态:操作系统从可执行进程组中释放出的进程,或者是因为它自身停止了,或者是因为某种原因被取消. 进程的三态转换 原文地址:https://www.cnblogs.com/taoshihan/p/11279814.html

Linux进程调度器概述--Linux进程的管理与调度(十五)

日期 内核版本 架构 作者 GitHub CSDN 2016-06-14 Linux-4.6 X86 & arm gatieme LinuxDeviceDrivers Linux进程管理与调度 内存中保存了对每个进程的唯一描述, 并通过若干结构与其他进程连接起来. 调度器面对的情形就是这样, 其任务是在程序之间共享CPU时间, 创造并行执行的错觉, 该任务分为两个不同的部分, 其中一个涉及调度策略, 另外一个涉及上下文切换. 什么是调度器 通常来说,操作系统是应用程序和可用资源之间的媒介. 典型

Linux进程调度器的设计--Linux进程的管理与调度(十七)

日期 内核版本 架构 作者 GitHub CSDN 2016-06-14 Linux-4.6 X86 & arm gatieme LinuxDeviceDrivers Linux进程管理与调度 前景回顾 进程调度 内存中保存了对每个进程的唯一描述, 并通过若干结构与其他进程连接起来. 调度器面对的情形就是这样, 其任务是在程序之间共享CPU时间, 创造并行执行的错觉, 该任务分为两个不同的部分, 其中一个涉及调度策略, 另外一个涉及上下文切换. 内核必须提供一种方法, 在各个进程之间尽可能公平地

Linux环境编程之进程(五):竞争条件以及exec函数

(一) 当多个进程企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,就认为它们发生了竞争关系.避免竞争的条件,给出apue上的一个代码吧: #include "apue.h" static void charatatime(char *); int main(void) { pid_t pid; TELL_WAIT(); /*set things up for TELL_XXX & WAIT_XXX*/ if((pid == fork()) < 0){ e

Linux下0号进程的前世(init_task进程)今生(idle进程)----Linux进程的管理与调度(五)

日期 内核版本 架构 作者 GitHub CSDN 2016-05-12 Linux-4.5 X86 & arm gatieme LinuxDeviceDrivers Linux进程管理与调度-之-进程的创建 前言 Linux下有3个特殊的进程,idle进程(PID = 0), init进程(PID = 1)和kthreadd(PID = 2) * idle进程由系统自动创建, 运行在内核态 idle进程其pid=0,其前身是系统创建的第一个进程,也是唯一一个没有通过fork或者kernel_t

Linux进程管理与调度-之-目录导航【转】

转自:http://blog.csdn.net/gatieme/article/details/51456569 版权声明:本文为博主原创文章 && 转载请著名出处 @ http://blog.csdn.net/gatieme 目录(?)[-] 项目链接 进程的描述 进程的创建 进程的加载与运行 进程的退出 进程的调度 调度普通进程-完全公平调度器CFS 日期 内核版本 架构 作者 GitHub CSDN 2016-07-21 Linux-4.6 X86 & arm gatieme

Linux进程信息的深入分析

Linux下进程信息的深入分析 这里我们主要介绍进程的状态,进程的状态可以通过/proc/PID/status来查看,也可以通过/proc/PID/stat来查看.如果说到工具大家用的最多的ps也可以看到进程的信息.这里我们通过/proc/PID/status来分析进程的信息.在2.6.18之后的内核,多了capibilty/cpusets等信息.[[email protected] 1499]# ps -He[[email protected] 1499]# cd /proc/1499/[[e

Linux自学笔记——linux进程及作业管理

内核的功用主要有进程管理.文件系统.网络功能.内存管理.驱动程序.安全功能这几个方面,本文主要讨论linux进程及作业管理. 进程管理: 进程,process,运行中程序的一个副本,且存在生命周期: 进程分为CPU bound和I/Obound. CPU bound:CPU密集型(类似于高清视频) I/O bound:IO密集型(编辑器等频繁IO操作) Linux内核存储进程信息的固定格式为:task struct(结构体) 多个任务的task struct组件的链表:task list 进程创

第一次作业:基于Linux进程模型分析

一.关于线程和进程 1.进程  进程是指在系统中正在运行的一个应用程序 2.线程 线程是系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元 3.进程与线程的关系  · 对于操作系统而言,其调度单元是线程.一个进程至少包括一个线程,通常将该线程称为主线程. · 一个进程从主线程的执行开始进而创建一个或多个附加线程,就是所谓基于多线程的多任务 · 一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行. · 相对进程而言,线程是一个更加接近于执行体的概念,它可以与同