BG

ロードアベレージ

Load Average On Linux

Classically, UNIX systems have calculated the load average by counting the number of processes that are either running on the CPU or runnable (ready & waiting for a CPU to run them). Linux does this, but it also counts the number of processes in uninterruptable sleep. Uninterruptable sleep usually means a process is blocking on I/O (waiting for disk, etc.).

Linux は, 他の伝統的な UNIX とはロードアベレージの算出方法が違い, 実行中・実行可能プロセスだけでなく, I/O 待ちなどの割り込み不可休止(uninterruptable) なプロセスもカウントに含まれるとの話。

実際に確認してみました。

FreeBSD

FreeBSD(7.2) のスケジューラは 4BSD と ULE があるのですが 4BSD を覗いてみました。(現在のデフォルトは ULE です)

==== sys/kern/kern_synch.c ====
506 static void
507 loadav(void *arg)
508 {
509         int i, nrun;
510         struct loadavg *avg;
511
512         nrun = sched_load();
513         avg = &averunnable;
514
515         for (i = 0; i < 3; i++)
516                 avg->ldavg[i] = (cexp[i] * avg->ldavg[i] +
517                     nrun * FSCALE * (FSCALE - cexp[i])) >> FSHIFT;

ロードアベレージは loadav 関数で計算されていますが, その元となる nrun 値は

==== sys/kern/sched_4bsd.c ====
1382 int
1383 sched_load(void)
1384 {
1385         return (sched_tdcnt);
1386 }

から取ってきているので, sched_tdcnt に対して操作(加減算)を行っている場所を見ます。

==== sys/kern/sched_4bsd.c ====
242 static __inline void
243 sched_load_add(void)
244 {
245         sched_tdcnt++;
246         CTR1(KTR_SCHED, "global load: %d", sched_tdcnt);
247 }
248
249 static __inline void
250 sched_load_rem(void)
251 {
252         sched_tdcnt--;
253         CTR1(KTR_SCHED, "global load: %d", sched_tdcnt);
254 }

さらに sched_load_add の呼び出し元を辿っていくと, スケジューラの初期化時(sched_setup), ランキューにプロセスが追加される時(sched_add), コンテキストスイッチが行われる時(sched_switch) となります。

sched_setup は OS 起動後の最初のプロセス用に, sched_add は追加するプロセス用にカウントが行われます。(これらの対象プロセスは実行可能状態になります)

肝は sched_switch で, td が現在実行中のプロセスであり, newtd (NULL の場合はランキュー内で優先度の一番高いプロセス) が次に実行中となるプロセスです。

==== sys/kern/sched_4bsd.c ====
824 void
825 sched_switch(struct thread *td, struct thread *newtd, int flags)
826 {

(snip)

844         if ((p->p_flag & P_NOLOAD) == 0)
845                 sched_load_rem();

(snip)

867                 if (TD_IS_RUNNING(td)) {
868                         /* Put us back on the run queue. */
869                         sched_add(td, (flags & SW_PREEMPT) ?
870                             SRQ_OURSELF|SRQ_YIELDING|SRQ_PREEMPTED :
871                             SRQ_OURSELF|SRQ_YIELDING);

845 行目で, 現在のプロセスがランキューから外れるのでデクリメントされ, TD_IS_RUNNING マクロが真であれば 869 行目で再度ランキューに加えます。

TD_IS_RUNNING は

==== sys/sys/proc.h ====
411 #define TD_IS_RUNNING(td)       ((td)->td_state == TDS_RUNNING)

と定義されている通りなので, プロセスが実行状態(TDS_RUNNING) にあった場合のみ (sched_add 経由で)カウントされます。
即ち, プロセスが実行不可状態の時にコンテキストスイッチが起きた場合はカウントされないことになります。

Linux

次に Linux (2.6.27)を見ていきます。

==== kernel/timer.c ====
1081 static inline void calc_load(unsigned long ticks)
1082 {
1083         unsigned long active_tasks; /* fixed-point */
1084         static int count = LOAD_FREQ;
1085
1086         count -= ticks;
1087         if (unlikely(count < 0)) {
1088                 active_tasks = count_active_tasks();
1089                 do {
1090                         CALC_LOAD(avenrun[0], EXP_1, active_tasks);
1091                         CALC_LOAD(avenrun[1], EXP_5, active_tasks);
1092                         CALC_LOAD(avenrun[2], EXP_15, active_tasks);
1093                         count += LOAD_FREQ;
1094                 } while (count < 0);
1095         }
1096 }

ロードアベレージは, calc_load 関数で計算されていますが, その元となる active_tasks の値は

====  kernel/sched.c ====
2715 unsigned long nr_active(void)
2716 {
2717         unsigned long i, running = 0, uninterruptible = 0;
2718
2719         for_each_online_cpu(i) {
2720                 running += cpu_rq(i)->nr_running;
2721                 uninterruptible += cpu_rq(i)->nr_uninterruptible;
2722         }
2723
2724         if (unlikely((long)uninterruptible < 0))
2725                 uninterruptible = 0;
2726
2727         return running + uninterruptible;
2728 }

各 CPU 毎のランキューにある nr_running と nr_uninterruptible の合計を取っています。
変数名からして結果は判っているようなものですが, 一応 nr_uninterruptible を操作している所を見ます。

====  kernel/sched.c ====
1704 /*
1705  * activate_task - move a task to the runqueue.
1706  */
1707 static void activate_task(struct rq *rq, struct task_struct *p, int wakeup)
1708 {
1709         if (task_contributes_to_load(p))
1710                 rq->nr_uninterruptible--;
1711
1712         enqueue_task(rq, p, wakeup);
1713         inc_nr_running(rq);
1714 }
1715
1716 /*
1717  * deactivate_task - remove a task from the runqueue.
1718  */
1719 static void deactivate_task(struct rq *rq, struct task_struct *p, int sleep)
1720 {
1721         if (task_contributes_to_load(p))
1722                 rq->nr_uninterruptible++;
1723
1724         dequeue_task(rq, p, sleep);
1725         dec_nr_running(rq);
1726 }

カウント操作はプロセスをランキューに追加・削除する場合に限られています。
ここで, task_contributes_to_load は

==== include/linux/ sched.h ====
 204 #define task_contributes_to_load(task)  \
 205                                 ((task->state & TASK_UNINTERRUPTIBLE) != 0)

ですので, ランキューからプロセスを取り除く際に, 対象プロセスが「割り込み不可休止」(TASK_UNINTERRUPTIBLE) であれば nr_uninterruptible がインクリメントされます。

結果

見てきた通り, FreeBSD は「実行中」と「実行可能」の物のみが, Linux ではそれに加え「割り込み不可休止」がロードアベレージの算出に利用されていました。(その他の UNIX システムまでは確認できませんでしたが, 元記事を信用するなら Linux が他と異なると考えてもよさそう?)
例えば, 大量データをファイルに読み書きするプロセスが複数居た場合, FreeBSD では(I/O 待ちが増える分) ロードアベレージが下がり, Linux では(I/O 待ちもカウントされるので) ロードアベレージが上がることになります。

ロードアベレージのみでシステムの負荷状況を判断できないとは, 重々知りつつも「ロードアベレージが高い」→「vmstat, sar で原因を探る」の流れで作業を行うことが多かったのですが, OS の違いにより値が変わってくることも頭に入れて置かないと駄目なようです。(やはり, どんなシステムを動かしているのか? が重要ですね)

# ちなみに, どうして Linux では旧来の UNIX とは算出方法を変えたのでしょうか?理由を知りたかったのですが見つけられませんでした。