Linux ultra-scalable O(1) Scheduler 软实时概要分析

转自:  http://www.linuxfans.org/nuke/modules.php?name=News&file=article&thold=-1&sid=2368

Linux ultra-scalable O(1) Scheduler 软实时概要分析


Author : balancesli
Email : balancesli@thizlinux.com.cn

1.序言
在一个多任务的操作系统中如何让其各个任务公平的享有CPU的控制权,充分的利用CPU所提供
的服务是验证一个OS性能的重要指标之一,在最新的Linux 2.5 ~ 2.6内核中正是由于加入了一个新的调度
器(?ultra-scalable O(1) scheduler)替换的老式的goodness算法, 大幅度的提高的CPU的利用率并在Linux
OS的整体性能上产生了一个质的飞跃.

2. 调度器(Scheduler)
调度器作为OS的核心部件,它承担着在多个任务能否充分的享用CPU资源(即CPU时间片)的重大责任,因此
调度器的好坏影响的OS的整体性能,尤其是在RTOS(Runtime Operation System)领域。?在新版的Linux内核中加
入的全新的O(1)级调度算法更使得Linux能在嵌入式的领域大展拳脚。这里让我们对O(1) scheduler的软实时
做一个大致的分析,去体会一下O(1)设计思想的神妙之处。我这里所讲述的主要是被移植到2.4内核中
的O(1) Scheduler,且主要讨论非普通任务的调度策略,疏漏之处望请指正。

3. 核心数据结构
在老式2.4内核中,多个CPU共享一个全局的就绪队列(runqueue),调度器对它的所有操作都会因全局
自旋锁(spinlock)而导致系统各个处理机之间的等待,使得就绪队列成为一个明显的瓶颈, 且造成某个
任务在多个CPU之间跳跃的执行,对于支持多SMP的整体负载平衡的能力大幅降低。

在新版的内核中,每个CPU都拥有一个自己的就绪队列(runqueue),且每个runqueue拥有一个属于自己
的自旋锁(spinlock),这样一来就不会影响其他CPU的运行,各个CPU之间独立的使用自己的就绪队列(runqueue),
这对于CPU的利用率和多CPU系统的负载平衡能力带来了极大的提升。

因此,O(1)调度器绝对值得我们去好好的分析一下其独特的设计思想。
至于Ingo Molnar的灵感来源就不得知了。

以下为与O(1) scheduler相关的核心数据结构

struct prio_array (/usr/src/linux/kernel/sched.c)
{
int nr_active;
//该字段记录了就绪队列中具有剩余时间片的任务总数,它也可以作为衡量一个CPU负荷轻重的指标之一,


unsigned long bitmap[BITMAP_SIZE];
//该字段用于判断队列queue(即下一个字段)中对应优先级链表中是否具有剩余时间片的任务(即是否为空)。
//且它也是我们查找并取得就绪队列中高优先级任务一个位图字段.

// 当然我们可以想象一下,scheduler对于多个CPU中到底最先查找那个CPU上的runqueue呢?
// 对于多个CPU,每个CPU都有一定的负荷,要提高系统整体的吞吐量,必然是要尽快的选择一个负荷最重
// 的CPU上的runqueue,以保持多个CPU上负载的平衡。
// 同样对于新创建的任务到底加入到那个CPU上的runqueue呢? 当然是选择一个负荷最轻的CPU上的runqueue
// 来加入到其中对应的优先级链表当中,这同样也是保持多个CPU上负载平衡的关键因素。
// 当然这里只是一个简单的想法,但我们可以相信正是我们生活中对于各种事物的不同看法才造
// 就了一切灵感的来源。


struct list_head queue[MAX_PRIO];
//该字段是每个CPU上对应的优先级队列数组,且每个元素对应一个具有相同优先级的任务链表。
//可以想象一下,具有相同级别的任务当然是有先来后到的,那么从公平的角度出发当然是
//先到者优先获取CPU的控制权。

//哈哈,现实生活中可能不是这样,但对于我们整天面对代码的
//职业程序员所操控的计算机而言,当然我们选择的是公平竞争。
};

以下我用一个图例来说明 unisigned long bitmap[BITMAP_SIZE]字段域
和 struct list_head queue[MAX_PRIO]字段域的关系;

在新版内核当中有如下定义 (/usr/src/linux/include/linux/sched.h)


#define MAX_USER_RT_PRIO 100

#define MAX_RT_PRIO MAX_USER_RT_PRIO

#define MAX_PRIO (MAX_RT_PRIO + 40)

任务的优先权值得的范围 : 0..MAX_PRIO - 1 (0 ~ 139)

其中实时优先权值的范围 : 0..MAX_RT_PRIO-1 (0 ~ 99)

非实时任务的优先权值范围 : MAX_RT_PRIO..MAX_PRIO-1 (100 ~ 139)


#define BITMAP_SIZE ((((MAX_PRIO+1+7)/8)+sizeof(long)-1)/sizeof(long))
通过以上计算我们的出BITMAP_SIZE的值为5, 即通过5个四字节的整数位(160位)作为
运行队列queue[MAX_PRIO]的位图掩码。

list_head queue[MAX_PRIO]
typedef struct list_head
{
struct list_head *prev, *next;
} list_t; (/usr/src/linux/include/linux/list.h)

list_head 为linux kernel中为实现各种通用队列或链表而采用的一种寄生体,它附着在kernel中
各种核心的数据结构当中,作为kernel中一些重要的核心数据结构体链表的链接部件。
它是一个kernel中用于构造通用队列或链表的核心链接部件, 哈哈,它可是被疯狂的使用啊!

这个宏可是用于通过这个寄生体获取宿主体(叫母体也可以,不过有点不专业!)起始入口的关键噢!
其中ptr是指向这个寄生体的指针
其中的type为宿主体的数据类型
其中的member指定了这个寄生体的名称而已。
#define list_entry(ptr, type, member)
((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))

看看下面是不是有点像STL模板库里的迭代器(iterator)噢 :)
#define __list_for_each(pos, head)
for (pos = (head)->next; pos != (head); pos = pos->next)

#define list_for_each_entry(pos, head, member)
for (pos = list_entry((head)->next, typeof(*pos), member),
prefetch(pos->member.next);
&pos->member != (head);
pos = list_entry(pos->member.next, typeof(*pos), member),
prefetch(pos->member.next))

好了有点跑题了,回来看看吧

其实以上的说明只是让大家不要疑惑为什么queue这个优先级任务数组的元素是list_head型,
它只是起到一个链接相同优先级任务链表的一个部件而已... 以便大家不要对下图感到疑惑。

Bitmap queue
____
0 | |-----> queue[0] Task0 Task1 Task2 ...
|---|
1 | |---- > queue[1] Task0 Task1 Task2 ...
|---|
. | |
. |---|
. | |
|---|
| |
|---|
100 | |----> queue[100] Task0 Task1 Task2 ...
. |---|
. | |----> queue[101] Task0 Task1 Task2 ...
. |---|
139 | |----> queue[139] Task0 Task1 Task2 ...
|____|



上边的位图数组中每一位的索引值用作任务的优先权值。

(记住优先权值尽管是从小到大排列,可是我们的任务可是由高到低来排列的,多数的OS都是这样的)

struct runqueue (/usr/src/linux/kernel/sched.c)
{
spinlock_t lock;
//该字段是针对某个CPU上的runqueue所使用的自旋锁,用于操作该CPU上的runqueue而用。

prio_array_t *active, *expired, arrays[2];
//上面的字段是每个CPU上的就绪队列按时间片的使用是否耗尽分为两个优先级队列

// 我们可以看到就绪任务队列使用struct prio_array 的结构来维护:

// prio_array_t * active --> arrays[0] or array[1]
// prio_array_t * expired --> arrays[1] or array[0]
// active指针变量指向时间片尚未耗尽可以被调度的就绪任务队列
// expired指针变量指向时间片已经耗尽但还需调度的就绪任务队列。

//该字段可能从字面意义上记录了runqueue中具有就绪任务总数,
//那么它应该也可以作为衡量一个CPU负荷轻重的指标之一
unsigned long nr_running,

nr_switches,
expired_timestamp,
nr_uninterruptible;
struct mm_struct *prev_mm;
int prev_cpu_load[NR_CPUS];
int nr_cpus;
cpu_t cpu[MAX_NR_SIBLINGS];

atomic_t nr_iowait;

......
};
那么如何访问runqueue中的array[0]和array[1]就绪任务队列呢?
自然是由runqueue中的两个指针变量active,expired来干。
这两个指针变量,它们的值是变化的。

假设array[0]代表所有时间片尚未耗尽可以被调度的就绪任务队列
假设array[1]代表所有时间片已经耗尽的就绪任务队列

active指针变量始终指向时间片尚未耗尽的就绪任务队列. active -> array[0]
expired指针变量指向时间片已经耗尽但还需调度的就绪任务队列。expired -> array[1]

那么当array[0]中所有的任务全部耗尽了时间片, 在这个过程中array[0]中的某个就绪任务一旦
使用完其CPU时间片(timeslice), 它会立即被送往array[1]的就绪任务队列并分配一定的CPU时间片,
这与2.4中的goodness算法完全不同,在新版的内核中每个任务的CPU时间分配同样是O(1)级的,
并不是当所有array[0]的就绪任务都用完以后一块放到array[1]中并一次性给每个任务重新分配CPU时间片,
以前版本的内核无论在调度开销和CPU时间片分配上都是O(n)级的。回来吧,这样一来当array[0]中
的就绪任务都耗尽CPU时间片并都已经被送往array[1]时. 此时array[0]状态时其中已经没有就绪任务
存在(即为空时),而array[1]中状况则是收集了array[0]中耗尽CPU时间片并已经被重新分配CPU时间片的
所有就绪任务。那么这时我们交换active和expired这两个指针变量的值,array[1]此时成为时间片尚未耗
尽可以被调度的就绪任务队列,array[0]则成为一个专门收集array[1]中耗尽CPU时间片任务的收集器(即collector)

就这样反反复复的通过操纵runqueue中的active和expired指针变量的交换来把所有的就绪任务运行完毕。


值得注意的一点是CPU时间片的耗尽是与时钟中断(time_interrupt)是息息相关的。
linux kernel的时钟节拍是100HZ(即10ms), CPU时间片的分配这是以jiffies为单位的。
然而任务的时间片的分配有与以下几个宏相关的。

在Linux Kernel中的nice值在-20~19之间(其实也就是task_struct结构中的static_priority的值)
NICE值和内核中所使用的优先权值PRIO的变换如下
#define NICE_TO_PRIO(nice) (MAX_RT_PRIO + (nice) + 20)
这样能保证赋予nice值非实时任务的优先权值在100到139之间

#define PRIO_TO_NICE(prio) ((prio) - MAX_RT_PRIO - 20)
这样能保证非实时任务的优先权值对应的NICE值在-20到19之间


#define TASK_NICE(p) PRIO_TO_NICE((p)->static_prio)
NICE和static_prio是等效的

这里是用户级的优先权值的计算方式 ,在调整调度参数时使用
USER_PRIO的值域时0~39
#define USER_PRIO(p) ((p)-MAX_RT_PRIO)
#define TASK_USER_PRIO(p) USER_PRIO((p)->static_prio)
#define MAX_USER_PRIO (USER_PRIO(MAX_PRIO))

#define MIN_TIMESLICE ( 10 * HZ / 1000) 分配的最小时间片10ms
#define MAX_TIMESLICE (200 * HZ / 1000) 分配的最小时间片200ms


这里时用于计算任务开始创建时分配CPU时间片的计算方式
#define BASE_TIMESLICE(p) (MIN_TIMESLICE +
((MAX_TIMESLICE - MIN_TIMESLICE) * (MAX_PRIO-1-(p)->static_prio)/(MAX_USER_PRIO - 1)))


说了一堆的关于就绪任务的问题,那么我们如何提取拥有CPU时间片的就绪任务队列queue中最高
优先级别的任务呢? 说白了也就是如何快速的查找到高优先级且最值得运行的任务,Linux kernel
提供了一个仿造HASH查找的函数sched_find_first_bit,它用于快速定位就绪队列中高优先级任务,让我们来
看看它的代码吧 ...

(/usr/src/linux/include/asm/bitops.h)中
static inline int sched_find_first_bit(unsigned long *b)
{
if (unlikely(b[0]))
return __ffs(b[0]);
if (unlikely(b[1]))
return __ffs(b[1]) + 32;
if (unlikely(b[2]))
return __ffs(b[2]) + 64;
if (b[3])
return __ffs(b[3]) + 96;
return __ffs(b[4]) + 128;
}

我们能够看到该函数的参数是一个指向整型的指针变量,它正是用于接收我们上面所提到
的bitmap[BITMAP_SIZE]的数组名而设立的,通过逐个扫描bitmap[BITMAP_SIZE]中的每个元素,
来快速定位高优先级的任务,并返回其在位图中对应的位索引值。
这样就的到了最高优先级的任务队列链表的表头,这样我们只要提取相同优先级的第一个头节点对应的
任务就可以让其获取CPU的控制权就大功告成了。

在sched_find_first_bit中我们看到它调用了__ffs这个内联函数
(注意内联函数可是C++的独有啊,no, GCC对标准C做了扩展,也支持内联函数,
这样的话可以近可能的提高程序的执行效率)
在__ffs函数中它采用了内嵌汇编的方式来加快定位速度,这里只是使用了一条intel的汇编指令bsfl.
很简单内嵌汇编的方式还是足够快的吗...:)

static __inline__ unsigned long __ffs(unsigned long word)
{
__asm__("bsfl %1,%0"
:"=r" (word)
:"rm" (word));
return word;
}

因此我们O(1)scheduler的调度开销还是足够快的,应该算是O(1)级的。



4.OK,大致先介绍到这里,由于O(1) scheduler 的设计比较复杂,所提到部分还比较片面,
要想更深刻的体会其设计思想,还需进一步深入内核源码研究,待续。。。

5.Reference:

1>.Linux Kernel Do*****entation
sched-coding.txt
sched-design.txt

2> Linux Kernel Source

3> Understanding The Linux Kernel' 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值