2012年8月13日星期一

部分cfs调度器的tuning参数

最近看了rhel6.2内核的cfs进程调度代码,作为学习总结,把相关的部分tuning参数做了下整理,摘录如下。

* sched_child_runs_first
  当创建子进程时,保证子进程会在父进程之前运行。在创建子进程时,首先会
  将子进程的虚拟运行时间设置为min_vruntime,这个min_vruntime大约等于当
  前运行队列中所有进程的虚拟运行时间的最小值,然后,看sched_features中
  是否设置了START_DEBIT位,如果这一位被设置了,表示要给新创建的进程的
  虚拟运行时间再增加一些,这样会让它的运行时间向后延迟,以防进程通过不
  停的fork来不停的获得cpu时间。当设置完子进程的虚拟运行时间之后,会判
  断sched_child_runs_first是不是设置为1,如果是的话就比较父进程和子进
  程的虚拟运行时间,如果父进程的虚拟运行时间比较小的话,就交换父子进程
  的虚拟运行时间,这样子进程就会在父进程之前运行了。

* sched_min_granularity_ns
  这个值在两个地方会用到。一个是计算nice值为0的进程运行一次需要的时间。
  如果进程的总数大于sched_latency/sched_min_granularity个,就用
  sched_min_granularity_ns的值乘以进程的个数,将乘积作为nice值为0的进程
  调度一次运行的时间。然后每个进程会以此为基准,按照自己的优先级来计算
  自己被调度一次的运行时间。。另一个用途是判断当前进程是否要被调度下
  CPU的时候。如果当前进程的执行时间已经超过了它被调度一次所允许的执行时
  间(其实也可以说是时间片,尽管cfs声称自己没有时间片),那么必然要被调
  度下CPU。但如果它的时间片还没到期,在判断它的运行时间是不是比
  sched_min_granularity_ns小。如果是的话,那么就肯定不把它调度下CPU,如
  果进程的运行时间比sched_min_granularity_ns大的话,就用当前进程的虚拟
  运行时间减去runqueue上虚拟运行时间最小的进程的虚拟运行时间,如果差值
  比当前进程的时间片(这个时间片确实不是传统的时间片,总是随当前负载的
  状况变化)还大的话,那么当前进程也会被调度下CPU。

* sched_latency_ns
  计算nice值为0的进程运行一次所需的时间时用到。。如果进程总数小于等于
  sched_latency/sched_min_granularity个,就将sched_latency_ns作为nice
  值为0的进程运行一次的时间,否则按照前面介绍sched_min_granularity_ns
  时的方法计算。

* sched_wakeup_granularity_ns
  判断一个进程是否可以抢占当前进程时用到。如果该进程的虚拟运行时间比当
  前进程的虚拟运行时间大,那么肯定不能抢占。如果该进程的虚拟运行时间比
  当前进程的虚拟运行时间小的话,就计算出两者之差vdiff。如果vdiff大于一
  定的范围的话,就可以抢占,否则不可以抢占。
  sched_wakeup_granularity_ns就是用来确定这个范围的。它表示一个nice值
  为0的进程要抢占当前进程,它的虚拟运行时间需要被当前进程的虚拟运行时
  间小多少,其他优先级的进程以此为基准进行调整,优先级越高的进程,需要
  的差值越小。

* sched_tunable_scaling
  当内核试图调整sched_min_granularity,sched_latency和
  sched_wakeup_granularity这三个值的时候所使用的更新方法,0为不调整,1
  为按照cpu个数以2为底的对数值进行调整,2为按照cpu的个数进行线性比例的
  调整。

* sched_features
  每个bit都表示调度器的一个特性是否打开。在sched_features.h文件中记录
  了全部的特性。

* sched_migration_cost
  用来判断一个进程是不是cache hot的。如果进程的运行时间小于
  sched_migration_cost就认为是cache host的,在多CPU之间进行负载均衡的
  时候不会转移cache hot的进程。

* sched_nr_migrate
  在多CPU情况下进行负载均衡时,一次最多移动sched_nr_migrate个进程到另
  一个CPU上。

2012年8月4日星期六

关于linux内核抢占的一点思考

偶然看了一下rhel6.2系统内核中处理软中断的内核线程,很意外的产生了一点困
惑,这是ksoftirqd线程的主循环:

1    while (!kthread_should_stop()) {
2        preempt_disable();
3        if (!local_softirq_pending()) {
4            preempt_enable_no_resched();
5            schedule();
6            preempt_disable();
7        }
8
9        __set_current_state(TASK_RUNNING);
10
11        while (local_softirq_pending()) {
12            /* Preempt disable stops cpu going offline.
13               If already offline, we'll be on wrong CPU:
14               don't process */
15            if (cpu_is_offline((long)__bind_cpu))
16                goto wait_to_die;
17            do_softirq();
18            preempt_enable_no_resched();
19            cond_resched();
20            preempt_disable();
21            rcu_sched_qs((long)__bind_cpu);
22        }
23        preempt_enable();
24        set_current_state(TASK_INTERRUPTIBLE);
25    }

假设在23行执行完之后,有人试图唤醒这个线程,此时,线程还处于
TASK_RUNNING的状态,所以什么都不会发生。然后,在第24行,线程将自己设置
成了TASK_INTERRUPT状态,在执行第24行之后,线程被抢占了,那么,它在
TASK_INTERRUPT状态下被抢占,除非再次有人唤醒它,它岂不是再也返回不了了?
这样,不就丢失了一次唤醒吗?

这让我很困扰,于是,我自己写了个测试程序,启动一个内核线程,然后让它把
自己设置成TASK_INTERRUPTIBLE的状态,再进行几秒钟的忙循环。正常来说,执
行几秒钟的忙循环的话,肯定会被抢占了。然后我看它还能不能再被调度上来。
结果意外的发现rhel6的内核编译时是配置成不可抢占的(后来发现rhel5也这
样),我那个单核的虚拟机一执行忙循环就hang住。于是,重新编译了一个可抢
占的内核跑起来。结果发现,执行忙循环后,还是能被调度上来的。但如果我不
执行忙循环,而是调用schedule()函数,那么,除非被唤醒,否则就不能再被调
度上来了。

用systemtap跟踪了一下我的内核线程进入schedule()函数时的一些信息,结果
发现如果我主动调用schedule()函数,在进入schedule()函数时,thread_info
中的preempt_count的值是0x00000005,而如果在执行忙循环时被抢占调用
schedule函数时,preempt_count的值是0x10000005。这个最高位1在内核中被定
义为PREEMPT_ACTIVE。

原来,每当在抢占点上执行schedule函数时,这个标志会被设置,比如,从中断
返回时,调用:
retint_kernel
-> preempt_schedule_irq
   -> schedule

在preempt_schedule_irq函数中,就会调用这几行代码:

        add_preempt_count(PREEMPT_ACTIVE);
        local_irq_enable();
        schedule();
        local_irq_disable();
        sub_preempt_count(PREEMPT_ACTIVE);

设置PREEMPT_ACTIVE后,再调用schedule()函数,然后再清除PREEMPT_ACTIVE标
志。

而在schedule()函数中,有这样一段代码:

    if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
        if (unlikely(signal_pending_state(prev->state, prev)))
            prev->state = TASK_RUNNING;
        else
            deactivate_task(rq, prev, DEQUEUE_SLEEP);
        switch_count = &prev->nvcsw;
    }

prev就是当前要被调度下cpu的进程。prev->state为0表示TASK_RUNNING的状态。
如果进程的状态不是TASK_RUNNING并且PREEMPT_ACTIVE标志没有被设置,再判断进程是否有pending的信号,如果有,就把进程状态设置成TASK_RUNNING状态,
如果没有pending的信号,就把进程从RUNNING的进程队列里拿下来。

因此,逻辑关系是这样的:
如果PREEMPT_ACTIVE被设置了,说明进程是由于内核抢占被调度下CPU的,这时不
把它从RUNNING的队列里移除,如果进程不是由于内核抢占被调度下来的,看它
有没有未处理的信号,如果有的话,也不把它从RUNNING的队列里移除。只有再
上述两种情况都为假的情况下,进程才会被从RUNNING的队列里移除。

这就解释了为什么内核线程把自己设置成TASK_INTERRUPTIBLE状态之后,只要不
是主动调用schedule()函数,而是被抢占调度下cpu的,它就还会获得机会运行。

2012年7月27日星期五

linux内核的cfq IO调度算法的tuning参数

                              cfq_tuning
                              ==========

Author: yu peng
Date: 2012-07-27 19:58:01 HKT


Table of Contents
=================
1 概要
2 back_seek_max 和 back_seek_penalty
3 slice_async,slice_sync,和 slice_async_rq
4 low_latency
5 quantum
6 fifo_expire_async 和 fifo_expire_sync
7 slice_idle
8 group_idle
9 group_isolation


1 概要
-------
  以rhel6系统所用的2.6.32内核为基础,分析cfq io调度器的tuning参数。

2 back_seek_max 和 back_seek_penalty
-------------------------------------
  这两个参数是用来确定,对于给定的两个IO,哪个更适合作为“下一个”IO被
  传送给设备。判定的方法就是谁距离磁头更近。磁头的位置就是上一次IO完成
  的位置。比如磁头的位置last是sector 100。两个IO,io1的起始扇区号是105,
  io2的起始扇区号是110那么,io1距离磁头更近,下一次要发送IO,就先发io1,
  再发io2。
  如果io1的起始扇区号是90,io2的起始扇区号是110呢?两个io距离磁头的距离
  都是10,这时该如何选择呢?一般来说,把磁头向前移动(从扇区100向扇区
  90移动)需要花费的时间要多一些,所以cfq倾向于选择向后移动(从扇区100
  向扇区110移动)。
  如果io1的起始扇区号是95,io2的起始扇区号是110呢?虽然io1在磁头的前面,
  io2在磁头的后面,但io1距离磁头更近一些,这时该如何选择呢?具体的判断
  方法就用到了back_seek_penalty。比如io1在磁头的前面,距离磁头的距离是
  s1,io2在磁头的后面,距离磁头的距离是s2,如果s1*back_seek_penalty小于
  s2,就认为io1距离磁头比较近,否则,就认为io2距离磁头比较近。
  如果io1在磁头的前面,而io2在磁头的后面,但io1距离磁头太远了,超过了
  back_seek_max,那么无论io2是不是距离磁头更远,都选择io2。

3 slice_async,slice_sync,和 slice_async_rq
---------------------------------------------
  每个进程发下来的IO都被分成三类:异步,同步,idle。cfq先处理所有进程
  发下来的同步IO,然后处理所有进程发下来的异步IO,最后处理idle IO。进一
  步的,同步和异步的IO又被分别分成了8个不同等级的优先级。同一个进程发
  送下来的同步IO,都有同样的优先级,异步的也是如此。对于每个进程的IO,
  都被组织到一个叫做cfq_queue的结构体中。对于一个进程发送下来的IO,所
  有的同步IO有一个cfq_queue,所有的异步IO有另一个cfq_queue。cfq会给每
  个cfq_queue都分配一个时间片。比如当一个进程的同步IO的时间片用完后,就
  开始发送另一个进程的同步IO。时间片的大小由两个因素决定,一个是IO的优
  先级,这是和进程的IO优先级相关的,如果进程没有设置IO优先级,就按照
  nice值成正比计算出一个,另一个因素就是slice_async和slice_sync这两个
  参数。如果这两个参数越大,那么所有异步和同步IO的时间片粒度就越大,反
  之则粒度越小。对于异步IO,还有一个限制,如果在一个时间片内,已经发送
  的异步IO总数大于slice_async_rq,那么也视为时间片到期。idle类型的IO没
  有时间片,每次只发送一个。
  注意,这里说的异步IO和linux内核提供的AIO机制是两码事,一点关系都没有。
  一般来说,用户进程发送的读操作和设置direct标志的写操作都是sync类型的
  IO,普通的写操作会写道cache里面,再由内核进程通过cache发送到IO调度器
  的是async类型的IO。

4 low_latency
--------------
  如果这个参数被设置了,那么当前面计算出的cfq_queue的时间片大于
  low_latency时,会把时间片强行设置为low_latency。

5 quantum
----------
  除了idle类型的IO是一个一个的放到块设备的queue里面之外,sync和async类
  型的IO每次都是批量放入queue里面的。quantum就是一次批处理的数量。

6 fifo_expire_async 和 fifo_expire_sync
----------------------------------------
  cfq对不同进程发下来的IO分时间片进行处理,但在处理同一个进程发下来的
  IO时,采用与dead line类似的做法。尽量按照减小磁头移动的方式选择IO,
  但如果有些IO等待的时间太久了的话,就转而处理这些等待太久的IO。
  fifo_expire_async和fifo_expire_sync就是分别用来设置async类型IO和sync
  类型IO的超时时间的。

7 slice_idle
-------------
  在cfq中,最后被处理的IO是idle类型的IO,当调度器中已经没有任何的同步
  或异步类型的IO,并且这种状况已经持续了slice_idle这么长的时间,那么
  cfq就认为现在处于idle状态了,就发送一个idle类型的IO,然后再等待
  slice_idle这么长的时间,如果还是没有其他IO,就再发送一个idle类型的IO。
  补充一下,这个参数,以及所有本文中提到的和时间有关的参数,单位都是毫秒。

8 group_idle
-------------
  我的理解,不一定准确:
  当这个参数被设置之后,每当属于一个cgroup的进程的IO都提交完了,但时间
  片还没有耗尽,那么不会马上提交属于另一个cgroup的IO,而是会等待
  group_idle这么长的时间,如果在这个时间段内,该cgroup又有IO提交了进来,
  那么就继续处理。
  这个参数的引入是为了针对磁盘阵列或者固态硬盘这样的设备做优化用的,参
  看这篇文章:
  [http://lwn.net/Articles/395769/]
  以及内核文档blkio-controller.txt中的描述:
CFQ sysfs tunable
=================
/sys/block/<disk>/queue/iosched/slice_idle
------------------------------------------
On a faster hardware CFQ can be slow, especially with sequential workload.
This happens because CFQ idles on a single queue and single queue might not
drive deeper request queue depths to keep the storage busy. In such scenarios
one can try setting slice_idle=0 and that would switch CFQ to IOPS
(IO operations per second) mode on NCQ supporting hardware.

That means CFQ will not idle between cfq queues of a cfq group and hence be
able to driver higher queue depth and achieve better throughput. That also
means that cfq provides fairness among groups in terms of IOPS and not in
terms of disk time.

/sys/block/<disk>/queue/iosched/group_idle
------------------------------------------
If one disables idling on individual cfq queues and cfq service trees by
setting slice_idle=0, group_idle kicks in. That means CFQ will still idle
on the group in an attempt to provide fairness among groups.

By default group_idle is same as slice_idle and does not do anything if
slice_idle is enabled.

One can experience an overall throughput drop if you have created multiple
groups and put applications in that group which are not driving enough
IO to keep disk busy. In that case set group_idle=0, and CFQ will not idle
on individual groups and throughput should improve.

What works
==========
- Currently only sync IO queues are support. All the buffered writes are
  still system wide and not per group. Hence we will not see service
  differentiation between buffered writes between groups.

  在高速存储上,这样一组经验值可以获得较好的性能:
  slice_idle = 0
  quantum = 64
  group_idle = 1
  这些值在/sys/block/devicename/queue/iosched/中可以找到。

9 group_isolation
------------------
kernel document中有一个blkio-controller.txt文件,对这个参数做出了解释,
将其原文摘抄如下:
CFQ sysfs tunable
=================
/sys/block/<disk>/queue/iosched/group_isolation

If group_isolation=1, it provides stronger isolation between groups at the
expense of throughput. By default group_isolation is 1. In general that
means that if group_isolation=0, expect fairness for sequential workload
only. Set group_isolation=1 to see fairness for random IO workload also.

Generally CFQ will put random seeky workload in sync-noidle category. CFQ
will disable idling on these queues and it does a collective idling on group
of such queues. Generally these are slow moving queues and if there is a
sync-noidle service tree in each group, that group gets exclusive access to
disk for certain period. That means it will bring the throughput down if
group does not have enough IO to drive deeper queue depths and utilize disk
capacity to the fullest in the slice allocated to it. But the flip side is
that even a random reader should get better latencies and overall throughput
if there are lots of sequential readers/sync-idle workload running in the
system.

If group_isolation=0, then CFQ automatically moves all the random seeky queues
in the root group. That means there will be no service differentiation for
that kind of workload. This leads to better throughput as we do collective
idling on root sync-noidle tree.

我的理解,不一定准确:
cfq支持cgroup机制,可以区别对待属于不同cgroup的进程发下的io,给他们分
配不同的带宽。但这样会对效率造成比较大的影响。因为每个进程发送下来的读
写请求的位置可能差别很大,这样就会造成磁盘磁头来回的移动。所以,cfq提
供了这样一个参数,当group_isolation为0的时候,就把所有属于
SYNC_NOIDLE_WORKLOAD类型的cfq_queue都移动到root_group这个cgroup中。每
个进程都有几个它自己的cfq_queue,进程发送下来的IO,会根据IO的种类分别
放到不同的cfq_queue中去。从这个函数里我们可以看到怎么判断cfq_queue的种
类:
static enum wl_type_t cfqq_type(struct cfq_queue *cfqq)
{
        if (!cfq_cfqq_sync(cfqq))
                return ASYNC_WORKLOAD;
        if (!cfq_cfqq_idle_window(cfqq))
                return SYNC_NOIDLE_WORKLOAD;
        return SYNC_WORKLOAD;
}
如果cfq_queue里面放的是同步的IO,那么它就是ASYNC_WORKLOAD类的,如果
cfq_queue里面放的不是idle类型的IO(优先级最低的一种IO,只在空闲时才处
理),那么它就是SYNC_NOIDLE_WORKLOAD类型的,否则(也就是idle类型的IO),它
就是SYNC_WORKLOAD类型的。