ロードアベレージ
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 とは算出方法を変えたのでしょうか?理由を知りたかったのですが見つけられませんでした。