081224

|



昨日はNSR50を整備して散髪に行って、今日朝、秋ヶ瀬に行く準備をしていたの
ですが、起きてみたらあまりに寒いのでパスしました。なんか去年、今年と寒
過ぎないか?とデータをみてみると2004年の秋ヶ瀬は12/8 20.7℃、12/17
13.6℃、12/24 14.4℃、1/14 13.6℃。いずれも昼過ぎ。2004-2006まで暖かかっ
た。

やっぱりもうちょっとH8/3052開発機ソフトを進めます。ノリノリだし。
シリアルコンソールの部分がおざなりなので、まずはそれのためにリングバッファを 用意しました。バッファリングはリングバッファ一個で済ませる予定。

struct ringbuffer
{
  struct monitor *mon;
  uint8_t *buffer;

  size_t input;
  size_t output;
  size_t size;	// must be power of 2
  size_t free;
} ringbuffer;

#define	RINGBUFFER_FULL		0x1
#define	RINGBUFFER_EMPTY	0x2

ringbuffer_t
ringbuffer_init (uint8_t *buf, size_t size)
{

  ringbuffer.mon = monitor_allocate ();

  ringbuffer.buffer = buf;
  ringbuffer.input = 0;
  ringbuffer.output = 0;
  ringbuffer.size = size;
  ringbuffer.free = size;

  return &ringbuffer;
}

void
ringbuffer_write (ringbuffer_t ctx, uint8_t *buf, size_t size)
{
  struct ringbuffer *rb = ctx;
  size_t i, j, sz;

  while (size > 0)
    {
      monitor_enter (rb->mon);
      if (rb->free == 0)
	monitor_wait (rb->mon, RINGBUFFER_FULL);

条件変数の場合はここでロックをとり直すけれど、モニタの場合は既にロックを
握っているのでいらない。

しかし、この前実装したモニタには間違いがあった。シグナルで起こされたイ
ベント待ちのスレッドを全て起しているのだけど、モニタとしては全てではな
く、一つを起こして、monitor_waitが確実にイベントをとれるようにしないと
いけない。僕の実装だと一つのイベントに対して二つ以上が待っていた場合に
レースになる。

このリングバッファの場合、二つの書き手がいた場合、バッファが空いて最初
の書き手はこのままmonitor_exitまで進んでモニタを開放、次の書き手がここ
からはじまるのだけど、既に前の書き手によってrb->free == 0になってしまっ
ているかもしれない。

ここを
      while(rb->free == 0)
	monitor_wait (rb->mon, RINGBUFFER_FULL);
としてもいいかも。waitでthread_sleepに入るので、レディキューから抜かれて、
monitor_exitでロック取得待ちのがモニタに入ってこれるはず。
イベントに対してリソースを取り合いがない(参照するだけ)場合は、今の仕様
の方が扱いやすい。

もうちょっと考えます。

      sz = rb->free < size ? rb->free : size;

      for (i = 0; i < sz; i++)
	{
	  j = (rb->input + i) & (rb->size -1);
	  rb->buffer[j] = *buf++;
	}

      rb->input += sz;
      rb->input &= (rb->size - 1);
      rb->free -= sz;
      size -= sz;
      monitor_signal (rb->mon, RINGBUFFER_EMPTY);
      monitor_exit (rb->mon);
    }

  SCI_PRINTF ("W%d %d %d\n", rb->input, rb->output, rb->free);
}

size_t
ringbuffer_read (ringbuffer_t ctx, uint8_t *buf, size_t size)
{
  struct ringbuffer *rb = ctx;
  size_t buffered, sz, i, j;

  monitor_enter (rb->mon);
  if ((rb->size - rb->free) == 0)
    monitor_wait (rb->mon, RINGBUFFER_EMPTY);

  buffered = rb->size - rb->free;

  sz = buffered > size ? size : buffered;

  for (i = 0; i < sz; i++)
    {
      j = (rb->output + i) & (rb->size -1);
      buf[i] = rb->buffer[j];
    }

  rb->output += sz;
  rb->output &= (rb->size - 1);
  rb->free += sz;

  SCI_PRINTF ("R%d %d %d\n", rb->input, rb->output, rb->free);

  monitor_signal (rb->mon, RINGBUFFER_FULL);
  monitor_exit (rb->mon);

  return sz;
}

モニタを使って書いてみると、ほとんど条件変数を使った場合とコーディング
は変わらない。このくらいならむしろセマフォ二つでロックとイベントをまか
せた方が素直。条件変数と同じように書けるけれど、条件変数の場合、
monitor_enterとmonitor_exitにあたる部分は単なるmutexなので、ただのロッ
クにもそのmutexを使える。しかしモニタの場合monitor_exitがwaitで起こされ
るスレッドの処理までするのでちょっとオーバーヘッドがある。いろいろ考え
ると、セマフォだけ実装するのが一番いいかもしれない。でも今回はモニタだ
けでやります。モニタの良し悪しを見極めたい。

リングバッファも最初はこのくらいでいいのだけれど、「待たないでやれる分
だけやりたい」とか、「待ちに入っちゃったのをキャンセルして終わりたい」
とかいろんな要望で、泥沼に入っていく。スレッドは、終了させましょうとい
う時に、待ちを解除してリソースを開放させるのがとてもつらい。

そしてスレッド脳なうちに、もうちょっと実装しました。ファイバ(fiber)。こ
れは継続(continuation)とかコルーチン(coroutine)とか呼ばれているもののマ
イクロソフトの呼称だと思う(ちょっとわからない)。要はスケジューラのない
スレッドかな。スケジューラがないので、それぞれのファイバは明示的に次の
ファイバを指定することで制御を移行します。ファイバは局所記憶をそれぞれ
が持つのでスレッドに近い。


それぞれのファイバはサブルーチンコールだけによって順々に実行されるので、
ファイバ間で共有するリソースにロックは必要ない。

スケジューラがないのでsleep/wakeupはない。明示的にfiber_switchでつなぐか
つながないかだけ。

ただスレッドシステムの場合、スケジューラに属さないと動きようがないので、
ファイバはスレッドに属することになる。なかなかいいネーミングです。

実装してみました。

#ifndef _FIBER_H_
#define	_FIBER_H_
typedef struct fiber
{
  uint32_t er4;		// 0x00
  uint32_t er5;		// 0x04
  uint32_t er6;		// 0x08
  uint32_t sp;		// 0x0c (er7)
  uint32_t pc;		// 0x10
  uint32_t pad;		// for alignment adjustment.
  uint8_t stack[0];
} *fiber_t __attribute__((packed)); //24byte

明示的にサブルーチンコールでしかスイッチしないのでcallee savedと、スタッ
ク、次に実行するPC(jsr fiber_switchの次の行)をとっておけばいい。パディ
ングがあるのは、fiber_init用にsizeof (struct fiber)をとったけれども、
4byteアラインじゃなかった時に4byteアラインにラウンドしても全体のサイズ
がとれるように。

stack[0]はこのファイバ用のスタック領域の先頭。実際にはこのストラクチャ
とスタックを合わせた領域をとって、これにキャストする。


__BEGIN_DECLS
void fiber_switch (fiber_t, fiber_t);
fiber_t fiber_create (uint8_t *, size_t, void (*)(void));
fiber_t fiber_init (fiber_t);

__END_DECLS

#endif

ファイバ切り替えルーチン。

	.h8300h
	.section .text

	/* void fiber_switch (fiber_t(er0), fiber_t(er1)) */
	.globl _fiber_switch
_fiber_switch:
	; save current context.
	; no need to store caller saved and ccr.
	mov.l	@sp, er2	; return address
	mov.l	er2, @(0x10, er0) ; pc
	mov.l	sp,  @(0x0c, er0) ; sp
	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	@(0x10, er1), er2 ; pc
	mov.l	er2, @sp	  ; set return address and ccr
	mov.l	@(0x08, er1), er6 ; callee saved
	mov.l	@(0x04, er1), er5 ; callee saved
	mov.l	@(0x00, er1), er4 ; callee saved
	rts

現在のファイバのコンテキストが第一引数(er0)、次のファイバのコンテキストが
第二引数er1に入る。ファイバは割り込みから状態を変更されることはないので
割り込みを禁止する必要はない。スイッチングの速さはいい。とはいえサブルーチン
コールにくらべればずっと重いです。

スレッドの場合、割り込みからも状態が変更されるので割り込みを禁止し、
割りこみからスイッチされたコンテキストのために、caller savedまで復帰しない
といけない。次のスレッドを決めるにはthread_context_switchを呼んで、それは
優先度ごとのレディキューをまわって、次のスレッドを探している。

	.globl _thread_switch
_thread_switch:
	; disable interrupt
	stc.b	ccr, r2l
	orc	#0x80, ccr
	; save context.
	mov.l	@_current_thread, er0
	mov.l	@sp, er1	; return address
	mov.l	er1, @(0x20, er0) ; pc
	mov.b	r2l, @(0x20, er0) ; ccr    |ccr(8)|   pc(24)  |
	mov.l	sp,  @(0x1c, er0) ; sp
	mov.l	er6, @(0x18, er0) ; callee saved
	mov.l	er5, @(0x14, er0) ; callee saved
	mov.l	er4, @(0x10, er0) ; 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
	mov.l	@(0x20, er0), er2 ; pc
	mov.l	er2, @sp	  ; set return address and ccr
	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



static void *round_buffer_4 (uint8_t *, size_t *);

// Return own fiber control block.
fiber_t
fiber_init (fiber_t fiber_local_store)
{
  size_t sz; // dummy.

  return round_buffer_4 ((uint8_t *)fiber_local_store, &sz);
}

ファイバを作成しようとするスレッドが、自分をファイバとして登録する領域を
とっておきます。4byteアラインにするだけです。自分のファイバのスタックは
現在実行時点でのスタックをそのまま使います。

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

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

  return f;
}

新しいファイバを作ります。ファイバのコントロールブロックとスタックを合わせた
ものは呼び出し元から設定されたのを使います。
|            fiber_local_store (size byte)        |
| struct fiber |           stack               |PC|

PCの領域にこのファイバがはじめるべき関数のアドレスを入れておき、SPに設定。
fiber_switchの最後の'rte'で、そこにジャンプしてくれます。

void *
round_buffer_4 (uint8_t *b, size_t *sz)
{
 // aligned to 4byte.
  addr_t f = ROUND ((addr_t)b, 4);
  *sz = TRUNC ((addr_t)b + *sz, 4) - f;

  return (void *)f;
}

fiber用の領域を__aligned (4)で設定してもいいのだけど、一応。


テストプログラム。やる事に対してこれは、メモリのかなりの無駄だし、遅い。
あくまでテスト用です。このくらいならファイバなんてやらずにswitch文です。
そう考えると、この先使う事があるのかどうか微妙ではある。

void thread_test (void);
void button_0 (void);
void button_1 (void);
void button_2 (void);
void button_put_thread (int);

uint8_t rbuf[16];
ringbuffer_t rb;
struct
{
  struct monitor *mon;
  int data;
} button;
void button_put (int, int);
int button_get (int);
int button_data;

fiber_t fiber[4]; ファイバコンテキストの参照用です。
uint8_t fls[3][128]; ファイバコンテキストです。 ファイバ0はスレッドスタックを
そのまま使うので、残りの3つしか用意しません。

void
thread_test ()
{
  int id;
  struct fiber myself;

  fiber[0] = fiber_init (&myself);
ファイバを使うにあたって、自分用のファイバコンテキストブロックを設定します。

  fiber[1] = fiber_create (fls[0], 128, button_0);
  fiber[2] = fiber_create (fls[1], 128, button_1);
  fiber[3] = fiber_create (fls[2], 128, button_2);
ボタンそれぞれの分に三つ作ります。

  rb = ringbuffer_init (rbuf, 16);
メッセージ伝達テスト用のリングバッファーです。

  button.mon = monitor_allocate ();
  thread_create (button_put_thread, &id, 0);
  thread_start (id);
ボタンを監視して、入力に変化があったらリングバッファーにつっこむスレッドです。

#if 1
  *ITU_TSTR = 0;	// All timer stop.
  *ITU0_TCR = ITU__TCR_ICLK8;	// .08us (CPU 25MHz) ->OVF 5.242 msec
  *ITU0_TIOR = 0; // compare match. don't output.
  *ITU0_TIER = ITU__TIER_OVIE;
  *ITU0_TCNT = 0;
  *ITU_TSTR |= ITU_TSTR_STR0;	// timer 0 start

  *ISCR |= 0x30;		// Falling edge.
  *IER |= 0x30;
  intr_enable ();
#endif
 タイマ割り込みでラウンドロビンさせます。

  while (/*CONSTCOND*/1)
    {
      button_data = button_get (PS_0 | PS_1 | PS_2);
      button_getは入力がなければブロックします。
      ボタンの入力があったところで起きます。

      SCI_PRINTF ("fiber dispatch\n");
ボタンそれぞれのファイバにスイッチしていきます。
      fiber_switch (fiber[0], fiber[1]);
全てのファイバがスイッチされるとここに戻ってきます。

      SCI_PRINTF ("fiber dispatch done.\n");

      thread_debug_stack_check ();
    }
}

void
button_put (int data, int channel)
{

  monitor_enter (button.mon);
  button.data = data;
  monitor_signal (button.mon, channel);
  monitor_exit (button.mon);
}

int
button_get (int channel)
{
  int data;

  monitor_enter (button.mon);
  monitor_wait (button.mon, channel);
  data = button.data;
  monitor_exit (button.mon);

  return data;
}

void
button_put_thread (int arg)
{
  int id;

  id = thread_id ();
  SCI_PRINTF ("thread %d(sender): arg=%d\n", id, arg);
  while (/*CONSTCOND*/1)
    {
      int button;
      if ((button = push_switch ()) != 0)
	{
	  SCI_PRINTF ("thread %d: SEND %d channel %d\n", id, button, button);
	  button_put (button, button);
	  //	  thread_switch ();
      ボタンに変化があったら、ボタン状態を送ります。
	}
    }
}

char write_msg[] = "/usr/pkg/bin/h8300-hms-gcc -mh -fomit-frame-pointer -mquickcall -O -mrelax -Wall -Werror -DDEBUG -I/home/uch/h8/w -I/home/uch/h8/w/h8/3052 -I/home/uch/h8/w/include -Wp,-MD,.deps/thread_test.P -c -o thread_test.o ud01/thread_test.chello ohayo";

void
button_0 ()
{
  int id;
  char *p = write_msg;

  id = thread_id ();

  SCI_PRINTF ("thread %d fiber 1 (receiver):\n", id);
  while (/*CONSTCOND*/1)
    {
      if (button_data & PS_0)
	{
	  SCI_PRINTF ("thread %d fiber 1: RECV %d\n", id, button_data);
	  lcd_clear (0x00);
	  led (0, TRUE);
	  led (1, FALSE);
	  SCI_PRINTF ("write: %c\n", *p);
	  ringbuffer_write (rb, p, 8);
      !!!ここで寝る可能性があります。
	  p += 8;
      ボタン0が押されていたら、メッセージを8byte送ります。

	}
      fiber_switch (fiber[1], fiber[2]);
      次のファイバに移ります。
    }
}

char read_msg[1024];

void
button_1 ()
{
  int id;
  char *p = read_msg;

  id = thread_id ();

  SCI_PRINTF ("thread %d fiber 2 (receiver):\n", id);
  while (/*CONSTCOND*/1)
    {
      if (button_data & PS_1)
	{
	  SCI_PRINTF ("thread %d fiber 2: RECV %d\n", id, button_data);
	  lcd_home ();
	  led (0, FALSE);
	  led (1, TRUE);
	  size_t sz = ringbuffer_read (rb, p, 64);
      !!!ここで寝る可能性があります。
	  SCI_PRINTF ("read %d:%s\n",sz, read_msg);
	  p += sz;
      ボタン1が押されていたら、メッセージを64byteまで読みます。
	}
      fiber_switch (fiber[2], fiber[3]);
      次のファイバに移ります。
    }
}

void
button_2 ()
{
  int id;

  id = thread_id ();

  SCI_PRINTF ("thread %d fiber 3 (receiver):\n", id);
  while (/*CONSTCOND*/1)
    {
      if (button_data & PS_2)
	{
	  SCI_PRINTF ("thread %d fiber 3: RECV %d\n", id, button_data);
	  lcd_test ();
      ボタン2が押されていたら、LCDのテストをしてみます。
	}
      fiber_switch (fiber[3], fiber[0]);
      次のファイバに移ります。(元に戻る)
    }
}

thread_init 0: 83b5c-83960
thread_init 1: 83d5c-83b60
thread_init 2: 83f5c-83d60
thread_init 3: 8415c-83f60
thread_init 4: 8435c-84160
thread_init 5: 8455c-84360
thread_init 6: 8475c-84560
thread_init 7: 8495c-84760
thread create 1: pc=827a4 sp=83d5c
thread 1(sender): arg=0
thread 1: SEND 2 channel 2
fiber dispatch
thread 0 fiber 1 (receiver):
thread 0 fiber 1: RECV 2
write: /
W8 0 8                ここでbutton_0がリングバッファに8個書いた。
thread 0 fiber 2 (receiver):
thread 0 fiber 3 (receiver):
fiber dispatch done.
thread 1: SEND 4 channel 4
fiber dispatch
thread 0 fiber 2: RECV 4
R8 8 16
read 8:/usr/pkg       ここでbutton_1がリングバッファから8個読みだした。
fiber dispatch done.
thread 1: SEND 2 channel 2
fiber dispatch
thread 0 fiber 1: RECV 2
write: /
W0 8 8
fiber dispatch done.
thread 1: SEND 4 channel 4
fiber dispatch
thread 0 fiber 2: RECV 4
R0 0 16
read 8:/usr/pkg/bin/h83
fiber dispatch done.
thread 1: SEND 4 channel 4
fiber dispatch

こうやってリングバッファがスリープに入らないうちはいいけれど、読み出し
か書き込みかがリングバッファでブロックされると終了。ファイバはスケジュー
リングの対象にならないからだ。寝たところを起すところが、次のファイバだ
からデッドロックしてしまうのだ。ファイバをスレッドにすればスケジューリ
ングの対象になるから大丈夫。

スレッドなしで(ファイバのみ)周期的に同期をとる、散逸したルーチンにはファ
イバはいいとは思う。