makecontext()でマルチタスクっぽく

つい最近まで知らなかったのですが、プログラムのコンテキスト操作用に getcontext(), setcontext(), makecontext(), swapcontext() というCの関数があるそうです(@ucontext.h)。面白そうだったので遊んでみました。

getcontext(), setcontext() はコンテキストの取得と設定を行うものです。古くからある setjmp(), longjmp() っぽい関数なのでイメージしやすいかと思います。

makecontext() は当該コンテキストとそこで実行したい関数を紐付ける(という解釈で良い?)関数、swapcontext() は各コンテキスト間を簡単に切り替えてくれる関数です。

使用例として manpage of makecontext のサンプルが分かりやすいので、こちらを見てみるとイメージしやすいかと思います。func2() の途中で、func1() の実行に切り替わり、再度 func2() に切り替わったりしています。

で、これを少し弄っていたところ「マルチタスクっぽいのが作れるのでは?」と思ったので、前述のサンプルを元にちょっとチャレンジしてみました。

方針は以下の通り。

  • 数本のコンテキスト(getcontext()で作成)と専用のスタック用メモリを用意。
  • 一定回数ループしながら標準出力する関数を上記のコンテキストと紐付け(makecontext()で)。
  • 数百ミリ秒毎にSIGALRMを発生させ(setitimer()で)、その度にコンテキストを切り替える(swapcontext()で)。

そんなこんなで、こんな感じのコードがかけました。

#include <ucontext.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys>

#define STACK_NUM 5

#define handle_error(msg) \
  do { perror(msg); exit(EXIT_FAILURE); } while (0)

static struct {
  int pos;
  struct {
    ucontext_t ctx;
    char stack[SIGSTKSZ];
  } proc[STACK_NUM];
} uctx;

static void next_uctx(int dummy)
{
  int old_uctx_pos = uctx.pos;

  uctx.pos = (uctx.pos + 1) % STACK_NUM;

  if (swapcontext(&amp;uctx.proc[old_uctx_pos].ctx, &amp;uctx.proc[uctx.pos].ctx) == -1)
    handle_error("swapcontext");
}

static void func(int id)
{
  int i, j;

  for(i = 0; i &lt; 10; i++) {
    for(j = 0; j &lt; 1000000000; j += (id + 1));
    printf("[id:%d]-[i:%d]-[j:%d]\n", id, i, j);
  }
}

int main(int argc, char *argv[])
{
  int i;
  ucontext_t uctx_main;
  struct sigaction sa, old_sa;
  struct itimerval itval;

  sa.sa_handler = next_uctx;
  sa.sa_flags = SA_RESTART;
  sigemptyset(&amp;sa.sa_mask);
  if (sigaction(SIGALRM, &amp;sa, &amp;old_sa) == -1)
      handle_error("sigaction");

  itval.it_interval.tv_sec = 0;
  itval.it_interval.tv_usec = 200 * 1000;
  itval.it_value = itval.it_interval;
  if (setitimer(ITIMER_REAL, &amp;itval, 0) == -1)
      handle_error("setitimer");

  for(i = 0; i &lt; STACK_NUM; i++) {
    ucontext_t *ucp = &amp;uctx.proc[i].ctx;

    if (getcontext(ucp) == -1)
      handle_error("getcontext");

    ucp-&gt;uc_stack.ss_sp = uctx.proc[i].stack;
    ucp-&gt;uc_stack.ss_size = sizeof(uctx.proc[0].stack);
/* ucp-&gt;uc_link = (i &gt;= STACK_NUM - 1) ? &amp;uctx_main: &amp;uctx.proc[i + 1].ctx; */
    ucp-&gt;uc_link = &amp;uctx_main;
    makecontext(ucp, (void (*)(void))func, 1, i);
  }

  printf("----- start ----- \n");

  if (swapcontext(&amp;uctx_main, &amp;uctx.proc[0].ctx) == -1)
    handle_error("swapcontext");

  printf("----- end ----- \n");

  exit(EXIT_SUCCESS);
}

動きが分かりやすいように後半に実行する処理の処理間隔を短くして、標準出力の間隔をずらしています。

実行してみると・・・

----- start -----
[id:4]-[i:0]-[j:1000000000]
[id:3]-[i:0]-[j:1000000000]
[id:2]-[i:0]-[j:1000000002]
[id:4]-[i:1]-[j:1000000000]
[id:1]-[i:0]-[j:1000000000]
[id:3]-[i:1]-[j:1000000000]
[id:4]-[i:2]-[j:1000000000]
[id:2]-[i:1]-[j:1000000002]
[id:3]-[i:2]-[j:1000000000]
[id:4]-[i:3]-[j:1000000000]
[id:0]-[i:0]-[j:1000000000]
[id:1]-[i:1]-[j:1000000000]
[id:2]-[i:2]-[j:1000000002]
[id:3]-[i:3]-[j:1000000000]
[id:4]-[i:4]-[j:1000000000]
[id:4]-[i:5]-[j:1000000000]
[id:3]-[i:4]-[j:1000000000]
[id:2]-[i:3]-[j:1000000002]
[id:4]-[i:6]-[j:1000000000]
[id:3]-[i:5]-[j:1000000000]
[id:1]-[i:2]-[j:1000000000]
[id:4]-[i:7]-[j:1000000000]
[id:2]-[i:4]-[j:1000000002]
[id:3]-[i:6]-[j:1000000000]
[id:4]-[i:8]-[j:1000000000]
[id:0]-[i:1]-[j:1000000000]
[id:1]-[i:3]-[j:1000000000]
[id:2]-[i:5]-[j:1000000002]
[id:3]-[i:7]-[j:1000000000]
[id:4]-[i:9]-[j:1000000000]
----- end -----

コンテキストid0〜id4がまばらに処理されていて、後半のコンテキスト程、標準出力間隔が短くなっているのが分かります。プリエンプティブっぽく処理がマルチに実行されているようです。

今回の実験コードでは、全然深いところまで考慮していないので汎用的に使えるレベルには程遠いのですが、限定された状況でこじんまりしたマルチタスクを行いたい場合など、選択肢の一つになりうるのではないかな、と思いました。