(第25回)では,ノン・プリエンプティブっていうのは?

2007/12/02

あなたは 人目のお客様です.

前回は優先度をベースにしたプリエンプティブな動作について試してみたが, 今回はノン・プリエンプティブな動作について試してみたい.

ノン・プリエンプティブとはどんな動作かというと いわゆる汎用OSがそーいう動きをするのだけど, まあ簡単に言うと前回当たり前のように行われていた 「あるタスクの動作中に,もっと優先度の高いタスクが平気で割り込んでくる」 という動作が無いものだ.というより,汎用OSではタスクに優先度というものが無く, みんな平等,と思ってくれていい (優先度を設定できる汎用OSもあるが,大抵は変に設定すると 即固まったりするので優先度の変更は推奨されていなかったり, 裏技のようなものであったりするので,ここではそーいうのは言及しない).

まあ論より証拠なので,汎用OSの詳しい説明は後にして とりあえずノン・プリエンプティブな動作を見てみよう. ノン・プリエンプティブな動作を見るためには,とりあえず前回のソースで sample1, sample2, sample3 の優先度を全部同じにしてしまうのがてっとりばやい.

diff -ruN kozos24/main.c kozos25_2/main.c
--- kozos24/main.c	Sun Dec  2 21:10:47 2007
+++ kozos25_2/main.c	Sun Dec  2 21:24:08 2007
@@ -10,7 +10,7 @@
   int i, count;
   char *p;
 
-#define INTERVAL 7 /* CPU能力に応じて調整してください */
+#define INTERVAL 5 /* CPU能力に応じて調整してください */
   while (1) {
     count = 0;
     kz_timer(INTERVAL - argc);
@@ -55,8 +55,8 @@
   httpd_id    = kz_run(httpd_main,    "httpd",     9, 0, NULL);
 
   sample1_id  = kz_run(sample_main,   "sample1",  10, 0, NULL);
-  sample2_id  = kz_run(sample_main,   "sample2",  11, 1, NULL);
-  sample3_id  = kz_run(sample_main,   "sample3",  12, 2, NULL);
+  sample2_id  = kz_run(sample_main,   "sample2",  10, 1, NULL);
+  sample3_id  = kz_run(sample_main,   "sample3",  10, 2, NULL);
 
   return 0;
 }
sample1, sample2, sample3 の優先度をすべて10で同じにしてみた. このため,他のもっと優先度の高いスレッドが割り込んでくることはあるが, この3つのスレッドに関してのみいうならばノン・プリエンプティブで動作する.

しかし,このような修正を入れて実行すると,こんな感じの結果になる.

...
8e9f0g1h2i3j4a5b6c7d8e9f0g1h2i3j4a5b6c7d8e9f
ghijabcdefghijabcdef[0]gAhBiCjDaEbFcGdHeIfJgAhBiCj[2]D
0E1F2G3H4I5J6A7B8C9D0E1F2G3H4I5J6A7B8C9D0E1F2G3H4I5J6A7B8C9D0E1F2G3H4I[1]5Ja6Ab7Bc8Cd9De0Ef1Fg2Gh3Hi4Ij5Ja6Ab7Bc8Cd9De0Ef1Fg2Gh3Hi4Ij5Ja6
b7c8d9e0f1g2h3i4j5a6b7c8d9e
fghijabcdefghijabcdefghijabcdefghij
[0]ABCDEFGHIJABCDEFGHIJABCDE[2]F0G1H2I3J4A5B6C7D8E9F0G1H2I3J4A5B6C7D8E9F0G1H2I3J4A5B6C7D8E9F0G1H2I3J4A5B6C7D8E9F0G1H2I3J4
5678901234567890123456789
[1]abcdefghijabcdefghijabcdefghijabcdefghija[2]b0c1d2e3f4g5h6i7j8a9b0c1d2e3f4g5h6i7j[0]8aA9bB0cC1dD2eE3fF4gG5hH6iI7jJ8
A9B0C1D2E3F4G5H6I7J8A9B0C1D2E3F4G5H6I7J8A9B0C1D2E3F4G5H6I7J8A9[1]B0aC1bD2cE3dF4eG5fH6gI7hJ8iA9jB
aCbDcEdFeGfHgIhJiAjBaCbDcEdFeGfHgIhJi
ja[2]b0c1d2e3f4g5h6i7j8a9b0c1d2e3f4g5h6i7j8a9b0c1d2e3f4g5h6i7j8a9b0c1d2e3f4g5h6i7j8
9012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFG[2]H0I1J2A3B4C5D6E7F8G9H0I1J2A[1]3Ba4Cb5Dc6Ed7Fe8Gf9Hg0Ih1Ji2Aj3Ba4Cb5Dc6Ed7Fe8Gf9Hg0Ih1Ji2Aj3Ba4Cb5Dc6Ed7Fe8Gf9Hg0Ih1Ji2
j3a4b5c6d7e8f9g0h1i2j3a4b5c6d7e8f9g0h1i2j3a4b5c6d7e8f9g
hijabcdefghij
[0]ABCDEFGHIJABCDEFGHIJABCDEFG[2]H0I1J2A3B4C5D6E7F8G^C
...
なんか3つのスレッドの出力が,文字単位で混ざってしまっている感じだ. なぜこんなことになるかというと,文字の出力を1文字ごとに outlog スレッドに お願いしているため,そこで kz_send() によるメッセージ送信が発生する. しかしこれはシステムコールであるため,その都度スレッドのスケジューリングが 行われ,sample1, sample2, sample3 は優先度が同じであるためラウンドロビンで 実行されてしまっているのだな.

なので今回は,出力を outlog スレッドに任せるのでなく, write() によって直接出力するように修正する.これならば文字出力時にも スレッドのスケジューリングは行われない.

ということで,以下が今回のソース.

(2009/04/10 ライセンスに関する文書として,KL-01とLICENSEを追加. 詳しくは第43回を参照)

修正点は以下.

diff -ruN kozos24/main.c kozos25/main.c
--- kozos24/main.c	Sun Dec  2 21:10:47 2007
+++ kozos25/main.c	Sun Dec  2 21:13:43 2007
@@ -1,5 +1,6 @@
 #include 
 #include 
+#include 
 
 #include "kozos.h"
 
@@ -8,29 +9,27 @@
 static int sample_main(int argc, char *argv[])
 {
   int i, count;
-  char *p;
+  char buf[128];
+  char *p = buf;
 
-#define INTERVAL 7 /* CPU能力に応じて調整してください */
+#define INTERVAL 5 /* CPU能力に応じて調整してください */
   while (1) {
     count = 0;
     kz_timer(INTERVAL - argc);
     kz_recv(NULL, NULL);
-    p = kz_memalloc(128);
     p[0] = '[';
     p[1] = '0' + argc;
     p[2] = ']';
     p[3] = '\0';
-    kz_send(outlog_id, 0, p);
+    write(2, buf, 3);
     for (i = 0; i < 70; i++) {
-      p = kz_memalloc(128);
       p[0] = outnum[argc][(count++) % 10];
       p[1] = '\0';
-      kz_send(outlog_id, 0, p);
+      write(2, buf, 1);
     }
-    p = kz_memalloc(128);
     p[0] = '\n';
     p[1] = '\0';
-    kz_send(outlog_id, 0, p);
+    write(2, buf, 1);
   }
   return 0;
 }
@@ -55,8 +54,8 @@
   httpd_id    = kz_run(httpd_main,    "httpd",     9, 0, NULL);
 
   sample1_id  = kz_run(sample_main,   "sample1",  10, 0, NULL);
-  sample2_id  = kz_run(sample_main,   "sample2",  11, 1, NULL);
-  sample3_id  = kz_run(sample_main,   "sample3",  12, 2, NULL);
+  sample2_id  = kz_run(sample_main,   "sample2",  10, 1, NULL);
+  sample3_id  = kz_run(sample_main,   "sample3",  10, 2, NULL);
 
   return 0;
 }
出力に write() を使用し, sample1, sample2, sample3 の優先度を10で同じにしてある.

で,以下が実行結果.

...
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[1]abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij
[2]0123456789012345678901234567890123456789012345678901234567890123456789
[0]ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
...
出力中に割り込まれること無く, すべてのスレッドが1行単位できれいに表示できている. 割り込まれまくってぐちゃぐちゃになっていた前回(第24回)の結果とはえらい違いだ.

ここで説明のために,KOZOSのスケジューリング部分を見てみよう. まずはスレッドをレディーキューに出し入れする getcurrent(), putcurrent() の処理だ.

static void getcurrent()
{
  readyque[current->pri] = current->next;
  current->next = NULL;
}

static int putcurrent()
{
  kz_thread **thpp;
  for (thpp = &readyque[current->pri]; *thpp; thpp = &((*thpp)->next)) {
    /* すでに有る場合は無視 */
    if (*thpp == current) return -1;
  }
  *thpp = current;
  return 0;
}
getcurrent() はレディーキューの先頭からスレッドを抜き取る. putcurrent() はレディーキューのお尻を検索し,そこにスレッドを接続する. ということで,KOZOSでは同じ優先度のスレッドはラウンドロビンでスケジュール されることになる.ラウンドロビンとは,さっき実行したものが最後に回される というスケジューリング方式だ.まあ全部にわたって均等に実行される, ということになる.

次に割り込み処理(syscall_proc())と, そこから呼ばれるシステムコールの呼び出し処理部分(thread_intrvec()).

static void syscall_proc()
{
  /* システムコールの実行中にcurrentが書き換わるのでポインタを保存しておく */
  kz_syscall_param_t *p = current->syscall.param;

  getcurrent();

  switch (current->syscall.type) {
  case KZ_SYSCALL_TYPE_RUN:
    p->un.run.ret = thread_run(p->un.run.func, p->un.run.name, p->un.run.pri,
			       p->un.run.argc, p->un.run.argv);
    break;
  case KZ_SYSCALL_TYPE_EXIT:
    /* スレッドが解放されるので戻り値などを書き込んではいけない */
    thread_exit();
    break;
  case KZ_SYSCALL_TYPE_WAIT:
    p->un.wait.ret = thread_wait();
    break;
  case KZ_SYSCALL_TYPE_SLEEP:
    p->un.sleep.ret = thread_sleep();
    break;
  case KZ_SYSCALL_TYPE_WAKEUP:
    p->un.wakeup.ret = thread_wakeup(p->un.wakeup.id);
    break;
  case KZ_SYSCALL_TYPE_GETID:
    p->un.getid.ret = thread_getid();
    break;
  case KZ_SYSCALL_TYPE_CHPRI:
    p->un.chpri.ret = thread_chpri(p->un.chpri.pri);
    break;
  case KZ_SYSCALL_TYPE_SEND:
    p->un.send.ret = thread_send(p->un.send.id, p->un.send.size, p->un.send.p);
    break;
  case KZ_SYSCALL_TYPE_RECV:
    p->un.recv.ret = thread_recv(p->un.recv.idp, p->un.recv.pp);
    break;
  case KZ_SYSCALL_TYPE_TIMER:
    p->un.timer.ret = thread_timer(p->un.timer.msec);
    break;
  case KZ_SYSCALL_TYPE_PENDING:
    p->un.pending.ret = thread_pending();
    break;
  case KZ_SYSCALL_TYPE_SETSIG:
    p->un.setsig.ret = thread_setsig(p->un.setsig.signo);
    break;
  case KZ_SYSCALL_TYPE_DEBUG:
    p->un.debug.ret = thread_debug(p->un.debug.sockt);
    break;
  case KZ_SYSCALL_TYPE_MEMALLOC:
    p->un.memalloc.ret = thread_memalloc(p->un.memalloc.size);
    break;
  case KZ_SYSCALL_TYPE_MEMFREE:
    p->un.memfree.ret = thread_memfree(p->un.memfree.p);
    break;
  default:
    break;
  }
  return;
}
...
static void thread_intrvec(int signo)
{
  switch (signo) {
  case SIGSYS: /* システムコール */
    syscall_proc();
    break;
  case SIGHUP: /* 外部割込み */
    break;
  case SIGALRM: /* タイマ割込み発生 */
    alarm_handler();
    break;
  case SIGBUS: /* ダウン要因発生 */
  case SIGSEGV:
  case SIGTRAP:
  case SIGILL:
    if (debug_sockt) {
      stub_proc(current, signo);
    } else {
      fprintf(stderr, "error thread \"%s\"\n", current->name);
      /* ダウン要因発生により継続不可能なので,スリープ状態にする*/
      getcurrent();
#if 1 /* スレッド終了する */
      thread_exit();
#endif
    }
    break;
  default:
    break;
  }
  extintr_proc(signo);
  schedule();

  /*
   * スタブでの read() 待ちブロック中に SIGALRM (および SIGHUP)が発生した
   * 場合に,以下の setcontext() によるコンテキストスイッチで SIGALRM が
   * 消えてしまう(これはスタブでのブロックに比べると発生頻度は低いが,OSの
   * 処理中にシグナル発生した場合も同様).
   * 対策として,シグナルのブロックを一瞬だけ開けて,SIGALRM が発生した
   * 場合には,再度シグナル処理を行う.
   * これはOSの割り込み処理の再入になるが,以下の位置に限定して再入が行われる
   * ので問題は無い.
   */
  on_os_stack = 1;
  sigprocmask(SIG_UNBLOCK, &block, NULL);
  sigprocmask(SIG_BLOCK, &block, NULL);
  on_os_stack = 0;

  /* ここで SIGALRM が発生するとシグナルを取りこぼす...要検討 */

  setcontext(¤t->context.uap);
}
割り込み発生時には割り込みベクタの処理として thread_intrvec() が呼ばれるが, ここではとくに何もせず,各割り込み処理を呼び出してからスケジューリングのために schedule() を呼び,最後に setcontext() によってコンテキスト切替え (スレッドのディスパッチ)を行っている.

割り込みの内容がシステムコールの場合には syscall_proc() が呼ばれる. syscall_proc() では getcurrent() が行われた後,必要ならば各システムコールの 処理の中で putcurrent() が呼ばれる.

getcurrent() が呼ばれているのは,ダウン要因発生時を除けば,システムコールの ときだけだ.ということは,システムコール以外の割り込み処理の場合には getcurrent(), putcurrent() が呼ばれないため,スレッドのラウンドロビンは 行われないということになる.

ここで sample1, sample2, sample3 について考えてみよう. たとえば sample1 の実行中に SIGHUP が入って,他のもっと優先度の高いスレッドが (プリエンプティブに)実行可能になったとしよう.この場合,次にスケジュールされる のはそっちのスレッドになるが,getcurrent() が呼ばれるわけではないので, sample1 はレディーキューの先頭にいるままだ.なので,割り込んできた優先度の 高いスレッドが処理を終了した後,スケジューリングされるのは再び sample1 になる.

さらに sample1 の実行中に,sample2 が設定したタイマ割り込みが発生し, sample2 に対してメッセージが投げられ,sample2 が実行可能になったとしよう. この場合,putcurrent() によって sample2 はレディーキューのお尻に接続される. よって,次にスケジューリングされるのはやはり sample1 だ. 結局のところ,システムコールによって getcurrent() が呼ばれてレディーキューから いったん抜かれないことには,sample1 が常にキューの先頭にあるため, sample2, sample3 に処理は回らずに sample1 が実行されることになる.

このようなことを考えると,優先度が同じである sample1, sample2, sample3 は, 互いに割り込まれることは無く実行される(そしてシステムコールが呼ばれると, そのタイミングでラウンドロビンが発生し,スレッドが切り替わる). まあもちろんこの3スレッドよりも優先度の高いスレッドが割り込んでくることは あるわけだが,この3つに閉じて言うならば,ノン・プリエンプティブに 動作しているといえる.

このようにあるスレッドの動作中に,他のスレッドに割り込まれること無く 動作することをノン・プリエンプティブと言う. まあとはいっても,これは sample1, sample2, sample3 のみ同じ優先度に しているのでこのようなことが起きているわけで, 優先度の高い他のスレッドが動作可能になった場合にはやっぱり割り込まれる わけだから,OS全体としてノン・プリエンプティブなのではなく, sample1, sample2, sample3 の3つの間だけでノン・プリエンプティブに動作 している,ということができる.

で,ここまではノン・プリエンプティブの説明なのだけど, ここからは汎用OSと組み込みOSの違いの話になる.

汎用OSだと,スレッド(汎用OSの場合はプロセスが主体だけど)はこーいうふうに 全部が同じ優先度で,ノン・プリエンプティブに割り込まれること無く動く. これに対して組み込みOSだと,優先度ベースのプリエンプティブな動作になるのが 普通だ.

まず汎用OSとはどんなものか? なのだけど,まあ一番身近なのは Windows だ.あと Macintosh とか. もしくは Linux や,この連載で利用している FreeBSD も汎用OSの仲間といえる.

汎用OSは,PCを使うためのOSともいえる. PCは,いろいろなアプリをインストールすることで多目的に使える, 多目的コンピュータだ.アプリをインストールすることで多目的に使えることが その最大の特徴だ.汎用OSはそれを使うためのOSなので, 多目的OSということもできるだろうか.

これに対して組み込みOSとはどんなものか? まず代表的なのは,我が日本が誇るμiTRONと,その実装であるToppersだろう.

なぜ組み込みOSが必要なのか? たとえば最近の携帯電話は,普通の電話機能の他にメーラになったり, ブラウザ代わりになったり,簡単なゲームができたりできる. そしてその最中にも着信があれば受けるし,時計も表示するし, つまり,複数の機能が同時に平行動作している.

これを汎用OSであるUNIX上で同じようなことをやろうとしたら簡単だ. ブラウザを起動して,メーラを起動して,時計アプリを起動するだけだ. これは複数のアプリを起動したときにも,時分割でそれぞれがそれなりに 動けるようにOSがCPU時間を配分してくれているからだ. プログラムを書く側からしても,各機能ごとに別々のアプリを作成して 別々の実行形式を作成しておけばよい. つまり,UNIXの上で動作しているからこそ, なにも考えずに複数のアプリが同時動作できるわけだ.

では,上記の機能をUNIX上で,ひとつのアプリでぜんぶ行うということを 考えてみてほしい.

この場合,複数の機能が同時動作するわけだから,一定時間ずつ処理を行っては いったんメインループに戻って,次の処理用の関数を呼び出して, これも一定時間たったら処理を中断してメインループに戻って,... ということをしなければならない.たとえばブラウザ機能部分が巨大ファイルの ダウンロード処理を行うとしたら, 一定時間ごとに中断して戻って時計表示を行ってまたダウンロード処理を呼ぶ, ということをしないと,ダウンロード処理を始めたら時計が固まる, ということになってしまう. もしくはダウンロード処理の中から時計処理を呼ぶという方法もあるが, これをやるときりがなくなる(あらゆる場所から,あらゆる処理用関数を ちょこまかと呼ばなければならなくなる)ので,まあまともな作りにはならない.

これが,OSが無いという状態だ.

しかしOSの上で動作させるならば,これらの機能は複数のアプリに分割して 書いてしまって構わない.OSが時分割で適当に,複数のプロセスを切替えながら 動作させてくれるため,なにもしなくても勝手に並列動作してくれるからだ. なのでブラウザは時計アプリのことは気にしないし,時計アプリはブラウザのことは 気にせずに書いてしまって構わない.

つまるところ,機能がひとつしかないのならば,OSなどなくてもかまわない. というより,単機能ならば,OSが存在するメリットはあまり無い. つまり組み込みOSの役割は,複数の機能が実装された際にも, それぞれを独立にプログラミングできるという点にある. だって時計アプリを書くときに,同時動作するかもしれないメーラやブラウザの ことなんて考えないよね.そういうとそんなの当たり前じゃん!といわれそうだけど, それはOSの上で書くから当り前になっているのであって,もしもOSが無いのならば, あるアプリがCPU使いすぎないようにするとか,CPU使いすぎてるときには適当にCPUを 開放するとかの対処が必要になる.でないと他のアプリが固まることになるわけだ.

で,組み込みOSと汎用OSの違いについて. まあこういうことはいろんなOSの説明資料で説明されていることなので いまさらなのだけど,ぼくが思う一番重要な違いだけいうと

ということにつきるとおもう.

上でも書いたように,汎用OSの役割はアプリをインストールすることで多目的に 使えることだ.ということは,アプリのインストールができること, そのために外部ストレージ(ハードディスクか,それに代わる何か) を持っていることが必須になる. (ここでいう「アプリ」はCPUが直接実行し,さらにOSのシステムコールを 直接実行できるという意味なので,たとえば携帯電話のJavaアプリとか フラッシュアプリとかいったものは除外する)

しかしアプリのインストールを許可するか,しないかというだけで, OSのポリシーはぜんぜん異なるものになる.

アプリをインストールできるようにするということは, ユーザがわけのわからんアプリを入れて動かしたりする,ということだ. それは製品版のアプリかもしれないし, GNUのようなそれなりに信頼できる団体やコミュニティで作られたフリーソフトウエア かもしれない.どこぞの馬の骨が作ったしょぼいフリーソフトウエアかもしれない. そもそもそのPCの持ち主がちょろっと作ったテスト用アプリかもしれない.

そして例えばそのアプリは,CPUやメモリを馬鹿食いするものかもしれない. やたらと時間のかかる計算処理をずっと行うかもしれないし, それどころか無限ループに陥ることすらあるかもしれない. バグがあって,頻繁にダウンするものかもしれない. 終了時にメモリをきちんと開放していないかもしれない. なのでそーいうようなアプリをユーザが勝手にインストールして動かしたとしても, そこそこ動く,問題があっても極力ほかのアプリに迷惑をかけない, といったことが重要となる.

ということで,汎用OSに求められる動作は

ということになる. まあひとことでいうと「アプリを信用しない」ということに尽きる.

この場合,アプリは優先度なしに,全部が同じ優先度で公平に(時分割で)動いて ほしい.そしてメモリの保護は,仮想メモリという考えになり, 各タスクはプロセス単位でそれぞれの独立した仮想メモリ上で動作する, という思想につながる.

アプリが無限ループに入ってしまったらどうするか? まあ無限ループではないにしても,すごく重い計算とかソート処理とかに 入ってしまうことはある.なんにせよ,そのへんから拾ってきたようなアプリを 走らせることもあるわけなので,アプリが何をするかは,汎用OSの場合は, ホント,わからない.

で,無限ループをそのまま許容したら,他のアプリは固まることになる. 汎用OSはノン・プリエンプティブなので,優先度をベースにして他のタスクが 割り込むことはできない. これでは困るので,汎用OSの場合はタイマで一定時間ごとにタスクを切り替える ようになっている.つまり,時分割で動作する. もしもOSを利用しないならば,その重い計算中に,一定時間ごとに他のタスクに 処理を渡すというか,そーいうめんどくさい書き方をしなければならなくなる. つまり,他の機能の事まで考えなければならないことになる.

例えばブラウザによる巨大ファイルのダウンロードとメーラとDVDプレイヤーによる 動画再生を並列で走らせたときのことを考えてみてほしい. これを汎用OSで,タスクごとに優先度をつけて動かしたらどうなるか?

この中でもっとも優先度を高くすべきなのは,きっと動画再生だ. 動画再生は一定時間ごとに確実に処理されないと,コマ落ちの原因になる. 次に優先度を高くすべきなのは,メーラだろう.ユーザが操作したときに, レスポンス良く動いてくれないとイライラするからだ. ダウンロードは,まあほっといてもそのうち終わっててくれればいいので, 一番優先度は低くなる.

しかし,実はこれではだめなのだ.たとえば動画再生が最高優先度の場合, 普段は問題なく動くだろう.しかし動画処理がCPUの限界性能にきてしまったとき, つまりある閾値を超えたときに,とつぜんマウスすら反応しなくなる, なんていうバグの原因になり得る. この場合,ほかのアプリはまったく動かなくなり,マウスポインタすら反応せず, ただ動画だけがコマ落ちしながら動いているという, とっても悲しい状態になるだろう.

では逆に,動画再生の優先度を最低にしたらどうなるか? これはこれで,ちょっと悲しい結果になる. というのはなにもなければ動画は普通に再生できるのだが, ブラウザでダウンロードとか始めると,一気にコマ落ちしまくる, という原因になる.このように,優先度の設計は他のアプリとの関係で 相対的に行わなければならないので,非常に難しい(そして間違えると固まるなどの 致命的な問題になる).

ということで,タスクに優先度をつけるという考え方は,汎用OSには向かない. すべてのアプリが自分に割り当てられた時間内でそこそこに, 均等に動いてほしいからだ. で,うまく動けないアプリがあったとしても,それはCPUパワーが足りない (このため割り当て時間が短い)ということなのでしかたがない,といえる. 重要なのは,CPUパワーが足りないような状況になったときに, 優先度の高いタスクのみ動き続けられても困る,ということだ (たぶん固まることになる). なので,すべてのアプリは優先度無しに一定時間ずつ動作する (無限ループや重い処理を行うようなアプリがあっても, CPU時間が他のアプリにきちんと割り当てられる),ということが望まれる.

これに対して,組み込みOSを考えてみよう.

組み込み機器では,基本的にユーザが好きなアプリをインストールして使うような ことはない. インストールするアプリはその製品の開発元が厳選し,十分なテストの後に 出荷することになる. このためアプリにバグがあったとしても,それは開発元で管理することができる. 製品をシステム全体として開発元で管理することができる. (汎用OSではこのような管理は無理.たとえば世の中に出回っている Windows 用の アプリ(フリーソフト含む)を Microsoft がすべてチェックする,なんてことは 現実的に無理)

組み込み機器ではインストールされるアプリが限定されるため, アプリごとに優先度を明確につけておくことができる. 優先度付けも含めたシステム全体の構成を,開発元で設計することができる.

もしも汎用OSでもこのような「優先度」を採用するとしよう. たとえば Windows ならば,Microsoftが世の中に出回っているすべての Windows 用 アプリを調べて適切に優先度をつけるようなことは無理だ. なので,ユーザが自分で設定するか,もしくはアプリを作った人が, 自分で好きな優先度を設定するかになる.しかし優先度というのは絶対的な値が 意味を持つものではなく,他のアプリとの相対値によって意味を持つものだ. なので,ちょっと優先度の設定を誤ると即システム全体が固まるとか, このアプリを入れるとこのアプリはまともに動作しないというような相性の問題に なり得る. 例えばマウスを動かしたときには,割り込みが入り,マウスが動かされたという情報が デバイスドライバに蓄えられる.これは割り込みの延長で行われるので, たとえ無限ループに陥っているアプリがあったとしても問題無く実行される. しかしその情報を吸い上げ,マウスカーソルを動かすのは,それはそれでそーいう アプリが行っている.で,他のアプリがCPUを食いまくって,そのアプリのほうが 優先度が高いならば,マウスがまったく動かない(ので,そのアプリを止めることすら できない)ということになりうる.

また組み込み機器では,メモリを保護する必要はさほど無い. というよりリアルタイム性が求められることも多いため, むしろ仮想メモリを使うべきではない,ということも多い (仮想メモリを実装せずに,局所的にメモリ保護を行う場合は多いが, この場合はタスク単位でメモリが保護されるわけではない). これは仮想メモリのマッピング切替えに時間がかかるということもあるが, ページングによりリアルタイム性がまったく確保できなくなる,という理由が大きい.

ここで,もう一度汎用OSと組み込みOSの違いを思い出してほしい.

これは,さらに付け加えると ということになる.こーしてみると, 汎用OSは性悪説,組み込みOSは性善説に基づいている, ともいえるように思う.

汎用OSは不特定多数が集まるなんかの大会の会場(なので人の迷惑になることをする人が いるかもしれないし,そのときに他の人に迷惑がかからないようなシステム (警備員を配置するとか,大会本部がきちんと管理するとか,大会規約を決めるとか) が必要), 組み込みOSは家族(なので悪いことをする人はいないし,いるとしたらそれは そもそもその人をその場で正すべきなので,他のメンバーに迷惑がかからないように するようなシステムは不要), と例えられるだろうか. うーん,あんまりわかりやすい例えになっていないかなあ... でも組み込みを家族とか言っちゃうと,まるで家族内で優先度があるみたいでやだね. やっぱしあまりいい例えじゃあないかしら.

まあもっとも組み込みにもいくつか種類があって, 汎用OSのようにCPUやメモリが足りない場合にはそこまでで許される, というものもあれば,厳密なリアルタイム性が求められるものもある. なので組み込み機器にLinuxのような汎用OSが用いられたり, Linux をもっと組み込みっぽく改造した組み込み Linux というものが 使われることもある.


メールは kozos(アットマーク)kozos.jp まで