1.Linux调度的实现
CFS调度算法的实现由四部分组成:
(1)时间记账:
- 所有的调度器都必须对进程运行时间做记账
- CFS使用调度器实体机结构来追踪进程运行记账(定义在sched。好的struct sched_entity)调度器实体结构作为一个名为se的成员变量,嵌入在进程描述符struct task_struct内。
- CFS使用vruntime变量来记录一个程序到底运行了多长时间以及它还应该再运行多久(定义在struct sched_entity中)
- 定义在kemeVsched_fair.c文件中的update_curr()函数实现了该记账功能
- update_curr()计算了当前进程的执行时间,并且将其存放在变量delta_exec中。Update_curr()是由系统定时器周期性调用
(2)进程选择
- CFS调度算法的核心:选择具有最小的Vruntime的任务
- 使用红黑树来组织可运行进程队列,并利用其迅速找到最小vruntime值的进程
- 红黑树(rb_tree)是一个自平衡二叉搜索树,是一种以树节点形式存的储数据,这些数据会对应一个键值,可以通过这些键值来快速检索节点上的数据(重要的是,通过键值检索到对应节点的速度),通常CFS调度算法进行进程选择时会:
A:挑选下一个任务:CFS的进程选择算法简单总结为运行红黑树中的最左边叶子节点所代表的那个进程 ,通过_pick_next_entity()实现,该函数本身不会遍历树找到最左的叶子节点,该值缓存在rb_leftmost字段中,函数返回值就是CFS选择的下一个运行进程。如果返回NULL表示树空没有可运行的进程,这是选择idle任务运行
B:向树中加入进程,发生在进程被唤醒或者通过fork()调用第一次创建进程时。函数enqueue_entity():更新运行时间和其他一些统计数据,然后调用__enqueue_entity(),进行繁重的插入工作,把数据项真的插入到红黑树中。在父节点上调用rb_link_node(),使新插入的进程成为其子节点。函数rb_inserrt_color()更新书的自平衡相关特性。
C:从树中删除进程:删除动作发生在进程堵塞或者终止时。相关函数是dequeue_entity()和__dequeue_entity(),rb_eraase()函数删除进程,更新rb_leftmost缓存,如果删除的是最左节点,还要调用rb_next()按照顺序遍历,找到新的最左节点
(3)调度器入口
- 进程调度的主要入口点函数是schedule(),他会调用pick_next_task(),pick_nexttask()会以优先级为序,从高到低依次检查每一个调度类,并且从最高优先级的调度类中选择最高优先级的进程,pick_next_task()会返回指向下一个可运行进程的指针,没有的话返回NULL。
- Pick_next_task()函数实现会调用pick_next_entity(),而该函数会调用__pick_next_entity()函数。
(4)睡眠和唤醒
- 休眠(被阻塞)的进程处于一个特殊的不可执行状态,为了等待一些事件
- 休眠的一个常见的原因就是文件I/O
- 休眠内核的操作都相同:进程把自己标记成休眠状态,从可执行红黑树中移出,放入等待序列,然后调用schedule()选择和执行一个其他进程
- 唤醒过程:进程被设置为可执行状态,然后再从等待队列中移到可执行红黑树中
- 休眠两种状态:TASK_INTERRUPTIBLE(接收到信号会被唤醒)和TASK_UNINTERRUPTIBLE(忽略信号)
- 等待队列:等待队列是由等待某些事件发生的进程组成的简单链表,休眠通过等待队列进行处理,内核用wake_queue_head_t来表示等待队列。等待队列可以通过DECLARE_WAITQUEUE()静态创建,也可以由init_waitqueue_head()动态创建。
- 进程通过执行以下几个步骤将自己加入到一个等待队列中
1)调用宏DEFINE_WAIT()创建一个等待队列的选项。
2)调用add_wait_queue()把自己加入到队列中。
3)调用prepare_to_wait()方法将进程的状态变更为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE。
4)如果状态被设置成TASK_INTERRUPTIBLE,则信号唤醒进程。
5)当进程被唤醒的时候,会再次检查条件是否为真,真则退出循环,否则再次调用schedule()并且一直重复这步动作。
6)当条件满足后,进程将自己设置为TASK_RUNNING并调用finish_wait()方法把自己移出等待序列。
函数inotify_read():负责从通知文件描述符中读取信息。
- 关于休眠有一点需要注意,存在虚假的唤醒。有时候进程被唤醒并不是因为它所等待的条件达成了才需要用一个循环处理来保证它等待的条件真正达成。
- 唤醒操作通过函数wake_up()进行,它会唤醒指定的等待队列上的所有进程。它调用try_to_wake_up(),该函数负责将进程设置成TASK_RUNNING状态,调用enqueue_task()将此进程放入红黑树中,如果被唤醒的进程优先级比正在执行的进程优先级高,设置need_resched标志。通常哪段代码促成等待条件达成,它就负责随后调用wake_up()函数