事務処理に必須の「ファイル・ロッキング」

事務処理に必須で、しかも、まとまった情報の少ない、 UNIX システムのファイ ル・ロッキングについて、その原理と手法を解説します。この解説は、この問題の すべてをカバーするわけではありませんが、現実に発生するすべての問題に対処す ることができます。

creat(), link(), open() 等のシステム・コールについては、 UNIX プログラミ ング・マニュアルの2章を併読するとわかります。POSIX 1003.1 のロック機構に ついては、規格書だけではわかりにくいと思いますので、詳述しました。

ファイル・ロッキングの必要性

「UNIX」は、マルチ・ユーザ/マルチ・タスクの OS ですから、もともと、複数の ユーザが協力して、同じ仕事をする形態の事務処理に適しているわけですが、UNIX 成功の基盤となった、
   一貫したテキストファイルの採用による、汎用ツールの実現
   コマンドの直交性とパイプラインによる、無駄のない柔軟な現実対応
   正規表現による、簡潔で効率的なパターン認識
の原理は、この分野に対する奇跡的な環境を生み出しました。コンピュータの文献 や、雑誌等の情報には、ほとんどでてこない話題ですが、本質的な理解に到達した ユーザの間では、コンピュータのアプリケーションとしては、例外的な成功を納め ています。

この種のアプリケーションの特徴は、会計データや在庫データを含む、「同じファ イルを多数のユーザが更新し参照する」ということで、「ファイル・ロッキング」 (file locking)、あるいは、「排他制御」(mutual exclution) と呼ばれる技法が 不可欠になります。

例えば、次のような例を考えてください。

  1) 一人のユーザが、在庫データを更新するため、在庫のマスタファイルを
     ディスクから読んで、データを更新し、書き換える。
  2) 上記のユーザがディスクのデータを書き換える前に、他のユーザが同じ
     データを読んで更新し、上記のユーザがディスクを書き換えた後で、自
     分もディスクを書き換える。
これで、いとも簡単に、最初のユーザのデータは、失われてしまいますが、このよ うな状況のバリエーションは、たくさんあります。

この種の問題は、 UNIX のような OS の「内部資源の共有」に対しても発生し、 古くから研究されてきましたが、大別して、次の二つの手法があります。

   資源を占有するデーモン・プロセス (daemon) の利用
   対象とする資源の排他制御の実施
前者は、いわゆる「クライアント・サーバー・モデル」(cleient-server model) と呼ばれるもので、 UNIX の世界では、守護神のイメージから、サーバー・プロ セスのことを「デーモン」(daemon) と呼んでいます。例えば、在庫データの更 新と照会の窓口を一手に引き受けるデーモン(サーバー)を常時稼働させ、他の プログラムはそのプログラムに、共有ファイルの更新と照会を依頼するという仕組 みです。つまり、管理者を通すことにより、「共用」そのものを避けてしまう手法 で、プログラミングは簡単になりますが、官僚機構と同じで、管理の対象が増える に従って、常駐プログラムが増え、収拾がつかなくなります。

後者の方法は、すべてのプログラムが対等に資源を共用し、お互いに矛盾がでない ように動作させるというもので、管理対象が多い場合には、非常に効率の良いプロ グラムが作れますが、排他制御が必要になり、それがこの解説のテーマです。

排他制御は、競合状態 (race condition) を避けるために、更新等の「競合」を 生ずるクリティカル(critical)な処理を行う間、他のプロセスによるファイル・ アクセスを禁止するもので、基本的には、次のようなプログラム構造になります。

  lock();       /* 他のプロセスによる、ファイル・アクセスを禁止 */
  critical_secion();  /* データ更新(書換) */
  unlock();      /* ファイルの占有を解除 */
lock() は、自分が更新中のファイルに、他のプロセスがアクセスできないように、 「施鍵」(lock) するというイメージです。更新が終わったら、鍵を外して (unlock)、他のプロセスが、再びそのファイルを使えるようにします。

この lock()/unlock() を実現する方法としては、「TSL (TEST AND SETLOCK)」命 令や「セマフォ (semaphore)」等、いろいろなものが考案されてきましたが、ここ では、 UNIX で利用可能で、容易かつ効率的な手法について、解説を試みること にします。

UNIX の伝統的な手法

前記の lock()/unlock() を実現するために、 UNIX の世界で、古くから使われて きた方法は、次のようなもので、すべて、「ファイルの存在」が、排他制御のため の「フラグ (flag)」ないし「セマフォ」(信号機) として使われます。

creat() の利用

一般ユーザが、既に存在する、書き込み許可のないファイルに対して creat() シ ステム・コールを実行するとエラーになることを利用したもので、例えば、次のよ うなコードになります。


  #include <errno.h>

  lock(file) char *file;
  {
    extern int errno;
    int fd;

    while ((fd = creat(file, 0)) < 0) {
          if (errno != EACCES)
                  error("creat() error (%d)", errno);
          sleep(1);
    }
    return close(fd);
  }

  unlock(file) char *file;
  {
    return unlink(file);
  }

このようなコーディングは、昔の UNIX コマンドのあちこちに見られますが、こ の方式を使うときの注意点は、「スーパーユーザに対しては、排他制御がきかない」 ということです。理由は、スーパーユーザの場合は、書き込み禁止が無効になるた め、creat() がエラーにならないことにあります。そのため、この方式を使う場合 は、プログラムの中に、利用者がスーパーユーザかどうかをチェックする機能が必 要になることがあります。getuid(), geteuid() のシステム・コールが、この目的 で使われます。

この方法の利点は、すべての UNIX で使えることです。access() のシステム・ コールで、ファイルの存在をチェックして、存在しない場合は、作成するという 方法では、うまくゆかないことに注意してください。存在しないことを確認してか ら、作成するまでの間に、他のプロセスも同じ判断をして、同じファイルを作って しまうことがあるからです。このような難しさそのものが、排他制御を必要としま す。

link() の利用

上記と良く似た考え方で、同一名の「リンク」(link) は一つしか作れないことを 利用したものです。例えば、次のようになります。


  #include <errno.h>

  lock(file) char *file;
  {
    extern int errno;
    int fd;
    char tmpfile[32];

    sprintf(tmpfile, "/tmp/LCK%d.tmp", getpid());
    if ((fd = creat(tmpfile, 0444)) < 0)
          error("can't creat %s", tmpfile);
    close(fd);

    while (link(tmpfile, file) < 0) {
          if (errno != EEXIST)
                  error("link() error (%d)", errno);
          sleep(1);
    }

    if (unlink(tmpfile) < 0)
          error("can't unlink %s", tmpfile);

    return 0;
  }

  unlock(file) char *file;
  {
    return unlink(file);
  }

この方法は、少しまだるっこしいのですが、スーパーユーザに対しても使えるのが 利点です。ただし、リンクそのものが、BSD-UNIX で実現された「シンボリック・ リンク」(symbolic link) を別にすると、同じファイル・システム内でしか使えま せんから、汎用性がありません。そのため、移植性のない BSD-UNIX 固有のプロ グラムを書く人に好まれる手法です。

open() の利用

最近の UNIX で実装された、新しい open() の O_EXCL フラグを利用する方法 で、既に存在するファイルに対する open(file, O_EXCL|O_CREAT, pmode) がエ ラーになるという機能を利用したものです。O_EXCL フラグは、ひとつのプロセス だけが、新たにファイルを作成できることを保証します。

例えば、次のようになります。


  #include  <fcntl.h>
  #include  <errno.h>

  lock(file) char *file;
  {
    extern int errno;
    int fd;

    while ((fd = open(file, O_RDWR | O_CREAT | O_EXCL, 0666))  < 0) {
          if (errno != EEXIST)
                  error("open() error (%d)", errno);
          sleep(1);
    }
    return close(fd);
  }

  unlock(file) char *file;
  {
    return unlink(file);
  }

最初の二つの方法で問題になった、ユーザとファイルシステムに対する制限は克服 されていますが、システムの故障や、ロックファイルを作ったプロセスの中断等に より、「デッドロック」(deadlock) になったり、次にシステムをたちあげたとき、 プログラムが稼働できなくなったりすることがあります。

そのため、ロックファイルを使うときは、「/etc/rc」等に、システムの起動時に、 すべてのロックファイルを消去する手続きを入れておくのが普通です。

この方法は、最近の UNIX のほとんどで使えます。

POSIX 1003.1 の手法

UNIX の新しい標準である、「POSIX 1003.1」でも、今までの方法が使えるのは当 然ですが、fcntl() による新しい排他制御が使えるようになりました。例えば、次 のようになります。


  #include  <sys/types.h>
  #include  <fcntl.h>
  #include  <stdlib.h>

  lock(fd)
  {
    extern int errno;
    struct flock lock;

    lock.l_type = F_WRLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start = (off_t) 0;
    lock.l_len = (off_t) 0;
    if (fcntl(fd, F_SETLKW, &lock)  < 0)
          error("fcntl(): F_SETLKW failed (%d)", errno);
    return 0;
  }

  unlock(fd)
  {
    extern int errno;
    struct flock lock;

    lock.l_type = F_UNLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start = (off_t) 0;
    lock.l_len = (off_t) 0;
    if (fcntl(fd, F_SETLKW, &lock)  < 0)
          error("fcntl(): F_SETLKW failed (%d)", errno);
    return 0;
  }

この方法は、余分なファイルを必要としませんから、とてもすっきりします。以下、 この POSIX 1003.1 のファイル・ロッキングについて、もう少し詳しく解説します。

まず、struct flock のメンバのうち、l_type に指定できるのは、次の 3つのい ずれかです。

  F_RDLCK リードロック read lock (シェアードロック shared lock)
  F_WRLCK ライトロック write lock (排他ロック exclusive lock)
  F_UNLCK アンロック (上記ふたつのロックを解除)

F_WRLCK は、ファイルの特定の領域を、ひとつのプロセスだけがロックできるよう にするためのもので、これは、排他制御そのものですから、「排他ロック」 (exclusive lock) と呼ばれることがあります。前記の creat(), link(), open() を使った方式に比べて、ファイルの更新に必要な一部分だけをロックすることがで きますから、更新の場所が違う限り、複数のプロセスが同時に、同じファイルにア クセスすることが可能で、データ処理の効率が良くなります。F_WRLCK は、対象 とするファイルに、書き込みのためのパーミションを必要とします。

F_RDLCK の方は、通常、ファイルの読み取りに使用され、ファイルの更新を禁止す るために使用します。この場合は、既に他のプロセスが F_RDLCK した部分に、オ ーバーラップした F_RDLCK をかけることができます。もちろん、F_WRLCK された 部分に F_RDLCK を書けることはできません。また、F_RDLCK された部分に、 F_WRLCKをかけることもできません。つまり、書き込みのためのロックと違って、 ファイルのロックされた部分を複数のプロセスが、読み込みのために共有すること ができるため、「シェアードロック」(shared lock) と呼ばれることがあります。 もちろん、これを使うためには、リードのパーミションが必要です。

F_UNLCK は、F_RDLCK や F_WRLOCK によるロックの解除に使います。これらの定数 は、<fcntl.h> で定義されます。

ファイルのロック範囲は、l_whence と l_start, l_len で指定しますが、 l_whence がその原点で、l_start がロック区間の先頭、l_len が長さになります。

l_whence は、lseek() と同じ発想で、次の 3 つのいずれかを指定します。

  SEEK_SET ファイルの始めから
  SEEK_CUR ファイルの現在位置から
  SEEK_END ファイルの終わりから

l_start と l_len がそれぞれ、原点から数えた、ロック区間の先頭と、長さにな りますが、いずれもバイト数を off_t (普通は long) 型で指定します。l_len を 0 にすると、ファイルの全区間がロックの対象になります。また、ロック区間は、 現在のファイルの終わりを越えて、もっと後の方まで指定することができます。

struct lock を引き数とした fcntl() のファイル・ロッキング用コマンドには、 次の 3 つがあります。

  F_GETLK  既存のロック状態を調べる
  F_SETLK  ロックを実施
  F_SETLKW ロックが成功するまで待つ

まず、l_type, l_whence, l_start, l_len に、自分が必要とする方式と区間をセ ットして、F_GETLK コマンドの fcntl() を実行すると、次のいずれかの結果が得 られます。

   指定区間がロック可能なら、l_type に F_UNLCK がセットされ、
       その他のメンバは、変わりません。
   指定区間が既存のロックにかかる場合は、l_whence, l_start, l_len
       に既存のロック区間がセットされ、ロックを実施したプロセス
       ID が l_pid にセットされます。
struct flock のメンバである l_pid は、これ以外の用途には使われません。

F_GETLK で注意すべき点は、このコマンドで、ロックされていないことを確認した としても、実際にアクセスするときまでに、他のプロセスによってロックされる可 能性があるということです。つまり、F_UNLCK になったといっても、安心できま せん。本当に、ロックされていないかどうかの確認は、自分がロックしてしまう以 外にないのです。

F_SETLK と F_SETLKW は、共にファイルにロックをかけるコマンドですが、前者は、 既に他のプロセスによりロックされているため、ロックをかけるのに失敗した場合 は、errno に EACCES か EAGAIN (いずれになるかは、システム依存) をセットし て戻ることになっていますので、ロックできない場合は、他の仕事をするような構 造のプログラムで使われます。ただし、恋人の代用品を探す難しさがあります。

F_SETLKW の方は、ロックに成功するか、signal による割り込みがかかるまで待ち ますから、このメカニズムの中では、最も使いやすいコマンドです。デッドロック からの回復や、異常に長い時間待たされたときの対策に、alarm() を組み合わせて 使うこともあります。シグナルで中断された場合は、他のシステムコールと同様に、 EINTER を errno 外部変数にセットして、-1 が戻ります。

F_SETLK, F_SETLKW によるロックは、対象となるファイルが close() された場合、 自動的に解除されますので、creat(), link(), open() によるロック方式のように、 ロックファイルが残る危険がありません。

ファイル・ロックの方式

今まで述べた、すべてのロック方式は、目的のファイルに対するアクセスそのもの を禁止するわけではありません。単に、「そのファイルはロックされていますよ」 ということを、OS が教えてくれるだけで、ユーザプロセスは、その「勧告」を無 視して、読み込みや書き込みを強行することができます!

例えば、上記の方式で動作する「在庫管理プログラム」がマスタファイルを更新し ているとき、ファイル・ロッキングなど気にもかけない「エディタ」で、マスタフ ァイルを変更するといった事態を防ぐことはできないのです。

そのため、この方式のロック機構は「advisory file locking」(勧告方式の施鍵) と呼ばれます。

これに対して、対象とするファイルそのものに施鍵する方式を「Mandatory locking」(本質的施鍵) と呼び、この方式を実現したシステムもありますが、 「POSIX 1003.1」では、いくつかの副作用を懸念して、採用しませんでした。この 方式のロックは、「強制モード」(enforcement-mode) のロックと呼ばれることが あります。

強制ロックを必要とすることは、めったにありませんが、どうしてもという場合は、 chmod() システム・コールを併用して、実現することができます。

デッドロック

排他制御につきまとう問題のひとつに「デッドロック」(deadlock) があります。 これは、プロセスが身動きつかなくなる状態で、例えば、次のケースを考えてみて ください。

  最初のプロセスが、単価ファイルをロックし、次に、発注先
      ファイルをロックしようとする。
  別のプロセスが、上記のプロセスが発注先ファイルをロック
      する前に、発注先ファイルのロックに成功し、次に単価
      ファイルをロックしようとする。
これで、この二つのプロセスは、にっちもさっちもゆかなくなります。

この解決には、いくつかの方法がありますが、例えば、

  ファイルをロックする順番を決める。
  複数のファイルをロックする場合は、二つ目以降のロックに失敗した時、
      それまでのロックを解除する。
といった手法が、よく使われます。複数のロックを併用する場合は、注意が必要で す。

シグナルの処理

ファィル・ロッキングを行おうとするとき、ロックが完了する前、あるいは、ロッ クを解除する前に、端末からの割り込みが入るとか、プロセスが kill されるとい った場合があります。

問題になるのは、シグナルによってプロセスが停止したとき、ロックファイルが残 ってしまうケースで、ロック・ファイルを使う場合は、次のような「クリーン・ア ップ」(clean up) 処理のプログラムを入れておくのが普通です。


  #include  <signal.h>

  void onint();
  char *lock_file;        /* ロックファイル名 */

  ..
  signal(SIGINT, onint);  /* 端末からの割り込み */
  signal(SIGHUP, onint);  /* モデムの回線断 */
  signal(SIGTERM, onint); /* kill() によって殺されたとき */
  ..

  void onint(sig)
  {
    unlink(lock_file);
            exit(0);
  }

以上で、ファイル・ロッキングの代表的な手法と問題点を解説しましたので、以下、 この応用例について述べます。

一連番号を求めるプログラム

注文書や納品書、見積書のように、組織として、ユニークな一連番号を得たいとい う場合は、システムのどこかに、その番号を記憶したファイルを維持する必要があ りますが、この場合も、排他制御が必要です。

POSIX 1003.1 のファイル・ロックを利用したプログラムの一例は、次のようになり ます。従来の方式と違って、余分なファイルを必要としない、とてもシンプルな構 造になっていることに、注意してください。 MINIX で勉強される方は、1.6 以降 のバージョンが必要になります。古い UNIX の場合は、lock()/unlock() を、先 に述べた方法に変更する必要があります。

多くの場合、この中の seqno() 関数だけが、注文書の発行等、他のプログラムの 一部に組み込まれて使われることになりますが、このように独立したプログラムは、 「sh スクリプト」で使えるのが利点で、現場のユーザが簡単に利用できる点で、 大いに役立つことがあります。

また、ここでは、事務用の「ナンバー・リング」のように、単に一つの数値を記憶 するファイルが使われていますが、「キー」と「一連番号」をセットで使うことに より、発注、受注、仕訳等、複数の管理対象の識別番号を一つのファイルに入れて おくことも可能です。

POSIX のファィル・ロッキングが使えない場合は、lock(), unlock() を、前記の 他の方法のいずれかに変更します。


  /*
   * seqno - get a unique sequential number
   */
  #include  <sys/types.h>
  #include  <fcntl.h>
  #include  <stdio.h>
  #include  <errno.h>
  #include  <unistd.h>

  #define SEQFILE "/usr/etc/SEQNO"  /* file to store sequential-No. */

  char buf[BUFSIZ];

  main(argc, argv) char **argv;
  {
    int n;

    if ((n = seqno()) == -1)
          return 1;
    printf("%d\n", n);
    return 0;
  }

  seqno()
  {
    int fd, i, j, n;
    char buf[32];
    extern errno;

    if ((fd = open(SEQFILE, O_RDWR | O_CREAT, 0664)) == -1)
          return -1;
    lock(fd);
    if ((i = read(fd, buf, sizeof(buf))) == -1)
          return -1;
    else if (i == 0)
          sprintf(buf, "0\n");    /* just created */

    n = atoi(buf);
    sprintf(buf, "%d\n", n+1);    /* next available number */
    j = strlen(buf);
    lseek(fd, 0L, SEEK_SET);      /* rewind */
    if ((i = write(fd, buf, j)) != j)
          return -1;
    unlock(fd);
    close(fd);
    return n;
  }

  lock(fd)
  {
    struct flock lock;

    lock.l_type = F_WRLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start = (off_t) 0;
    lock.l_len = (off_t) 1;
    if (fcntl(fd, F_SETLKW, &lock)  < 0) {
          fprintf(stderr, "lock(): F_SETLKW failed\n");
          return -1;
    }
    return 0;
  }

  unlock(fd)
  {
    struct flock lock;

    lock.l_type = F_UNLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start = (off_t) 0;
    lock.l_len = (off_t) 1;
    if (fcntl(fd, F_SETLKW, &lock)  < 0) {
          fprintf(stderr, "unlock(): F_SETLKW failed\n");
          return -1;
    }
    return 0;
  }

コマンドの排他実行

ファイルの排他的アクセス以外に、自分自身を含めて、何かのコマンドを排他的に 実行したいという場合も多く、例えば、次のプログラムは、もともと排他的でない コマンドを排他的に実行させます。

このプログラムでは、オーソドックスなロック・ファイルを使う方式になっていま すから、例えば、


  $ xr ls /usr/tmp

を実行すると、このプログラムの実行中だけ、「/usr/tmp/ls.LCK」というファイル ができていることを確認することができます。

このプログラムは、open() を使った排他制御を使用しています。ユーザが指定し たコマンドを実行するプロセスは、シグナルで殺されては困りますので、fork() で分離した上で、シグナルを無視するようにしていることに注意してください。ロ ック・ファイルには、「/usr/tmp/コマンド名.LCK」という名前を使っています。 そのため、別のディレクトリにある同名のコマンドを、同時に実行することはでき ません。もしそれをしたい場合は、which() を生かして、絶対パス名で識別するよ うに変更してください。この関数は、指定されたコマンドの「絶対パス名」を求め ます。


  /*
   * xr - run command exclusively
   */

  #include  <sys/types.h>
  #include  <fcntl.h>
  #include  <signal.h>
  #include  <stdlib.h>
  #include  <string.h>
  #include  <unistd.h>
  #include  <errno.h>
  #include  <stdio.h>

  void usage();
  void error();
  void onint();

  char *file;
  char lockfile[128];
  char *cmd;

  int main(argc, argv)  char **argv;
  {
    int pid, status, w;

    cmd = argv[0];
    if (argc  < 2)
          usage();

    file = argv[1];
    sprintf(lockfile, "/usr/tmp/%s.LCK", file);
    signal(SIGINT, onint);
    signal(SIGHUP, onint);
    signal(SIGTERM, onint);
    lock(lockfile);
    if ((pid = fork()) == 0) {
          signal(SIGINT, SIG_DFL);
          signal(SIGHUP, SIG_DFL);
          signal(SIGTERM, SIG_DFL);
          execvp(file, ++argv);
          error("can't exec %s", file);
    }
    while ((w = wait(&status)) != pid && w != -1)
          ;
    if (w == -1)
          status = 1;
    unlock(lockfile);
    return status;
  }

  void onint()
  {
    unlock(lockfile);
    exit(1);
  }

  void usage()
  {
    fprintf(stderr, "Usage: %s command [arg ..]\n", cmd);
    exit(1);
  }

  void error(s, t) char *s, *t;
  {
    fprintf(stderr, "%s: ", cmd);
    fprintf(stderr, s, t);
    fprintf(stderr, "\n");
    exit(1);
  }

  lock(file) char *file;
  {
    extern int errno;
    int fd;

    while ((fd = open(file, O_RDWR | O_CREAT | O_EXCL, 0666))  < 0) {
          if (errno != EEXIST)
                  error("open() error (%d)", errno);
          sleep(1);
    }
    return close(fd);
  }

  unlock(file) char *file;
  {
    return unlink(file);
  }

  #if 0
  char *which(cmd) char *cmd;
  {
    static char pathnam[512];
    char *path, *s, *t, *getenv();

    if (strchr(cmd, '/') != NULL) {
          strcpy(pathnam, cmd);
          if (access(pathnam, 1) == 0)
                  return pathnam;
    }
    else {
          if ((path = getenv("PATH")) == NULL)
                  path = ".:/bin:/usr/bin";
          for (s = path; *s; ) {
                  for (t = pathnam; *s && *s != ':'; )
                          *t++ = *s++;
                  *t++ = '/';
                  strcpy(t, cmd);
                  if (*s == ':')
                          s++;
                  if (access(pathnam, 1) == 0)
                          return pathnam;
            }
    }
    return (char *)0;
  }
  #endif

排他制御の実験

最後に、排他制御の効果を実感するための実験をしてみましょう。[6] の一連番号 を作るプログラムに少し細工をして、排他制御をしないようにするためのオプショ ン「-d」を追加し、同時に、衝突が起き易いように、DELAY() を挿入して、動作を 遅くします。

例えば、


  $ sn & sn

を実行すると、次のようになって、
    0, pid(189)
    1, pid(189)
    3, pid(189)
    2, pid(188)
    5, pid(188)
    4, pid(189)
    7, pid(189)
    6, pid(188)
    8, pid(189)
   10, pid(189)
    9, pid(188)
   12, pid(188)
   11, pid(189)
   14, pid(189)
   13, pid(188)
   15, pid(189)
   16, pid(188)
   17, pid(188)
   18, pid(188)
   19, pid(188)
排他制御の結果、複数のプロセスが、それぞれ重複しない、一連番号を手に入れる ことができますが、排他制御を禁止して、

  $ sn -d & sn -d

を実行すると、例えば、次のようになって、
    20, pid(180)
    20, pid(181)
    21, pid(181)
    21, pid(180)
    22, pid(181)
    22, pid(180)
    23, pid(180)
    23, pid(181)
    24, pid(181)
    24, pid(180)
    25, pid(181)
    26, pid(181)
    25, pid(180)
    26, pid(180)
    27, pid(181)
    27, pid(180)
    28, pid(180)
    28, pid(181)
    29, pid(181)
    29, pid(180)
得られた番号が重複してしまいます。

実験用プログラムは、次のとおりですが、POSIX のロッキング機構が使えない場合 は、前記のいずれかの方法に書き換えてください。また、フロッピー・ディスクか らの起動のように、プログラムの起動に時間がかかる場合は、DELAY() の時間を長 くする等、ご自分のシステムに合わせて、調整してください。


  /*
   * sn - get a unique sequential number
   */

  #include  <sys/types.h>
  #include  <fcntl.h>
  #include  <stdio.h>
  #include  <errno.h>
  #include  <unistd.h>

  #define SEQFILE         "SEQNO"

  int dflg;       /* disale mutual exclution */

  main(argc, argv) char **argv;
  {
    char *s;
    int i, n, pid;

    while (--argc > 0 && (*++argv)[0] == '-')
          for (s = argv[0]+1; *s; s++)
                  switch(*s) {
                  case 'd':
                          dflg++;
                          break;
                  }
    pid = getpid();
    for (i = 0; i  < 10; i++) {
          n = seqno();
          printf("%d, pid(%d)\n", n, pid);
    }
    exit(0);
  }

  seqno()
  {
    int fd, i, j, n;
    char buf[32];
    extern errno;

    if ((fd = open(SEQFILE, O_RDWR | O_CREAT, 0664)) == -1)
          return -1;
    if (!dflg)
          lock(fd);
    if ((i = read(fd, buf, sizeof(buf))) == -1)
          return -1;
    else if (i == 0)
          sprintf(buf, "0\n");    /* just created */

    DELAY();
    n = atoi(buf);
    sprintf(buf, "%d\n", n+1);    /* next available number */
    j = strlen(buf);
    lseek(fd, 0L, SEEK_SET);      /* rewind */
    if ((i = write(fd, buf, j)) != j)
          return -1;
    if (duflg)
          unlock(fd);
    close(fd);
    return n;
  }

  DELAY()
  {
    int i, j;

    for (i = 0; i  < 10000; i++)
          for (j = 0; j  < 10; j++)
                  ;
  }

  lock(fd)
  {
    struct flock lock;

    lock.l_type = F_WRLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start = (off_t) 0;
    lock.l_len = (off_t) 1;
    if (fcntl(fd, F_SETLKW, &lock)  < 0) {
          fprintf(stderr, "lock(): F_SETLKW failed\n");
          return -1;
    }
    return 0;
  }

  unlock(fd)
  {
    struct flock lock;

    lock.l_type = F_UNLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start = (off_t) 0;
    lock.l_len = (off_t) 1;
    if (fcntl(fd, F_SETLKW, &lock)  < 0) {
          fprintf(stderr, "unlock(): F_SETLKW failed\n");
          return -1;
    }
    return 0;
  }

ネットワーク固有の問題

以上、排他制御をもとにした、競合状態の対処法を述べてきましたが、更新すべき ファイルが他のマシンに於かれている、ネットワークの場合は、クライアント・サ ーバー・モデルを利用するのが、簡単です。

この場合は、サーバー側で、トランザクション (transaction) と呼ばれる、不可分 の「原子動作」(atomic action) を用意し、クライアントがトランザクションの開 始から終了を宣言するまでの間、他のクライアントへのサービスをしないようにす るのが普通です。トランザクションを中断する場合は、もとの状態に戻します。

原子動作というのは、他のプロセスによって中断されずに実行される、動作の最少 単位という意味ですが、これ以上分解できないという意味に、「原子」(atom)を使 うのは、かなり昔の発想で、コンピュータ学者は、物理学など勉強しないか、忘れ てしまったということでしょうか。

また、ネットワーク固有の、面白くて実用的な問題に、銀行間の資金移動の決裁等、 多数のシステムに分散するファイル更新がありますが、この場合は、CCR (commitment concurrency, and recovery) と呼ばれる手法を使うのが普通で、これ は「two-phase commit」と呼ばれる技法を基にしています。具体的には、例えば、 手形交換の場合、次のようになります。

  手形交換所は、資金の引き落とし銀行と送金先の銀行に、金額、相手
      先等の情報をしらせる。

  知らせを受けたシステムは、その要求が実行可能な場合、その要求を
      記憶すると共に、対象となるデータをロックし、成功の応答を
      返し、他行の失敗に備えて、初期状態も記憶する。預金不足や
      口座がない等の理由で、うまくゆかないときは、失敗の応答を
      返す。

  手形交換所は、すべての銀行から成功の応答が戻ったら、「コミット」
      (commit - 許可) メッセージを送って、更新を実行させる。いず
      れかの銀行が失敗した場合やクラッシュを検出した場合は、初期
      状態への復帰とロックの解除を指示する。

参考書

理論としての排他制御については、いろいろな文献がありますが、「MINIX のバイ ブル」と呼ばれている OS の解説書、

   Andrew S. Tanenbaum,- Operating System: Design and Impementation
   (Prentice-Hall, USA) ISBN: 0-13-637331-3
の 2.2 から 2.3 の解説と参考文献のリストが、良くできていると思います。和 訳は、
   大西照代訳,- MINIXオペレーティング・システム
   (アスキー出版) ISBN: 4-7561-0000-7
ですが、多くの訳書の例に洩れず、かなりの誤訳がありますから、「理解不能部分 は誤訳」という頭でつき合う必要があります。ただし、そうひどい訳書とも思えま せん。 MINIX の現在の版から見ると、少し古くなっていますが、2.0 が完成した 時点で、書き換えられる予定です。多分、POSIX 関連の記述が入ると思います。だ だし、現状のままでも、OS や UNIX 全般の解説としては、少しも古くないですし、 最良の本です。

上記から、 MINIX 固有の部分を除いて、より一般化した形で書かれたのが、

   Andrew S. Tanenbaum,- Modern Operating Systems
    (Prentice-Hall, USA) ISBN: 0-13-595752-4
です。USENET のニュースグループである「\tt{comp.os.minix} \rm」で「MS-DOS のことはまるでわからないし、恥ずかしいとも思わない」なんて書いていた著者が、 MS-DOS の明快な解説を書いているという、面白い本ですが、とにかく、よくでき ています。和訳は、
  引地信之、引地美恵子訳,- OSの基礎と応用
  (プレンティスホール) ISBN4-8101-8543-5
です。分散システムの記述が追加されました。

また、ネットワークの問題については、同じ著者の、

   Andrew S. Tanenbaum,- Computer Networks
    (Prentice-Hall, USA) ISBN: 0-13-166836-6)
が、良く読まれています。和訳は、以下の通りです。
   斉藤他訳,- コンピュータネットワーク
     (丸善) ISBN: 4-621-03699-8
平林 浩一, 1993