前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux O(1)调度器

Linux O(1)调度器

作者头像
DragonKingZhu
发布2020-03-24 17:26:33
2.8K0
发布2020-03-24 17:26:33
举报

前面我们学习了O(n)调度器的设计,以及它的核心算法。在这里复习下。

O(n)调度器核心:

O(n)调度器采用一个runqueue运行队列来管理所有可运行的进程,在主调度schedule函数中会选择一个优先级最高,也就是时间片最大的进程来运行,同时也会对喜欢睡眠的进程做一些补偿,去增加此类进程的时间片。当runqueue运行队列中无进程可选择时,则会对系统中所有的进程进行一次重新计算时间片的操作,同时也会对剩余时间片的进程做一次补偿。

O(n)调度器的缺陷:

  1. 时间复杂度是O(n)
  2. SMP系统扩展不好,访问runqueue需要加锁
  3. 实时进程不能及时调度
  4. CPU空转的现象存在
  5. 进程在各个CPU之间跳跃,性能影响

O(1)调度器的引入

基于O(n)调度器的种种问题,linux内核社区则在2.6内核版本引入了O(1)调度器,当然了引入的目的也正是要解决O(n)调度器面临的问题。我们这片文章以Linux2.6.2版本来学习,在Linux内核文档中有一篇关于O(1)调度器的目的,如何设计的,以及实现有一个详细的介绍:sched-design.txt文档,有兴趣的可以去阅读。

O(1)调度器的工作原理

  • 系统中的runqueue是一个PER_CPU变量,也就是说每一个CPU维护这一个runqueue,这样在SMP系统就可以有效的避免多个CPU去访问同一个runqueue。
  • 每一个runqueue运行队列维护两个链表。一个是active链表,表示运行的进程都挂载active链表中;一个是expired链表,表示所有时间片用完的进程都挂载expired链表中。
  • 为了解决O(n)中所有的进程都无序排列在runqueue中,O(1)算法中奖进程按照优先级排列,而且相同优先级的都挂在同等优先级的链表中
  • 同时提供了一个bitmap结构,用来存放那些优先级中有可以运行的进程。当每次pciknext的时候,只需要坚持bitmap,然后去对应的优先级列表中按照优先级策略选择进程。
  • 当acitve中无进程可运行时,说明系统中所有进程的时间片都已经耗光,这时候则只需要调整active和expired的指针即可。

从以上几点来看,可以看出O(1)的算法的改进都是针对O(n)算法存在的问题来修改的。

O(1)调度器涉及的数据结构

代码语言:javascript
复制
struct runqueue {
	spinlock_t lock;
	unsigned long nr_running, nr_switches, expired_timestamp,
		      nr_uninterruptible, timestamp_last_tick;
	task_t *curr, *idle;
	struct mm_struct *prev_mm;
	prio_array_t *active, *expired, arrays[2];
	int best_expired_prio, prev_cpu_load[NR_CPUS];
	
	task_t *migration_thread;
	struct list_head migration_queue;

	atomic_t nr_iowait;
};

static DEFINE_PER_CPU(struct runqueue, runqueues);

可以看到struct runqueue是一个PER_CPU的变量,则对应的是SMP系统中每一个CPU都维护一个struct runqueue结构。

代码语言:javascript
复制
/*
 * These are the runqueue data structures:
 */

#define BITMAP_SIZE ((((MAX_PRIO+1+7)/8)+sizeof(long)-1)/sizeof(long))

typedef struct runqueue runqueue_t;

struct prio_array {
	int nr_active;
	unsigned long bitmap[BITMAP_SIZE];
	struct list_head queue[MAX_PRIO];
};

struct prio_array就代表的两个优先级数组,nr_active代表当前有多少进程处于active或者expried。bitmap是为了方便查找进程引入的,这样当寻找进程的时候只需要查询bitmap,而bitmap的大小是固定的,则算法的时间复杂度是O(1)

O(1)调度器的核心算法

O(1)的核心算法,我们可以直接看schedule函数

代码语言:javascript
复制
array = rq->active;	
idx = sched_find_first_bit(array->bitmap);
queue = array->queue + idx;
next = list_entry(queue->next, task_t, run_list);

这三行就是算法的核心,首先去从runqueue的active队列中的bitmap找到一个下标,这个下标就是对应的优先级,然后获取到对应优先级的链表,然后从中获取一个next进程。后面的操作就是执行进程切换,调度了。

系统中无可运行进程时

当系统中无可运行进程时,也就是进程的时间片都耗光了,则需要重新给进程设置时间片

代码语言:javascript
复制
	array = rq->active;
	if (unlikely(!array->nr_active)) {
		/*
		 * Switch the active and expired arrays.
		 */
		rq->active = rq->expired;
		rq->expired = array;
		array = rq->active;
		rq->expired_timestamp = 0;
		rq->best_expired_prio = MAX_PRIO;
	}

操作也是相当的简单,只需要切换active和expried的指针即可。

O(1)调度器优先级的设置

进程的优先级分为静态优先级和动态优先级。普通优先级是进程创建时默认设置的优先级,动态优先级会在进程运行时经过动态的调整。

普通进程的静态优先级为static_prio

实时进程的静态优先级为rt_priority

O(1)调度器中所有进程的动态优先级为p->prio。

代码语言:javascript
复制
#define CURRENT_BONUS(p) \
	(NS_TO_JIFFIES((p)->sleep_avg) * MAX_BONUS / \
		MAX_SLEEP_AVG)

static int effective_prio(task_t *p)
{
	int bonus, prio;

	if (rt_task(p))
		return p->prio;

	bonus = CURRENT_BONUS(p) - MAX_BONUS / 2;

	prio = p->static_prio - bonus;
	if (prio < MAX_RT_PRIO)
		prio = MAX_RT_PRIO;
	if (prio > MAX_PRIO-1)
		prio = MAX_PRIO-1;
	return prio;
}

在系统运行中,会跟踪此函数来重新计算进程的动态优先级。实时进程只需要返回对应的p->prio。而普通进程则就需要进行赏罚了。通过进程的睡眠时间sleep_avg来计算进程是否需要赏罚。当一个进程经常睡眠,则会增加它的优先级。当一个进程常占CPU,则需要惩罚,降低其优先级

时间片的更新

当系统的tick到来时,会走到schedule_tick函数中

代码语言:javascript
复制
	if (!--p->time_slice) {
		dequeue_task(p, rq->active);
		set_tsk_need_resched(p);
		p->prio = effective_prio(p);
		p->time_slice = task_timeslice(p);
		p->first_time_slice = 0;

		if (!rq->expired_timestamp)
			rq->expired_timestamp = jiffies;
		if (!TASK_INTERACTIVE(p) || EXPIRED_STARVING(rq)) {
			enqueue_task(p, rq->expired);
			if (p->static_prio < rq->best_expired_prio)
				rq->best_expired_prio = p->static_prio;
		} else
			enqueue_task(p, rq->active);
	}

当时间片耗完时,则需要将进程从active队列中移除掉,需要设置需要重新调度的标志TIF_NEED_RESCHED。同时还需要重新计算此进程的优先级,时间片。而O(1)调度器算法比O(n)不是那么的粗暴,还需要判断是否是交互式进程,或者此进程是不是饥饿进程,如果是则将又添加到active队列中,否则添加到expried队列。

总结:

  • O(1)调度器的引入主要是为了解决O(n)调度器的不足
  • O(1)调度器在赏罚机制上比O(n)调度器考虑的因素比较多,不再时像O(1)那样直接考时间片的大小来调度
  • 但是O(n)和O(1)调度算法上核心还是通过判断一个进程的行为,比如爱睡眠来进程赏罚机制,爱睡眠来增大优先级,增大时间片的机制来获取更多的运行时间。
  • 如果去看O(1)调度器的实现,没有O(n)算法那么简单明了,O(1)中加了需要时间的判断,各种情况的考虑,导致代码的阅读性很差,读起来很费劲。
  • 当然了时代还是要前进的,O(n)和O(1)调度器是为CFS调度器出现地提供了很好的环境。
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • O(n)调度器核心:
  • O(n)调度器的缺陷:
  • O(1)调度器的引入
  • O(1)调度器的工作原理
  • O(1)调度器涉及的数据结构
  • O(1)调度器的核心算法
    • 系统中无可运行进程时
    • O(1)调度器优先级的设置
    • 时间片的更新
    • 总结:
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档