プロセススケジューラ
用語
タイムスライス
各プロセスが切り替えられる適当な長さの時間
コンテキストスイッチ
タイムスライスに基づいて発生するプロセス間の切り替え
スループット
単位時間あたりの総仕事量。高いほど良い
CPU の場合、アイドル時間の割合が低いほど、高いといえる
レイテンシ
各処理の開始から終了までの経過時間。短いほど良い
プロセスの動きを計測する
プロセスの動きについて計測するために、以下のようなコードを書く。
code:c
#define NLOOP_FOR_ESTIMATION 1000000000L // MSEC -> NSEC 用
// SEC -> NSEC 変換用
/**
* @param timespec before ベース時間
* @param timespec after 経過時間
* @return 二つの時間の間の差分を計算する。単位はナノ秒
*/
static inline long diff_nsec(struct timespec before, struct timespec after) {
return ((after.tv_sec * NSECS_PER_SEC + after.tv_nsec)
- (before.tv_sec * NSECS_PER_SEC + before.tv_nsec));
}
/**
* CPU 時間を1ms使う処理に必要な計算量を推定する
* 適当な回数ループさせ、それを所要時間で割れば、何回ループすれば 1ms になるかわかる
*
* @return 1msec 毎のループ数
*/
static unsigned long loops_per_msec() {
struct timespec before, after;
clock_gettime(CLOCK_MONOTONIC, &before);
// 経過時間計測のためにループする
unsigned long i;
for (i = 0; i < NLOOP_FOR_ESTIMATION; i++)
;
clock_gettime(CLOCK_MONOTONIC, &after);
int ret;
return NLOOP_FOR_ESTIMATION * NSECS_PER_MSEC / diff_nsec(before, after);
}
/**
与えられた回数ひたすらループする
* @param nloop ループ回数
*/
static inline void load(unsigned long nloop) {
unsigned long i;
for (i = 0; i < nloop; i++)
;
}
/**
* @param id プロセスID
* @param buf ログ
* @param nrecord 記録回数
* @param nloop_per_resol 採取一回毎のループ回数
* @param start 計測開始時間
*/
static void child_fn(int id, struct timespec *buf, int nrecord, unsigned long nloop_per_resol, struct timespec start) {
int i;
// nrecord 回記録
for (i = 0; i < nrecord; i++) {
struct timespec ts;
// ループを実施
load(nloop_per_resol);
// 時刻採取
clock_gettime(CLOCK_MONOTONIC, &ts);
}
// 採取毎の経過時間出力
for (i = 0; i < nrecord; i++) {
printf("%d\t%ld\t%d\n", id , diff_nsec(start, bufi) / NSECS_PER_MSEC, (i+1)*100/nrecord); }
exit(EXIT_SUCCESS);
}
static void parent_fn(int nproc) {
int i;
for (i = 0; i < nproc; i++)
wait(NULL);
}
static pid_t *pids;
/**
* ひたすら CPU 時間を使うプロセスを1つないし複数動かし、統計情報を取得する
*
* - ある時点で、論理 CPU 上ではどのプロセスが動作しているか
* - それぞれの進捗はどうか
*
* n 個のプロセスを同時に動作させる
* - CPU 時間を total ミリ秒使った後に終了する
* - CPU 時間を resol ミリ秒使う毎に、以下を記録する
* - PID
* - プログラム開始時点からの経過時間
* - 進捗
*
* @param nproc 同時に動かすプロセス数
* @param total プログラムを動作させる合計時間
* @param resol 統計情報の採取間隔 (ミリ秒単位)
*/
int main(int argc, char *argv[]) {
int ret = EXIT_FAILURE;
if (argc < 4) {
fprintf(stderr, "引数が足りません: %s", argv0); }
// 引数のバリデーション
if (nproc < 1) {
fprintf(stderr, "nproc %d は 1 以上にしてください", nproc);
exit(EXIT_FAILURE);
}
if (total < 1) {
fprintf(stderr, "total %d は 1 以上にしてください", total);
exit(EXIT_FAILURE);
}
if (resol < 1) {
fprintf(stderr, "resol %d は 1 以上にしてください", resol);
exit(EXIT_FAILURE);
}
if (total % resol) {
fprintf(stderr, "採取間隔が合計稼働時間より短くなってしまっています");
exit(EXIT_FAILURE);
}
// 処理全体における記録回数
int nrecord = total / resol;
// ログ記録用
struct timespec *logbuf = malloc(nrecord * sizeof(struct timespec));
if (!logbuf)
err(EXIT_FAILURE, "logbuf 用のメモリ確保に失敗");
// 採取間隔毎にプロセスが何回走るかの推定
unsigned long nloop_per_resol = loops_per_msec() * resol;
// pid 記録用
pids = malloc(nproc * sizeof(pid_t));
if (pids == NULL) {
warn("malloc 失敗");
goto free_logbuf;
}
// 計測開始
struct timespec start;
clock_gettime(CLOCK_MONOTONIC, &start);
int i, ncreated;
for (i = 0, ncreated = 0; i < nproc; i++, ncreated++) {
// nproc 分プロセスを起動する
// pid が 0 なら子、0 以上なら親
// 子プロセスの場合のみ計測を開始する
goto wait_children;
child_fn(i, logbuf, nrecord, nloop_per_resol, start);
}
}
ret = EXIT_SUCCESS;
wait_children:
if (ret == EXIT_FAILURE)
for (i = 0; i < ncreated; i++)
if (kill(pidsi, SIGINT) < 0) warn("kill(%d) failed", pidsi); for (i = 0; i < ncreated; i++)
if (wait(NULL) < 0)
warn("wait() failed.");
free_pids:
free(pids);
free_logbuf:
free(logbuf);
exit(ret);
}
複数プロセスの走り方
AWS 環境があったので Amazon Linux を立てる。ローカルでは色々動いているし。たてた後は開発ツール一式をインストール。
以下でコンパイル & 実行。taskset で実行する CPU を制限できる。
code:shell
table:experiments
プロセス数 プロセスあたりのCPU利用時間 採取間隔
1 100 1
2 100 1
4 100 1
グラフの出力のためには下記のようなコードを書いた。
code:python
import numpy as np
import matplotlib.pyplot as plt
import csv
f = file("./4_100_1.log")
nprocess = 4
reader = list(csv.reader(f, delimiter='\t'))
log = sorted(reader, key=lambda d:d1, reverse=False) pid = []
sec = []
processing = []
for i in range(nprocess):
pid.append(map(lambda d: int(d0), filter(lambda d:d0==str(i), log))) sec.append(map(lambda d: int(d1), filter(lambda d:d0==str(i), log))) processing.append(map(lambda d: int(d2), filter(lambda d:d0==str(i), log))) fig = plt.figure()
ax1 = fig.add_subplot(2,1,1)
ax2 = fig.add_subplot(2,1,2)
for i in range(nprocess):
ax2.scatter(seci, processingi) plt.show()
結果は以下
https://gyazo.com/775e26cd45773e1d7db2586a0dbc3a12
https://gyazo.com/3c0b221947184d0c143dcabeed29b5dd
https://gyazo.com/4c40c74675796320c35d8877f34fe35a
書籍 ([試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識 )と同様の結果になっていることがわかる。
複数のプロセスがある場合は均等に分割してラウンドロビン方式で順次実行されている。また、スループットとレイテンシについては、以下がわかる。
スループット
単位時間あたりの仕事量
プロセス数が多くても少なくても変わらない
プロセスあたりだいたい 120 msec
レイテンシ
仕事を終えるまでの時間
プロセス数が多いと悪化し、少ないとよくなる
各プロセスがタイムスライスの単位で均等に実行されるようになっているため
複数CPUにおける複数プロセスの走り方
CPU が複数ある場合は、スケジューラ内の、ロードバランサ, あるいは グローバルスケジューラ という機能が動作する。これらは、 複数の CPU 間でプロセスを公平に分配する。
複数の CPU に対して実験する場合には、以下に注意する。
CPU 0 と CPU数/2 の番号の CPU を選ぶ。(キャッシュメモリの共有等がなく、独立性が高い)
ハイパースレッドが有効な環境で、CPU が認識している CPU が 2 つでないかどうか確認する
今回は t2.midium で実験してみた。
code:shell
table:experiments
プロセス数 プロセスあたりのCPU利用時間 採取間隔
1 100 1
2 100 1
4 100 1
https://gyazo.com/6d1912d7ba7e4a44a26647cd37e7d69e
https://gyazo.com/ecfd9d34a9c4539b6d3e744b47d5afab
2 プロセスの場合、先ほどとはことなりほぼ同時に実行が行われている。一方、4 プロセスの場合は、0 と 2、1 と 3 が、各々同じ CPU 上で実行されているように見える。また、スループットとレイテンシについては、以下がわかる。
スループット
単位時間あたりの仕事量
CPU数が多いと良くなるが、プロセスが同時に走っている必要がある
同時に走っている部分は複数CPUに分散されるためスループットが向上するが、1つのCPUの処理能力は変わらない
CPU数よりプロセス数を多くしてもそれ以上は上がらない
CPU数分しか同時にできる仕事量は増やせない
レイテンシ
仕事を終えるまでの時間
CPU数が多いと良くなるが、スループットと同様の制約がある
プロセスの状態遷移
プロセスディスクリプタには以下の状態がある。TASK_RUNNING は実行可能な状態であって、必ずしも実行されている訳では無いので注意。
table:process_states
TASK_RUNNING 実行可能状態。CPUが空けばいつでも実行可能
TASK_INTERRUPTIBLE 割り込み可能な待ち状態。復帰時間が予測不能な長時間の待ち状態。ex) スリープ, ユーザ入力待ち
TASK_UNINTERRUPTIABLE 割り込み不可能な待ち状態。短時間で復帰する場合の待ち状態。ex) ディスク入出力
TASK_STOPPED サスペンドシグナルを送られて実行中断になった状態
TASK_ZOMBIE ゾンビ状態。子プロセスが exit して親プロセスにリープされるまでの状態
プロセスの状態は ps ax で見ることができる。STAT 欄は下記に対応している。
R (Run) : TASK_RUNNING
S (Sleep) : TASK_INTERRUPTIBLE
D (Disk Sleep) : TASK_UNINTERRUPTIBLE
Z (Zombie) : TASK_ZOMBIE
ほとんどのプロセスは Sleep であることがわかる。
code:shell
PID TTY STAT TIME COMMAND
1 ? Ss 0:00 /sbin/init
# 中略
2640 ttyS0 Ss+ 0:00 /sbin/agetty ttyS0 9600 vt100-nav
2642 tty1 Ss+ 0:00 /sbin/mingetty /dev/tty1
2646 tty2 Ss+ 0:00 /sbin/mingetty /dev/tty2
2648 tty3 Ss+ 0:00 /sbin/mingetty /dev/tty3
2650 tty4 Ss+ 0:00 /sbin/mingetty /dev/tty4
2652 tty5 Ss+ 0:00 /sbin/mingetty /dev/tty5
2654 tty6 Ss+ 0:00 /sbin/mingetty /dev/tty6
2685 ? Ss 0:00 sshd: ec2-user priv 2687 ? S 0:00 sshd: ec2-user@pts/0
2688 pts/0 Ss 0:00 -bash
2711 pts/0 R+ 0:00 ps ax
アイドル状態
動作しているプロセスが無い間は、アイドルプロセスにより CPU が休止状態にされ、プロセスが一つ以上実行可能状態になるまで待つ。アイドル状態になっているかどうかは sar コマンドで確認できる。
参考文献
武内 覚 (2018/2/23)[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識
サーバ/インフラを支える技術 ‾スケーラビリティ、ハイパフォーマンス、省力運用 (WEB+DB PRESS plusシリーズ)