2009年11月アーカイブ



久々に飲み過ぎた。今日は酒がまずい。いつもこのくらまずければ楽々節酒な
のだけど...。はしごした先では本当久々の酒友達に会えて楽しかった。しかし
はしごするともう体がだめだな〜。しばらく反省しよう。

スケジューラアクティベーション、ちょっと構造がつかめてきた。基本的には シグナル配送のようなものだ。そうなると、ユーザ層だけのスレッドパッケー ジの実装で、ブロックのシステムコールを、ノンブロックにして、シグナルで スケジューリンングをするようなことの一般化の面があるか。Machではシグナ ルはタスク例外ポートを使ってUXサーバが実装する。スケジューラアクティベー ションもこの枠組みで構成できないだろうか?効率は悪いけど。現実にはここで 実装したような一般的なアップコールの仕組みを特定の状況のために作ってお いた方が、いいとは思う。
Adding Scheduler Activations to Mach 3.0

Paul Barton-Davis, Dylan McNamee, Raj Vaswani, and Edward D.Lazowska
University of Washington

Technical Report 92-08-03 Reversed March 1993
続き。
2. スケジューラアクティベーションのためのカーネル支援

この章ではスケジューラアクティベーションをサポートするためにMach 3.0 カー
ネル*(MK78)に加えた変更について説明する。* UXサーバへの変更は必要ないカー
ネルからの観点では、スケジューラアクティベーションは伝統的なMachのカー
ネルスレッドであり、タスクに起因する(あるいは影響する)、イベントをユー
ザ層に反映するという追加的な特性をもつ。

2.1 初期化

変更されたMachカーネルは、伝統的なMachのタスク(これらはMachのカーネルス
レッドを要求する)と、スケジューラアクティベーションを使うタスクの両方を
サポートする。タスクは次に示す新しいシステムコールを使って、スケジュー
ラアクティベーションを使用することをカーネルに通知する:

 + task_register_upcalls (task, upcalls). ユーザ空間の入口の組を登録す
   る: ユーザ層へのアップコールルーチンのアドレスである。

 + task_recycle_stacks (task, count, stacks). カーネルに'count'個のユー
   ザ空間のメモリを提供する。カーネルがアップコールをする時、これらの領
   域の一つがスケジューラアクティベーションのアップコールのユーザ層の実
   行スタックとして使われる。特定のスタック上の情報が、タスクにとって有
   用でない時、カーネルは検知できないので、タスクがこれらのスタックを管
   理する必要がある。タスクはアップコール中のカーネルの使用のために、ス
   タックは常に使用可能であることを保証しないといけない(4.4.2章を見よ)

 + task_use_scheduler_activations (task, TRUE). カーネルのタスクの構造
   体のスケジューラアクティベーションを使うビットを設定。タスクがこ
   の'using_sa'ビットを設定したなら、そのタスクで作られる全てのカーネル
   スレッドは、(これに関連する)ビットを設定する。この'is_sa'ビットはカー
   ネルスレッドは事実上、スケジューラアクティベーションだということを意
   味する。カーネルはこのビットを以下に述べる特定のイベントを、どう処理
   するかの決定に使う。

   この呼び出しの副作用として、呼び出したカーネルスレッド(タスクの中の
   たった一つだけ実在するスレッドを想定する)は、スケジューラアクティベー
   ションに変化する。この呼出しから戻ってきた時には、タスクはスケジュー
   ラアクティベーションの意味論での、その通常な実行を続けるだろう。

タスクの状態を管理するのに、柔軟性のために複数のシステムコールを使うこ
とにした。この方法は初期化時間に多少のオーバーヘッドがあるが、最終的に
タスクの状態をよりよく管理できる(例えば、いつでも、登録されたアップコー
ルハンドラを変更できるというような)ということに対して許せる代価であると
みなした。


朝からもりもり切削。



完成。途中、コラムを固定するのを忘れて切削を初めてしまい、一部汚なくなっ てしまった。最後、ダイヤモンドヤスリで糸面とりしている時の満足感がたま らない。

装着。新しく買ったセンサー(Topeakのコンプ140)の配線がとても細くなってい るのが気掛り(前はコンプ130を使っていた)。ノイズ拾わないかな。そもそも自転車用だからノイズの心配な んてすることのないものだし。だめなら根本から配線し直そう。

今日はこれから新宿に出撃です。


部品取りとなっていたRS125整備。リアブレーキは予備部品から拾ってきて装着、
フロントは、ヤフオクで落としてブラストかけたところで中座していたフロン
トキャリパーに新品のシール、ピストン、ピンで組んで装着。ブラストが下手
で、ちょっと残念風味。もっと大容量のコンプレッサがあって、エア圧の調整
ができたらなとは思うものの、思わないように。もうガレージに場所がないん
だ。

マスタシリンダは1/2インチ(12.7mm)がよかったのだけど(CR85で使ってるやつ。 88'NSR250R)、手持ちのはどれも14mmだったので、14mmで。カッツン気味になる けど、大丈夫でしょう。カッツン好きだし。

スピードセンサはリアブレーキロータにつけていて、今迄、CR85とRS125でロガー をつけかえる時にはキャリパーごと交換していたのだけど、それぞれ別に作る ことにしました。使わなくなったApe用のバックステップから切り出し。
ここで問題が。DROの表示ユニットが動かない...。

バラして調べてみたところ、なんと電源のトグルスイッチが接触不良になって いた。トグルをぐりぐりすると電源が入ったりもする。ガレージ用にはもっと 屈強なのがいいということで、もうちょっと大きいスイッチに交換。
机の上でいじっている時と、ガレージに装備されて実際にいじられる時とだと 操作の荒さが違うんだよね。
今見ると、結構このモジュール頑張って作ってる。こういうのそろそろ基板を 作ってみたいとも思いつつ、野望的には、フライスをCNC化して、これで基板を 切削なんだ。
PWMのテスト → サーボモータを動かす
フライスをボールネジ仕様にする。
サーボモータを取りつけ。
モータ制御のアプリケーション
これもいつになるかは予測もつかないプロジェクト。ここずっとサーボモータ 買ったところで進んでない。

Adding Scheduler Activations to Mach 3.0

Paul Barton-Davis, Dylan McNamee, Raj Vaswani, and Edward D.Lazowska
University of Washington

Technical Report 92-08-03 Reversed March 1993
続き。スケジューラアクティベーションの概念がわかってないので、ちょっと 訳がつらい。このあたり一度マイOSで思うがままに実装してみると状況がわかると思う のだけど、ユーザランドまで実装するまでの時間もなく。
1.4 枠組の設計

スケジューラアクティベーションのモデルにおいて、それぞれのジョブのユー
ザ層スレッドシステムは、それに割当てられたプロセッサの制御をし、この割
当てが変更されたら、通知される。カーネルは、ジョブに割当てられたプロセッ
サ毎にスケジューラアクティベーション(実行コンテキスト)を提供し、関連す
るイベントがあれば、通知する責任を負う。

Machのカーネルスレッドの振舞いを変更することでスケジューラアクティベー
ションを実装した。我々の実装の構造に影響する、一つの重要な設計上の決定
はプロセッサ割当てモジュールの導入で、これは、ある意味Machの既存のカー
ネルスレッドのスケジューラを置き換えるものである。Machカーネルのスケー
ジューリング方針はスレッド基底で、"quantum-driven"である。この方針は伝
統的な仕事量に対しては適切だが、スケジューラアクティベーションのアプリ
ケーションと、カーネルの間で伝達される情報を活用するという方針において
は、荷が重い。ただし、Machの「プロセッサセット[Bla90]」によって、標準の
スレッド基底の方針(これは我々の目的には不適)を回避でき、それを我々のタ
スク基底のものに置き換えることができる。プロセッサセットは、タスク、ス
レッド、プロセッサの割当てのカーネルの機能である。
我々の設計は、それぞれのタスクはそれ自身のプロセッサの組を持つ。

方針モジュールは、タスクに応じた割当ての判定と、タスク間の優先度に基い
て様々なプロセッサの要求を監視する。この新しい方針モジュールはカーネル
内でもユーザ層のサーバでも実装できる。色々な方針を試すことのできる柔軟
性のある後者を選んだ。これから先に述べる、我々が最終的に選択した方針は、
ユーザ層の「プロセッサ割当てサーバ」にカプセル化し、選択すれば既存のカー
ネル機構を使うことができるものである。: プロセッサはそのタスクのプロセッ
サセットによってよって割当てられ、そしてそれは、そのセットから除かれる
ことで横取りされる。この設計は、このシステムが三つの要素から構成される
ことを意味する。

+ イベントの通知のような基本的な機構を実装するためのカーネルの変更。こ
  れらは次の章で検討する。

+ スケジューラアクティベーションで使われるタスクの間で割当てられるプロ
  セッサの管理をするユーザ層のサーバ。このサーバの実装と、その方針につ
  いては三章で検討する。

+ この新しいカーネルの機構を使うためのユーザ層のスレッドパッケージ。こ
  れは四章で述べる。



加速度モジュールをH8/3664続き。H8/3664のI2Cモジュールは、そこぶる評判が
悪いのは知っていたのだけど、どうにかなるだろう。と進めていたのだけど、
I2C自体初めてというのもあって、はまっていました。データシートに「I2Cバ
スインターフェースの仕様を満足しません」「入力タイミングがこの出力タイ
ミングを許容するスレーブデバイスを選択する」(!!!開き直った) とか書いて
あり、非常に心細い。なんとかモジュールの御機嫌をとりながらデータはとれ
ました。割込みも上がる。モジュールをいろいろ動かすと重力の方向が変わる
のでとれてるでしょう。しかしこのモジュールは2Gまで。2Gまでで足りるのか?

なんにしろ、もうH8/3664のI2Cモジュールは使いません。この後継のH8/3694で はI2Cモジュールは一新されているので、使いまわしも効かないし。
SPIはいいな。4線(CS#を入れて)から2線(I2C)になるとここまで面倒になるとい うことなのか。
void
i2c_init ()
{

I2Cのレジスタは多重化されていて、I2Cが無効だとSAR,SARXが有効になって、
I2Cが有効になると、ICDR, ICMRが有効になる。

  //I2C Disable. I2C_SAR and I2C_SARX are accessible.
  *I2C_ICCR = 0;

常にマスターなので、スレーブアドレスはスレーブとかち合わなければいいはず。
が、値によってはストールする。
  // I2C format. only SAR used as slave address.
  *I2C_SAR = 0;	// Slave addr
  *I2C_SARX = SAR_FSX;	// Don't use SARX

  //I2C Enable.
  *I2C_ICCR |= ICCR_ICE;	// read required

ここでICDR,ICMRが設定できる。

I2Cのクロックを設定します。あまり遅くすると謎にストールします...。

  // Now register ICMR and ICDR are accessible.
  *I2C_ICMR = ICMR_CKS0;// CCLK16MHz 400kHz 8bit MSB1st
  //  *I2C_ICMR = ICMR_CKS0 | ICMR_CKS1 | ICMR_CKS2;// CCLK16MHz 125kHz 8bit MSB1st
  *I2C_TSCR = 0; //IICX = 0, IICRST = 0(noreset)
}

void
i2c_start ()
{
  uint8_t r;

まずバスがフリーなのを待ちます。

  // Check bus free.
  while (*I2C_ICCR & ICCR_BBSY)
    ;

マスター送信モードにして

  // Master transfer mode.
  *I2C_ICCR |= ICCR_MST | ICCR_TRS;

スタート条件に設定します。

  // Start condition.
  r = *I2C_ICCR;
  r |= ICCR_BBSY;
  r &= ~ICCR_SCP;
  *I2C_ICCR = r;

  // Wait until start condition is generated.
  while (!(*I2C_ICCR & ICCR_IRIC))
    ;
}

void
i2c_repeated_start ()
{
  uint8_t r;

これは、スタートして、ストップせずにマスタが送信から受信に変化にする時に
送ります。H8/3664ではかなりデリケートです。

  // Restart condition.
  r = *I2C_ICCR;
  r |= ICCR_BBSY;
  r &= ~ICCR_SCP;
  // Don't reorder here.
  r &= ~ICCR_IRIC;
  *I2C_ICCR = r;
  // Wait until start condition is generated.
  while (!(*I2C_ICCR & ICCR_IRIC))
    ;
}

void
i2c_stop ()
{

ストップ条件を出します。この後はバスはフリーです。

  *I2C_ICCR &= ~ICCR_IRIC;
  *I2C_ICCR &= ~(ICCR_BBSY | ICCR_SCP);
}

bool
i2c_write (uint8_t data)
{
  // Transmit data.
データをセットして、
  *I2C_ICDR = data;

このICCR_IRICのセット条件が複雑過ぎて手に負えない。
  *I2C_ICCR &= ~ICCR_IRIC;
  while (!(*I2C_ICCR & ICCR_IRIC))
    ;

  // Transmit done.

スレーブのACKを確かめます。

  if (*I2C_ICSR & ICSR_ACKB)
    {
      iprintf ("no ack\n");
      // Stop condition.
      return FALSE;
    }
  // Ack received.

  return TRUE;
}

void
i2c_read_n (uint8_t *buf, int n)
{
  int i;

マスタ受信モードにして、
  // Master receive mode.
  *I2C_ICCR &= ~ICCR_TRS;
  *I2C_ICMR |= ICMR_WAIT;
  *I2C_ICSR &= ~ICSR_ACKB;


多重リードはうまくいったり、うまくいかなかったり。マスタからの
ACKはでてるのか?
  //XXX multiple read may fail and stall I2C module.
  for (i = 0; i < n; i++)
    {
      *I2C_ICCR &= ~ICCR_IRIC;
      buf[i] = *I2C_ICDR;
      while (!(*I2C_ICCR & ICCR_IRIC))
	;
    }

終了なのでNACK
  // NACK
  *I2C_ICSR |= ICSR_ACKB;
  *I2C_ICCR |= ICCR_TRS;
  *I2C_ICCR &= ~ICCR_IRIC;
  while (!(*I2C_ICCR & ICCR_IRIC))
    ;
  *I2C_ICMR &= ~ICMR_WAIT;
  //  r = *I2C_ICDR;
  *I2C_ICCR &= ~ICCR_IRIC;
}

void
i2c_status (const char *s __attribute__((unused)))
{
  uint8_t iccr, icsr;
  iccr = *I2C_ICCR;
  icsr = *I2C_ICSR;

  iprintf ("CR:%x SR:%x %s\n", iccr, icsr, s);
}

これはKXP84が、動いただけの実装。もういじりません

KXP84のスレーブアドレスは0x18か0x19です。
#define	KXP84_I2C_ADDR	0x18
#define	KXP84_INTR_ENABLE

void
board_main (uint32_t arg __attribute__((unused)))
{
  intr_enable ();

  kxp84_init ();
  i2c_init ();
  kxp84_main ();
}

void
kxp84_init ()
{
このように配線しました。ADDR

  // P55 I/O -> RESET
加速度が閾値を越えた時の割込みです。
  // P54 WKP4 <- FF
  // P53 WKP3 <- MOT
これはスレーブアドレスの最下位ビットの設定です。(二個までバスに接続できる)
  // P52 I/O -> ADDR0
これはI2CかSPIを選択します。P52,P51の設定は固定でかまわない。これはテストのため。
  // P51 I/O -> CS#
  *PMR5 = (1 << 4) | (1 << 3);	// WKP input
  *PCR5 = (1 << 5) | (1 << 2);	// Output port.
  *PUCR5 = (1 << 5) | (1 << 2) | (1 << 1); // Enable pullup MOS

  // Interrupt setting.
#ifdef KXP84_INTR_ENABLE
  *IEGR2 = (1 << 4) | (1 << 3);	// WKP rising edge.
  *IENR1 |= IENR1_IENWP; // WKP interrupt enable.
#endif
  // CS# deassert. (active low) -> I2C mode
  *PDR5 = 1;
  if (KXP84_I2C_ADDR & 1)
    *PDR5 |= (1 << 2);
KXP84のレジスタを初期化します。
#if 1
  *PDR5 |= (1 << 5);
  udelay (100);
  *PDR5 &= ~(1 << 5);
  udelay (100);
#endif
}

void
kxp84_reg_write (uint8_t slave_addr, uint8_t reg_addr, uint8_t data)
{
これはKXP84のデータシート通りの操作です。
  i2c_start ();
  i2c_write ((slave_addr << 1) | 0);	// SAD + W
  i2c_write (reg_addr);
  i2c_write (data);
  i2c_stop ();
}


void
kxp84_reg_read (uint8_t slave_addr, uint8_t reg_addr, uint8_t *buf, int n)
{
これはKXP84のデータシート通りの操作です。
  i2c_start ();
  i2c_write (slave_addr << 1);	// SAD + W
  i2c_write (reg_addr);
  i2c_repeated_start ();
  i2c_write ((slave_addr << 1) |  1);	// SAD + R
  i2c_read_n (buf, n);
  i2c_stop ();
}

void
kxp84_main ()
{
KXP84の内部レジスタを設定します。割込みを出す閾値の設定とか。ラッチとか。

#ifdef KXP84_INTR_ENABLE
  kxp84_reg_write (KXP84_I2C_ADDR, 0x0b, 0x06);	// MOT FF interrupt enable.
#else
  kxp84_reg_write (KXP84_I2C_ADDR, 0x0b, 0x00);
#endif
  kxp84_reg_write (KXP84_I2C_ADDR, 0x0a, 0x00);
  kxp84_reg_write (KXP84_I2C_ADDR, 0x06, 0x14);
  kxp84_reg_write (KXP84_I2C_ADDR, 0x07, 0x14);
  kxp84_reg_write (KXP84_I2C_ADDR, 0x08, 0x4d);
  kxp84_reg_write (KXP84_I2C_ADDR, 0x09, 0x14);
#if 1
  uint8_t buf[6];
  int16_t x, y, z;
  while (/*CONSTCOND*/1)
    {
#if 1
加速度を3軸とってきます。
      int i;
      for (i = 0; i < 6; i++)
	{
	  buf[i] = 0;
	  kxp84_reg_read (KXP84_I2C_ADDR, i, buf + i, 1);
	}
#else
これはI2Cモジュールがストールします。
      kxp84_reg_read (KXP84_I2C_ADDR, 0, buf, 6);
#endif
ここもちょっとおかしい。でもH8/3664上ではもうこれ以上追究しません。
      x = (((buf[1] << 4) & 0xf00) | buf[0]) & 0xfff;
      y = (((buf[3] << 4) & 0xf00) | buf[2]) & 0xfff;
      z = (((buf[5] << 4) & 0xf00) | buf[4]) & 0xfff;
      iprintf ("%d %d %d\n", x, y, z);
      mdelay (300);
    }
#endif
}

void
i2c_intr ()
{
  iprintf ("i2c intr\n");
}

void
wkp_intr ()
{
  iprintf ("wkp! %x\n", *IWPR);	// inquire interrupt status.
  *IWPR = 0;	// clear interrupt.
}
Adding Scheduler Activations to Mach 3.0

Paul Barton-Davis, Dylan McNamee, Raj Vaswani, and Edward D.Lazowska
University of Washington

Technical Report 92-08-03 Reversed March 1993
の続き。

1.3 Machにおけるスケジューラアクティベーション

Andersonの試作実装はDEC SRCのFireflyマルチプロセッサワークステーション
上のOS、Topazの変更によってなされた[TSS88]。これは優秀な試作環境ではあ
るが、その実装は他者には触れることのできないものであるし、その実験は最
大で6プロセッサに限られていた。

我々の目標は、様々なプラットフォームに広く一般に使われているカーネルに、
スケジューラアクティベーションを統合することである。それにMach 3.0を選
び、この研究を20プロセッサのSequent Symmetryで動かすことになった[LT88]。

我々の実装は幾つかの役割りを果たすだろうと思う。一つは、適度な数のプロ
セッサにおいて、スケジューラアクティベーションという概念が、妥当かどう
かの最終的な結論を出すだろう。二つ目には、これが、プロセッサの割当ての
アルゴリズム、スレッド割当てのアルゴリズム、効果的な通信プリミティブと
共にユーザ層のスレッドを統合すること、I/O、仮想記憶の管理をユーザ層です
る準備、そしてその先、に対する研究の基本となること。三つ目にはMachの他
のプラットフォームにスケジューラアクティベーションを実装する際の手本と
なるだろう。

この実装に着手するにあたって、Machの都合のいい互換性と機種非依存性を選
択した。その目標に関して、システムへの変更は最少にしたく、できる限り現
存のカーネルの機構を使うことにした。結果としてのシステムが広く使えるよ
うに、Machの後方互換性にも努めた。例えば、既にあるカーネルスレッドのイ
ンターフェースもサポートしている。最終的に、できる限り機種非依存に実装
して、他のプラットフォームにも簡単に実装できるようにした。スケジューラ
アクティベーションを基本として全体を再構成するのではなく、実験のたたき
台となるように試みた。最高の結果を求めるのはこの実装の目的ではない。む
しろ、一般にあるマシンで、どこを最適化するのが効果的なのかを実験するの
を期待する。この取りかかりによって、この実装がより理解でき、変更可能に
なることを期待している。




加速度モジュールをH8/3664とつないでみました。まずはH8/3664のI2Cモジュー
ルでテスト。これでH8/TinyのI2Cまわりを整備がてら、うまくいったら、
LPC2388にもっていく目論み。このKXP84モジュールのVddは2.7V-5.25Vなので
5VのH8でも3.3VのLPC2388でもどっちでもいける。



並行性の細粒度、粗粒度というのは、細粒度はまめにスレッドの操作がいる、 粗粒度はあまりスレッドの操作がいらないということ。この粒度はアプリケー ションによって決まる。そのアプリケーションが取り扱うものに対して、並行 性を抽出した結果、粗粒度であれば、それは並行実行で十分に性能を出せると いうこと。逆に、細粒度にしかならなければ、本質的にその問題は並行実行に 向かないということだ。これはOSの如何を問わずアプリケーションに本質的な こと。
現実問題として、どうしても並行実行に向かない問題では、がんばって並行性 をひっぱり出したところで、同期にかかる時間の方が長くてまったく性能がで ないということになってしまう。
そういう意味では、細粒度のパフォーマンスを上げていくというのは、本来あ まり並列向けではない問題を、それでもなんとか並列で性能を上げようという ことになる。
無駄っぽいけれど、これはトレードオフの問題で、システムのサポートで 性能を上げれる余地がある。
しかし、本当にマルチプロセッサを活用できるのは、粗粒度に分解できる問題 だけだ。そして、そういう問題というのはかなり限られている。もう既にどう いう類いのものしか並列化が効かないというのはわかりきっている。
これとは別に、スレッドにするとプログラムが明瞭に書けるという、プログラ マ視点の楽さがある。僕はこの類いで、すぐに機能ごとにスレッド立ててしま う。これはこれで性能の低下、うっかりしたロックの忘れが嫌な問題だ。ロッ クについては、本質的に横取りが必要でなければ、コルーチンにすることでロッ クの必要性をなくして、性能も向上できる。というのがマイOSにファイバを 導入している理由なのだけど、あまり活用できていない。
本当にベスト尽くすなら、割込みハンドラの終了でそれを処理するスレッドコ ンテキストに切り替えてやるのがいいのかとは思っている。今なら割込みスレッ ドは継続をつかってスタックなしにできている。ならば、割込みハンドラの終 了でその継続を呼べば(その時に割り込まれたスレッドのスタックを使って)、 処理ができる。その処理の後は継続をともなってブロックになるので、またス レッドの切り替えに入る。...いいかも。いずれ実装してみたい。
Adding Scheduler Activations to Mach 3.0

Paul Barton-Davis, Dylan McNamee, Raj Vaswani, and Edward D.Lazowska
University of Washington

Technical Report 92-08-03 Reversed March 1993
の続き。

1. 導入

ユーザ層スレッドを伝統的なカーネルスレドの上に作ると、作成、終了、同期
のような一般の操作の性能に非常に効果がある。しかし残念ながら、、I/Oや、
ページフォルト、プロッセッサの横取りによるカーネル操作によって低性能で
あったり、正しくない動きをする。これはアプリケーションプログラマの板狭
みとして提起された。システムのサービスとしてよく統合されてはいるが普通
の場合の操作が重いカーネルスレッドを使うか、あるいは普通の場合の操作に
は良い性能だが、システムに十分に対応していないユーザスレッドを使うかだ。

この板狭みを解決するために、Andersonら[ABLL92]はスケジューラアクティベー
ションという新しいカーネルの要素を設計した。スケジューラアクティベーショ
ンはユーザ層のスレッドの管理に適切なサポートを提供する。スケジューラア
クティベーションの上に構築されたユーザ層のスレッドはカーネルスレッドの
機能と、ユーザ層のスレッドの柔軟さと性能を組み合わせる。ここ数年で、ス
ケジューラアクティベーションは、ユーザ層の並行性を管理する正しいカーネ
ルの機構だという大方の意見になった。

この論文で述べる、試みの目標はスケジューラアクティベーションをMach3.0に
実装することだった。[RBF+89] この論文はその概念よりもその実装に重点を置
く。 設計上の判断、必要とされるカーネルの変更、CThreadsライブラリにこの
新しいカーネルの構造を使うために追加した事、について述べる。

1.1 ユーザ層スレッド

並行実行を表現する時、並行度の単位が小さければ、細粒度の並行実行はまあ
まあのオーバーヘッドでサポートすることができる。カーネルプロセス(Unixの
ような)は最も粗粒度の並行プログラム以外にはそのコストがあまりにも高いと
認められてきた。MachやTopazのようなマルチプロセッサOSはこの問題にカーネ
ルスレッドを提供することで取り組もうとした。しかしカーネルスレッドもま
たあまりにもコストがかかることがわかり、MachとTopazは現在、そのカーネル
スレッドの上に構築されたユーザスレッドを提供している。Andersonら
[ABLL92] はカーネルスレッドがユーザスレッドに較べたコストは、実装上の問
題ではなく、2つの原因による、それ本来の問題であることを論じた。

+ カーネルサービスのコスト。トラップされてカーネルに入るのは単純な手続
   き呼び出しに較べて、よりコストがかかる。アーキテクチャの潮流は、カー
   ネルトラップは相対的にコストがかかる方向になっている[ALBL91]。さらに、
   間違った振舞いのユーザプログラムから、カーネルを保護するために、シス
   テムを壊すような引数でないかどうか検査しないといけない。ユーザ層のス
   レッドパッケージは、スレッド管理の操作にカーネルトラップのオーバヘッ
   ドを避け、手続き呼出しを使うことができる。さらに、スレッドパッケージ
   は(カーネルスレッドのような)自分自身を守ることをする必要がない。間違っ
   た振舞いのプログラムが壊れるだけだからだ。

+ 一般化のコスト。 カーネルスレッドは全てに対して、全てを提供しないとい
   けない。あるプログラムがいくつかの特別な機能(例えば、ラウンドロビン
   割当て)が必要なくても、その機能が存在しているためのコストを払わない
   といけない。ユーザ層のスレッドパッケージは、それぞれのアプリケーショ
   ンの必要に応じて特別に最適化することができる[BLL88]。

ユーザ層のスレッドは比較的細粒度の並行性を表現するのに有効だということ
がわかった。残念ながら、ユーザ層のスレッドをカーネルスレッドのような、
今あるOSの機構の上に実装するのには難しい点がある。最初の問題は、カーネ
ルスレッドは、ユーザ層の状態にかかわらずスケジュールされること。カーネ
ルのスケジューラと、ユーザのスケジューラは、お互いに衝突してしまう可能
性がある。二番目の問題はI/O、ページフォルト、プロセッサ横取りのようなカー
ネルのイベントによって、ブロックする状態が、ユーザ層にはわからないこと
だ。様々な機構が、これらの問題のそれぞれについて取り組まれた。
[MSLM91][TG89] [Her91]、しかし、全ての困難さを扱った研究はなかった。ス
ケジューラアクティベーションは統合された解決法を提示する。

1.2 スケジューラアクティベーション

スケジューラアクティベーションはカーネルスレッドに代わって、ユーザ層の
並行性の管理をサポートをする。[ABLL92]に紹介されたように、スケジューラ
アクティベーションの環境は、以下の重要な特徴がある。

 + プロセッサはカーネルによってジョブに割合てられる。 プロセッサの割当
   てはカーネルによってユーザ層と通信した情報を元にしてなされる。カーネ
   ルはジョブにスケジューラアクティベーションを与えることで、プロセッサ
   を割当てる。その実体は伝統的なカーネルスレッドと同じようなものだが、
   以下に示す、追加的な特性がある。


 + ユーザ層のスレッドスケジューラはジョブに割当てられたプロセッサで走る
   スレッドの管理をする。カーネルによって与えられたスケジューラアクティ
   ベーションは、ユーザ層のスケジューラがスレッドを多重化する単純な容器
   である。スレッドのスケジューリングや、同期のような一般的な操作はカー
   ネルの介入なしに、ユーザ層において効果的に実行される。

 + ユーザ層が、カーネルにプロセッサ要求の変化を通知する。 ジョブの並行
   性が、その現在のプロセッサ割当てから上下する時にカーネルは通知される。

 + カーネルは、ユーザ層のスケジューラに、そのジョブに影響を及ぼすシステ
   ムイベントを通知する。これらのイベントはカーネルの中で起こる、プロセッ
   サの割当て、横取りや、ブロック、起床を含む。カーネルの役割りは、「イ
   ベントを処理する」から、適切なジョブのユーザ層のスレッド管理システム
   に「イベントを通信する」ことに変わる。これによって、ジョブに対して適
   切な方法でイベントを処理することができるようになる。

 + カーネルはスケジューラアクティベーションを時分割しない。スケジューラ
   アクティベーションはカーネルがジョブにプロセッサを割当てる手段である。
   ジョブは常に正確に、できるだけ多くのスケジューラアクティベーションを
   物理的なプロセッサがあるだけ持つ。カーネルは(与えられた数の)スケジュー
   ラアクティベーションを(より少ない数の)物理的なプロセッサの上に多重化
   することはない。

 + アプリケーションはこの新環境を使うとしても、変更の必要はない。これは
   イベントの管理はユーザ層のスレッド管理システムにカプセル化されている
   からで、そのインターフェースに変更はない。

このシステムに馴染みのない読者は、この先に進む前に[ABLL92]を読むことを
強くお勧めする。



美味しんぼで、魚の調達といえば、まずは三崎だ。



呑屋ツアーで行ってきました。



店はここ。

魚喰いにはたまらない。これも食べたい、あれも食べたい。

これ買って隣りで調理してもらって、その場で食べれるのだ。

これはマンボウの刺身。シャクシャクとした食感が今迄にない味わい。

そして、黄色いのがマンボウの肝。肝にしては淡い味です。これを白身にから めて食べると、旨い。まだまだ世の中旨いものが残ってる。

これはダツ。これも実にうまい。写真撮っている最中にガバっと持ってかれて ますが...。そうそう「菊姫の先一杯」持ってきてくれてたのがうれしかった。 これ一番好きなのよ。

マグロのカマ。これが出てきた時には既に腹一杯だったのだけど、試しに食べ てみたら、あまりのうまさに別腹が出現。炙って油が落ちたところでちょうど いい油加減。うまかった〜。
あと生サバも食べた。これもおいしいけれど、サバは痛みやすいというのが逆 にその食べ方を工夫することで、独自のおいしさを作ってきたという気がした。

これはお土産にそこで買ってきた塩辛。内蔵の割合いがとても高く、その味わ いは絶品過ぎ。食べると、ギューッと目を瞑らざるを得ない旨さ。
この塩辛を食べながら日本酒を呑むと、この世の幸せを一人占めした気になる よ。

そして、このあたり、いいストラクチャーの資料が現存している。







広がる大根畑の先には海が拡がり、ところどころに、たくわん用に大根が干し てある。三浦半島、風光明媚。

そして新宿に帰ってきたら花園神社、二の酉だ。

この猥雑さにまみれて茶屋で呑むのも最高だ。

お酉さんに行ったのは本当久々。あらためて新宿はいいなと思った。

この後さらにおでん屋まで連れていってもらったのだけど、もう腹パンパン。 ワンカップの熱澗だけ呑んでた。こういう雰囲気だとむしろワンカップがうま く感じるんだな〜。
呑んだくれ過ぎ。しばらく自重方向で。



ニンジンを間引きしました。これはそのまま生で食べてみた。意外に苦みがあ
る。



CR85の整備。ピストンは結局、ナラシ済みの新品ピストンは使わずに、筑波選 手権、岡山選手権、BS走行会と走ったピストンを使いました。このピストンと てもいい気がして。

リアサス今年は、O/H出すかな...。シーズンオフだし、とりあえず全バラで洗 浄です。

今日は要約のとこまで。Machへの実装はCMUではなくスケジューラアクティベー ションをTopazに実装したワシントン大学のチームによるもの。これ実際にマー ジされたのだろうか。Mach 3.0 MK84のソースを見ているのだけど見つからない。
Adding Scheduler Activations to Mach 3.0

Paul Barton-Davis, Dylan McNamee, Raj Vaswani, and Edward D.Lazowska
University of Washington

Technical Report 92-08-03 Reversed March 1993

Mach 3.0へのスケジューラアクティベーションの追加。

要約

ユーザ層のスレッドを伝統的なカーネルスレッドの上に構築すると、I/Oや、ペー
ジフォルト、プロッセッサの横取りによるカーネル操作によって低性能であっ
たり、正しくない動きをする。この問題は、ユーザ層のスレッドをスケージュ
ラアクティベーションという新しいカーネルにおける存在の上に構築すること
によって解決することができる。この論文で述べる効果の目的はMach3.0にスケ
ジューラアクティベーションを実装することだった。設計を決定し、必要なカー
ネルの変更、CThreadsライブラリに対して、新しいカーネルの構造を使う追加
を述べる。それとともに、スケジューラアクティベーションによる性能のコス
トを分離し、これらのコストよりこのアプローチの利点が上回ることを実証す
る。


菜園状況。これは今シーズン最後の大玉。驚くことにまだトマトは新しく花を
咲かせている。



長ネギ。なんとか芽は出た。

これは玉ネギ。

最後の一本大根、よくよく見てみたら思いっきり虫に喰われていた。急いで収 穫。引っこぬいたら、4cm長のヨトウムシを発見。

大根の二次隊はヨトウムシによって全滅。アオムシなら葉っぱから食べていく のでまだいいのだけど、ヨトウムシは茎の部分からいきなり食べてしまうので リカバリ不能。

キャベツはまだ順調。寒冷紗かけてるのに、はっと気付くと青虫がいる。ヨト ウムシじゃないだけまだいい。

スケジューラアクティベーション。これはカーネル層とユーザ層で二段でスレッ ドをスケージュリングする方法の一つで、Thomas E.Andersonらが91年に体系化 したものだ。これまでにも、こういったアプローチはあったけれど、体系化し たのはこれが最初のようだ。これはDEC SRCのFireflyという6CPUの実験用ワー クステーション上のTopazで実装された。そしてその後、92年にはMach 3.0に実 装されている。NetBSDへの実装はNathan J.Williamsによって02年になされた (その論文の塩崎さんによる和訳はBSD magazine 13号にある)。
現在のNetBSDではこのスケジューラアクティベーションはなくなっている。
他OSでもこの傾向だ。が、Windows NTから1:1スレッドだったのがWidnows 7で はUMS; User Mode Schedulerという名で(大義の)スケジューラアクティベーショ ンが実装されることになった。それまではユーザ層だけでスイッチする軽量さ が欲しければ、ファイバを使って自分で制御を移行せよということだった。(こ のファイバは専用の局所記憶をもったもので、マイOSは当初これと同じ方針で 実装したのだけど、空間的なきつさからスタックなしへの改装を目論みつつ、 スタックがないなら関数コールと同じだよというジレンマに陥っている。改装 はとりあえず先延ばし。)
原典
Scheduler Activations: Effective Kernel Support for the User-Level
Management of Parallelism

Thomas E.Anderson, Brian N.Bershad, Edward D.Lazowska and Henry M.Levy
University of Washington

ACM Transactions on Computersystems, Vol.10, No.1, February 1992,p53-79.
そのMach 3.0への実装
Adding Scheduler Activations to Mach 3.0
Paul Barton-Davis, Dylan McNamee, Raj Vaswani, and Edward D.Lazowska
University of Washington

Technical Report 92-08-03 Reversed March 1993
これらをゆっくり読んでみたいと思う。まずは実地的でわかりやすいと思われ るMach 3.0への実装から。


今は結構楽に論文とってこれるのね。ACMからとれなくても、著者のページから
落とせたりするし。つまみ読みしているとついつい時間が経ってしまう。今日
は"SPIN - An Extensible Microkernel for Application-specific OperatingSystem Services", Brian Bershad, Craig Chambers, Susan Eggers, Chris
Maeda, Dylan McNamee, Przemyslaw Pardyak, Stefan Savage, Emin Gun
Sirer, University of Washington, Technical Report TR-94-03-03.

これは、アプリケーションに特化したサービスをカーネルにランタイムに乗せ
ようというもの。ローダブルカーネルモジュールといえばその通りなのだけど、
その保護について。最終的なSPINでは電子署名という無難な形になったようだ。
この段階では、ロードしたモジュールが他に被害を与えないように、特別の言
語を使って、そのモジュールの妥当性を見極めれるようにし、実行時にコンパ
イルされて(ここで妥当性がチェックされる)ロードされる。そしてそのモジュー
ルも、カーネルで用意された特別の領域で実行される(例えば無限ループを組ま
れても大丈夫なように)。

SPINはマイクロカーネル(当初Mach3.0からはじめていたようだ)といっても、も うカーネルから追いだす方針から反転してカーネルに持ってくることになった。 その状況でも、マイクロカーネルの優位性としていた、保護の問題を厳密なロー ドモジュールの生成手段として確保しようとしたように見える。
ここで面白い方針が、アプリケーション毎にベストなカーネルインターフェー スを用意する方向にしたことだ。これは計算機科学の分野でとても活きると思 う。大規模計算の場合、メモリ割り合てやディスクへのスワッピングは全てプ ログラム側でやってるんだ。正直、OSのメモリ割り合てなんてそれがとれるか どうか確実じゃないし、ページングなんて効率悪すぎる。というかページング が始まったら計算が終る見込みがない。OSのページングというのは、終末期の 延命治療のようなものだ。その計算プログラムのOSのようなことをしている部 分をこういう形で組み込めたらいいはず。
結局、どんどんカーネルに入れていくことになっていく。
むしろじゃあ全部ユーザにしてしまえという方向が最近の(とはいえ源泉は 1960年代の大型機にある)仮想化かな。
アドレス空間をまたぐのが大変なら単一アドレス空間にしてしまえばいいじゃ ない。という方針なのがWindows CEだ。これはプロセスの領域は32MBとして32 個のスロットに収まる。保護の問題は、MMUのASID不一致で例外を起す。SHの MMUには単一アドレス空間用のモードがある。これはカーネルモードであれば ASIDが一致しなくてもマッチを許すモード。これがあるとカーネルからのユー ザ空間のアクセスで余計な例外が出ないのでいいはず。
こうして単一アドレス空間にすることで、ユーザの空間をそのまま(Windows CE の場合、実行プロセスは一番下の0x0からのスロットで動かすということに なっているので、実際のスロットの場所とのオフセットをポイントに足す操作 が必要)、アクセスできる。
単一アドレス空間はマイクロカーネルと相性がいいと思う。相性がいいといっ てそれがマイクロカーネルならではの何か優位性を示せるかというと...。モノ リシックで単一アドレス空間の方が効率的だ。
なんでマイクロカーネルに魅かれるんだろう。一つにはその強制がモジュール 化を強制するから。なんとなくバッファをとってきて関数コールすればいいと いうことができない。でもきちっと設計したモノリシックカーネルでもそのモ ジュール化は実現できる。
FORTRANでもオブジェクティブに書けるんだよ。なんで言語としてオブジェクト 指向のがあるかというと、「こういう枠組みで書くんですよ」というのを縛り として規定することで、書く人の意識を統一することにある。この縛りがない 場合、そのプログラムに応じて、このプログラムではこういう構造でこうする 時はこうしなくてはいけなくて、こういったことはしてはいけませんみたいな ことを宣言しなくてはいけない。そして守らなくてもいじれてしまう。
なんとなくそういった縛りが課せられるマイクロカーネルが「綺麗」になり得 るような気持ちを抱くところだろうか。
とはいえ実際のとこプログラムにおいて綺麗なんてどうでもいいことだ。そん なのは実装者の自己満足で、ちゃんと動いて速いのが正しい。そしてOSを書く 人間は一般にユーザの数に較べて極小数だ。極少数なので、強制的な枠組みを 決めずとも話し合いでなんとかうまくいく。
SPINのように、外部に自由なカーネルインターフェースを提供したとしても、 そこまで書けるユーザはそんなにいないので活用されない。カーネルモジュー ル書くのにさらに、そのカーネルモジュール用の言語で書いてというのは無理 だろう。そこまで書けるなら、カーネルをいじれる。
最後の生き残りは、リアルタイム系でリアルタイムの保証をする枠組みを用意 して、その一つのプロセスとして優先度を低くOSを動かすことで、ユーザイン ターフェースだけリッチな環境を用意する方向か。
そこまでしてインターフェースを用意する必要があるかどうかも疑問だ。リア ルタイムな部分との通信でやればいい。同じCPUでやる必要がない。


ちょっと秋葉で買物してきました。液晶工房でデバッグ用とかの予備にLCDモ
ジュールを二つほどと、秋月で3軸加速度センサ、なんとなく棚を見ていたら、
測距モジュールというのがあって、興味をひかれたので買ってきた。フレーム
側にこれをつけて、スイングアームに反射板をつければストロークをとれない
だろうか...。と思って。400円だし。



なんでLISPのスタックは1960年代からこんな七面倒臭いことをしてるんだ。そ してなんでいつまでたっても(現在でも)浅い束縛、深い束縛とかやってるんだ。 とググっていたら今日は終了。
そもそも一番のミスリーディングがLISPが最初から動的スコープで設計された ことのようだ。だからリソースの限られた時代から、その関数が評価される時 に最後に値が束縛された場所を探すということをしないといけなくなった。そ こでその実装に浅い束縛と深い束縛の二つのやり方ができた。Emacs lispは動 的スコープで浅い束縛のようだ。しばらくして静的スコープのSchemeができた。 静的スコープならシンボルの値を実行時に探しにいく必要はない(よね?)
ALGOL系でもdisplay方式というのがこれと同じようなことをしていたようだ。
あまり深入りしない方がよさそうだな。しかし、これは...と思いつつ、構造と してはなんとなく魅力を感じてしまうところがLISPの息の長さか。いわゆるコ ンピュータ詐欺の条件を満たしている。マイクロカーネルとか^^、Ne
ググっている最中にちょっと面白いのをみつけた。「Continuation base Cコン パイラのGCC-4.2による実装」与儀健人 河野真治 2008? これは今回マイOSに導 入したUsing Continuations to Implement Thread Management and Communication in Operating Systems. Richard P. Draves Brian N.Bershad Richard F.Rashid Randall W.Dean Oct 1991 のような事を言語レベルに落とし ている。OSへの適用も示唆しているけれど、OSだとこれが適用する部分に専用 の言語を入れるくらいならCとアセンブラでサブルーチン形式に実装した方がい いのでは? でもこれの提案には非常に共感する。そんなところを考えていた。
継続をもっと使ってみたいというのは思ってるんだよね。ファイバの改装計画 はそこを考えている。しかしそこで専用スタックはもたないようにするという 条件にしてるので(今はもっている)局所記憶はあらかじめ用意されたとこにとっ ておくことになる。となると、コンテキストを指定したfunc(context)とそう変 わらなくなってしまう。それでもまだそうしようとする理由には、Cのリターン ではなく継続を呼ぶことでtry catchのような末尾呼び出しができる。といって もそれは多少コードの量が減る程度。ファイバにスタックとして局所記憶を持 たせれるならば、かなり複雑な状況(スタックを使った)で継続を渡すことがで きるのに、制御を渡した時にスタックを放棄するとなると、それって関数呼び 出しと変わらないよね。ということになってしまう。
ここでなんでスレッドに継続を適用してよかったかというと、それは
  • ブロックしてる間に他のスレッドにスタックを開け渡せるから空間的が節約できる。
  • 継続でブロックしているスレッドはその制御領域を見て、その継続がどこで あるかがわかる。だとすれば、データの受け渡しの場合、供給側がブロックす る時に、今ブロックしているスレッドの中から消費側を見つけることができて、 その場合、スタックを介してデータを渡せる。(マイOSでは未実装)
  • 明示的に末尾呼び出しの最適化ができる。
  • 状態遷移が書きやすい。
ファイバに実装するとなると、スレッドとの違いはそこにスケジューリングが あるかないかだけだ。なんでスケジューリングがあるかというと、スレッドで はもうやる事がないから、他のスレッドの結果を待つためだ。ここはファイバ では、やる事が終わったら次に渡し、やる事がなければ親に戻るということに なる。コンテキストスイッチが必要でないということは、関数呼び出しでもい い。スレッドの場合、呼び出すことは可能ではない。必要な供給者スレッドの 継続を呼ぶこともできるかもしれない(この操作は割込み形態になると思う)、 それも割込み待ちなら、何をすればいいということになる。マイOSの場合、何 もすることがなければ、スケジューラでスピンして割り込みを待つ。その割り 込みがどこかのスレッドを起こすので、そこから制御がはじまる。
ファイバが専用のスタックを持たないなら、ファイバ間をthread_blockとする ことも可能だ(これはこのファイバの親のスレッドがスタックを放棄できる時だ け)。これはきめ細かいけれど、時によってオーバヘッドの大きいスケジューリ ングになる。ファイバの先でブロックするのにその継続先は親のスレッドの先 頭だ。もし、スイッチせずに同じスレッドに戻ってくるなら、かなりの無駄骨。 そのままファイバで継続すればいい。
この部分を全部Cの関数で実装したとしたら、これは全部ファイバの継続でやる ことと同じになる。もしthread_blockをして何かの性能があがるなら、やる価 値はあるか...。
そして、ファイバの継続とした場合、そのデータの受渡しの手順をどうするの か。今は呑気にグローバル変数を介している。


関数型言語のスタックの扱いについて、調べていた。ちょっと衝撃。

まずびっくりしたのは浅い束縛と深い束縛だ。深い束縛というのはCと同じよう な感じでスタックフレームを形成していく。そしてここでCにはないのは、呼び 出された先からスタックを登って変数を参照することだ。
なんでこんなことをするのかというと、LISPは動的に変数を拾ってくるから。 これはクロージャの時だろう。クロージャは関数+状況なので、クロージャが定 義された時のスタックフレーム上の値を参照するのにスタックを上らないとい けない。浅い束縛はスタックフレームではなく、その変数名に対して値のキュー としてとっておく。このキューを走査して値をとる。(これ、実行状況がよくわ からない)
そもそもこんな操作は絶対したくない。これに対してSchemeは静的らしい。つ まりクロージャができた時点でその参照した値になる。
やっぱりUsing Continuations to Implement Thread Management and Communication in Operating Systems.にあった通り、ちょっと、言語系とは違 うかな...。
実際のとこ、そこまでスタック操作した性能の劣化以上の何かがあるようには 思えない。



マイトランポ。運転席の後のスペースにバッテリーを追加してアイソレータを
つけてます。これでサブバッテリの電圧が下ってもメインから流れてくること
はないので、サブバッテリがなくなるまでバッテリを使っても、メインは生き
てるのでセルはかかります。

実際のケースだと、夜中ずっと電気毛布を使っても10V程度までしか落ちない。 さらにそこで電気ポットで湯沸かしは無理。容量が足りなくて不安定です。

インバーターは定格500W、最大1000Wを塔載。これで電気毛布や、電気ポットが 使えます。12Vだとかなりの電流が流れることになるので、それを流せるだけの ケーブルは、とてつもなく太いので大変でした。

カーステは(ラジオさえ)ないけれど、どでかいパワーアンプ塔載。iPodをこれ に直結です。

ギミック好きなので折り畳み机。あまり有効に使われたことがない。上に置い てある、家具の耐震用のすべらなくするのを使うと、移動中でも乗せた物が動 かなくていい。大元の電源のスイッチはとても電流が流れるのでモーターボー トのスイッチを使ってます。今だったらリレーにするけどね。でもこのガシャっ と回す感覚がレトロフューチャー好きの自分にはたまらない。

左右ドアのスピーカーと、タイヤハウスカバー兼ウーハーになってます。左右 ドアのスピーカーは、t2.0くらいのポリプロピレンの板に取りつける形になっ ているのだけど、ちょっとよくない。今はフルレンジをつけてるのだけど、思 いきってツイーターをつけた方がよさそう。

この前読んだ論文に出ていた関数型言語の継続におけるスタックの扱いに興味 をもってググっていたところ、スパゲティスタックになるようだ。この原典は A model and stack implementation of multiple environments Daniel G. Bobrow, Ben Wegbreit October 1, 1973。みたい。それをググっている間に なんとなくみつけたのがAn open operating system for a single-user machine Butler W. Lampson, Robert F. Sproull, 1979。これはあのXeroxの Altoに乗せたOSについてだ。参考になる点はなにもないけれど、当時がわかっ ておもしろい。BCPLだし。基本的にはOSはサブルーチン形式になっていて、最 低限のハードウェアのサポートをして、モニタからオーバーレイでディスクか らプログラムをロードして、OS部分とはジャンプテーブルを書き替えるという 形式。プログラム間の制御の移行には現在の実行状況をディスクに保存し、次 のプログラムをその保存された状態とともにディスクから復帰する。これはコ ルーチンと表現されている。BCPLではコルーチンが実装されていた。プログラ ム間のデータの通信はディスクに書きこんだデータを参照していた。


先週のMCFAJ最終戦筑波の写真です。ta-1さん、ありがとうございます。

これは予選出走。写真見るとやっぱり太ったかも。体重はこの時60kg。最悪で も58kgまでしないとだめかも。歳を追うごとに絞るのが辛くなる。身長は 173cm なので、56kg以上が標準体重なんだ。20あたりの頃から同じ身長だけど、 当時は体重50kg。あの頃は飯代を一日500円にして(これ、本当にキツい)ガス代 とタイヤ代にあててたからね〜。
レースやめてから後、ブクブク太って70kg越えた。これでも頑張って落とした んだ。
今日の体重は60kg。レース後の暴飲暴食をリカバリしました。

これは予選中。このビビッドな色合いが我ながら最高。

これは決勝中。肩のチェッカーの一番下のラインはない方がいいかも。

なんか自分のフォーム見るのも久しぶりだな。

ロガー続き。いつもなんとなくパパっと書いたコードになっていたのをちょっ と整理した。今回、スレッドのブロックに継続を使えるようにOSを変更した。 これを使ってみると、びっくりするほど書き易いの気付いた。スレッドとファ イバの合いの子のように組める。ので、状態遷移のあるスレッドが事の他書き やすい。とはいえ、使い所で、ファイバの方が確実にスイッチは速い。ファイ バなら呼出し先退避だけを退避して、継続の呼出し先退避を復帰するだけなの に対し、スレッドの継続だと、それに加えて待ち行列の走査、選択、そして全 部のレジスタの復帰がいる。さらにはスタックは巻き戻される(これがプログラミングにとても制限を課す)。
今のファイバではファイバそれぞれに局所記憶をスタックとしてもたせている。 これが空間的な制約をきつくしていて、そう簡単には使えなくなっている。こ れをファイバの局所記憶は16byte程度にして、実行時のスタック切り替えにで きないかと思っている。それにはfiber_yieldがスタックの底近くで呼ばれるこ とが前提で、想定される使い方において、なんとかなりそうだ。
OSの領域から自由自在に設計、実装できるというのは、桃源郷を自ら作るあげ る幸せ感がある。
スレッドの継続があることでこんな感じにすることができた。
これはルートのスレッドから呼ばれます。
thread_t
data_init ()
{
  thread_t th;
スタックなしで(スタックはスケジューリング時に割当てられる)スレッドを作成
  th = thread_create_no_stack (&data_tc, "data", data_thread, 0);
  thread_start (th);

準備完了まで待ちます。
  while (!data_thread_ready)
    thread_block (NULL);継続をNULLとしているので起こされた時はここから。

  return th;
}

void
data_thread (uint32_t arg __attribute__((unused)))
{
  data_th = current_thread;

  pin_init_speed ();
  pin_init_plap ();

  *VICIntSelect &= ~VIC_EINT3;	// IRQ
  *VICIntEnable |= VIC_EINT3;

  // Logger timer. Free count. 1msec.
  __timer_config (TIMER3, TIMER_COUNTER_RESET, TIMER_MSEC);
  timer_start_nointr (TIMER3);
  buf_intr = rbuf_nolock_init (__buf_intr, INTR_RBUF_SIZE);

  // Logger setting.
  iprintf ("display: %s, magnet=%d\n", dipsw_laptime ? "lap time" : "elapsed",
	   dipsw_nmagnet);
  memset (&__data, 0, sizeof __data);
  // Data buffer. directly write to DMA buffer.
  data_dma_buffer_setup ();

      // Now OK to log.
  data_thread_ready = TRUE;
  // Setuped. wakeup master.
  thread_wakeup (shell_th);準備ができたので親を起こします。
  // This thread resumed from data_main.
  thread_block (data_idle);そして、次に起こされた時にはdata_idleから。
ここには戻ってきません。
  // NOTREACHED
}

void
data_idle ()
{
上のthread_block (data_idle)から、次にスケジューリングされた時にはここから。
割り込みは禁止で入ってくるので割り込みを開けます。
  intr_enable ();

IDLE状態の間はここでブロックするとdata_idleの最初からはじまり、そうでな
ければdata_samplingからはじまります。
  thread_block (log_status_flag == LOG_STATUS_IDLE ? data_idle : data_sampling);
  // NOTREACHED
  while (/*CONSTCOND*/1)
    ;
}

void
data_stop ()
{
  intr_enable ();

  // Flush
  printf ("flush.\n");
  *__data.p = 0xffffffff;	// End marker.
  // Change current buffer to write buffer.
  dma.write_buffer_bank ^= 1;
  // For next logging.
  memset (&__data, 0, sizeof __data);
  current_speed = 0;
  assert (storage_th);
  // Write out remaining data.
  thread_wakeup (storage_th);

次にスケジューリングされた時はdata_idleからはじまります。
  thread_block (data_idle);
  // NOTREACHED
  while (/*CONSTCOND*/1)
    ;
}

void
data_sampling ()
{
  uint8_t buf[INTR_RBUF_SIZE];
  uint32_t *q;
  size_t sz, i;
  cpu_status_t s;

  // contination is called with interrupt disable. enable again.
  intr_enable ();
サンプリングが終了したら、後処理をして、ここには戻ってきません。
  if (log_status_flag != LOG_STATUS_SAMPLING)
    data_stop ();

  // Read from interrupt context buffer.
  s = intr_suspend ();
  sz = rbuf_nolock_read (buf_intr, buf, INTR_RBUF_SIZE);
  intr_resume (s);

  // Process data.
  for (i = 0, q = (uint32_t *)buf; i < sz; i += sizeof (uint32_t))
    {
      data_process (*q);
      *__data.p++ = *q++;
      printf ("sanacnt=%d\n", __data.cnt);
      __data.cnt += sizeof (uint32_t);
      if (__data.cnt == 512)
	{
	  // Flip data bank.
	  data_dma_buffer_setup ();
	  if (storage_thread_write_busy)
	    {
	      *__data.p++ = LOGGER_STATUS_ERROR | *T3TC;
	      __data.cnt += sizeof (uint32_t);
	    }
	  // Kick storage thread.
	  thread_wakeup (storage_th);
	}
    }
次はまたこの関数からスタックを巻き戻して実行されます。
  thread_block (data_sampling);
  // NOTREACHED
  while (/*CONSTCOND*/1)
    ;
}





菜園状況。チンゲンサイを収穫しました。ヨトウムシにやられずに済んだ本当
に数少ない収穫。大根の二次隊も、いいところまで来てやられてしまった...。



ジャガイモが生えていた。これは初夏に収穫したジャガイモの収穫し残しから 発芽したようだ。他にも、もう一株。これは楽しみ。

久々にハイエースを洗車。明日車検に出すので。ハイエースは5年で45000km。 後もう少しするとディーゼル規制にかかってしまう。やっぱりガソリンにして おけばよかったかな...でもディーゼルのサウンド、パワーフィールが好きなん だよね。
さて、スレッドの継続サポート、最後にx86。x86ポートはまだ中途で割込みま わりもきちんと書いてないけれど。
// construct IRET frame when stack is newly allocated.
void
thread_x86_iret_setup (struct iret_frame *iret, addr_t func, uint32_t arg)
{

IRET命令でCPUによって操作されるスタックの部分をstruct iret_frameとして、
そこを操作し、%espをこの構造体の一番最初にしてやることでスレッドをスイッ
チします。

  iret->eip = func;
  iret->arg = arg;
これは継続された先が引数を必要な時、即ち一番スレッドが一番最初に呼ばれる時用。

  iret->cs = cs_get ();
セグメントは現在のもの。マイOSではCSは固定。

  iret->eflags = eflags_get () & ~0x200;// Disable interrupt.
現在のEFLAGSをそのまま割込み禁止にします。

  iret->noreturn = (addr_t)md_thread_noreturn_assert;
継続された関数が、間違ってリターンすることがあればこの場所にあるアドレスに
リターンするので、デバッグ用のフックを入れておきます。
}

thread_t
md_thread_create (thread_t tc, thread_func_t start, uint32_t arg)
{
  /*
    Interrupt handler.
    Stack usage with no privilege-level change.

    +---------------+
    |     EFLAGS    |-4
    +---------------+
    |     CS        |-8
    +---------------+
    |     EIP       |-12 <-ESP
    +---------------+
    |  Error code   |-16 <-ESP (8,10,11,12,13,14,17)
    +---------------+
    |               |
           ...
    |               | stack_top
    +---+---+---+---+
      3   2   1   0


    THIS ROUTINES SETTING

                     stack_bottom
    +---------------+
    |  1st arg      |-4
    +---------------+
    |return address |-8
    +---------------+
    |     EFLAGS    |-12
    +---------------+
    |     CS        |-16
    +---------------+
    |     EIP       |-20  <-ESP
    +---------------+
    |               |
           ...
    |               | stack_top
    +---+---+---+---+
      3   2   1   0
   */
  // install return address for 'IRET'
  if (tc->tc_stack)
    {
      tc->regs.sp = (addr_t)tc->tc_stack->bottom - sizeof (struct iret_frame);
      // Setup stack for 'IRET'
スタックが既に用意されていれば準備します。
      thread_x86_iret_setup ((struct iret_frame *)tc->regs.sp,
			     (addr_t)start, arg);
    }
  else
    {
      // IRET frame is constructed when stack is allocated.
スタックが用意されてない場合、継続先はtc->continuationでとっておけるけ
れど、引数はとっておけない。(今迄のARM,SH,H8は最初のいくつかの引数はレ
ジスタ渡しだったので、この段階で復帰されるスイッチフレームのレジスタに
引数を設定しておけばよかった。)
ここでは仕方なく、引数はESPに置いておくことにした。スイッチフレームはス
レッドが使われるまでいじられないので、どこでもいい。
でもちょっとコードの見た目的に素直じゃないけれど、一番最初のスレッドの
呼出しのためだけに、なにかを用意するのも無駄だということで。
      tc->regs.sp = arg;	//XXX kludge hack.
      tc->continuation = (continuation_func_t)start;
    }
  LPRINTF ("[%d]:%s func=%A esp=%x\n",  tc->id, tc->name, start, tc->regs.sp);

  return tc;
}

void
md_thread_stack_discard (thread_t tc)
{

  tc->regs.sp = 0;
}

void
md_thread_continuation_setup (thread_t tc)
{
  uint32_t arg = tc->regs.sp;	//XXX kludge hack.
上記でしたように、一番最初にスレッドが起動する時にはその引数がspに入っ
ているのでそれをとっておきます。

  // Setup register set for the next context switch.
  // Already stack is attached.
  tc->regs.sp = (addr_t)tc->tc_stack->bottom - sizeof (struct iret_frame);
IRET命令が操作するフレームを差すようにスタックポインタを設定。

  if (tc->continuation)
    {
      // Setup stack for 'IRET'
新しく割当てられたスタックなので、その底にIRET用のフレームを設定。
      thread_x86_iret_setup ((struct iret_frame *)tc->regs.sp,
			     (addr_t)tc->continuation, arg);
      tc->continuation = NULL;	// scheduled now.
    }
}

void
md_thread_continuation_call (continuation_func_t cont)
{
  // Rewind stack and call continuation.
  current_thread->continuation = NULL;	// dispatch here.
  __asm volatile ("movl %1, %%esp\n"
		  "pushl %0\n"
		  "jmp *%2" ::
		  "g"(md_thread_noreturn_assert),
		  "g"(current_thread->tc_stack->bottom), "r"(cont));
  // NOTREACHED
}

この関数はブートストラップ時に一番最初のスレッドを初めるのにしか使われない。

void
md_thread_continuation_call_with_arg (thread_func_t cont, uint32_t arg)
{
   // Rewind stack and call continuation with arg.
  __asm volatile ("movl %2, %%esp\n"
		  "pushl %0\n"
		  "pushl %1\n"
		  "jmp *%3" ::
		  "r"(arg),
		  "g"(md_thread_noreturn_assert),
		  "g"(current_thread->tc_stack->bottom), "r"(cont));
}

これで実装終わり。ロガーに戻ります。ロガーのスレッドをこの継続を使うよ
うに書き直します。実際書き直してみると

while (1)
{
  thread_sleep()
   何かする
}

を

start:
  何かする
  thread_block(start)

に書き替えることになるので、while(){}をdo{}whileに書き替えるような不自
由さがある。

延々と書き替えて、ついにここまで。

log> help
---Monitor---
U ringbuffer
        lock : 
        event: 3(uart send) 
U ringbuffer
        lock : 
        event: 
---Ready Queue---
<0>: 
<1>: 
<2>: 
<3>: 1 
---Thread Status---
id   pri(used/total)
[7] W 3 (no stack) gps
[6] W 1 (no stack) data
[5] W 1 (no stack) storage
[4] W 3 (no stack) controller
[3] W 0 (no stack) uart send
[2] W 0 (no stack) uart recv
[1] R 3 (256/1028) root
avaliable command: help reset gps font contrast dac rtc rm ls logger test fs write read sdinit 
log> 

rootスレッドはシェルプロンプトで、これは結構深い位置でブロックするので
スタックを放棄するのが難しく、専用スタック。それ以外は全て放棄できるよ
うにした。
ここで、いやらしく思いっきりスピードセンサの割り込みを入れると、スタック
が足りなくなった。スタックは放棄できるスレッド用に二つだけ用意した。

Asssertion failed. ../../..//kernel/thread_stack.c, 116 caller:0x4000216c
---Monitor---
U ringbuffer
        lock : 
        event: 3(uart send) 
W ringbuffer
        lock : 
        event: 1(root) 
---Ready Queue---
<0>: 3 
<1>: 6 
<2>: 
<3>: 7 4 
---Thread Status---
id   pri(used/total)
[7] r 3 (716/1028) gps
[6] r 1 (440/1028) data
[5] W 1 (no stack) storage
[4] r 3 (no stack) controller
[3] r 0 (no stack) uart send
[2] W 0 (no stack) uart recv
[1] W 3 (412/1028) root

これはどういう事態かというと、gpsは一番優先度を低くしている。そしてgps
のパーサが時間がかかる。その間にスピードセンサからの割り込みが入って、
その終了時に再スケジューリンングされて、優先度の高いdataスレッドが走る
ので、gpsスレッドはスタックを持ったまま、制御を渡し、dataスレッドが実行
中にprintfして、さらに優先度の高い(この設定は間違い)uart sendスレッドに
渡ってしまった。優先度が高くとも割込みさえ入らなかければ制御は渡さない
のだけれど、どこか、controllerの10msecごとの割り込みか、次のスピードセ
ンサからの割り込みからかで、制御が移ってしまった。

つまりスレッドの優先度の数で、最悪消費されるスタックの数が決まる。この場合
スレッドの優先度が0,1,3の3つあるので、割り合て可能なスタックを三つはも
たないといけない。スレッドが同じ優先度にある分には、自分で制御を明け渡
さない限りその優先度にあるスレッドには制御がまわらないので、スタックを
さらに消費することはない。

これはUnix/Mach的な時分割スケジューリングとは違う。ITRON的なスケジュー
リング。


H8/300H(3052), H8/300(3664)にも継続を使えるスレッドを実装しました。H8は
バス幅が16bitなので16bitCPUなのだけど、レジスタは32bitある。アドレス空
間はH8/300は16bit、H8/300Hは24bitと、レジスタ長もバス幅もアドレス空間も
異なるという挑戦的なCPUなのだ。

そして、このCPUの場合、ARM,SHのようなリンクレジスタはなく、スタックにそ れを退避する。そしてステータスレジスタ(CCR)もスタックに退避される。 それは32bitの領域に退避されて、
|CCR|   |adr|adr| ... H8/300
|CCR|adr|adr|adr| ... H8/300H
のようになる。通常のサブルーチンのリターン命令(rts)ではアドレス部分だけで リターンし、例外からのリターン命令(rte)ではこのステータスレジスタの内容も 復帰してリターンする。
なのでスタックを放棄したり、つけかえたりする場合には、そのスタックにこ れらを設定するということが必要になる。
#ifdef __NORMAL_MODE__
#define	ADDRESS_MASK	0x0000ffff	// 16bit address
#else
#define	ADDRESS_MASK	0x00ffffff	// 24bit address
#endif
#endif

thread_t
md_thread_create (thread_t tc, thread_func_t start, uint32_t arg)
{

  tc->regs.er0 = arg; // pass 1st arg.
  // install return address for 'rte'

既にスタックが割りあてられているのであれば、そのスタックにリターンアドレス
を設定します。これは'RTE'命令で戻るので、割り込み禁止で走るように設定。
  if (tc->tc_stack)
    *(uint32_t *)tc->tc_stack->bottom =
      (addr_t)start | 0x80000000; // disable interrupt.
  else
    tc->continuation = (continuation_func_t)start;
まだスタックがない場合は、スタート場所を継続として設定。これはいずれ
スタックが割りあてられた時に、使われる。

  return tc;
}

void
md_thread_stack_discard (thread_t tc)
{

  tc->regs.sp = 0;
}

void
md_thread_continuation_setup (thread_t tc)
{
  // Setup register set for the next context switch.
  // Already stack is attached.
  tc->regs.sp = (addr_t)tc->tc_stack->bottom;

  if (tc->continuation)
    {
ここで、はじめて設定されたスタックにリターンアドレスとステータスレジスタ
を設定します。このtc->regs.ccrはdo_thread_switchで保存されたステータス
レジスタ。
しかしこれは冗長。割り込みから中断されたコンテキストなら、継続はないの
でそもそもここは呼ばれない。ここが呼ばれるということは明示的に
do_thread_switch を呼んだということなので、常にそのステータスレジスタは
割込み禁止。
ここは、単に割り込み禁止のフラグを立てるだけでいい。はず。それはつまり
継続の一番最初は割込み禁止で入ってくるということ。
      // [CCR | return address] preserve control register.
      assert (!(tc->regs.ccr & ADDRESS_MASK));
      assert (!((addr_t)tc->continuation & ~ADDRESS_MASK));
      *(uint32_t *)tc->tc_stack->bottom = tc->regs.ccr |(addr_t)tc->continuation;
      tc->continuation = NULL;	// scheduled now.
    }
}

ここ、継続の引数なし版と、スレッドの引数あり版の二つにしたいとも思っている。
void
md_thread_continuation_call (continuation_func_t cont)
{
  // Rewind stack and call continuation.
  current_thread->continuation = NULL;	// dispatch here.
  __asm volatile ("mov %0, sp \n jmp @%1"::
		  "r"(current_thread->tc_stack->bottom), "r"(cont));
  // NOTREACHED
}
実際はこの程度でよかったのだけど、久々のH8、そしてH8はメモリがない。それで メモリ破壊バグをやらかしたりして、絶望的に時間がかかってしまった。
メモリ破壊例。
000fdfc B _uart_recv
0000fe88 B _uart_send
0000ff14 B _app_tc
0000ff60 B ___sys_flag
0000ff62 A _bss_end	いくらなんでもスタックがなさ過ぎ! BSS破壊していた。
0000ff80 A _stack_start

これでなんとか。
000fc7a B ___st_recv
0000fc7c B _uart_recv
0000fd08 B _uart_send
0000fd94 B _app_tc
0000fde0 B ___sys_flag
0000fde2 A _bss_end
0000ff80 A _stack_start
メモリ2KBはつらい。
あと、まだx86が残っている。これも同じようにスタックに退避されているのだけど、 セグメントレジスタとフラグレジスタも復帰するので合計12byteのスタック領域を 設定しないといけないんだ。


シリアルドライバを継続を使うように書きかえた。このように単純に割り込み
を待って処理してまたすぐ寝るような、実行時だけのスタックがあればいいよ
うなスレッドにはスタック放棄が効く。これでマイOSでもスタックの量が減っ
たのだけど、スタックの量は確定しない。それはスタックを放棄できるスレッ
ドでもスタックを放棄せずにブロックすることがあるからだ。ここは微妙だ...。
void
uart_recv_thread (uint32_t arg)
{
  __st_recv = (struct uart_thread *)(addr_t)arg;
  __st_recv->started = TRUE;
  thread_wakeup (parent_thread);

  recv_loop ();
  // NOTREACHED
}

受信スレッドにおいては単純な継続への置き換えができる。
void
recv_loop ()
{
  uint8_t buf[UART_FIFO_SIZE];
  size_t sz;
  cpu_status_t s;

  s = intr_suspend ();
  sz = rbuf_nolock_read (uart_recv_buf, buf, UART_FIFO_SIZE);
  intr_resume (s);
ここのリングバッファの書き込みでは、消費者スレッドがバッファを消費して
なく、バッファが満杯であれば、スタックを保持したままブロックする可能性
がある。
  if (sz)
    ringbuffer_write (__st_recv->rb, buf, sz);
ここでブロックしたところでスタックを放棄して、次にスケジューリングされた
時にはrecv_loopの最初からスタックは巻戻されて実行される。
  thread_block (recv_loop);
  // NOTREACHED
  while (/*CONSTCOND*/1)
    ;
}

送信の場合は、実のところスレッドにしなくてもいい。ここは富豪的。
void
uart_send_thread (uint32_t arg)
{
  __st_send = (struct uart_thread *)(addr_t)arg;
  __st_send->started = TRUE;

  thread_wakeup (parent_thread);

  send_loop ();
  // NOTREACHED
}

void
send_loop ()
{
  uint8_t buf[UART_FIFO_SIZE];
  size_t i;
  while (/*CONSTCOND*/1)
    {

バッファが空の場合、ブロックし、供給者スレッドがバッファに入れたところ
で起こされる。このブロックするところは結構ネストしたところでブロックす
るため(ARMだとスタックを120-140byteほど使ったところ)、ちょっと苦心がい
る。ここでringbuffer_read_extという継続を使うためのインターフェースを新
に用意した。

      size_t sz = ringbuffer_read_ext (__st_send->rb, buf, UART_FIFO_SIZE, send_loop);

      for (i = 0; i < sz; i++)
	md_uart_putc1 (buf[i]);
    }
}


size_t
ringbuffer_read_ext (ringbuffer_t rb, uint8_t *buf, size_t size,
		     continuation_func_t cont)
{
  monitor_t mon = &rb->mon;

  monitor_enter (mon);
  DPRINTF ("\t\t\t\tR< i(%d) o(%d) f(%d) s(%d)\n", rb->input, rb->output,
	   rb->free, rb->size);

  if (rb->free == rb->size)
    {
      DPRINTF ("\t\t\t\tR:empty!\n");
      if (!rb->event_hooked)
	{
	  rb->event_hooked = TRUE;
バッファが空なのであれば、モニタのロックを外して、継続を呼ぶ。この例の場合
これはsend_loopの先頭に戻るということ。
	  monitor_event_hook (mon, RINGBUFFER_EMPTY, cont);
	  thread_block (cont)
	  // NOTREACHED (jump to continuation.)
	}
    }
もしバッファがあって、このスレッドを起すフックがあるならそれを解除。
  if (rb->event_hooked)
    {
      rb->event_hooked = FALSE;
      monitor_event_unhook (mon);
    }

  return __read_subr (rb, buf, size);
}

これによって、この手のスレッドは全てスタックを共有できるようになった。
これはSH4Aの例

=> go 0x89000000
## Starting application at 0x89000000 ...
stack_start: 0x89100000
RAM data: 0x89009558-0x89009ac4 1388byte
bss: 0x89009ae0-0x89015620 47936byte
Privilege-mode, bank 1, Exception disabled, FPU enabled, IMASK=0xf
bss memory check passed.
CPU mode 16, 32bit address mode, input clock 33MHz
PMB page size 16MB mask=ffffff VPN:a4000000 PPN:4000000
PMB page size 128MB mask=7ffffff VPN:90000000 PPN:58000000
: INTC: 1f1f0000 89008804
udelay_param = 12
thread_stack_setup: 8900a0b0-89009cb0 (400)
thread_stack_setup: 8900a4b0-8900a0b0 (400)
thread_stack_setup: 8900a8b0-8900a4b0 (400)
thread_create: [2]:uart recv pc=890053e0 sp=3d00d stack=0byte
thread_start: [2]
thread_create: [3]:uart send pc=890053b0 sp=3d00d stack=0byte
thread_start: [3]
thread_create: [4]:test pc=89001988 sp=3d00d stack=0byte
thread_start: [4]
Pck 1/24 50MHz
DUck 1/-1 Disabled.
GDTAck 1/8 150MHz
DDRck 1/4 300MHz
Bck 1/12 100MHz
SHck 1/4 300MHz
Uck 1/4 300MHz
Ick 1/2 600MHz
test_thread:1234abcd 136
hello 0
test4> 
test4> 
test4> help
---Monitor---
W ringbuffer
        lock : 
        event: 3(uart send) 
U ringbuffer
        lock : 
        event: 
---Ready Queue---
<0>: 
<1>: 
<2>: 
<3>: 1 
---Thread Status---
id   pri(used/total)
[4] W 1 (0/0) test	スタックなし
[3] W 0 (0/0) uart send スタックなし
[2] W 0 (0/0) uart recv スタックなし
[1] R 3 (328/1028) root 専用スタック
avaliable command: help reset vm mmu tlbclear tlbdump cacheflush memtest mem pmb cache timer exception test 
test4> 
しかし、この見通しの悪さはどうだ。古のUnixではシステムコールでカーネル に入って、そこからユーザ層に戻るのにsetjmp/longjmpを使っていたと聞いた ことがある。この継続の実装は、それの最適化もできる。しかしだ。やる事と してスタックを巻き戻すことを考慮に入れないといけない点で、結局は setjmp/longjmpにオブラートを被せた程度にしかならない。とはいえ、この手 の最適化が、実行時には有用なのも事実。そしてこの面倒臭さは、割込み形態 のOSは常に明示的に自分の継続先を設定してブロックしないといけないという のがとても制限になるということだ。


マイOSに継続を使うことのできるスレッドを実装しはじめました。今日のとこ
ろはスタックの切り離し、つけ直しができるところまで。スレッドの概念はも
う随分慣れ親しんできた。しかし、ここに継続が入るとなるとプログラミング
する段階でスタックを全て開放できる条件になるように、しむけないといけな
い。このあたりがこの方式が市民権を得なかった理由だと思う。ここまでやる
なら言語に組みこんだ方がいい。でも言語の縛りだとカーネルに使うには縛り
がきつ過ぎるという微妙なとこだ。

当初、論文にあったインターフェースから初めたのだけど、マイOSとは作りが 違うので、ちょっと見た目違った実装になった。マイOSはUnix,Machとはスケ ジューリングポリシーが違う(クロック割り込みでスケジューリングをするとい う方針ではなく、割込みの終了でスケジューリングされる。なので、割り込み が起きなければ絶対に横取りされることがない)し、ARMポートでは割込みスタッ クはper-processorにとってある。これには使われなくなったブートストラップ スタックを転用している。その割にそれを使うのは入口だけで、割込みハンド ラは割り込まれたスレッドのスタックを使うようになっている。
もうちょっと仕様まとめておけばよかった。夏前に書いたのに完全に忘れた。

今迄スレッド制御領域の後に続く領域だったスタックを別体にするために

#define	THREAD_STACK_MAGIC	0xaa

struct thread_stack
{
  uint8_t *bottom;	//スタックの一番最初
  uint8_t *top;		//スタックの上限
  size_t size;		//スタックサイズ
  size_t remain;	//スタックの残り。
// これはデバッグの時に、まず最初にスタックにマジック
// (THREAD_STACK_MAGIC)を書き込んでおいて、割り込み毎にそのマジックがど
// れだけ残っているかを数えた結果。
  bool active;		//このスタックがどこかのスレッドが使っている。
  SIMPLEQ_ENTRY (thread_stack) ts_link;
};

取り外し可能なスタックはプールにしておきます。
SIMPLEQ_HEAD (thread_stack_pool, thread_stack);
extern struct thread_stack_pool thread_stack_pool;

__BEGIN_DECLS
プールの初期化。
void thread_stack_init (void);
任意の場所のスタックを、このスタック形式にする。
void thread_stack_setup (struct thread_stack *, uint8_t *, size_t);
スタック形式になったのをプールに入れる。
void thread_stack_load (struct thread_stack *);
取り外しが不可能な形式の場合、そのスタック領域をこの方式に変換
void thread_stack_dedicate (uint8_t *, size_t);
取り外し可能なスレッド同士の間でスタックを付け替え。
void thread_stack_handoff (thread_t, thread_t);
プールからスタックを割当て。
struct thread_stack *thread_stack_allocate (void);
スタックをプールに返還。
void thread_stack_deallocate (struct thread_stack *);
スレッドにスタックを新たにつける。
void md_thread_stack_attach (thread_t, struct thread_stack *, continuation_func_t);
スレッドからスタックを取り外す。
void md_thread_stack_detach (thread_t);
__END_DECLS


// 取り外し可能なスタックのプール。
struct thread_stack_pool thread_stack_pool;
int thread_stack_loaded;

void
thread_stack_init ()
{
//プールの初期化
  SIMPLEQ_INIT (&thread_stack_pool);
}

void
thread_stack_setup (struct thread_stack *ts, uint8_t *top, size_t size)
{
このスレッドシステム用の構造にして、スタックの内容を、残りスタックを
計れるようにマジックで埋めて、さらにそうでない状況でも最悪スタックが
溢れた時をみつけるためにカナリヤを入れておきます。
  ts->top = top;
  ts->size = size;

  // Fill with magic number to estimate stack usage.
  memset (ts->top, THREAD_STACK_MAGIC, size);

#if BYTE_ORDER == BIG_ENDIAN
  ts->bottom = ts->top + ts->size - 4;
  *(uint32_t *)(ts->top + 4) = (uint32_t)0xac1dcafe; // canary
#else
  ts->bottom = ts->top + ts->size;
  *(uint32_t *)ts->top = (uint32_t)0xac1dcafe; // canary
#endif
  LPRINTF ("%x-%x (%x)\n", ts->bottom, ts->top, ts->size);
}

void
thread_stack_load (struct thread_stack *ts)
{
  cpu_status_t s = intr_suspend ();
プールに入れるだけ。
  thread_stack_loaded++;
  ts->active = FALSE;
  SIMPLEQ_INSERT_HEAD (&thread_stack_pool, ts, ts_link);

  intr_resume (s);
}

void
thread_stack_dedicate (uint8_t *thread_area,
		       size_t sz /* don't include thread_control area*/)
{
これは今迄の形式のスレッドの制御領域に続いてスタックが続く形式をサポート
するため。今迄スタック上限だったところにスタック制御領域をとります。
  uint8_t *p = thread_area;
  struct thread_control *tc = (struct thread_control *)p;
  p += sizeof (struct thread_control);
  struct thread_stack *ts = (struct thread_stack *)p;
  p += sizeof (struct thread_stack);

  thread_stack_setup (ts, p, sz - sizeof (struct thread_stack));
  tc->tc_stack = ts;
}

struct thread_stack *
thread_stack_allocate ()
{
  struct thread_stack *ts = NULL;
  cpu_status_t s = intr_suspend ();
空いてるスタックを探して返します。デバッグにフックしたいのでリストは
崩さずにおいてます。
  SIMPLEQ_FOREACH (ts, &thread_stack_pool, ts_link)
    {
      if (!ts->active)
	{
	  ts->active = TRUE;
	  break;
	}
    }
  assert (ts);
  LPRINTF ("%x\n", ts->bottom);
  intr_resume (s);

  return ts;
}

void
thread_stack_deallocate (struct thread_stack *ts0)
{
  struct thread_stack *ts = NULL;
  cpu_status_t s = intr_suspend ();
これもデバッグのためにリストを残してるので冗長です。
  SIMPLEQ_FOREACH (ts, &thread_stack_pool, ts_link)
    {
      if (ts == ts0)
	{
	  assert (ts->active);
	  ts->active = FALSE;
	  break;
	}
    }
  assert (ts);
  LPRINTF ("%x\n", ts->bottom);
  intr_resume (s);
}

void
thread_stack_handoff (thread_t old_thread, thread_t new_thread)
{
スイッチする両者のスレッドがスタックを放棄できるのであれば、それらで
スタックの領域だけ渡します。

  // We assume current_thread can detach stack.
  new_thread->tc_stack = old_thread->tc_stack;

同じスレッドでスイッチすることもあるので、その時を気をつけます。
  if (new_thread != old_thread)
    old_thread->tc_stack = NULL;
}

ここからは機種依存部分。これはARM

これは、継続の関数が'return'してしまった間違いを捕獲するための埋め草。
void
md_thread_machdep_noreturn_assert ()
{
  printf ("*** noreturn function return from 0x%x. ***\n",
	  __builtin_return_address (0));
  while (/*CONSTCOND*/1)
    ;
  // NOTREACHED
}

thread_t
md_thread_create (uint8_t *thread_area, size_t stack_size, const char *name,
		  void (*start)(uint32_t), uint32_t arg)
{
  cpu_status_t s = intr_suspend ();
  struct thread_control *tc = (struct thread_control *)thread_area;

  thread_setup (tc, name);
  if (stack_size)
    {
スタックサイズが指定されているというのは、今迄の形式なので、このスタッ
クをこのスレッド専用のスタックとして登録します。
      thread_stack_dedicate (thread_area, stack_size);
      tc->regs.sp =(addr_t)tc->tc_stack->bottom;
      stack_size = tc->tc_stack->size;
    }
  else
    {
スタックサイズが指定されていなければ、スタックプールを利用する、継続ス
レッドなので、このスレッドのスタックはこのスレッドに制御が移る時にその
場で割当てられます。
      tc->regs.sp = NULL; // will be allocated when thread is scheduled.
    }

スレッドの一番最初というのは、それが呼び出し元に戻ることはないという意
味で継続。もし戻るようなバグを発見するために、戻った先をつけておきます。
  tc->regs.lr = (addr_t)md_thread_machdep_noreturn_assert;
スレッド本体の関数。
  tc->regs.pc = (addr_t)start;
マイOSでは一つだけ引数を渡せるようにしています。でも、今迄有効に使われた
ことはない。
  tc->regs.a1 = arg;
  tc->regs.psr = PSR_MSYS | PSR_I | PSR_F;// System mode. Interrupt disabled.
  LPRINTF ("[%d]:%s pc=%A sp=%x stack=%dbyte\n",
	   tc->id, tc->name, start, tc->regs.sp, stack_size);
  intr_resume (s);

  return tc;
}

void
md_thread_stack_attach (thread_t tc, struct thread_stack *ts,
			continuation_func_t cont)
{
スタックをスレッドに割当てます。この引数のcontはNULLでありえる。
  tc->tc_stack = ts;
  tc->continuation = cont;
  md_thread_continuation_setup (tc);

  LPRINTF ("[%d] %A-%A\n", tc->id, tc->tc_stack->bottom, tc->tc_stack->top);
}

void
md_thread_stack_detach (thread_t tc)
{
スレッドからスタックを取り外します。
  // Return stack to pool
  thread_stack_deallocate (tc->tc_stack);
  tc->regs.sp = 0;
  tc->tc_stack = NULL;
}

void
md_thread_continuation_setup (thread_t tc)
{

スレッドスタックと、スレッドの設定を合わせます。継続があるならば、この
スタックを使った一番最初のコンテキストスイッチは、継続になる。そうでな
ければ、あらかじめこのスレッドの作成時に設定した関数に継続する。これは
md_thread_cerateで、スタックなしでスレッドを作ることを可能にしているの
で、継続はなくて、スタックもないという状況で呼ばれるから。

  tc->regs.sp = (addr_t)tc->tc_stack->bottom;
  tc->regs.lr = (addr_t)md_thread_machdep_noreturn_assert;
  if (tc->continuation)
    tc->regs.pc = (addr_t)tc->continuation;
}

void
md_thread_continuation_call (continuation_func_t cont)
{
現在のスレッドで継続を呼びます。継続はスタックによる記憶が必要ないとい
う決まりなので、スタックを巻戻してジャンプします。
  // Rewind stack and call continuation.
  __asm volatile ("mov sp, %0; mov pc, %1"::
		  "r"(current_thread->tc_stack->bottom), "r"(cont));
  // NOTREACHED
}

これに共ないスレッドシステムの初期化も変更。これを呼ぶ前にスレッドプール
を用意しておくこととした。

void
thread_system_init (thread_t tc)
{
  int i;
  cpu_status_t s = intr_suspend ();
既にスタックがロードできていることを前提。これはこのルートのスレッドの
スタックを放棄することができるようにするため。
  assert (thread_stack_loaded);

  // Ready queue.
  for (i = 0; i < THREAD_PRIORITY_MAX; i++)
    SIMPLEQ_INIT (thread_ready_queue + i);
  // Thread list.
  SLIST_INIT (&thread_list);

  // Initialize myself.
  thread_setup (tc, "root");

ここでスタックプールからスタックを持ってきます。継続はなし。なのでこの
スレッドの動く先は決まってない。これはこのルートが特別で、一番最初のス
レッドは直接場所を指定して制御が移行するのによっている。それはブートス
トラップの状態はスレッドではないので元の状態を保存するということができ
ないので、最初は継続からしか入ることができない。
  // Allocate stack.
  md_thread_stack_attach (tc, thread_stack_allocate (), NULL);

  current_thread = tc;
  current_thread->state = THR_RUN;
  SIMPLEQ_INSERT_HEAD (__ready_queue (current_thread), current_thread, tc_link);
  intr_resume (s);
}

ここで実際のコンテキストスイッチ。マイOSではdo_context_switchが入口とな
る。これはそれが呼ばれた時のレジスタ状態をスイッチフレームに保存して、
thread_context_switchで、次に走るスレッドが決まったところで、その状態復
帰して戻る。ここで、新しくこれに継続を渡せるように変更した。(r0をそのま
まthread_context_switchに渡す。)ここでdo_thread_switchは明示的なサブルー
チンコールでしか呼ばれないので、呼出し元退避(caller saved)は保存する必
要がない。

	/* void do_thread_switch (continuation_t) */
	/* Assume already interrupt disabled */
	/* Called from thread context */
	.long	current_thread
FUNC (do_thread_switch)
	// save context.
	mrs	r1,	cpsr
	ldr	r2,	[pc, #-16]	// r0 = current_thread
	ldr	r2,	[r2]		// r0 = *current_thread
	str	r1,	[r2], #20	// *r0 = r1, r0 += skip caller saved.
	stmia	r2!,	{r4-r15}	// save callee saved all.
	str	lr,	[r2, #-4]	// overwrite PC to LR. *(r0 -4) = lr
	bl	thread_context_switch
	ldr	r0,	[pc, #16]
	ldr	r0,	[r0]
	ldr	r1,	[r0], #4	// r1 = *r0++;
	msr	cpsr_cf,r1		// restore all CPSR
	ldmia	r0,	{r0-r15}
	bl	md_thread_machdep_noreturn_assert	// for debug.
	.long	current_thread


継続を共なうことのできるブロックはこう実装した。

int
thread_block (continuation_func_t cont)
{
  cpu_status_t s = intr_suspend ();
  thread_t old_thread;
  bool block;

  old_thread = current_thread;
  block = --old_thread->wakeup_request < 0;
  assert (old_thread->state == THR_RUN);

  if (block)
    {
      // Remove old_thread from run queue.
      assert (SIMPLEQ_FIRST (__ready_queue (old_thread)) == old_thread);
      SIMPLEQ_REMOVE_HEAD (__ready_queue (old_thread), tc_link);
      old_thread->state = THR_WAIT;
    }
  else
    {
ブロックしない場合、継続がなければそのままリターンすればいいのだけど、
継続を指定された場合はそこにジャンプします。この場合、スタックは完全に
巻き戻されます。
      // If no need to block, directly call continuation.
      intr_resume (s);
      if (cont)
	{
	  md_thread_continuation_call (cont);
	  // NOTREACHED
	}
      return E_OK;
    }
ブロックする場合は継続の情報とともにブロックします。
  do_thread_switch (cont);

  intr_resume (s);

  return E_OK;
}

do_thread_switchでは、呼び出したスレッドのコンテキストをスイッチフレー
ムに保存した後でthread_context_switchを呼び、その返り値のスレッドのコン
テキストを復帰する。

int
thread_context_switch (continuation_func_t cont)
{
  thread_t old_thread, new_thread;

  if (cont)
    {
新しくこの関数の引数に継続が増えた。
      LPRINTF ("CONTINUATION=>%A\n", cont);
    }

継続があるなら、それを次にこのスレッドが呼び出される時のために登録して
おきます。
  old_thread = current_thread;
  if (cont)
    old_thread->continuation = cont;

次に走るスレッドを探します。もし何もない場合、thread_selectの中で割り込
みを開けてbusy loopします。割り込みが入ってきて、それでもまだ走るべきス
レッドがない場合は、thread_select()は0で戻るので、もう一度busy loopに戻
ります。
  if ((new_thread = thread_select ()) == 0)
    return 0;	// No thread is available. spin again.

ここまで来たら、確実に次に走るスレッドがあるということ。

  // Thread is changed. update thread status.
  if (old_thread != new_thread && old_thread->state == THR_RUN)
    {
      DPRINTF ("%d preempted by %d\n", old_thread->id, new_thread->id);
      old_thread->state = THR_READY;
    }

  // It is possible that new_thread has no stack. currently use
  // old_thread's stack
  // If new_thread has continuation, that thread don't have dedicated stack.
前のスレッドに継続が定義されていたら、それはスタックを放棄できるということ。
  if (old_thread->continuation) // old_thread has discardable stack,
    {
      // handoff stack to new_thread
次のスレッドに継続が定義されていたら、前のスレッドのスタックを付け替えます。
      if (new_thread->continuation)
	{
	  LPRINTF ("STACK HANDOFF (%s->%s)\n", old_thread->name,
		   new_thread->name);
	  thread_stack_handoff (old_thread, new_thread);
	  // Now current_thread == new_thread;
ここで、このスレッドのリターンアドレスを継続に変更します。
	  md_thread_continuation_setup (new_thread);
	}
      else
	{
前のスレッドのスタックを受渡し先がないのでプールに戻します。
	  LPRINTF ("DISCARD STACK (%s)\n", old_thread->name);
	  // discard it for the other thread.
	  md_thread_stack_detach (old_thread);
	}
    }

  // If new_thread has no stack, newly allocate. thraed is resumed
  // from continuation in such a thread.
この状況は、スレッド一番最初の時にあり得る。スタックも継続もなくて、
スタート位置だけ指定されているという状況。
  if (!new_thread->tc_stack)
    {
      LPRINTF ("ALLOCATE STACK (%d:%s)\n", new_thread->id, new_thread->name);
      md_thread_stack_attach (new_thread, thread_stack_allocate (),
			      new_thread->continuation);
    }

  // Now change thread.
  current_thread = new_thread;
  current_thread->state = THR_RUN;
#ifdef DEBUG
  thread_debug_state_check ();
#endif

  //  md_thread_debug_reg (¤t_thread->regs);
  DPRINTF ("switch %d->%d: sp=%x pc=%x\n", old_thread->id, new_thread->id,
	   new_thread->regs.sp, new_thread->regs.pc);

  return 0;
}

ユーザ側は
まずスタックプールを少なくとも一つ以上用意して、
  // Prepare stack pool.
  thread_stack_init ();
  int i;
  for (i = 0; i < 2; i++)
    {
      thread_stack_setup (stack_pool + i, stack_pool_area[i],
			  sizeof (stack_pool_area[0]));
      thread_stack_load (stack_pool + i);
    }
最初のスレッドを用意。
  thread_system_init (&root_tc);

  // Switch to buffered console.
  console_init (boot_flag & BUFFERED_CONSOLE_ENABLE);

  shell_init ();
  // Jump to main with changing stack to thread local storage.
最初のスレッドは継続から入ります。
  md_thread_continuation_call ((continuation_func_t)board_main);


ユーザ側でこのスタック放棄を使えるようにするには、

continuation_func app0_main __attribute__((noreturn));
void
app0_thread (uint32_t arg)
{
  rtc_start (VIC_IRQ);
  rtc_counter_incr (SEC, TRUE);

  iprintf ("%s: arg=%x\n", __FUNCTION__, arg);
#if 0
これは今迄の例。thread_blockの中でスタックを保持している。
  while (/*CONSTCOND*/1)
    {
      thread_block (NULL);
      //      thread_block (NULL);
      iprintf ("wakeup!\n");
      led_blink ();
    }
#else
これはスタックを放棄する例。
  app0_main ();
#endif

  // NOTREACHED
}

void
app0_main ()
{
  led_blink ();
このthread_blockでスタックが放棄されて、次にスケジューリングされた時には
完全にスタックになにもない状態で、app0_mainから走ることになる。
  thread_block (app0_main);
  // NOTREACHED
  while (/*CONSTCOND*/1)
    ;
}

void
rtc_intr_counter_incr ()
{

  thread_wakeup (app_th);
}


mon> l
Send S-record file.
~>Local file name? a.mot
5772 lines transferred in 5 seconds 
!
Read 135589 byte. success
Start address: 0x40003f54
stack_start: 0x4000f800
RAM data: 0x4000adfc-0x4000b5fc 2048byte
bss: 0x4000b5fc-0x4000e6f4 12536byte
current stack=0x4000f7f8
Clock: MAIN, PLL connected, PLL MSEL:11 NSEL:0, CCLKCFG 3
Clock: MAIN, PLL connected, PLL MSEL:11 NSEL:0, CCLKCFG 3
sysclock:288000000Hz cpuclock:72000000Hz
PCLK0: 0, PCLK1: 0 PCONP: 4280ffe
calibrated. delay_parm=74
bss RAM check passed.
port 0 pin 26 func 0 => r=e002c044 shift=20
thread_stack_setup: 4000c958-4000b958 (1000)
thread_stack_setup: 4000d958-4000c958 (1000)
thread_stack_allocate: 4000d958
md_thread_stack_attach: [1] 4000d958-4000c958
thread_stack_setup: 4000e61c-4000e234 (3e8)
md_thread_create: [2]:uart recv pc=400011d4 sp=4000e61c stack=1000byte
thread_start: [2]
thread_stack_setup: 4000e158-4000dd70 (3e8)
md_thread_create: [3]:uart send pc=40001170 sp=4000e158 stack=1000byte
thread_start: [3]
md_thread_create: [4]:app0 pc=400007a8 sp=0 stack=0byte
thread_start: [4]
thread_context_switch: ALLOCATE STACK (4:app0)
thread_stack_allocate: 4000c958
md_thread_stack_attach: [4] 4000c958-4000b958
app0_thread: arg=11133aab
thread_context_switch: CONTINUATION=>40000758
thread_context_switch: DISCARD STACK (app0)
thread_stack_deallocate: 4000c958
user> 
user> thread_context_switch: ALLOCATE STACK (4:app0)
thread_stack_allocate: 4000c958
md_thread_stack_attach: [4] 4000c958-4000b958
thread_context_switch: CONTINUATION=>40000758
thread_context_switch: STACK HANDOFF (app0->app0)
thread_context_switch: CONTINUATION=>40000758
thread_context_switch: STACK HANDOFF (app0->app0)
これは何もなくてスピンしている時にタイマ割り込みが入って、自分のスレッドを
自分につけかえている。

thread_context_switch: CONTINUATION=>40000758
thread_context_switch: DISCARD STACK (app0)
thread_stack_deallocate: 4000c958
シリアルの割り込みで別のスレッドに切り替えることになったのでスタックを放棄した。

user> thread_context_switch: ALLOCATE STACK (4:app0)
thread_stack_allocate: 4000c958
md_thread_stack_attach: [4] 4000c958-4000b958
また戻ってきて、スタックをプールから取り直した。

thread_context_switch: CONTINUATION=>40000758
thread_context_switch: STACK HANDOFF (app0->app0)
thread_context_switch: CONTINUATION=>40000758



シーズンオフ。減量しなくていいし、夜更ししていいし、なんといってもイン
フルエンザにかかってもいい(不要不急の外出を控えてました)。さっそく新宿
に繰り出して、呑んだくれてきました。優勝のメダルを見せびらかしては、自
慢してきました。あっという間に3kg弱太ってしまった。

太るのは一瞬だけれど、落とすのは大変だ。このあたりにしておこう...。とい いつつ、呑み屋主催の旨い物食べに行くツアーに参加することにしてしまった。
菜園状況、やっとチンゲンサイが収穫できる大きさになった。これ夏の終わり に蒔いたのだけど、秋は意外に成長が遅い。

キャベツは寒冷紗をかけておいたにもかかわらず、一株完全にヨトウムシに食 べられてしまった。おおむね順調。まだキャベツっぽさはない。

さてRichard P. Draves Brian N.Bershad Richard F.Rashid Randall W.Dean. Using Continuations to Implement Thread Management and Communication in Operating Systems. SOSP, Octover 1991.、これで読み終わり。
5 関連する研究

言語のコミュニティは20年ほど継続についての研究をしてきた。Wardは継続を
μ計算よばれるメッセージの受け渡しの代数の基本要素として定義した[Ward
& Halstead 80]。そして、全ての制御の移行はその代数を使って表現できるこ
とを示した。並行実行と'first-class'の継続をサポートする関数型言語は後者
を使って前者を成功裏に実装した[Wand 80,Haynes & Friedman 84, Cooper &
Morrisett 90]。これらの試みは同じアドレス空間のユーザ層どうしのコンテキ
スト間の制御の移行に集中している。関数型言語はしばしば 関数呼び出しのス
タックを実装するのに、非連続なデータ構造を使う。そしてスタックを放棄す
る能率を部分的に減らす。(カーネルスレッドの放棄できる状態の多くの部分は
活動中の一番下のコールフレームの上の使われていないスタック領域である)
さらに、多くの関数型言語は関数オブジェクトが同一であることの評価を許し
ていないので、継続認識をやりにくくしている。Lampson[Lampson et al. 74]
は初期のMesa言語[Geschke et al.77]に対する、継続を基本にした一般的な制
御移行のインターフェースをに書いた。

とても制限されたLampsonのインターフェースが、後にTopaz[Schroeder &
Burrows 90]のアドレス空間をまたぐRPCの実装として現れた。TopazはDEC SRC
の実験的なマルチプロセッサワークステーション[Thacker et al 88]、
Fireflyのために設計されたOSである。そのインターフェースはアセンブリ言語
で実装され、スタック受け渡しはするが、継続認識を使わず、共有スタックも
使わない。(SRCのRPCは高度に最適化されていて、全てはレジスタに保存される
ので、スタックには重要なコンテキストない) Topazは、カーネルスレッドがブ
ロックする時、それが再スケジューリングされた時に即座にユーザ層で実行さ
れる場合にはスタックを放棄することを可能にしていた。Topazはセマフォと条
件変数をカーネルで実装していたので、これはユーザ層のイベントでブロック
するスレッドに重要な最適化である。この二つの最適化にもかかわらず、
Topazのカーネルでは、スレッドがプロセス形態のままスタックを持ったままブ
ロックする場面が多くあった。DEC SRCの5プロセッサ、96MBのFireflyでの最近
の計測では、886個のカーネルスレッドが、212個のカーネルスレッドを使用し
ている。カーネルの内部スレッド(28)、完了待ち(106)、ネットワークパッケッ
ト待ち(20)、例外処理の待ち(38)である。継続によってFireflyのアドレス空間
をまたぐRPCは向上しないと思うが、多数のカーネルスタックによって消費され
る、バス、キャッシュ、メモリは軽減されるだろう。例えばMachでは886の似た
ようなブロックしているカーネルスレッドがあったとしても、6スタックしか必
要としないだろう。Fireflyの5つのプロセッサそれぞれのと、特別なカーネル
スレッドのだ。

QuickSilver[Haskin et al.88]、V[Ceriton 88]、MS-DOS[Duncan 86]のような、
割込み形態で実装されているOSは継続と等価なものをそれだけで使う。例えば
Vカーネルでは、"finish_up"関数をスレッドの記述子に結びつけ、それがブロッ
ク後に再開するスレッドの算定を可能にしている。しかしながら、これらのシ
ステムでは、プロセス形態を安全網として使うことができないので、カーネル
内部の構造が複雑になってしまっている。例えば、カーネルの中で実行する場
合ページフォルトは一般的に許されない。そして、マルチプロセッサに必要な
カーネル層の単純なロックですら難しい[Cheriton 91]。

継続認識を一般的な最適化技法として使用、あるいは、ユーザ-カーネル境界の
行き来をユーザ層のアップリケーションによって作用されることができる継続
を基本とした制御移行として扱うようなプロセス形態と割込み形態を組み合わ
せたシステムは他に見つからなかった。

6 将来の研究と結論

Machにおける継続の研究は進行中である。目下、ユーザ層のスレッドパッケー
ジであるC-Threadsで継続を使う実験中だ。ユーザ層のスレッドで継続が使用で
きるようにして、スタックを放棄できるようにし、もし可能であれば継続認識
をするようにするつもりである。ユーザ層で自身でスケジュールし同期をする
ようなアプリケーションには、継続によってその多数のユーザ層のスレッドに
よる空間とオーバーヘッドの時間を軽減すると期待している。

当初、コンテキスト間の制御の移行の実装や記述をする機構としての継続の柔
軟性と力を認識していなかった。この研究の新規性は、継続を一般的なOSカー
ネルに適用できるようにしたことにある。継続を使って、可搬性のある新しい
最適化をすることができるようになり、他のOSにある、いくつかの最適化を一
つの抽象化を使って作り直すことができる。結果として、相当なシステム性能
の向上を図ることができた。

この論文で述べた方法論と技術は他のOSカーネルにも適用して、同じような結
果をもたらすと思う。Mach 3.0カーネルのソースを含む我々のシステムを
cs.cmu.eduの匿名ftpから手に入れて調べることを勧める。
さっそくマイOSに実装しはじめてます。マイOSはRAM2KBの状況もあるため、リ ソースの確保は全てユーザがすることとしている。カーネルは一切のリソース の確保をしない。スレッドは、その制御領域とスタックを連続領域でカーネル に渡すという仕様なので、まずはスタックを別体にできるようにインターフェー スを変えないといけない。そして、スレッドを放棄できるようにするという設 計ということは、スレッドを作成した時にスタックを確定できないので、これ は全ての場合を考え直して作り直さないといけない。結構難しい。


MCFAJ最終戦筑波、優勝しました!!!!!!!ポール・トゥ・ウィン。うれし過ぎる!



予選は8:15から。気温は13℃と十分暖かい。ロガーはというと、エンジンかけ たところで、液晶の表示が上下逆に。どうもノイズを拾って、それが反転して しまったようだ...。時折り、液晶の表示が更新されるものの、まったく実用に ならなかった。1'09.093でクラスポールポジション。
決勝は11:12から。MCFAJは寒い時の開催はウォーミングアップラップを二周に してくれるのがうれしい。スタート。大成功。はじめて藤巻選手に負けなかっ た(失敗したらしい)。1コーナーまでにラインを塞いでクラスホールショット。 125と混走なので、ちょっと難しい時があるのだけど、全てうまくいって、バッ クストレッチまで押さえれた。が、赤旗。...ショック過ぎる。こんな抜群なオー プニングラップがなしになってしまうなんて...。
2回目のスタートはやはり藤巻選手に負けてしまう。インフィールドきっちり つけて、最終で気合いでパス。いやらしく蛇行して、2周目、バックストレッチ で十分スリップに入られないアドバンテージ。そのままトップでフィニッシュ!
決勝のベストは1'08.258まで。


決勝が終わってもまだ午前中なので、もつ定食べて帰ってきました。

ロガーは途中でハングしてしまい、ファイルとしては残らなかったのだけど、
5 partitions:
#        size    offset     fstype [fsize bsize cpg/sgs]
 a:     90655     33329    SysVBFS                     # (Cyl.     33*-    122)
 c:     90657     33327     unused      0     0        # (Cyl.     33*-    122)
 d:    123984         0     unused      0     0        # (Cyl.      0 -    122)
 e:     33264        63      MSDOS                     # (Cyl.      0*-     33*)


 dd if=/dev/ld0d of=aaa skip=33332 bs=512 count=1024
で取れる分だけ引っぱりだしてきました。カシミール3Dに喰わせてみると...コー スインの部分は押して歩いてるだけあってよくとれてるね。やっぱりサンプリ ンング間隔が一秒では...。さらにデータ落ちがあるのでこれは使えないね...。
ちょっとLPC2388の処理が間にあわなくなってきてる感がある。



今日読んだところでは継続を使って書き直したMachの性能向上と、継続のいろ
いろな使い道を示しています。

例えば、Unixのシグナルの配送をこの文脈にしてみると... 例外や割り込みが 入るとトラップフレームにレジスタを保存する。(トラップフレームは例外、割 込みの際に退避するフレームで全レジスタを退避する。スイッチフレームとい うのはカーネル内から明示的にプロセスをスイッチする時のフレームなので、 caller-savedはいらない。)これはアドレス空間とトラップフレームの組で、カー ネルから戻るべき継続になる。ここでシグナルを配送するとなると、シグナル スタックと、シグナルトランポリンの関数ポインタ、そして送られるプロセス のアドレス空間が、シグナル配送の継続になる。ここで「継続」とするのは、 元の関数に戻らないから。シグナルトランポリンはシグナルコードと、戻るべ きプロセスの継続(これはトラップフレームあるいはスイッチフレームから構成 したものだ)を引数に呼ばれ、シグナルハンドラを実行した後に、プロセスの継 続を呼ぶ。という風になる。
Richard P. Draves Brian N.Bershad Richard F.Rashid Randall W.Dean.
Using Continuations to Implement Thread Management and Communication
in Operating Systems. SOSP, Octover 1991.


3 性能

この章では性能における継続の効果を調べる。空間に関しては、カーネルの中
の制御の移行のほぼ全てを継続を使うようにして、スタックなしでスレッドを
ブロックできるようになったことを示した。これは効果的にカーネルのスタッ
クをプロセッサ毎の資源とできる。時間に関しては、大くの制御移行を継続認
識を使うようにした。これはアドレス空間をまたぐ通信と、ユーザ層の例外処
理の遅延を軽減する。

3.1 実験環境

三つの版のMachカーネルを評価した: MK32, MK40とMach2.5。 MK32とMK40は
Mach 3.0の純粋カーネルで、Unixシステムコールインターフェースをカーネル
アドレス空間で実装していない。MK32は継続を使っていないが、アドレス空間
をまたぐRPCのオーバーヘッドを軽減する最適化[Draves 90]を含んでいる。
MK40は二章で述べた継続を使っている。Mach 2.5はBSD Unixインターフェース
をカーネル空間で実装した、合いの子カーネルで、MK32におけるRPCの最適化も、
継続も使っていない。

全てのカーネルはDECstation 3100(DS3100)とToshiba 5200/100で動く。
DS3100はMIPS R2000を基本としたワークステーションで、64KBの命令データ分
離キャッシュと4段の書き込みバッファをを持つ。それは16.67MHzで、キャッシュ
落ちや、書き込みストールがなければ、1サイクルごとに1命令を実行する。我々
のDS3100は16MBのメモリと250MBのHitachiのディスクドライブで構成されてい
た。Toshiba 5200はIntel 80386 20MHzを基本にしたラップトップで、32KBの命
令、データ統合キャッシュである。我々のToshiba 5200は8MBのメモリと100MB
のConnerディスクドライブから構成されていた。

Mach 3.0カーネルはUnixサーバへのRPCとして実装されたUnixシステムの環境で
試験した。さらにToshiba 5200ではMS-DOSエミュレーションも測定した。
MS-DOSエミュレータMS-DOSのシステムコールと特権命令から起きるフォルトを
ユーザ層の例外処理が掴まえる。その例外処理スレッドはエミュレートした
MS-DOSプログラムのアドレス空間で動く。

3.2 継続の使用の動的な頻度

継続の価値はどれだけの頻度でそれが使われるかによる。これを測定するため
に、Toshiba 5200でMK40カーネルでブロックする処理が継続を使う回数を計測
する3つのテストをした。一つ目のテストは短かいCのコンパイルベンチマーク。
二つ目はAFS(分散Andrewファイルシステム[Satyanarayanan et al.85]上で
Mach 3.0カーネルを作成。三つ目のテストはMS-DOSのビデオゲームである「ウィ
ングコマンダー」で測定した。小さいコンパイルと、MS-DOSはシングルユーザ
モードでテストした。カーネルの作成はマルチユーザモードでテストした。
AFSはネットワークサービスとユーザ層のファイルキャッシュマネージャが必要
だからだ。表1に結果をまとめた。DS3100 でも頻度は似ている。MS-DOSのゲー
ムはToshibaでしか動かないので例外。
 表によって99%の制御移行が継続を使い、スタック放棄を使っている。一番頻
度が高いのはメッセージの受信と例外処理である。他の処理は、ページフォル
ト、自発的な再スケージューリング[Balck 90a]、自発的でない横取り、そして、
カーネル内部のスレッドによるブロックである。残る、継続を使わずにブロッ
クする処理はカーネルのページフォルト、メモリ割当て、ロックの獲得で起こ
る。これらの状況で継続を作成することは難しいため、MK40ではプロセス形態
を使ってカーネルスタックをブロックの間保持したままとしている。
 表2はスタック渡しがほぼ全ての制御移行で起っていることを示している。さ
らにアドレス空間をまたぐRPCと例外処理で起こる、継続認識は、ブロック処理
の60%以上で起こる。

3.3 継続に起因する速度の向上

継続がアドレス空間をまたぐRPCと例外処理の実行時の性能を向上されることを
示す。RPCテストはアドレス空間をまたぐ"null"RPCの往復の時間を測定した。
例外処理のテストはスレッドの例外をユーザ層のサーバが処理する時間を測定
した。例外サーバはフォルトするスレッドと同じアドレス空間で動かした。フォ
ルトを起したスレッドの状態は調べず、変更もしないので、例外は出続ける。
MK40, MK32, Mach2.5に対して、この二つのテストを大量に反復した平均結果を
表3に示した。

RPC性能の向上

MK32のRPC経路はMach 2.5に比較して既にかなり最適化されていたので、向上の
余地は少ししかなかった。プロセス形態(スレッド毎にスタックをもつ)だが、
MK32はRPC転送の間の一般的なスケジューラのコードを回避した。その代わりに
直接、送信スレッドから受信スレッドにコンテキストスイッチするようにした。
対照的に、Mach 2.5はメッセージを待ち行列に入れ、一般的なスケジューラが
次に走る受信スレッドを決定していた。

以前の最適化にもかかわらず、MK40のRPCは14%速い。その向上は主に、コンテ
キストスイッチをスタック渡しによって取って代えたことによる(2)。表4がス
タック渡しとコンテキストスイッチの命令、読み込み、書き込みの数の差であ
る。十分にスタック渡しはコンテキストスイッチより効果的である。

(2) MK40のToshiba 5200のRPC遅延は、バグによって少々増加している。
Toshiba 5200のトラップ処理はカーネルに入る時にユーザのレジスタを、別の
機種依存のデータ構造でなく、スタックに保存する。結果として、機種依存の
スタック渡しの手続きは現在のスレッドの状態をスタックからコピーし、新し
いスレッドのスタックにコピーしなくてはならない。この修正によって
Toshiba 5200の時間は50usecほど向上すると予想している。

継続の実行時の犠牲

Machで継続を使うことにに関連する実行時の犠牲が少しある。表4にあるように、
カーネルに入るのと出るのにかかる時間がMK40はMK32較べて少し長い。これは
継続と、そのアーキテクチャの呼び出し規約の相互作用によるものである。
MK32では、カーネルのシステムコールの入口ではどのレジスタもスタックに保
存する必要がない。呼び出し元退避(caller-saved)のレジスタは既にユーザ層
のスタックに保存されていて、そして呼び出し先退避(callee-saved)のレジス
タはカーネル層のスタックにコンパイラがシステムコールの必要に応じて保存
する。これは暗黙的にプロセス形態であることを想定していて、呼び出し先退
避のレジスタは、'return'の際にスタックから元に戻される。

継続を使って、そしてスタックが放棄されると、呼び出し先退避のレジスタで
あっても、返る際に元に戻されなくなってしまう。('return'は決っして起こら
ないからだ)結果として、カーネルの入口で全ての呼び出し先退避のレジスタを
補助的な機種依存のデータ構造に保持し、カーネルの出口ではこれらを元に戻
さないといけなくなった。DS3100の例では、9つの呼び出し先退避のレジスタが
あり、表4の追加の犠牲になる。例外と割込みに関しては、カーネルの入口では
呼び出し先退避でなくとも、必ず全てのレジスタを(退避しなくてはならない。
これはMK32でも同じように必要である。なので呼び出し先退避のレジスタを保
存することの相対的な犠牲は減っている。

例外処理の向上

表3に示すように、例外処理はMK40は2,3倍MK32に較べて速い。RPCと違い、
MK32では例外処理は最適化されていなかった。結果としてMK40の例外処理は継
続の一番良い状況を示した。そして、OSのカーネルで一般的な枠組のように継
続を使う重要な点も示している。高速だが可搬性のあるアドレス空間をまたぐ
RPC構造にから、制御移行の一般的な枠組みを開発することになった。一度イン
ターフェースが決まれば、簡単にそれを例外処理の経路に適用することができ
た。3日程の作業で、例外処理の実行時の性能の向上が見れた。スタック放棄に
よる空間の削減も実感した。さらに、最適化は機種非依存に実装されているの
で、一度作業するだけである。カーネルの経路に継続を使う経験も同じである。
継続を性能に重大な経路に適用することができ、それは少ない努力の割に良い
結果をもたらしたと結論づけた。

3.4 継続に起因する空間の節約

継続によって効果的にカーネルのスタックをスレッド毎でなく、プロセッサ毎
の資源に変更できる。カーネルスレッドの数を24から43で変化する(我々がメー
ルを読む機械は通常100から200スレッドあるが)3つのテストプログラムのため
に、MK32では多くのカーネルタックがカーネル層スタックとしてある。MK40で
はカーネルスタックの数は平均で2.002である。99%以上の時間でたた二つのス
タックが使われる。一つは、現在走行中のスレッドのためで、もう一つは継続
とともにブロックしていない、内部のカーネルスレッドのためである。そのス
レッドは継続を使うには難しい制御の流れである。そのスタックは機械毎(プロ
セッサ毎ではない)の不変のオーバーヘッドを表現する。残りの0.002スタック
はいくつかの制御移行が継続を使わないことに起因する(表1の下の行を見よ)。
最悪の状況では、コンパイルテストとMS-DOSエミュレーションは3スタックを使
い、カーネル作成では6使う。しかし安定した状況では2カーネルスタックだけ
が使われる。

表5: DS3100におけるスレッド管理のオーバーヘッド(byte)

継続に起因する節約を評価する別の方法としては、それぞれのスレッドが消費
するカーネルメモリの平均を考察する。表5はDS3100上のMK32とMK40のスレッド
毎のデータ構造のサイズを示している。この機械では継続によって平均でスレッ
ドに使う量を85%削減している。Toshibaでは同程度の削減がある。

 カーネルスレッドによって要求される空間は機種依存と機種非依存の状態と、
スタックがある。MK40では機種非依存状態は継続(4バイトの関数ポインタ)と
28バイトのスクラッチ領域の空間の分、MK32より大きい。機種依存スレッド状
況は例えば、カーネルに突入した時のユーザレジスタ等を含む。MK32において
はスレッドの機種依存状態はそのスレッドの専用スタックに保存される。MK40
は専用のカーネルスタックを持たないので、機種依存状態は別のデータ構造に
保存される。

スタックによって消費される空間はスタックそれ自体(4KB)と、カーネルアドレ
ス空間のスタックを維持する仮想メモリシステムによって使われる全てのデー
タ構造を含む。MK32ではカーネルスタックはページ可能で、なので追加の116バ
イト(4)のVMデータ構造を必要とした。MK40ではカーネルスタックが少ないので、
スタックをページ可能とする必要がなく、VMシステムの空間を節約する。さら
にMK40はタックを物理メモリから割り合てできるアーキテクチャであれば、
TLBエントリの節約になる。(他の目的に使える)

(4) カーネルスタックがページ可能だとしても、それらのスタックがメモリに
残っているのに十分な頻度で走る。MK32での例では、90%のカーネルスタックは
ステムがページを別のメモリにしたとしてもメモリに残っていた。休眠してい
るスレッドのスタックが実際にページアウトされている時、追加的なスレッド
毎に220バイトのVMに関係するデータ構造が必要なので、ページアウトされたス
タックは336バイトを消費する。

4. 以前の最適化の継続による一般化


継続は他のOSに見つかる制御移行の最適化に機種非依存な枠組を提供する。例
として、Machの継続を基本にしたRPCと、軽量遠隔手続き呼び出し
(LRPC)[Bershad et al. 90]の制御移行の様子を比較することができる。LRPCは
アドレス空間をまたぐRPC一般に対しての高性能プロセス間通信として設計され
た。LRPCの一部は高性能で、それはスレッドがアドレス空間の境界を越えるこ
とができるということによっていた。呼び出し元のアドレス空間のスレドはカー
ネルにトラップされ、それはサーバのアドレス空間に入り、そこでサーバのコー
ドを即座に実行し始める。戻る時は呼び出し元のスレッドはサーバのアドレス
空間からカーネルにトラップして戻り、次に続くトラップの命令で呼び出し元
のアドレス空間に戻る。シングルスレッドやり方の一番の性能的な優位性は、
全ての作業はシングルスレッドのコンテキストで終了するため、スケジューリ
ングとメッセージの待ち行列がLRPCの経路からまったくなくすことができるこ
とだ。

Machの継続を基本としたRPCはLRPCのような性能的な優位性の多くを達成するこ
とができた。:待ち行列なし、スケジューリングなし、そしてカーネルスタック
を呼び出し元と呼び出し先で共有する。実際、カーネルのロックを通る制御の
流れは二つのシステムで似ている。: 制御は、あるアドレス空間からカーネル
の手続きに入り、そして同じ手続きをもう一方にもして、出る。さらに、継続
を基本にしたRPCはクライアントとサーバの論理的な分離も支える。スレッドは
それらのアドレス空間の中で固定されたままで、スレッドがアドレス空間の間
を移動する時に起る保護、デバッグ、ごみ集めの問題の多くを取り除いた。
[Bershad 90]

継続の自然な拡張によって、完全にLRPC転送規約を真似することができた。
Machのスレッドがカーネルにトラップされる時、このトラップが起きたユーザ
層のコンテキストに制御を戻すための継続を作成する。IPCインターフェースを
スレッドがシステムコールから返るためのユーザ層の継続を上書きできるよう
にする拡張する実験をしている(5)。この拡張はサーバスレッドがレジスタを退
避/復旧する手間を取り除き、サーバスレッドはRPC要求の待ちでブロックして
いる間、ユーザ層のスタックを放棄することが可能になる。

カーネルの外部に戻る時の継続を一つではなくカーネルが入った時に活性であっ
た継続のどれかに戻ることができるようにすることで、継続は制御移行の機構
を豊富に集めたものをを一般的なやり方として実装することに使える。例えば、
上への呼びだし(upcall)はx-kernel[Hutchinson et al.89]に必要であり、
Scheduler Activations[ANderson et al. 91]は、基本「ユーザ層に戻る」継続
のブロックしたスレッドのプールをカーネルにとっておくことで実装できる。
上への呼びだしを実行するため、基本の継続はユーザ層の特定したアドレスへ
とカーネルの外に制御を移す継続によって取り替えられる。非同期I/O [Levy
& Eckhouse 89]は似たように作用する。非同期I/Oをスケジューリングすると、
スレッドはI/Oが完了した時に呼ばれる継続をカーネルに、提供する。

(5)この拡張はMachのメインリリースの一部ではない。



Richard P. Draves Brian N.Bershad Richard F.Rashid Randall W.Dean.
Using Continuations to Implement Thread Management and Communication
in Operating Systems. SOSP, Octover 1991.

続き。

スタックを放棄するのはこれは使いどころがある。要するに、カーネル スタックの一番上くらいしか使ってなくて、毎回ブロックする時には巻き上る ようなスレッドがたくさんあるなら、それらはブロックしている間にスタック の内容を保持している必要がないから共有で使ってしまおうという戦略だ。し かし、RPCの受信者から送信者にスタックを移行してデータを渡してしまおうと いうのはどうだ。これはマイクロカーネルだと、アドレス空間をまたいだデー タ転送が大量になる。サーバをユーザ空間で動かしているから。そのボトルネッ クを解消する狙いなのだろうけど、やり過ぎかな。この論文でも指摘している ように継続というのはとても使いどころが難しい。マイOSでも継続はファイバ という形で実装しているけれど、これも当初いろいろ遊んでみて、よっぽど縛 りをきつくしない限りデバッグ不可能なものになると感じて、結構縛りをきつ くしてある。それでもこの前のGPSパーサでは、結構はまった。

2.4 アドレス空間をまたいだRPCで継続を使う

MachにおけるOSサービスはアドレス空間をまたぐRPCを用いてユーザ層のサーバ
によって使われる。継続を使ってカーネルのRPC経路を再構成して、性能を向上
させた。

図2はMach RPCの半分を呼び出しの高速な経路を示している。単一のシステムコー
ルmach_msgはRPCの送信と受信の段階を一つの操作に統合する。依頼スレッドは
mach_msg をRPC要求メッセージをサーバにに送り、返信メッセージを受信する
ために使う。サーバスレッドはmach_msgを返信メッセージを依頼スレッドに送
り、そして次の要求メッセージを受信するのに使う。両方の場合で送信スレッ
ドは受信スレッドを起こし、そしてメッセージが届くまで自分自身をブロック
する。

送信スレッドはカーネルに入り、そしていずれユーザ層に戻る時の継続を作成
する。そして、受信することのできるスレッドを探す。もしそのようなスレッ
ドがみつからなかったら、遅い経路を使い、メッセージは待ち行列に入る。も
し送信者がスレッドを見つけたら、スタック渡しを受信者に対して行なう。こ
れは、送信スレッドをmach_msg_continueで、継続を共なったブロックされた状
態にしておき、そして、カーネルスタックを持たない。受け渡しは現在走行中
のスレッドから受信スレッドに変更するが、すぐには受信スレッドの継続を呼
ばない。その代わりに受信スレッドは送信者のmach_msgシステムコールのコン
テキストで走る。ここでスレッドは自分自身の継続をそれを使う前に確認する。

もしそれがmach_msg_continueであれば、受信者は同じ経路でブロックしたので、
mach_msgは高速RPC経路で完成し、カーネルの外に受信者のコンテキストで移行
する。それ以外に、mach_msgは受信者の保持された継続をカーネル内のメッセー
ジの処理のために呼びだす。XXXこれは起こり得る。例えば、受信者が最大量の
ような珍しいオプションや制限を特定した時。これは余分な処理が全ての受信
に対して必要で、なので受信者はさらに仕事をするために別の継続でブロック
しているかもしれない。実際のところ、余分な仕事はまれに必要になるだけで、
ほとんどのスレッドはmach_msg_continueと共にブロックする。

 継続を使用することによってRPC中の空間と時間のオーバーヘッドが軽減する。
Machの中のほとんど全てのスレッドは、通常メッセージを待っていているので、
それらがブロックしている間、そのカーネルスタックを放棄している。呼出し
の遅延は受渡しによって、ブロックしているスレッドはそのまだ活動中の関数
が再開するスレッドのコンテキストを呼ぶことができるので軽減される。XXX
再開するスレッドは以前に退避した継続を使って制御の以降を完成させてもい
いし、あるいは継承されたコンテキストの情報を使って、別の高速な経路を使っ
てもいい。この状況で、送信者と受信者のメッセージ処理はお互いに最適化す
ることができる。例えば、高速RPC経路は待ち行列の操作、何度もの同期、何度
も例外状況を調べるためにメッセージを解析することを避ける。その替わりに
メッセージは暗黙的に共通のスタックの上にあり、送信者は共有データ構造を
ロックし、受信者はそのロックを外す。そして送信者だけが、受信者でなく、
メッセージの例外的な状況(大きいデータのような)を確認する。

2.5 他の種類の制御の移行に継続を使う

RPC経路が一番繁雑に使われるけれど、いくつかの他のカーネル経路についても
継続を使用した。これらの経路は三つで、例外処理、スレッドプリエンプショ
ン、ユーザ層ページフォルトで、それぞれ異なるやり方で継続を適用すること
ができることを示そう。

 + 例外処理
 Machにおいて、全てのスレッドは例外サーバを持っていて、それはカーネルが
RPCとともにスレッドが例外を起こした時には呼ぶ。例えば、スレッドが読み込
み専用ページに書き込もうとして例外が起きた(それは共有の仮想メモリ上で動
いているため[Appel & li 91])、あるいは、特権命令を実行した(Machは
MS-DOS、Macintosh OSのような別のOSをエミュレートするため[Rose & Hacker
85])。例外サーバはカーネルカラの要求メッセージを受信して、それを処理し
ようと試み、ちょうど受けとった例外の状況の返答メッセージをカーネルに送
り返す。ほとんどの場合、サーバは例外を処理することができ、その返答メッ
セージによってカーネルが即座にフォルトを起こしたユーザ空間のスレッドを
再開する。
ユーザからユーザのRPCと違うにもかかわらず、例外サーバとやりとりでは、カー
ネルは通信の終点になる。継続を使って例外処理の遅延を軽減した。例外メッ
セージを送信するのに通常の経路に入る前に、フォルトを起こしたスレッドは
ここではカーネルモードで実行されているが、サーバスレッドがメッセージを
待っている状態かどうかを調べる(継続、mach_msg_continueを使って)。もしフォ
ルトしたスレッドがサーバを見つけたら、要求メッセージを作るのを延期して、
即座にサーバスレッドに対してスタック受け渡しをして、フォルトの情報を共
有されたスタックで直接渡す。これはフォルトしたスレッドがメッセージを送
信し、サーバがこれを受けとるようにした時のコピー、解析、待ち行列を操作
することを回避する。もし、mach_msg_continueで待っているサーバがない時は
遅い経路が使われる。

 + プリエンプティブスケジュール
スレッドの横取りは時計割込み中、もし現在のスレッドの実行単位が終了した
ら行なわれる。もし割込まれたスレッドがユーザ層であれば、割込まれてから
積まれたスタックは重要でなくなる。なぜならスレッドは単純にユーザ層の場
所から再開されるからだ。なので割り込まれたスレッドはユーザ層へのスレッ
ドに戻る継続を共なってブロックする。
 横取りへ継続を使うことが意味することは、最も走ることができて、しかし実
際には走っていないスレッドはカーネルスタックを必要としないということだ。
それはまた再スケジュールの遅延を少し軽減する。というのは、横取りされた
スレッドは再スケジュールされた後にカーネルスタックを巻き戻す必要がない
からだ。

 + ユーザ層ページフォルト
スレッドがユーザ空間の用意されていないページでフォルトした鴇、カーネル
が空いている物理ベージを見つけてそこにデータを積むまでブロックしないと
いけない。カーネルのフォルト処理はスレッドを、新しいページをマップして、
そしてユーザ層のスレッドを再開する継続と共にブロックする。フォルトした
スレッドのスタックの消費を軽減し、ほとんどメモリが残っていない時のカー
ネルのメモリの消費を簡便に軽減する。
 この最適化はユーザ層ページフォルトにのみ適用できる。カーネルでフォルト
した場合、そのカーネルの状態とスタックは保存される。カーネル層のページ
フォルトは継続を適用することがとても難しい例で、それは一般的にスレッド
はカーネル内の実行中のどこでもフォルトするからだ。この難しさのため、こ
の場合については、プロセス形態に頼ることにした。

継続はスレッドがユーザ層から自発的にそのプロセッサを放棄する時や、カー
ネルの内部スレッドが作業を待つためにブロックする時にも使われる。最初の
場合では、保存するカーネルの状態はない(横取りの時と同じように)。二番目
の場合では、2.2章で述べた末尾再帰を実装するのに継続を使用できる。

2.6 可搬性のあるOSカーネルでの継続の実装

stack_attach(thread, stack, cont)
  	switch_contextがスレッドを再開し、制御を用意された継続の関数に
	前に走っていたスレッドを引数として渡し、制御を移行するように、
	機種非依存な継続を機種依存の変形し、割当てられたカーネルスタッ
	クに積み、スタックを初期化する。
	
stack_detach(thread)
	スレッドのカーネルスタックを取り外して返す。

stack_handoff(new-thread)
	スタック受渡しをする。つまり現在のスレッドのカーネルスレッドを
	新しいスレッドに移動する。stack_handoffは必要であればアドレス空
	間を変更する。stack_handoffは新しいスレッドとして返る。

call_continuation(cont)
	現在のカーネルスタックポインタを初期化して、提供された継続を呼
	ぶ。この関数は、長く継続の呼び出し続く時にスタックが溢れるのを
	防ぐ。

switch_context(cont, new-thread)
	新しいスレッドをその保存されたカーネルスタックで再開する。もし
	必要ならアドレス空間を変更する。もし、現在のスレッドの継続が提
	供されたらswitch_contextはレジスタを保存せず、返らない。そうで
	なければ、switch_contextは現在のスレッドのレジスタとスタックを
	保存し、呼び出し元のスレッドが再スケジュールされたら、以前走っ
	ていたスレッドに返る。

thread_syscall_return(return-value)
	ユーザ空間からのシステムコールに返り値を特定して現在のスレッド
	のユーザシステムコールの継続を呼ぶ。(低層の機種依存トラップが
	システムコールの継続を作成する)

thread_exception_return()
	ユーザ空間から例外あるいはページフォルトしたスレッドに戻るために
	現在のスレッドの例外継続を呼ぶ。(低層の機種依存トラップが例外の
	継続を作成する)	

図3:機種依存の制御移行関数のカーネルインターフェース


thread_handoff(cont, new_thread) {
	old_thread = current_thread();
	old_thread->cont = cont;

	/* stack_handoff は current_thread()を変更する */
	stack_handoff(new_thread)
	/* ここで current_thread() == new_thread */

	/* スケージュリング状態を更新 */
	old_thread->state = WAITING;
	new_thread->state = RUNNABLE;
}

thread_continue(old_thread) {
	new_thread = current_thread();
	thread_dispatch(old_thread);
	(*new_thread->cont)();
	/*NOTREACHED*/
}

thread_dispatch(old_thread) {
	if (old_thread->cont) {
		stack = stack_detach(old_thread);
		/* スタックをプールに返す */
		stack_free(stack);
	}
	if (old_thread->state == RUNNABLE)	
		/* old_threadをrun queueに返す */
		thread_setrun(old_thread);
}

thraed_block(cont) {
	/* 現在のスレッドを止める */
	old_thread = current_thread();
	
	/* ready queueから次に走るスレッドを選択 */
	new_thread = thread_select();
	
	if (new_thread->cont) {
		if (cont) {
		   /* stack_handoffはcurrent_thread()を変更する */
		   stack_handoff(new_thread);
		   /* ここでcurrent_thread() == new_thread */
		   
		   old_thread->cont = cont;
		   if (old_thread->state == RUNNABLE)
			/* old_threadをready queue に返す */
			thread_setrun(old_thread);
		   call_continuation(new_thread->cont);
		   /*NOTREACHED*/
		} else {
		   /* 新しいスタックを作成 */
		   stack = stack_allocate();
		   stack_attach(new_thread, stack, thread_continue);
		}
	}
	
	thread_dispatch(switch_context(cont, newthread));
}

図4:制御移行インターフェースの使用


Mach 3.0カーネルはあらゆるプロセッサで動く。この可搬性はカーネルを機種
非依存部分、これはMachのカーネルインターフェースであり、機種依存の部分
は機械を管理する。機種非依存の部分は、スケージュリング、プロセス間通信、
仮想記憶を管理する。機種依存の部分は下層の機械的なトラップ、例外やMMUを
実装し、新しくスタックと継続の内部インターフェースを提供する。

新しいインターフェースは機種非依存のスレッド管理とIPC部分が、アドレス空
間を変更したり、スタックとスレッドの関係を管理したり、継続を生成、呼び
出すことを可能にする。その操作は図3に表にした。インターフェースはブロッ
クしたスレッドの継続に対して確認をしない。それはカーネルの機種非依存な
スレッドのデータ構造として保存されているので、カーネルモードで走るスレッ
ドは直接調べることができる。

図3のルーチンは高レベルのスレッド管理の操作をする操作の基本部分である。
図4に、いくつかのこれらの操作と、インターフェースがカーネルの中でどのよ
うに使われるかを紹介する。thread_handoffは特定されたスレッドに対して制
御を与える。それは、スタック受渡しをして、スレッドのスケジューリングの
情報を、古いスレッドはブロックされ、新しいスレッドが走っているように更
新する。関数は新しいスレッドの制御として返るけれど、新しいスレッドの継
続は呼ばない。これによって、thread_handoffの呼び出し元は継続認識(RPCや
例外処理の経路のような)をする機会がある。対称的にthread_blockは実行可能
なスレッドを選ぶ。もし、新しいスレッドが継続を持っていて、thread_block
の呼び出し元が継続を提供していたら、より効果的なstack_handoffの経路を使
う。そうでなかったら、witch_contextを呼ばなくてはならなくて、これはスタッ
クを変更する。

thread_blockの実装はswitch_context,stack_attach,stack_detachの相互作用
を説明している。thread_blockは古いスレッドのスタックの上で実行している
間は、古いスレッドのスタックを外すことも開放することもできないし、他の
プロセッサが見つかるかもしれない走行待ち行列にこの古いスレッドを置くこ
ともできない。したがって、その実装の最初のswitch_cotextは新しいスレッド
のスタックに変更し、新しいスレッドはthread_dispatchを使って古いスレッド
を処分する。もし新しいスレッドがスタックを持ってなかったら、
stack_attachはそれを初期化して、thread_continueを実行する。それは新しい
スレッドが古いスレッドを処分した後に呼ばれる継続を呼ぶ。

2.7 マイクロカーネル(原文Kernelized)の優位さ

Machにおける継続の有効さはMachがマイクロカーネルのOSだとい事実に起因す
ると思う。それは少ないインターフェースを提供し、いくつかの抽象概念だけ
を実装している。結果として、Machカーネルの中でスレッドがブロックするの
は小数の場所であり、実際にスレッドがそこでブロックするのいはさらに少な
い。Mach 3.0カーネルでスレッドがブロックし得る場所は大体60の場所がるけ
れど、99%以上のブロックは6つの場所だけである。期待されたように、この再
構成をこれらの「熱い場所」に焦点をあてた。カーネルの中の経路でまだ継続
が使用されていない場所はあるけれど、それらはまれな経路であり、それらの
性能への影響は無視できる範囲だ。
 対照的に、Mach 2.5のようなモノリシックOSに、継続を使うことはより難しい
ことだと思う。あのシステムではBSD Unixインターフェースを全部カーネルの
アドレス空間で実装していた。大くのスレッドはUnix互換層の深い位置でブロッ
クする。このようなスレッドに対して継続を作成するのは、大量の状態がその
カーネルスタックに乗った状態であるため、難しい。さらに、Mach 2.5では
180以上の場所でスレッドはブロックする可能性があり、「熱い場所」もない。
これらの理由から、少ない場所で継続を使うことができて、それが全体的な空
間と時間の軽減となったと思う。

2.8 ソフトウェア工学との関係

実際の継続の懸念はそれを使い過ぎることにある。割込み形態のカーネルが導
入で記述した問題に苦しんで終わってしまったことを避けるために、継続は思
慮深く適用されなければならない。この継続の使い方の鍵は、プロセス形態の
上でほとんどのカーネルの作業が同じように作業(ブロック)するというところ
だ。結果として大部分のシステムは、割込みモデルにするより、安定している。
継続を使った経路のコードは、呼び出し元が確実であることを信用しないとい
けないのでそういう意味では壊れやすい(例えば、thread_syscall_returnを使
うコードは、これ用に実装されたシステムコールでないといけない)。しかし、
システムの性能の向上の高く抽象化された方法を適用して、妥当な譲歩だと思
う。XXX

2.9 要約

継続はOSの抽象化の'first-class object'を進めることによって得られる一貫
性とその作用によって表現される。'first-class object'とはカーネルによっ
て操作されるものである。この意味で、継続はMachのpmapの抽象化[Rashid et
al. 87]に似ている。pmapは仮想から物理のメモリのアドレスマップを反映す
る'first-class object'である。メモリマップを抽象化し、それを機種依存の
実装(ページテーブルやセグメントレジスタ)と分離することで、pmapインター
フェースは可搬性があり、本来できなかった方法で最適化[Young et al. 87]す
ることができた。カーネルの'first-class'の抽象化としての継続は似たような
結果をあげた。


今度は
Richard P. Draves Brian N.Bershad Richard F.Rashid Randall W.Dean.
Using Continuations to Implement Thread Management and Communication
in Operating Systems. SOSP, Octover 1991.
を読みはじめてみました。これはなにか示唆がありそうだと思って以前プリン
トアウトしておいたんだ。

以下、日本語の羅列。途中まで。これははやく実装してみたい欲に狩られるけ れど一応最後まで読んで見通しをつけよう。マイOSにもはやくユーザ空間で動 くプロセスが欲しいとこだ。
OSのスレッド制御と通信の実装に継続を使う

Richard P. Draves Brian N.Bershad Richard F.Rashid Randall W.Dean
Oct 1991

要約

 我々は。内部のスレッドとプロセス間通信の枠組を継続をその制御の移行の基
本として使うように再設計することによって、Mach3.0 OSの性能を向上させた。
 以前の版のMach 3.0に較べて、新しいシステムはスレッドあたり85%の少ない
空間を消費する。アドレス空間をまたがる、遠隔手続き呼び出しでは14%早くな
る。例外処理は60%速くなる。
 システム性能の向上に加えて、継続をOSに共通な制御移行の最適化として一般
化し、これらの最適化を一つの実装方法論の視点から作り直した。この論文で
はMach OSで継続を使用するにあたっての我々の経験について記述する。

1. 導入

 継続を実行コンテキスト間の制御移行の基底として使うように、Mach 3.0 OS
を再設計することで大幅な性能の向上を達成した。我々のシステムでカーネル
内でスレッドがブロックされるのは二つの方法がある。一つは、レジスタ状態
とスタックを保存し、その状態を元に戻すことで実行を再開する。もう一つは、
継続としての再開(スレッドが走る時に実行されるべき関数)を指定する。継続
としてスレッドをブロックすることを許すことによって、カーネルプログラマ
は、スレッドの管理の時間と空間を押さえることができる。継続を使うことに
よって、スレッドはそのスタックをブロックされている間、捨てることができ
る。その結果カーネルの中のスレッドの空間を押さえることができる。

 継続によってまたスレッドはブロックされている間の実行状態の高レベルの表
現をすることができ、その状態を調べて、作用することができるので、制御移
行のオーバーヘッドを軽減することができる。

 継続を使うことが難しい場合には、継続によってスレッドをブロックしないこ
とによって、カーネルプログラマは伝統的な様式の並行処理プログラミングが
できる。

 継続によってMachカーネルの記憶領域の必要量を減らすことができ、実行時の
性能も向上した。以前に較べて、例えばDECstation 3100上の最適化版のMach
3.0はスレッド毎に資源を持つのではなくプロセッサ毎にカーネルスタックを持
つスレッドは85%の空間で済む。

 アドレス空間をまたぐ遠隔手続き呼びだし(RPC)は14%速い。例外処理の性能は
UNIXではないOSのエミュレーションに決定的で、2倍から3倍で性能が向上した。
さらに、継続によって一つの実装方法論の見地から多くの制御移行の最適化が
可能になった。

 この論文で述べるシステムはカーネギーメロンや他の場所の研究者が日常的に
使用している。マルチスレッドプログラミングと分散コンピューティングをサ
ポートするのにスレッドとプロセス間通信に頼ったMachのようなOSのカーネル
に対して、この制御移行を管理するのに継続を使う技術は似たような結果をも
たらすと信じている。

1.1 OSカーネル内での制御の流れの管理。

過去において、OSはカーネル内の実行形態は二つのうちのどちらかによってき
た。プロセス形態と割り込み形態だ。プロセス形態では

カーネルのアドレス空間はシステム内の全てのスレッドのために一つのスタッ
クを持っている。スレッドが、システムコールやフォルトによってカーネルに
トラップされると実行状態を保持するために専用のカーネルスタックを使う。
このやり方では、スレッドはブロックされている間その全ての状態を保持して
あるので、カーネル内の実行中はいつでも、そのスケージュリングをすること
ができる。UNIX[Ritchie & Thompson 78]はこのプロセス形態によるOSの例だ。

プロセス形態とは対照的に、割り込み形態はシステムコールとフォルトを割り
込みのように扱う。:全てのカーネル内実行は、カーネルアドレス空間内のプロ
セッサ毎の一つのスタックを使う。カーネル内でブロックしたスレッドは、は
じめにその実行コンテキストに対する情報を退避しなければならない。この退
避された情報は後にブロックされたスレッドが適切な状態で再開するために使
われる。QuickSilver[Haskin et al. 88]とV[Cheriton 88]はこの割り込み形態
によるOSの例だ。

プロセス形態の一番の利点はプログラムが簡単なことだ。カーネルスレッドは
ブロックしたり、ページング可能な記憶領域を参照するのに「特別な」制限が
ない。残念ながらプロセス形態には性能に関する問題が二つある。一つ目はそ
れぞれのスレッドがカーネルアドレス空間のスタックを必要とするため、プロ
セス形態は大量の記憶領域を消費してしまう。二つ目は、カーネルスタックが
ブロックされたスレッドの状態が、リターンアドレスや退避されたレジスタ、
自動変数を機械レベルで反映しているので、その状態を評価するのと、スレッ
ドの移行管理の遅延を減らすための最適化をすることが難しい。

1.2 Machにおける制御移行の歴史の概要

初期の版のMachカーネルは二つの理由によりプロセス形態によっていた。一つ
はMachカーネルはAccent[Rachid & Robertson 81]の後に作られ、それはプロセ
ス形態を使っていた。Machの全体的な設計と実装は直接Accentに由来していた
のでAccentのプロセス形態を使うのが自然だった。二つ目は初期の版のMachは
UNIX互換層をカーネルモードで実行していた。この層はプロセス形態である
BSD Unix[Leffler et al.89]を使って実装されていた。これらのコードがカー
ネル内に残っているため異なる形態を使うことは大変な努力が必要だ。

Machの発展につれて、プロセス形態は適切でないことが明らかになってきた。
Accentではスレッド管理の基本操作はマイクロコードによって行なわれ、そし
てそれらは機械のCPU速度にくらべて十分に速かった。対象的にMachは広い範囲
のアーキテクチャで動くように設計された。なのでマイクロコードがスレッド
管理の手間を軽減するという仮定はできなかった。さらにAccentと違って、
Machは一つのアドレス空間に対して多数のスレッドの管理をサポートする。こ
れは結果として少量のプログラムが大量のカーネルスレッドを使うことになっ
た。プロセス形態ではこれらのスレッドはそれぞれ4KBのスタックをカーネルア
ドレス空間にもつ。最終的にUnix互換コードはカーネルからユーザ空間に移動
[Golub et al.90]したので、純粋にUnix互換をサポートするためのプロセス形
態を使う必要はもはやなくなった

プロセス形態の再検討は小さいシステムや、速いRISC単一プロセッサ、マルチ
プロセッサへの効果的な実装の要求によって加速した。今日一般に見つかるワー
クステーションは32MB、それ以上のメモリを積んでいるけれど、我々はMachを
小さいPC、ラップトップやノートブックで効率的に走らせたいと欲した。それ
らは大抵8MB以下程度のメモリである。結果としてカーネルのメモリ消費量の低
減が重要になった。さらに、プロセッサの速度の増大とともに、キャッシュと
TLBのミスの相対的なコストも増した。もしカーネルスタックをスレッド毎では
なくプロセッサ毎とできれば、カーネルスタックを参照する時のキャッシュ、
TLBミスの数が減るだろうと期待した。さらに、Machはキャッシュ一貫性マルチ
プロセッサの上でも走る。そのような機械ではプロセッサ毎のデータ構造とし
て扱うと最も効率的だ。[Anderson et al. 89]

あるスレッドから他のスレッドに制御が移行する遅延時間についても関与した。
我々の高速プロセス間通信(IPC)システム[Draves 90, Bershad 90]のデザイン
の経験では低い遅延時間の制御移行についていくつかの重要な教えがあった。
我々はこの知恵を一般的な方法として、カーネル層での制御移行に適用したかっ
た。とりわけOSは多くの部分はユーザ層で実装されているので、アドレス空間
間のRPCに低遅延は重要とはいえ、それ以外例外処理のような経路も重要になっ
てきた。高速例外処理は、例えば、仮想メモリの基本操作をユーザ層から使う
[Appel & Li 91]場合や、OSの上で他のOSをエミュレートする時に必要になって
くる。さらに、Machは広範囲のプロセッサ上で動くので一般的な解決方を機種
非依存に適用できることが重要だ。その場限りのアセンブリ言語による性能の
向上策は許容できない。

1.3 いくつかの適切でない解決策

非常に初期、カーネル内の大量のスレッドを管理に関連する大きさと速度のの
問題を処置をする必要があることに気付いた。最初の解決策として、ユーザ層
のスレッド、C-Threads[Cooper & Daves 88]を、ユーザ層のスレッドをカーネ
ル層のスレッドの上に多重化するように変更した[Golub et al 90]。一つのカー
ネル層スレッドが同じアドレス空間を同じくする多数のユーザ層スレッドをサ
ポートすることで、大量のスレッドのために必要とされるカーネル空間を低減
しようという狙いだ。さらに他[Anderson et al. 91, Marsh et al.91]で注意
したように、ユーザ層のスレッドは、同じアドレス空間内でのスレッドスイッ
チの遅延を低減できる。

我々のC-Threadsの使用は緩和したが、プロセス形態の空間的問題は解決されな
かった。第一に、カーネルスレッドはアドレス空間の間で共有できないので、
アドレス空間毎に少くとも一つのカーネル層のスレッドがまだ必要だ。Machを
このようにしたことで、重大な保護の問題が発生した。それぞれのエミュレー
トされたUnixプログラムは一つのカーネルスレッドをUnix「プロセス」として
使う。そして、C-Threadのマルチスレッドプログラムは少なくとも一つのカー
ネルスレッドをそれを「仮想プロセッサ」として使う。

ユーザ層スレッドをカーネルのプロセスの上に置く形態の二番目の問題はカー
ネルの中で実行中にブロックされたスレッドはカーネルスタックを消費したま
まだということだ。結果として平均して、C-Thredadはシステム中のカーネル層
スレッドの数の低減は1/2程度にしかならないことに気付いた。

ユーザ層スレッドはアプリケーションとカーネルの関わりが少ない場合、性能
の向上に優れているマルチプロセッサプログラムはカーネルから離れている傾
向にあるが、サーバのようなマルチスレッドプログラムの我々の経験では、そ
れらは例えばIPC、ページフォルト、例外によってカーネルに集中する。なので、
ユーザ層スレッドは部分的にマルチスレッドプログラムの空間要求に手段を講
じるが、それだけでは十分ではない。カーネルの問題を、カーネル側の解決方
が必要だった。

QuickSilverやRIG[Ball et al. 76]のようなシステムの経験により、割込み形
態を考慮することになったが、最終的にはMachカーネルには不適切と結論づけ
た。割り込み形態は、スレッドがそれ専用のカーネルスタックを持たないため、
カーネルの使う資源が少ないのが魅力だ。他方で、この形態はプログラムが難
しい。というのは、ブロックする可能性のある操作は全て、状態を退避/復帰す
るための特別の目的のコードを必要とするからだ。

さらにこのコードはモジュールの境界を越えて覗かないといけないかもしれな
い、ブロックするモジュールはその状態をその呼び出し元のために保存するこ
とができるため。これはシステムの維持管理性の障害となる。Machはマルチプ
ロセッサ上で動く(すなわち内部でロックが使われる)、そして仮想メモリをサ
ポートする(つまりカーネルはページフォルトできる)ので割込み形態では管理
不能と感じ、だから許容できない。

1.4 継続による再構成

スレッドがブロックする際にプロセス形態でも割込み形態でも使えるように
Machカーネルを再構成した。プロセス形態を使ってスレッドがブロックする時
はその現在の実行状態はスタックに記録される。ブロックされたスレッドはコ
ンテキストスイッチによって再開される。スレッドが割込み形態を使ってブロッ
クする時は、実行状態をそれが再開されるべき場所の補助的なデータ構造に記
録する。それは継続[Milne & Strachey 76]と呼ばれる。ブロックしたスレッド
はとっておいた継続を呼び出すことで再開される。この新しいやり方は、プロ
セス形態、割込み形態をそれひとつで使うより、いくつかの利点がある。

+ プロセス形態の「使い易い」という利点がある。スレッドはそのスタックの
  内容をそのままでカーネル内のどんな時でもブロックしてもいい。これは割
  込み形態が便利ではない状況の時に重要だ。セマフォでブロックする時スレッ
  ドが深くネストした関数の中である時や、カーネルで実行中にページフォル
  トをする場合だ。

+ 割込み形態の性能のよさがある。 カーネルのコンテキストがない、あるいは
  ほとんどないスレッドの時、言ってみればそれが他のスレッドからのメッセー
  ジを受信するために待っている時、あるいは、次の命令がユーザ空間で実行
  すべき時、そのカーネルスタックは全部放棄していい。さらに、継続は機種
  非依存なインターフェースで使われるので継続を実行時に調べて、そしてそ
  れを使うのを回避するすることがしばしば可能だ。システムの現在の状態は
  その使用が必要ないようにできる。XXX

+ 他のOSに見つかる、多くの実行時の最適化の実装を一般的なフレームワーク
  とインターフェースとして提供できる。多くの制御の移行に対する低層の最
  適化は継続によって作り直すことができる。例えばハンドオフスケジューリ
  ング[Black 90b, Thacker et al 88]、スタックなしカーネルスレッド
  [Thacker et al.88],非同期I/O[Levy & Eckhouse 89]、カーネルからユーザ
  のアップコール[Hutchinson et al.89, Anderson et al.91, Scott et
  al.89]、軽量遠隔手続き呼出し[Bershad et al. 90]それぞれはIPCの最適化
  と、継続を使って記述し実装できるスレッド管理システムを表現している。
  さらに、継続の機種非依存なインターフェースを定義することによって、こ
  れらの最適化は可搬性のあるコードで達成できる。

Machカーネルの中の様々な制御の移行を継続によって扱い、多くの場所に小さ
い最適化を一様なやり方で適用することでシステムの性能の向上を可能にした。

この論文ではMach 3.0 OSでの継続の使用とその性能について述べる。二章では
Machでの継続の実装について述べる。三章では継続を使用し、それによる最適
化の性能向上について調べる。四章では他のOSで見られるいくつかの制御移行
の関数にどうやって継続を実装することができるかを明らかにする。五章では
関連する研究について議論する。最後六章では我々の結論を要約し、紹介する。

2. OSカーネルで継続を使用する

この章ではカーネルプログラマの視点で継続を述べる。プロセス形態のカーネ
ルを継続が使えるように変換し、継続によっていくつかの一般的な最適化技術
が可能になる。そしていくつかの重要なカーネルサービスの継続による効果を
明らかにする。最後にMachカーネルでの継続を管理する機種非依存なインター
フェースを紹介する。

2.1 継続を作る

継続を必要とする二種類の制御の移行がある。スレッドがトラップあるいはフォ
ルトしてユーザ空間からカーネル空間に入る時のユーザとカーネルの境界の移
行と、カーネルの中であるスレッドから他のスレッドに制御が移る時だ。

ユーザ層でのシステムコール、例外、割込みは制御をカーネルに移行する。カー
ネルの入口では、カーネルから呼ばれた時に、制御をユーザ層に戻す継続を作
成する。制御は呼び出し元に戻らない。システムコールはそれを呼び出した所
へシステムコールの返り値とともに戻る継続を生成する。ユーザプログラムへ
の返り値はない例外と割込みは、後に呼び出されるところに継続を生成する。

カーネルの中ではスレッドはプロセッサを放棄する時に継続を作成する。スレッ
ドをブロックするカーネル手続きに関数ポインタを渡すことでこれはなされる。
関数はスレッドの継続になり、それはカーネルの機種非依存なスレッドデータ
構造として保存される。一層の実行時最適化がなければ、スレッドはその継続
の呼び出しによって再開される。


/* よく使われるシステムコール (変換前)*/
example(arg1, arg2) {
	P1(arg1, arg2);
	if (ブロックする必要) {
		/* プロセス形態を使う */
		thread_block();
		P2(arg1);
	} else {
		P3();
	}
	/* 制御をユーザに戻す */
	return SUCCESS;
}

/* よく使われるシステムコール (変換後)*/
example(arg1, arg2) {
	P1(arg1, arg2);
	if (ブロックする必要) {
		/* 継続を使う */
		コンテキストをスレッドに退避する;
		thread_block(example_continue);
		/* NOTREACHED */
	} else {
		P3();
	}
	/* 制御をユーザに戻す */
	thread_syscall_return(SUCCESS);
}

example_continue() {
	コンテキストをスレッドから復帰する;
	P2(復帰したarg1);

	/* 制御をユーザに戻す */
	thread_syscall_return(SUCCESS);
}

図1: ブロックするカーネル手続きの変形

継続として特定された関数は通常の関数のように返らない。それは他の関数を
呼ぶか、継続のみしてよい。この点がクロージャと継続の違いだ。割込み形態
として、もしブロックするスレッドがどんな状態もブロック中に保持していな
ければならないのなら、明示的に全て保持しないといけない。カーネルのスレッ
ドデータ構造体は、28バイトのスクラッチ領域を含んでいる。もし、スレッド
がもっと大きな状態を保持しておきたいのなら、追加のデータ構造を割当てな
いといけない。そのような、簡便に、あるいは有効に継続とともにブロックを
することが不可能な場合、NULLを引数としてブロックする関数を呼べば、プロ
セス形態としてスレッドをブロックする。スレッドのコンテキストはスタック
に保持され、スレッドは同じコンテキストで再開する。

2.2 カーネルを継続を使うように変換する。

Machカーネルを継続を使うように変換するのは卒直な処理だった。まず最初に
カーネルの関数のうち、ブロックする可能性があり、そして少量の状態だけが
ブロックされている間に保持されればいい関数を見きわめた。そして、これら
の関数を二つの部分に分けた。:ブロック前と、その後に。ブロックの後、ある
いは継続からなる新しい関数を定義し、ブロック前の部分だけを元の関数に残
した。次に二つの部分のに共通なスタックの内容を見きわめて、それらをブロッ
クするスレッドのスクラッチ領域に蓄えるようにした。ブロック前の関数にお
いて、カーネルのブロックする関数に、ブロック後の関数を引数とするように
変更した。その関数はスレッドの継続となる。最後に、ブロック後の関数を呼
び出し元に戻るのではなく、継続を呼ぶように変更した。図1は変換例を図解し
ている。一つの関数は二つになり、二番目の関数はブロックする一番目の関数
の引数になる。

ほとんどの場合、ロックするカーネルの関数を継続に書き替えるのは難しくな
かった。ユーザスレッドがトラップされてカーネルに入る場合、一番最初にブ
ロックが起きるのはメッセージの受信、例外、ページフォルト、プリエンプショ
ンだ。それぞれは、ユーザからカーネルの移行(システムコール、例外、割込
み)の結果として起き、そしてそれぞれは処理されて、制御がカーネルに移った
時に作成された継続を使って、ユーザ層へ戻る。

 カーネルの中だけで走るスレッドに対しては「ユーザ層に戻る」継続はない。
実際問題として、ほとんどのカーネルスレッドは無限ループで、イベントが起
こるまでブロックし、なにか作業をして、またブロックに入る。このようなス
レッドに対して、ループの本体を含む関数への継続を定義する。関数の最後の
文はこの関数自体への継続を共なったブロックになるので、その結果、末尾再
帰呼び出しによって無限ループを作ることになる。


2.3 継続を使った最適化技法

継続によってカーネルの性能を高める三つの一般的な制御の移行の最適化をす
ることができる。スタック放棄、スタック渡し、継続認識の三つだ。

継続はスレッドが再開するコンテキストを指定する。なのでスレッドのカーネ
ルスタックはブロックしている間、放棄することができる。これは空間と時間
を節約する。カーネルはブロックされているスレッドのスタックを他のスレッ
ドのスタックとして使うことができるので、空間を節約できる。さらに、もし
次に走るスレッドが継続を共なってブロックされていて、そのスタックを放棄
していたら、スレッドはブロックしているスレッドが放棄したスタックを直接
使うことができる。この二番目の最適化は「スタック渡し」と呼ばれる。カー
ネルのメモリ要求の軽減によって、継続は時間も節約することができる。それ
はカーネルの作業領域が減るので、TLBとキャッシュを有効に使えるようになる
からだ。

継続に制御を移す間の時間は最初に継続を実行時に(保持された継続、それは関
数ポインタであり、知られた値と比較することができる)調べることで減らすこ
とができる。この技術は継続認識と呼ばれ、スレッドが再開される時に、より
特定の(そして速い)コードを、スレッドの継続の替わりに使うことができる。
XXX さらに、スタック渡しの後にスタックポインタの変更なしに、再開された
スレッドは特定のコードをブロックしているスレッドの関数呼び出しコンテキ
ストで実行することができる。XXX

2.4 アドレス空間をまたいだRPCで継続を使う

MachにおけるOSサービスはアドレス空間をまたぐRPCを用いてユーザ層のサーバ
によって使われる。継続を使ってカーネルのRPC経路を再構成して、性能を向上
させた。


前からきちっと読んでおきたかった記事をじっくり読んでみた。911030の
comp.unix.internalsのChris TorekのSubject: Re: signal trampoline code

シグナルというのはプロセス間でインデックスだけで通信をするという単純至
極なシステムの割に、そのコードはとてつもなく面倒だ。実際面倒なのはアド
レス空間の異なる関数を呼び出すのが面倒というのに起因する。しかしそれ以
上に混迷して見えるのはそこにトランポリンコード(これは効率化のため)や、
呼び出す関数の受け渡しの手順が規定されていなかったからだろう。それは当
時は富豪的に規定するには許されないハードウェアスペックだったので仕方な
かった。


訳はあやふや。なんとなく理解したような気の範囲で。とても英語が苦手なの で...。訳以外のコメントは「俺」。この投稿の主点はSunOSのハンドラの設定 の実装が気にくわないことに力点がおかれている。
俺	これは質問者
> OSF MIPSとMultimaxのシグナル配送のシグナルトランポリンコードはu領域で
> はなく、Cライブラリにあることに気付いた。signal(あるいはsigaction)が呼
> ばれた時、ライブラリの中のトランポリンコードのアドレスが、これ以外のカー
> ネルに保持される引数と共にカーネルに引き渡される。事はBSDにおいても大
> 体そんなとこ(訳XXX) ユーザの(ハンドラの)プログラムカウンタがシグナルを
> 捕捉するためにこのアドレスにセットされる。
>
> 質問: 何故BSD(VAX用)はトランポリンコードをu領域に置くのか?それは機種依
> 存だし明らかに必要ない。

俺	OSF MIPSとMultimaxはシグナルトランポリンとシグナルハンドラを両
俺	方渡す実装ということなのだと思う。

俺	ここから御大。

機種依存で必要ないのは正しいが、それでも(BSD VAXでトランポリンコードを
u領域に置くのには)いい理由がある。

シグナル配送の間にユーザプロセスのコンテキストを退避し、その後で復帰さ
せるのには二つのやり方がある。

a) カーネルがやる。
b) ユーザプロセスがやる。

a)の方法の利点は特別なユーザコードが必要ないことだ。不利な点はカーネル
は特権モードなので、正しく動作させるには難しくなりがちなことだ。その難
しさは機種によるけれど、大体は(b)の方が望ましい。

俺	a)の方法でやるとすれば、まずカーネルはこのプロセス用のアドレス
俺	空間を設定して、シグナルハンドラ用のスタックを設定してユーザモー
俺	ドでシグナルハンドラにジャンプする。シグナルハンドラを実行した
俺	後にもう一度カーネルに戻ってきて、(何故戻ってこないといけないか
俺	というと、カーネルにしかスイッチフレームがないからだ) そのスイッ
俺	チフレームでプロセスを再開する。

((b)の実装にしたので)ユーザコードが自プロセスのコンテキストの退避、復帰
をしなくてはいけなくなった。一般的にこんな感じ

- いくつかのレジスタをしかるべきスタックに退避する。
- いくつかの引数とともにシグナルハンドラを呼ぶ。
- レジスタをスタックから復帰させる。
- "sigreturn"システムコールを呼ぶ。

sigreturnシステムコールもいくつかの引数をとる。それにはどこにリターンす
るかのアドレスと、ユーザコードで退避/復帰できなかったいくつかのレジスタ
sigreturn自体が使うのでいくつかのレジスタ(sigreturn自体で使われるので)
が含まれる

俺	「sigreturnはシグナルが送られたプロセスが以前に中断されたところ
俺	から実行する。アドレス空間はこのシグナルハンドラのために設定さ
俺	れた。あとはきっちりスイッチフレームを設定してやれば、そのプロ
俺	セスにそのまま戻れる。ここでいう操作はスイッチフレームの設定」

例えばVAXではsigreturnシステムコールはその場のスタックを使う。なので元
の(復帰すべき)スタックポインタをsigreturnの引数として渡してやらないとい
けない。これらの引数はカーネルによって用意されないといけない。もしそれ
らがメモリに積まれているなら、カーネルはそれらがどこにあるのかを知る必
要がある。つまりシグナルスタックとプロセスがそのスタックで実行している
かを知らせるフラグ。(通常シグナルは一つの'普通な'スタックで処理される。
しかしこうすることが難しいシステムのために、4BSDでは別のシグナルスタッ
クを用意することにした。VAXのハードウェア割り込み用のスタックのような。

俺	「sigreturnの引数がスタックに積まれているなら、その場所が分から
俺	ないといけない。その位置はsigtrampをカーネルが呼び出す時に設定
俺	したシグナルスタックを設定した場所だ」

これはソフトウェアで実装されるから、スタックがいくつあるかという想定に
関係なく機種非依存だ。

妥当なスタックはこのようにカーネルによって用意される。なので実際の実装
は(a)と(b)の中間になる。カーネルは最低限の用意をして、ユーザのトランポ
リンコードにジャンプする。なのでシグナルハンドラそれ自体は普通のCの関数
とできる。

ユーザのシグナルトランポリンコードを実行するためにカーネルはそれがどこ
にあるかを知らないといけない。もしアドレスがカーネルによって固定されて
いれば、これは簡単だ。VAXはこの方法を採用した。トランポリンコードはユー
ザから読み込み実行ができるu領域に配置し、そのアドレスは固定だ。

俺	「VAXではこのu領域がユーザスタックの一番上にリードオンリーでマッ
俺	プされている」

このように、カーネルがそれぞれのシグナルに対するユーザのシグナルハンド
ラのアドレスを保持していて、シグナルを配送する時は、この'sigtramp'コー
ドにユーザの関数のアドレスと、それに渡す引数を渡してやる。要するに

	/* in u. area: */
	dead void sigtramp(void (*f)(int, int, struct sigcontext *),
	    int sig, int code, struct sigcontext *scp) {
		(*f)(sig, code, scp);
		sigreturn(scp);
		/* if we get here, the user broke *scp; kill the process */
もしここに来たとしたら、ユーザがシグナルコンテキストを壊した。プロセスを
終了します。
		asm("halt");
		/* NOTREACHED */
	}

(実際のBSD VAXカーネルでは'callg'命令を使ってスタック操作を低減している)

俺	「sigactionがハンドラを渡して来て、それをカーネルは保持していて、
俺	カーネルはトランポリンコードの位置を知ってるからこれで実行でき
俺	るということ?」

ここでu領域を読みだせないBSD SPARCのような場合を想定しよう。ここでOSF
MIPSやMutimaxカーネルのやり方が使える。signal()を呼ぶ(実際はsigaction)
と、カーネルにユーザのハンドラではなく、sigtramp()のアドレスを渡す。
sigactionはこんな感じで実装する。

	/* libc sigaction(): massage the parameters and call the
	   real kernel sigaction(). */
	int sigaction(int sig, struct sigaction *act, struct sigaction *oact) {
		int ret;
		struct sigaction realaction, oldrealaction;
		extern void __sigtramp(<some arguments>);

		realaction.sa_handler = __sigtramp;
		<残りのrealactionの内容を*actを元に用意します>
		ret = __kernel_sigaction(sig, &realaction, &oldrealaction);
		<エラーチェックします>;
		<__sigtrampの結果をユーザ関数の返り値に合わせて変換します>;
		return (ret);
	}
ここで__sigtramp()はVAXのu領域にあるのと似たようなもの。

俺	「4BSDの場合、シグナルトランポリンコードはユーザエリアの中で固
俺	定だ。VAX,Tahoeではu領域、それはユーザスタックの頂上にリー
俺	ドオンリーでマップされている。HP680x0とSPARCではユーザスタック
俺	の頂上に書き込んである。SunOSの場合はユーザ空間のどこかにあ
俺	るという実装。なのでこれはSunOSについて説明している。4BSDであれ
俺	ばトランポリンコードは固定アドレスなのでカーネルから参照できる」

ここで問題がある。sigtramp()を呼び出してから、なんとかしてユーザのハン
ドラまで行かないといけない。カーネルはもはやこの情報を持っていない。カー
ネルは__sigtramp()のアドレスしか持っていない。

これに対処するのに二つの方法がある。
 1. カーネルインターフェースをsigtrampのアドレスとハンドラのアドレス両
方とるようにする。
 2. SunOSがやった方法:ユーザプロセスにテーブルを保持しておく。つまり上
のsigaction()で、__kernel_sigactionを呼ぶ前に、ユーザのハンドラのテーブ
ル記録しないといけない。(SunOSのように)こんな感じ

俺	「SunOSがやらない方法だとカーネルに保持しておく」

typedef void (*sig_handler)(int sig, int code, struct sigcontext *scp,
char *addr);

(SunOSは上の4BSDでは3つの引数だったのにさらに引数が追加されている。間違
い。それはsigcontextの中にあるべきでコードは元のままのべきだ。
sigcontext 構造体は両方向(引数/返り値)のインターフェースとして定義され
るべきで(返りだけに使うのではなく)、そしてその受け渡し内容は本質的に機
種依存だ。それは(struct sigcontextという形で)カプセルした方がいいだろう。

不幸にもSunOSがしたようにやるなら、新しい問題がでてくる。テーブルを変更
し、__kernel_sigactionを呼ぶまでの間にもシグナルは配送され得る。例えば
もし前のハンドラが通常のスタックで走って、新しいのがシグナルスタックで
走ったら、新しいハンドラを通常のスタっクで呼ぶことになるだろう。

俺	「ここよくわからない。シグナルハンドラが同時に別のスタックで走
俺	るような実装なの??? カーネルで設定したシグナルフレームだけだと
俺	思うのだけど...」

この問題に対するSunOSの解決は、状況を変更する時はsigblockとsigsetmaskを
使って、シグナルの配送をブロックすることにした。これは、あいにくにも一
回の変更に3回のシステムコールを必要となる。(SunOSは実際には4つのシステ
ムコールを作った。知る限り、良い理由はない)

俺	「これが4BSDだとどうなるか。シグナルトランポリンは固定アドレス
俺	にあるからsigactionから渡す必要はない。そこでトランポリンコード
俺	ではなくシグナルハンドラを渡せばいい。カーネルに渡った時点でロッ
俺	クが効くのでこうする必要がないとういうことだろうか?」

なのでこう書かないといけない。

	#ifdef __GNUC__
	#define dead volatile
	#else
	#define dead /*empty*/
	#endif
	extern dead void sigtramp(int, int, struct sigcontext *, char *);
	typedef void (*sig_handler)(int, int, struct sigcontext *, char *);
	sig_handler __sigtable[NSIG];

	/* libc sigaction(), probably correct, but untested */
	int sigaction(int sig, struct sigaction *act, struct sigaction *oact) {
		int ret, saverr;
		sigmask_t omask;
		sig_handler ofun;
		struct sigaction ra;

		/* verify arguments, as much as possible */
		if (sig < 1 || sig >= NSIG) {
			errno = EINVAL;
			return (-1);
		}
		/*
		 * If we are going to ignore or default the signal, we
		 * do not need to change the table.  A series of calls
		 * that set SIG_IGN, followed by nothing further ever,
		 * is a frequent special case.
		 *
		 * If the previous catcher is __sigtramp, the table contents
		 * must be valid (by definition).
		 */
		if (act->sa_handler == SIG_DFL || act->sa_handler == SIG_IGN) {
			ret = __kernel_sigaction(sig, act, oact);
			if (ret >= 0 && oact->sa_handler == __sigtramp)
				oact->sa_handler = __sigtable[sig];
			return (ret);
		}
		/*
		 * We are going to set a real catcher, so we must block
		 * occurrences of this signal while we change the table.
		 * Set ra.ra_mask and ra.ra_flags before blocking, in case
		 * either access faults.
		 */
		ra.ra_mask = act->sa_mask;
		ra.ra_flags = act->sa_flags;
		omask = sigblock(sigmask(sig));
		ofun = __sigtable[sig];
		__sigtable[sig] = act->sa_handler;
		ra.ra_handler = __sigtramp;
		ret = __kernel_sigaction(sig, &ra, oact);
		if (ret >= 0) {
			(void) sigsetmask(omask);
			if (oact->sa_handler == __sigtramp)
				oact->sa_handler = ofun;
			return (ret);
		}
		/*
		 * The change failed: restore the table, unblock, and return.
		 */
		saverr = errno;
		__sigtable[sig] = ofun;
		(void) sigsetmask(omask);
		errno = saverr;
		return (ret);
	}

この3つの中の一つを推薦する。

 a) カーネルのインターフェースを関数を含むように変更する。
 b) どこに__sigtrampがあるかをカーネルに教えるコールを追加し、それ以外
 のカーネルインターフェースはそのままに。(main()を呼ぶ前に、新しいシス
 テムコールを呼ぶようにする)
 c) 何らかの方法で(カーネルに)わかるアドレスにsigtrampを隠す。

俺	a)はシグナルトランポリンとシグナルハンドラを両方渡す (OSF MIPS)
俺	b)はシグナルトランポリンのコードを教えるシステムコールだけを追加。
俺	c)例えばスタックのてっぺん固定でシグナルトランポリンを配置(4BSD)
俺	d)SunOSはインターフェースをそのままにトランポリンコードを渡すよ
俺	うにして、ハンドラのテーブルをカーネルじゃなくユーザ空間にした
俺	ので余計なロックが必要になってしまった。

BSD SPARCカーネルでは、HP-BSDで使われている(c)の方法を採用した。exec()
で、トランポリンコードをユーザスタックに設定する。その場所はargvと環境
変数のスタックの上だ。あいにくSunOSとの互換性のために、その場しのぎのコー
ドをカーネルの機種非依存の部分に追加しないといけなかった。

そしてこれらの様々な解決法の中からOSFはどれを選んだ?

俺	a)

----------------------------------------------------------------------
以下原文
----------------------------------------------------------------------
From torek@horse.ee.lbl.gov Wed Oct 30 22:52:54 1991
From: torek@horse.ee.lbl.gov (Chris Torek)
Newsgroups: comp.unix.internals
Subject: Re: signal trampoline code
Date: 31 Oct 91 03:28:09 GMT
Reply-To: torek@horse.ee.lbl.gov (Chris Torek)
Organization: Lawrence Berkeley Laboratory, Berkeley
X-Local-Date: Wed, 30 Oct 91 19:28:09 PST

In article <1828@rust.zso.dec.com> schmitz@rust.zso.dec.com
(Michael Schmitz) writes:
>I have noticed that in the OSF MIPS and Multimax signal delivery
>code that the signal trapoline code is not in the u area, but in
>the C library.  When signal (or sigaction) is called, the address
>of the library's trampoline code is passed as another system call
>argument which is saved by the kernel.  Things are pretty much as
>in BSD -- the user's PC is set to this address to catch the signal.
>
>The Question:  Why does BSD (for the VAX) put the trampoline code 
>in the u area? That is machine dependent and obviously unnecessary.

It is true that it is machine dependent and unnecessary.  There is
still a good reason for it, though.

There are two ways to save the user's process context during signal
delivery, and restore it afterward: a) have the kernel do it; b) have
the user process do it.  The advantage of approach (a) is that there
need be no special user code.  The disadvantage is that, since the
kernel is privileged, it tends to be difficult to do (a) correctly.
The difficulty level is machine-dependent, but has always been high
enough to favor approach (b).

The code a user process must use to save and restore its own context
generally looks like this:

	- save some machine registers on the correct stack;
	- call the signal handler with several parameters;
	- reload the registers from the stack;
	- call the `sigreturn' system call.

The sigreturn system call also takes some parameters, including the
address(es) to which to return and any registers that could not be
saved and restored in user code because they are needed for the
sigreturn call itself.  On the VAX, for instance, the sigreturn system
call uses the current stack pointer, so the original stack pointer must
be in the arguments to sigreturn.  These parameters must clearly be set
up by the kernel.  If they are stored in memory (and they are), the
kernel thus needs to know where to store them, i.e., the address of the
`signal stack' and a flag to tell whether the process is already on
that stack.  (Normally, signals are handled on the single `normal'
stack, but this is a burden to some runtime systems, so 4BSD provides
an alternative signal stack, rather like the VAX's hardware interrupt
stack.  Since this is done in software, it is not machine-dependent,
beyond the assumption that one or more stacks exist.)  The correct
stack is thus normally set up by the kernel.  Thus, the approach
actually used is a mix between (a) and (b): the kernel sets up a
minimal amount of state, then `bounces off' the user `trampoline' code,
so that the signal handlers themselves can be ordinary C functions.

In order to reach the user's signal trampoline code, the kernel must
know where it resides.  If the address is fixed by the kernel, this is
easy.  This is the approach taken on the VAX: the trampoline code is in
the u. area, which is readable and executable by user code, and which
has a fixed address.  Thus, the kernel holds the address of the actual
user signal handler for each signal, and when delivering a signal,
calls this `sigtramp' code, passing the address of the user function
and the arguments to hand to it.  In effect, we have:

	/* in u. area: */
	dead void sigtramp(void (*f)(int, int, struct sigcontext *),
	    int sig, int code, struct sigcontext *scp) {
		(*f)(sig, code, scp);
		sigreturn(scp);
		/* if we get here, the user broke *scp; kill the process */
		asm("halt");
		/* NOTREACHED */
	}

(the BSD VAX kernel actually uses a `callg' instruction to call the
function, saving a few stack operations).

Now suppose that we do not have a readable u. area, as is the case on
the BSD SPARC kernel.  Here we can use the approach described for the
OSF MIPS and Multimax kernels.  Calls to signal() (actually sigaction)
pass to the kernel the address of the sigtramp() code, rather than the
address of the user's handler.  That is, sigaction is implemented
something like:

	/* libc sigaction(): massage the parameters and call the
	   real kernel sigaction(). */
	int sigaction(int sig, struct sigaction *act, struct sigaction *oact) {
		int ret;
		struct sigaction realaction, oldrealaction;
		extern void __sigtramp(<some arguments>);

		realaction.sa_handler = __sigtramp;
		<set up the rest of realaction based on *act>;
		ret = __kernel_sigaction(sig, &realaction, &oldrealaction);
		<check for errors>;
		<translate __sigtramp to user functions for returns>;
		return (ret);
	}

where __sigtramp() is similar to the one found in the u. area on the VAX.
Now we have a problem: somehow, we have to go from the call to sigtramp()
to the actual user's function.  The kernel no longer has this information;
the kernel has only the address of __sigtramp().

There are two ways to handle this.  We can either change the kernel
interface to provide the `sigtramp address' and the `handler address',
or we can do what SunOS does: keep a table in the user process.  That
is, in sigaction() above, before calling __kernel_sigaction, we have
to record the user's handler in a table per signal, something like
(as in SunOS):

	typedef void (*sig_handler)(int sig, int code,
	    struct sigcontext *scp, char *addr);

(Note that SunOS has here added an additional parameter beyond the
three found in 4BSD: a mistake; it should be in the sigcontext, as
should the code have been originally.  The sigcontext structure should
have been defined as the interface in both directions, rather than
just in the return direction, as both parts are inherently machine
dependent and it is wise to encapsulate machine dependencies.)

Unfortunately, if we do what SunOS does, we have a new problem.  The
signal could be delivered between the time we change the table and the
time we call __kernel_sigaction.  If, for instance, the old handler ran
on the regular stack, but the new one runs on the signal stack, we will
then call the new handler on the regular stack.  The SunOS solution to
this problem is to use sigblock and sigsetmask to block delivery while
the status is changing.  This, unfortunately, requires three system
calls per change.  (SunOS actually makes four system calls, for, as far
as I know, no good reason.)  That is, we must write:

	#ifdef __GNUC__
	#define dead volatile
	#else
	#define dead /*empty*/
	#endif
	extern dead void sigtramp(int, int, struct sigcontext *, char *);
	typedef void (*sig_handler)(int, int, struct sigcontext *, char *);
	sig_handler __sigtable[NSIG];

	/* libc sigaction(), probably correct, but untested */
	int sigaction(int sig, struct sigaction *act, struct sigaction *oact) {
		int ret, saverr;
		sigmask_t omask;
		sig_handler ofun;
		struct sigaction ra;

		/* verify arguments, as much as possible */
		if (sig < 1 || sig >= NSIG) {
			errno = EINVAL;
			return (-1);
		}
		/*
		 * If we are going to ignore or default the signal, we
		 * do not need to change the table.  A series of calls
		 * that set SIG_IGN, followed by nothing further ever,
		 * is a frequent special case.
		 *
		 * If the previous catcher is __sigtramp, the table contents
		 * must be valid (by definition).
		 */
		if (act->sa_handler == SIG_DFL || act->sa_handler == SIG_IGN) {
			ret = __kernel_sigaction(sig, act, oact);
			if (ret >= 0 && oact->sa_handler == __sigtramp)
				oact->sa_handler = __sigtable[sig];
			return (ret);
		}
		/*
		 * We are going to set a real catcher, so we must block
		 * occurrences of this signal while we change the table.
		 * Set ra.ra_mask and ra.ra_flags before blocking, in case
		 * either access faults.
		 */
		ra.ra_mask = act->sa_mask;
		ra.ra_flags = act->sa_flags;
		omask = sigblock(sigmask(sig));
		ofun = __sigtable[sig];
		__sigtable[sig] = act->sa_handler;
		ra.ra_handler = __sigtramp;
		ret = __kernel_sigaction(sig, &ra, oact);
		if (ret >= 0) {
			(void) sigsetmask(omask);
			if (oact->sa_handler == __sigtramp)
				oact->sa_handler = ofun;
			return (ret);
		}
		/*
		 * The change failed: restore the table, unblock, and return.
		 */
		saverr = errno;
		__sigtable[sig] = ofun;
		(void) sigsetmask(omask);
		errno = saverr;
		return (ret);
	}

This argues for one of three things:

 a) changing the real kernel interface to include the function;
 b) adding a call to tell the kernel where __sigtramp is, and otherwise
    leaving the kernel interface alone (the C startup would make the new
    system call before calling main());
 c) `hiding' sigtramp at a known address, in some way.

In my BSD SPARC kernel, I took approach (c) from the HP-BSD kernel: in
exec(), we build the trampoline code on the user's stack, above the
argv and evironment strings.  Unfortunately, for SunOS compatibility I
had to add kludges to otherwise machine-independent parts of the kernel.

Which, if any, of these various solutions have the OSF people chosen?





大根初収穫。よくできた!



早速大根の活け造りに(美味しんぼ:1巻)。確かにこれはいける。瑞々しさがす ごい。

やたらキノコが生えている。毒キノコがなければ食べるんだけどな〜。





最後にもう一度ロガーの動作確認して積み込み。天候も安定してそうだし、今 シーズンラストレース。楽しんでいこう。




リアブレーキ廻り組んでゼッケンも貼りかえました。そうだ。タコメータをTカー
から外しておかないと。ゼッケン切り出し用のシートも補充しないと。



美味しん本、全エピソードのリストが載ってるのがとてもうれしい。僕もそれ なりの美味しんぼラーと自負しているのだけど、「あの話どこだったっけ...」 と探し出せないときがたまにある。ストーリーから離れた単発話とか。
この前もホビロンがどこだったか探すのに苦労した。(答:66巻)
そしてまだまだ自分は美味しんぼの読み込みが足りないと痛感した。


こんな天気の日はロガーのテストにXLRで奥多摩にでも行きたいところだけど、
黙々と整備。東京近辺の林道は今は厳重に冊がついたか、舗装されたかでなか
なかいいところはもうないのよね。ツーリングは林道派。23年前はよかった。
なんだかんだいってツーリングも数年行ってない。

これは1987年当時の犬越路隧道。Wikipediaの犬越路隧道の石碑の2007年の写真 は落石に覆われているけれど、当時はこんなに綺麗だった。道志から丹沢湖ま で全部ダートで最高だった。丹沢湖まで抜けたらそのままUターンでまた満喫。 玄倉もあるけれど、あそこはあまり面白くなかった。

丹沢は早戸川集団林道だ。どこも最高だった。これは荒井林道(ここは高速コー ナーばっかりであまりおもしろくなかったけれど)から当時できたばっかしの虹 の大橋。荒井林道は今は湖の周遊道路になってるんじゃないかと思うんだけど...

これは中古ピストン。次の練習もこれで。

シリンダはちょっと疲れてきたか。そろそろ次の作っておこうか。



黙々と洗浄をしていると、リアキャリパーのスライド穴がガタガタになってい た。これ、歪んだのにデブコン積めて穴開け直して使っていたのだけど、ここ 最近、真面目にリアブレーキ使いはじめてボロボロになってしまった様子。
Tカーのと交換。ブレーキフルードを交換...と思ったらフルードが切れていた。
今日はここまで。ちょっと熱っぽい。

MonotaRO(モノタロウ)
あわせて読みたい