课本第四章读书笔记

进程调度

进程调度程序是确保进程能有效工作的一个内核子系统。负责决定哪个进程投入运行,何时运行以及运行多长时间。

基本工作:最大限度的李颖处理器时间的原则,只要有可执行的进程就总有进程在执行,一旦某一给定时刻会有一些进程无法执行,这些进程等待运行,在一组可运行状态的进程中选择一个来执行

4.1 多任务

多任务操作系统就是能同时并发的执行多个进程的操作系统

在单处理器上:会产生多个进程同时运行的幻觉
在多处理器上:使多个进程真正同时并行的运行

多任务操作系统可以分为两类:抢占式多任务和非抢占式多任务

抢占:调度程序来决定什么时候停止一个进程的运行,以便其他进程能够得到执行机会

时间片:预先设计好的进程在被抢占之前能运行的时间

非抢占:除非进程自己主动停止运行(让步),否则他会一直执行

缺点:程序无法对每个进程该执行多次时间做出同一规定,所以进程独占处理器的时间难以预料
	 一个绝不做出让步的悬挂进程就能是系统崩溃

4.2 Linux的进程调度

从Linux2.5开发系列的内核中进程调度做了大的改进,采用了一种叫做O(1)调度程序的新调度程序,主要引入了今天时间片算法和针对每一处理器的运行队列

由于O(1)虽然对大服务器的工作负载很理想,但是在有很多交互程序要运行的桌面系统上则表现不佳,因为其缺少交互进程

从Linux2.6内核系统开发初期,为了解决这一缺陷引入了新的调度算法“反转楼梯最后期限法”,最终在2.6.23版本中代替了O(1)算法,此时被称为完全公平调度算法(CFS)

4.3 策略

策略决定调度程序在何时让什么程序运行。

调度器的策略往往决定整个系统的整体印象,并且要优化使用处理器时间

4.3.1 I/O消耗型和处理器消耗型的进程

● I/O消耗型

大部分时间用来提交I/O请求或者是等待I/O请求。因此这样的程序通常处于可运行状态

● 处理器消耗型

进程把时间大多数用在执行代码上,除非被抢占,否则一直执行

从响应速度考虑,调度策略往往降低他们的调度频率

这种划分并非绝对,进程可以同时展示这两种行为

调度策略往往要在两个矛盾的目标中间寻找平衡:进程响应迅速(时间短)和最大系统利用率(高吞吐量)

Unix系统的调度程序更倾向于I/O消耗型程序,以提供更好的程序响应速度

Linux为了保证交互式应用和桌面系统的性能,对进程的响应做了优化(缩短响应时间),但也未忽略处理器消耗型的进程

4.3.2 进程优先级

调度算法中最基本的一类就是基于优先级的调度:根据进程的价值和对处理器时间的需求来对程序分级

调度程序总是选择时间片未用尽且优先级最高的进程运行。用户和系统都可以通过设置进程的优先级来影响进系统的调度

Linux采用的两种不同的优先级范围

● nice值(范围-20~+19):相比较高的nice值,低nice值的进程可以获得更多的处理器时间
可以通过ps-el命令插卡系统中的而进程列表,结果中标记NI的一列就是进程对应的nice值
● 实时优先级(范围0~99):与nice值相反,越高的实时优先级数值意味着进程优先级越高。任何实时进程的优先级都高于普通进程
可以通过
	pe-eo state,uid,pid,ppid,rtprio,time,coom.
命令查看系统中的进程列表,以对应的实时优先级(位于RTPRIO列下)

4.3.3 时间片

时间片是一个数值,表明进程在被抢占前佐能持续运行的时间

调度策略必须规定一个默认的时间片,

时间片过长:系统对交互的响应表现欠佳,让人觉得系统无法并发的执行应用程序
时间片过短:增大进程切换带来的处理器耗时

I/O消耗型和处理器消耗型的进程的矛盾显现:

I/O型长的时间片二处理器消耗型则希望越长越好

Linux的CFS调度器没有直接分配时间片到进程,而是将处理器的使用比划分给了进程,这样一来进程所获得的处理器时间其实是系统负载密切相关的。

这个比例进一步还会受到nice值的影响,nice值将作为权重来调整进程所使用的处理器时间使用比

在新的CFS调度器,其抢占的时间取决于新的可运行程序消耗了多少处理器使用比

4.4 Linux调度算法

4.4.1 调度器类

Linux调度器是以模块方式提供的,这样做的目的是允许不同类型的进程可以有针对的选择调度算法

这种模块化结构被称为调度器类,他允许多种不同的可动态添加的调度算法并存,调度属于自己范畴的进程

基础的调度器代码定义在kernel/sched.文件中

完全公平调度(CFS)是一个针对普通进程的调度类

CFS算法的实现定义在kernel/sched_fair.c中

4.2.2 Unix系统中的进程调度

在Unix系统中,优先级以nice值的形式输出给用户空间。听起来简单,但是在现实中会导致许多反常的问题

  • 若要将nice的值映射到时间片。就必须将nice单位值对应到处理器的绝对时间,但这样导致进程切换无法最优化进行

      事实上,给定高nice值的进程往往是后台进程,而且多是计算密集型;而普通优先级的进程则更多是前台用户任务
      这种时间片分配方式显然是歌初衷背道而驰
    
  • 设计相对nice值,同时和前面的nice值到时间片映射关系也脱不了关系

      nice值通常都是使用相对值,也就是说把进程的nice值减小1所带来的效果极大的取决于其nice的初始值
    
  • 如果执行nice值到时间片的映射,我们需要能分配一个必须能在内核的、测试范围内的绝对时间片

      最小时间片必然是定时器节拍的整数倍
      系统定时器限制了两个时间片的差异
      时间片会随着定时器节拍改变
    
  • 为了进程能更快的投入运行,而去对新要唤醒的进程提升优先级,即使他们的时间片已经用完了

实质问题:分配绝对的时间片引发固定的切换频率,给公平性造成很大的变数

4.4.3 公平调度

CFS的出发点基于一个简单的理念:进程调度的效果应如同系统具备一个理想中完美的多任务处理器(在一定时间内同时运行程序,分享使用处理器的能力),但现实并不能做到

CFS的做法是:

运行每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个运行进程
不在采用分配给每个进程时间片的做法
CFS在所有可运行进程总数基础上计算出一个进程该运行多久,而不是依靠nice值来计算时间片
nice值在CFS中作为进程获得处理器运行比的权重
每个进程按其权重在全部可运行进程中所占比例来运行

4.5 Linux调度的实现

CFS是如何得以实现的。相关代码位于kernel/sched_fair.c

  • 时间记账
  • 进程选择
  • 调度器入口
  • 睡眠和唤醒

4.5.1 时间记账

所有的调度器都必须对进程运行时间做记账

1、调度器实体结构

CFS不在有时间片概念,但是他也必须维护每个进程运行的时间记账,确保每个进程只在公平分配给他的处理器时间内运行

2、虚拟实时

vruntime变量存放进程的虚拟运行时间,该运行时间的计算是经过了可运行进程总数的饿标准化

update_curr()计算了当前进程的执行时间,并且将其存放在变量delta_exec中
然后将运行时间传递给了__update_curr()
由此根据当前可运行进程数对运行时间进行加权计算
最终将上述的去那种与当前运行进程的vruntime相加

4.5.2 进程选择

当CFS需要选择下一个运行进程时,体会挑一个具有最小vruntime的任务——核心

红黑树:自平衡搜索二叉树,以树节点形式存储的数据都会对应一个键值,通过这些键值来快速检索节点上的数据

1、挑选下一个任务

从树的根节点沿着左边的子节点一直向下找,一直找到叶子节点

函数:__ pick _next _entity()

定义在:kernel/sched _fair.c

static struct sched_rntity *__pick_next_entity(struct cfs_rq *cfs_rq)
{
	struct rb_node *life = cfs_rq->rb_leftmost'
	if(!left)
		return NULL;
	return br_entry(left,struct sched_entity,run_node);
}
2、向树中加入进程

发生在进程变为可运行状态或通过fork调用第一次创建进程时

函数:enqueue_entity

该函数更新运行时间和其他数据统计,然后调用__ enqueue_ entity进行繁重的插入操作。把数据插入到红黑树中

平衡二叉树的基本规则:

如果键值小于当前节点的键值,则需转向树的左分支。大于则右转;
一旦走过右分支说明插入的过程不会是新的最左节点,设置letfmost为0
如果一直左移,letfmost维持1
3、从树中删除进程

4.5.3 调度器入口

进程调度的主要入口点是函数schedule(),定义在kernel/sched.c中。是内核其他部分用于调用进程调度器的入口。他会调用pick_next_tast()函数以优先级为序,从高到低依次检查每一个调度类

每一个调度类都实现了pick _next _tast()函数,返回指向下一个可运行进程的指针或NULL

4.5.4 睡眠和唤醒

休眠(被阻塞)的进程处于一种特殊的不可执行状态

无论内种休眠原因,内核的操作都相同

进程把自己标志成休眠状态,从可执行红黑树中移出,放入等待队列
调用schedule(),选择一个执行的其他进程
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_INTERRUPTABLE);
		//将进程的状态改变
	if(signal_pending(corrnt))
		/*处理信号,检查条件是否为真*/
	schedule()
}
finish_wait(&q,&wait);   //把自己移出等待队列

函数inotify_ read(),位于fs/notify/inotify/inotify_user.c中,负责从通知文件描述符中读取信息

2、唤醒

通过wake _ up函数进行,唤醒指定的等待队列上的所有进程。调用try-o-wake-up()

4.6 抢占和上下文切换

上下文切换:从一个可执行进程切换到另一个可执行进程,有context_ switch负责。完成以下两项基本工作:

调用声明在<asm/mmu_context.h>中的switch_mm(),该函数负责把虚拟内存从上一个进程映射切换到新进程中
调用声明在<asm/system.h>中的switch_to(),该函数负责从上一个进程的处理器状态新的进程的处理器状态

4.6.1 用户抢占

原因:内核即将返回用户空间的时候,如果need-resched标志被设置,会导致schedule被调用,此时就会发生抢占

用户抢占发生的情况:

从系统调用返回用户空间时
从中断处理程序返回用户空间时

4.6.2 内核抢占

只要重新调度室安全的,内核就可以在任何时间抢占正在执行的任务。即只要没有持有锁(非抢占区域的标志),内核就可以抢占进程

内核抢占发生的情况:

中断处理程序正在执行,且返回内核空间之前
内核代码再一次具有可抢占性的时候
如果内核中的任务显示的调用schedule
如果内核中的任务阻塞

4.7 实时调度策略

SCHED_FIFO实现了一种简单的先入先出的调度算法

SCHED_RR进程在消耗事先分配给他的时间后就不能再执行了

这两种算法实现的都是静态优先级。内核补位实时进程计算动态优先级

软实时:内核调度进程尽力使进程在他的限定时间来到前运行,单内核不能总保证这些要求

硬实时:保证在一定条件下可以满足任何调度的要求

4.8 与调度相关的系统调用

4.9 总结

进程调度程序是内核的重要组成部分。Linux内核新的cfs调度程序尽量满足了各个方面的需求,并且以较完善的可伸缩性和新颖的方法提供了最佳的解决方案

posted @ 2016-04-14 10:36  20135236贾瑗  Views(242)  Comments(0Edit  收藏  举报