Edited at

sched: Do not account irq time to current task

More than 3 years have passed since last update.

今日はタイトルにもある sched: Do not account irq time to current task というカーネルパッチについて。そもそもはAndroidで先に取り込まれたみたいですが。少なくともLinux-2.6.37では入っている様子

何故このパッチに気づいたかというと、使用しているマシンのタスクスケジュール関連と思われるOSハングの原因を調査しているとき、新しいカーネルでランキュー構造体 struct rq に見慣れないメンバ変数が追加されてるな、と思ったことが契機。そのメンバ変数が clock_task です (タスクスケジューラのランキューとかの説明は割愛します。時間があったらまた書くつもりですが、最近は巷にCFSの説明も溢れてるので今更なんか書くことあるかなという感じ。)。

@@ -491,6 +491,7 @@ struct rq {

struct mm_struct *prev_mm;

u64 clock;
+ u64 clock_task;

atomic_t nr_iowait;

簡単に言うと、「今までのタスクスケジューラだと割り込み入ったときにもタスクのタイムスライスを消費してたよね。でも基本的に割り込みってそのタスクに関係ないんだからタイムスライス削っちゃだめだよね。」っていうパッチ。そのために、タスクのタイムスライスを計算するためのメンバ変数 clock_task が追加されてます。カーネルが新しくなるにつれて、このパッチに対する修正もちょろちょろ入ってるみたいですが、根本的な処理は変わってません。

ここからはCentOS 7.0のソースを参考にします(CentOS 7.0はLinux-3.10ベースなので、このパッチは入っている。少なくともCentOS 6.7には入ってないことを確認済み。)。

ランキューのクロック rq->clock の加算は update_rq_clock() でやってまして、ランキューへのタスク追加・削除、タイマ割り込み契機などで呼び出されては逐一更新されています。そして、update_rq_clock_task()rq->clock_task の更新箇所のメインです。

<linux-3.10.0-123.el7/kernel/sched/core.c>

void update_rq_clock(struct rq *rq)
{
s64 delta;

if (rq->skip_clock_update > 0)
return;

delta = sched_clock_cpu(cpu_of(rq)) - rq->clock;
rq->clock += delta;
update_rq_clock_task(rq, delta);
}

<linux-3.10.0-123.el7/kernel/sched/core.c>

static void update_rq_clock_task(struct rq *rq, s64 delta)
{
/*
* In theory, the compile should just see 0 here, and optimize out the call
* to sched_rt_avg_update. But I don't trust it...
*/
#if defined(CONFIG_IRQ_TIME_ACCOUNTING) || defined(CONFIG_PARAVIRT_TIME_ACCOUNTING)
s64 steal = 0, irq_delta = 0;
#endif
#ifdef CONFIG_IRQ_TIME_ACCOUNTING
irq_delta = irq_time_read(cpu_of(rq)) - rq->prev_irq_time;

/*
* Since irq_time is only updated on {soft,}irq_exit, we might run into
* this case when a previous update_rq_clock() happened inside a
* {soft,}irq region.
*
* When this happens, we stop ->clock_task and only update the
* prev_irq_time stamp to account for the part that fit, so that a next
* update will consume the rest. This ensures ->clock_task is
* monotonic.
*
* It does however cause some slight miss-attribution of {soft,}irq
* time, a more accurate solution would be to update the irq_time using
* the current rq->clock timestamp, except that would require using
* atomic ops.
*/
if (irq_delta > delta)
irq_delta = delta;

rq->prev_irq_time += irq_delta;
delta -= irq_delta;
#endif
#ifdef CONFIG_PARAVIRT_TIME_ACCOUNTING
if (static_key_false((&paravirt_steal_rq_enabled))) {
u64 st;

steal = paravirt_steal_clock(cpu_of(rq));
steal -= rq->prev_steal_time_rq;

if (unlikely(steal > delta))
steal = delta;

st = steal_ticks(steal);
steal = st * TICK_NSEC;

rq->prev_steal_time_rq += steal;

delta -= steal;
}
#endif

rq->clock_task += delta;

#if defined(CONFIG_IRQ_TIME_ACCOUNTING) || defined(CONFIG_PARAVIRT_TIME_ACCOUNTING)
if ((irq_delta + steal) && sched_feat(NONTASK_POWER))
sched_rt_avg_update(rq, irq_delta + steal);
#endif
}

IRQ時間を考慮してタスク消費時間を調整するのは CONFIG_IRQ_TIME_ACCOUNTING で囲まれているところです。コンフィグで切り替えられるようにしてるんですね。CONFIG_PARAVIRT_TIME_ACCOUNTING の方はひとまずほっておくとしましょう。

CPU毎に用意しているIRQアカウンティング時間から前のIRQ時間を差し引いた値を、更に引数で与えられた update_rq_clock() で計算済みのdeltaから差し引くことでタスクが本当に消費したdeltaを計算しており、最後に rq->clock_task に足しこんでます。なるほど。

Kconfigには説明もありますが、CONFIG_NO_HZ_FULL とは排他なんですね。まぁ、割り込みをなるべく少なくしようとする CONFIG_NO_HZ_FULL があれば、IRQ時間なんて微々たるものですからね(何もなければ1秒に1回の割り込みになるはず)。割り込みが多発しそうな Android からこのパッチが投稿されたのは、なんとなく理解できます。

<linux-3.10.0-123.el7/init/Kconfig>

config IRQ_TIME_ACCOUNTING
bool "Fine granularity task level IRQ time accounting"
depends on HAVE_IRQ_TIME_ACCOUNTING && !NO_HZ_FULL
help
Select this option to enable fine granularity task irq time
accounting. This is done by reading a timestamp on each
transitions between softirq and hardirq state, so there can be a
small performance impact.

If in doubt, say N here.

今日はここまで。カーネルは背景を理解した時が一番面白いと思います。

こんなにちゃんと書いてるパッチは最近でこそ多いが、昔は殆ど無い(よくわからない英語だったりする・・・)ので非常にありがたい。