081230

|


昨日のスレッドの実装は思慮が足りなかった。スピンから割りこまれて、その
ハンドラからスケジューラが呼ばれて、もう一度ここに来た時の対応。
  // No thread in ready queue.
  if (!tc)
    {
      if (saved_thread)
	{
	  assert (saved_thread == current_thread);
	  return 0;	// Spin again.
	}
昨日はここで二重は無理とあきらめていたけれど、ここにきた時のcurrent_threadの
コンテキストは、下のwhile(1);で割りこまれた時のものだ。なら、このままcurrent_
threadを変更しないで抜けてしまえば、また下のwhile(1);に戻る。

      saved_thread = current_thread;
      memcpy (&saved_regs, ¤t_thread->regs, sizeof (struct reg));

      DPRINTF ("\t\t\t\t\t%s: spin %d\n", __FUNCTION__, current_thread->id);
      intr_enable ();
      while (/*CONSTCOND*/1)
	; ←ここで割りこまれる。
      assert (0);
      /*NOTREACHED*/
    }
これでTSSのようなスケジューリングでも問題ない。 あと、コンテキストスイッチで余計なところを廃止しました。RESTORE_CCR_PCの部分。
	/* void thread_switch (int schedule_policy) */
	.globl _thread_switch
_thread_switch:
	; disable interrupt
	stc.b	ccr, r2l
	orc	#0x80, ccr

	; save context.
	mov.l	@_current_thread, er3

	mov.l	@sp, er1	; return address
#ifdef RESTORE_CCR_PC
	mov.l	er1, @(0x20, er3) ; pc
	mov.b	r2l, @(0x20, er3) ; ccr    |ccr(8)|   pc(24)  |
#endif
	mov.l	sp,  @(0x1c, er3) ; sp
	mov.l	er6, @(0x18, er3) ; callee saved
	mov.l	er5, @(0x14, er3) ; callee saved
	mov.l	er4, @(0x10, er3) ; callee saved
	; no need to store caller saved and ccr.

	jsr	@_thread_context_switch

	mov.l	@_current_thread, er0

	mov.l	@(0x1c, er0), sp  ; sp
#ifdef RESTORE_CCR_PC
	mov.l	@(0x20, er0), er2 ; pc
	mov.l	er2, @sp	  ; set return address and ccr
#endif
	mov.l	@(0x18, er0), er6 ; callee saved
	mov.l	@(0x14, er0), er5 ; callee saved
	mov.l	@(0x10, er0), er4 ; callee saved
	mov.l	@(0x0c, er0), er3 ; caller saved
	mov.l	@(0x08, er0), er2 ; caller saved
	mov.l	@(0x04, er0), er1 ; caller saved
	mov.l	@(0x00, er0), er0 ; caller saved
	; resume interrupt. and return
	rte
ここは手で復帰しなくてもSPが復帰されればそこに入っているので。(同じの上書きしてるだけ)
次はDRO基板の製作です。ちょっと秋葉原に行って、旋盤用DROのパーツを買っ てきました。ついついまた余計なものまで買ってしまった。
ちょっとお遊びを。ファイバの続きです。
	/* void fiber_switch_arg (fiber_t from, fiber_t to, uint32_t ret) */
                                    「er0」       「er1」       「er2」
	.globl _fiber_switch
_fiber_switch:
	; save current context.
	; no need to store caller saved and ccr.
	mov.l	sp,	@(0x0c, er0) ; sp (contain return address)
	mov.l	er6,	@(0x08, er0) ; callee saved
	mov.l	er5,	@(0x04, er0) ; callee saved
	mov.l	er4,	@(0x00, er0) ; callee saved
	; load next context.
	mov.l	@(0x0c, er1), sp  ; sp
	mov.l	@(0x08, er1), er6 ; callee saved
	mov.l	@(0x04, er1), er5 ; callee saved
	mov.l	@(0x00, er1), er4 ; callee saved
	mov.l	er2,	er0	  ; subroutine return value / 1st arg.
	rts

これはファイバのスイッチルーチン。ファイバは明示的なサブルーチンコールでしか
スイッチしないので、とても軽いです。最後のmov.l er2, er0は最初のファイバ
呼び出しの際には第一引数になり、通常のコールの際は返り値になります。


struct fiber_reg
{
  uint32_t er4;		// 0x00
  uint32_t er5;		// 0x04
  uint32_t er6;		// 0x08
  uint32_t sp;		// 0x0c (er7)
} __attribute__((packed)); //16byte

struct fiber;
typedef struct fiber *fiber_t;

struct fiber
{
  struct fiber_reg regs;
  fiber_t parent;    便宜上、ファイバの作成をしたファイバを親にします。
  fiber_t continuation; このファイバの継続先です。
  int32_t id;
  uint8_t stack[0];
} __attribute__((packed, aligned (4)));

__BEGIN_DECLS
fiber_t fiber_init (fiber_t);
スレッドがファイバを使う際に自分をファイバとして扱えるようにします。

fiber_t fiber_create (fiber_t, uint8_t *, size_t, void (*)(fiber_t));
新しくファイバを作ります。第一引数は自分です。

uint32_t fiber_switch (fiber_t, fiber_t, uint32_t);
ファイバを切り変えます。次の継続のリターン値はuint32_tにつめます。

fiber_t fiber_twist (fiber_t, int, ...);
ファイバの実行順序を設定します。第一引数は自分、第二引数はファイバの数、
その後には実行順にファイバを並べます。
__END_DECLS


static int __fiber_id;

// make myself (thread) as fiber.
fiber_t
fiber_init (fiber_t f)
{
ファイバIDを設定するくらいです。

  f->id = ++__fiber_id;
  f->parent = NULL;

  return f;
}

// create new fiber.
fiber_t
fiber_create (fiber_t parent, uint8_t *fiber_local_store, size_t size, void (*start)(fiber_t))
{
  fiber_t f = (fiber_t)fiber_local_store;
  size_t stack_size = size - sizeof (struct fiber);

  f->parent = parent;
  f->id = ++__fiber_id;

  f->regs.sp = (addr_t)f->stack + stack_size - 4;
  // install return address for 'rts'
  *(uint32_t *)f->regs.sp = (addr_t)start;

スタックの設定をして、'rts'命令用にジャンプ位置を設定しておきます。CCR
は設定してないので、確実に割り込みは開く。これは問題にはならないとは思
うけど、割り込み禁止の状態からファイバをまわすなら、この時点のCCRを読ん
で設定するようにする。
  printf ("%x %x %x\n", f->parent, f->regs.sp,*(uint32_t *)f->regs.sp);

  return f;
}

// link continuation.
fiber_t
fiber_twist (fiber_t myself, int n, ...)
{
  va_list ap;
  fiber_t f, pre_f, start_f;
  int i;

引数の順番にそれぞれのファイバの継続先を設定していきます。最後は自分に
戻ってくるように設定します。返り値はスタートすべきファイバです。

  f = pre_f = start_f = NULL;
  va_start(ap, n);
  for (i = 0; i < n; i++)
    {
      f = va_arg (ap, fiber_t);
      if (pre_f)
	{
	  pre_f->continuation = f;
	  printf ("%s: %d->%d\n", __FUNCTION__, pre_f->id, f->id);
	}
      else
	{
	  start_f = f;
	}

      pre_f = f;
    }
  va_end (ap);
  f->continuation = myself;
  printf ("%s: %d->%d\n", __FUNCTION__, f->id, myself->id);

  return start_f;
}

さて、ここまで用意した。シリアルコンソールはバッファリングされているので今は
  thread_priority (current_thread, PRI_LOW);	// lowest.
  while (/*CONSTCOND*/1)
    {
       sci_putc (sci_getc ());
    }

こんな感じで一番低い優先度でまわしておくだけで、echoはできる。シリアル
コンソールを叩くと、受信割りこみがかかって、受信スレッドを起こす。受信
スレッドは受信リングバッファに値を入れる。値待ちで寝ていたsci_getc()を
呼んでいるスレッドがおこされて値を取得、それはsci_putcにわたされて、送
信リングバッファに入れる。送信リングバッファで待っていた送信スレッドが
SCIをたたいて、エコーバックです。

これをファイバにしてみた。受信ファイバ、送信ファイバ、おまけにフィルタ
ファイバです。
  thread_priority (current_thread, PRI_LOW);	// lowest.
  while (/*CONSTCOND*/1)
    {
      fiber_switch (&myself, start_fiber, 0);
    }
受信ファイバ→フィルタファイバ→送信ファイバと継続して、ここに戻ってくる。



#define	FLS_SIZE	256
uint8_t fls[3][FLS_SIZE] __attribute__ ((aligned (4)));
ファイバのコントロールブロック+局所記憶です。ちょっと大いような気がする
かもしれないけれど、割り込まれた時の割り込みスタックがあるので、ある程
度のマージンが必要。割り込みスタックを別に用意してもいいんだけど...

void prompt (void);
void fiber_1 (fiber_t);
void fiber_2 (fiber_t);
void fiber_3 (fiber_t);

void
prompt ()
{
  struct fiber myself; スレッド自分自身をファイバとして使うための領域です。
  fiber_t input_fiber, output_fiber, filter_fiber;
  fiber_t start_fiber;

  // Fiber test.
  fiber_init (&myself);
  input_fiber = fiber_create (&myself, fls[0], FLS_SIZE, fiber_1);
  output_fiber = fiber_create (&myself, fls[1], FLS_SIZE, fiber_2);
  filter_fiber = fiber_create (&myself, fls[2], FLS_SIZE, fiber_3);

  fiber_switch (&myself, input_fiber, (uint32_t)input_fiber);
  fiber_switch (&myself, output_fiber, (uint32_t)output_fiber);
  fiber_switch (&myself, filter_fiber, (uint32_t)filter_fiber);
ここのスイッチはそれぞれのファイバに最初の引数を渡して、待機状態にまで
もっていかせるためのものです。それ用にファイバも書かれている必要があります。

#if 1
  start_fiber = fiber_twist (&myself, 3,
			     input_fiber, filter_fiber, output_fiber);
  入力→フィルタ→出力でまわります。
#else
  start_fiber = fiber_twist (&myself, 2, input_fiber, output_fiber);
  入力→出力でまわります。
#endif

  printf ("fiber return.\n");

  intr_enable ();

  thread_priority (current_thread, PRI_LOW);	// lowest.
  while (/*CONSTCOND*/1)
    {
      if (fiber_switch (&myself, start_fiber, 0) != 0)
	{
	  printf ("hello world\n");
	}
    }
}

void
fiber_1 (fiber_t myself)
{
  fiber_switch (myself, myself->parent, 0);
最初の呼ばれる時はコンテキスト設定用ということになっているので、すぐに
呼び出し元に帰ります。つぎにスイッチした時はwhile (...からはじまります。

  while (/*CONSTCOND*/1)
    {
      int8_t c = sci_getc ();
      if (c == 'q')
	fiber_switch (myself, myself->parent, 1);
'q'が押されたら、継続せずに呼び出しもとに戻ります。try-catchのようなジャンプ。

      fiber_switch (myself, myself->continuation, (uint32_t)c);
次の継続に、この値を渡します。
    }
}

void
fiber_2 (fiber_t myself)
{
  fiber_switch (myself, myself->parent, 0);

  while (/*CONSTCOND*/1)
    {
      int8_t c = (int8_t)fiber_switch (myself, myself->continuation, 0);
      sci_putc (c);
前の継続から渡された値をsci_putcに投げます。
    }
}

void
fiber_3 (fiber_t myself)
{
  fiber_switch (myself, myself->parent, 0);

  while (/*CONSTCOND*/1)
    {
      int8_t c = (int8_t)fiber_switch (myself, myself->continuation, c);
      c = c < 'a' ? c : c - 32;
前の継続から来た値を(アルファベットとして)、大文字にして次の継続に渡します。
    }
}



なんでこんなことをしているかというと、今日買ってきた本にインスパイアを 受けてます。魅力的なパラダイムだけれど局所記憶の無駄が多すぎ。スタック をなんとかできればな...