Operating System Lecture 6/4

Operating System Lecture 6/4

先週の復習 -- Scheduling

Process Synchronization

複数のプロセス(process)は、単一のプロセッサ(Processor)、また は、複数のプロセッサ上で動作している。このプロセスがおたがい に協調して動作するためには、なんらかの形で、プロセス同士のタ イミングをとる必要がある。これを、プロセスの同期 (Sychronization) と呼ぶ。



何故、同期が必要なのか?

プログラムが、要求された仕様を満たすことを 整合(consistent) という。 一般的な整合性(Consistency)は、並列に動作するプログラムが直列可能(Serializable)で ある場合にのみ得られる。並列環境下で直列可能性を保証するためには、 同期機構が必要となる。 (consitentの反対はinconsitent (矛盾)、従って、整合のことを無矛盾ともいう)

要求仕様の例

以下のような例を考えてみよう。

[操作A] 
a = count;
[操作B] 
a += 1;
[操作C] 
count = a;
これを複数のユーザ が同時に行うと困ったことが起きる。もちろん、プロセスの内部 での実行順序はA,B,Cの順だが、それらはスケジューラによって 分割されて、他のプロセスのA,B,Cと混じる(Interleaving) されることがある。
count は 256
[操作A1]  by User 1
   count == 256
[操作B1]  by User 1
   count == 256
[操作C1]  by User 1
   count == 257
[操作A2]  by User 2
   count == 257
[操作B2]  by User 2
   count == 257
[操作C2]  by User 2
   count == 258
もちろん、count は258になることが期待される。

問題

count が258以外になる操作の列を示せ。 (これは、明らかにAやBが予期していたAが単独で操作ABCを 行う結果とは異なる。 )

同期は、いろいろなレベルで行われる。

これらはお互いに独立なわけではない。より高いレベルのものが、より 低いレベルのものにより実装されるのが普通である。さまざまなレベルの 同期機構を使っても、個々のレベルでの干渉がないことが保証される 必要がある。しかし、それは自動的に保証されるわけではない。これらの 機構のユーザが、それぞれを正しく使う必要がある。特に、低いレベルの 実装はシステム全体のユーザに影響するので、完全なものである必要がある。

前の例の 操作 ABCのようなデータの変更のまとまりをというトランザクション(transaction)という。 トランザクションは、それを順番にやったようになって欲しい。それが 普通の直感である。この場合は、「1を加える」を2回おこなったのだから、 2だけ値が増えてほしい。これを直列可能性(Serializability)という。

直列可能性が成立していれば、プログラムはトランザクション の変更の積み重ねとなる。これは、トランザクションの積み重ねか らプログラム全体ができていることを意味する。これをプログラム の無矛盾性(Consistency)という。ある条件のもとでConsistencyと Serializabilityは同値であることが証明されている。



これは、a1a2,b1b2 の二つのトランザクションが、s1→ s2の順序 に依存するb1b2→ a1a2 という順序と、s2→ s3 によって生じるa1a2 → b1b2 という順序が矛盾していることから生じている。

前の人の値を上書きしているという単純なことだが、このよう に依存関係の矢印を書くことによって、より矛盾が明快になる。ま た、機械的に矛盾を見つける手順を提供することもできる。あたり 前のことを明確にすることがコンピュータサイエンスでは重要なこ とが多い。



さまざまな同期機構

並列システムでは、 Test-and-set または、read-modify-write というのが、ハードウェア レベルでは用いられている。これは、自分が書き込んだ値が相手に読まれる ことを保証している。これは、単なるatomic read, atomic write では 実現できない。これらを用いて、Critical Section や Semaphor, Monitor を実現することができる。

逐次システム上の疑似並列システムでは、割り込まれない区間である を作れば同期機構を作ることができる。これは、例えば割り込み禁止 やcritical sectionにより実現する。

Test-and-set などでの待ち合わせは、CPUがそこを繰り返し読むことで 行っても良い。これはspin-lockと呼ばれる。CPUを無駄に消費するが、 同期待ちからの回復は、これが一番高速である。複数のCPUが、協調して spin-lock を行うのはBarrier と呼ばれる。これらの同期機構は、thread level で、並列プログラムのPerformance tuning を行う時に 重要になる。

spin-lockでは無駄が大きくなる時には、プロセスを一旦、退避させる。 この退避を抽象化した機構が、Semaphor である。Semaphor には P動作とV動作がある。これは、Critical section へ入ることと 出ることに相当する。さらに、これを Monitor という形でより柔軟な 処理をおこなうことができる。Java などは言語レベルでCritical Section を採用している。(なぜ、Monitor を採用しなかったかは 謎である) 並列オブジェクト指向言語では、オブジェクトをMonitor として扱う。

Guard は、lock や critical section などと異なり複数の条件を 同時に待つことにより同期をおこなう。この機構は、より複雑な 同期状況を実装するのにすぐれている。つまり表現能力が高い 方法である。Unix の system call としては、select/listen というのが あり、これは一種の限られたGuradになっている。



Binary Semaphor or Lock

BSD/OSのthreadでは、ロックを使って同期をおこなう。これはバイナリセマフォと 同じである。 セマフォには、P命令とV命令がある。このプログラム では、セマフォを以下のように用いる。

     P命令
        きわどい部分 (critical section)
     V命令
pthreadのセマフォは、
     P命令    pthread_mutex_lock(pthread_mutex_t *mutex);
     V命令    pthread_mutex_unlock(pthread_mutex_t *mutex);
となってる。それぞれの引数は、初期化されたセマフォへのポインタ である。

Count Semaphor

バイナリセマフォでは、P命令を実行して通過できるのはただ一つの process/thread だけである。しかし、資源がただ一つだけなく複数 ある時には、複数のプロセスがP命令を実行できても良いと考えられる。 この場合、P命令が消費に相当し、V命令が生産に相当する。 この場合は、P命令を実行した回数とV命令を実行した回数は、最大N 個の差まで容認される。このようなセマフォを計数セマフォ(counting semaphor) という。

例えば、最初にN個の資源があり、 生産者がN 個まで随時、資源を生成し、消費者がそれを消費していく とする。消費者は資源がある限り、それを同時に使用して良い という生産者消費者問題を考えよう。



生産者消費者問題は計数セマフォを使うことにより実現できるが、 pthread には残念ながら計数セマフォはない。その代わり条件付き 変数があるので、それを用いて計数セマフォを作成する。 条件付き変数というのは歴史的な 理由でそういう名前がついているのだが、これは同期を待ち合わせる process / thread のqueue (キュー)である。このキューには、 条件付き変数
pthread_cond_wait(&sem->cond,&sem->mutex); 自分を待ち行列に加える。実行キューから取り除かれる
pthread_cond_signal(&sem->cond); 待ち行列から一つ取り出し、実行キューに加える

条件付き変数と、セマフォを組み合わせて計数セマフォを作る。cond は、 待ち合わせを行う軽量プロセスのキューがである。

    typedef struct sem_t {
        int value;
        pthread_mutex_t mutex;  /* セマフォの値の整合性を得るためのロック */
        pthread_cond_t cond;    /* 待ち合わせのキューを作るための条件付き変数 */
    } sem_t,* sem_ptr;
    void sem_init(sem_t *,int);
    void sem_v(sem_t *);
    void sem_p(sem_t *);

計数セマフォの初期化は、資源の量を指定する。

    void
    sem_init(sem_t *sem,int value) 
    {
         pthread_mutex_init(&sem->mutex,NULL);
         pthread_cond_init(&sem->cond,NULL);
         sem->value = value;
    }
P動作、バイナリセマフォと違って、複数の待ち合わせが行われる ことがある。

    void
    sem_p(sem_t *sem) 
    {
        /* まずvalueをみるためだけにlockする */
        pthread_mutex_lock(&sem->mutex);
        while(sem->value == 0) {    /* もう、残ってない */
            /* 条件付き変数に自分を登録して、ロックを解放して、他の
               プロセスが資源を解放してくれるのを待つ */
            pthread_cond_wait(&sem->cond,&sem->mutex);
        } 
        /* 自分の分の資源があったので、それを確保する */
        sem->value--;
        pthread_mutex_unlock(&sem->mutex);
        /* ロックを解放する */
    }
V動作では、解放される資源は一つしかないので、一人だけしか起こす 必要はない。

    void
    sem_v(sem_t *sem) 
    {
        /* まずvalueをみるためだけにlockする */
        pthread_mutex_lock(&sem->mutex);
        /* 資源を解放する */
        sem->value++;
        pthread_mutex_unlock(&sem->mutex);
        /* 他に資源が待って寝ている人がいれば、その人を起こす */
        pthread_cond_signal(&sem->cond);
    }
このように共有資源と同時に使わなくてはならないPosix threadの 同期機構は非常にださい。計数セマフォのP/Vの処理に比べて非常に 複雑な印象を受ける。しかし、たぶん、作った人の意図は、これらを 用いて、より高度な同期機構を作って欲しいということなのだろう。 それならば、条件付き変数の内容をより進んだ形で処理したいが、 それは用意されていない。 OSの設計者は、良くそういう手抜きをする。しかし、本来は同期機構は、 その使い方といっしょに設計されるべきものである。

通常プロセスでの共有資源

通常プロセスでの同期



Lock and Transaction

同期機構で実現したいのはConsistencyである。ただ、やみくもに同期機構を 使ってもConsistencyは得られない。例えば、lock によって直列可能性を 得るには、きまった方法によりlockをかける必要がある。一つのresource (資源)しかない場合には、lock は自明であるが、複数のresourceがある場合には、 lock をかける順序を工夫する必要がある。この問題は、データベースの 理論と同じ問題である。


Decd lock 閉塞

同期機構は、なんらかの待ちあわせを含む。この時に、いくつかのプロセスが 相互に待ちあわせてしまうことがある。これをDeck lockという。

問題

ある町のホテルの空室数をX、この町にいく飛行機の空席数をYとする。


以下のような操作を考えよう。 質問: さてSとTは、両方とも「飛行機の切符とホテルの予約」を取るという トランザクション(transaction)を実行したい。

一般的に複数のリソースを使うトランザクションでは、すべてをロ ックしてから処理をおこない、不要になったリソースからアンロッ クすることにより、矛盾を避けることができる。これを2 phase lock (2相ロック)という。しかし、2相ロックだけでは、デッドロックは、 避けることはできない。

読みだしの場合は、複数のプロセスから読みだしが あってもよい。これを利用して並列度を上げることが考えられる。 これを可能にするにはロックに段階をもうける。

shared lock どうしは、複数同時にかかる。しかし、exclusive lockは、 shared lock , exclusive lock ともにlockする。

select/client/server

ロックは結構難しい概念なので、より簡単な方法として、クライアント・ サーバモデルというのを使う場合がある。この場合は、サーバと呼ばれる プロセスを立ち上げ、サービスの要求はすべて、そこに転送される。 サービスは要求が到着した順に処理される。この時に、Unix では tcp socket と select というシステム・コールによりメッセージの選択が行われる。 この実験は後期のinfo2で行う。

この方法では、メッセージの到着順序により整合性が保たれる。しかし、 クライアント間の並列性がなくなるため、パフォーマンスは劣化する。

さらにこの欠点を緩和するために、サーバをマルチスレッド化すること が行われている。この場合は、サーバ内部で同期機構をまた別に使う 必要がある。同期機構としてはセマフォやロックが使われる。


アボート

多数のユーザがデータベースにアクセスする時に、2相ロックを使うと データベースの矛盾を防ぐことができる。しかし、デッドロックは 防ぐことができない。ロックするデータに順序付けをすればデッドロックは 防ぐことができるが、平行実行の制限が大きくなるのと、データの順序づけが 難しいので、あまり好まれない。

実際には、トランザクション一つにつきロックを一つだけとるような方法が 簡単であり効果的だ。しかし、この方法は一度にたくさんのアクセスが来る 場合には適さない。

もし、データのアクセス頻度が大きく、デッドロックは稀にしか起きない のならば、「取りあえずロックしていく。 ロックがとれないようだったら、しばらく待って、あきらめる」という方法が ある。(楽観的平行制御 Optimistic Concurrency Control)

この場合、あきらめたトランザクションは、今まで行った作業の取り消し をおこなう必要がある。これをトランザクションの アボート(abort)という。 アボートは、ディスクアクセスなどに失敗した場合、その他、 アプリケーションの都合によっても起きる。

前の例題で、飛行機だけ予約が取れても、ほてるだけ予約が取れても、役に たたない。もし、それぞれの残りが一つの場合、SとTが飛行機とホテルを別々に 予約できてしまう。(そして、お互いにキャンセル待ちに入る...) そうしない ためには、飛行機とホテルをセットにすれば良かった。

セットにしない場合でも、飛行機とホテルの予約をとって、どちらかが 取れなかったらアボートするという方法でも良い。この時にも、予約の 順序を決めると、それぞれが一つづつ残っているのに誰も予約できなかった というようなこと防ぐことができる。


カスケーディングアボート

一つのトランザクションがアボートしたとする。もし、そのトランザクションの データを他のトランザクションが使っていたとすると、そのトランザクションは だめになってしまう。つまり、そのトランザクションもアボートしなければ ならない。このようにアボートが連鎖的に起こることをカスケーディング アボート(Cascading Abort)という。

2相ロックを使っていれば、通常はロックによるアボートでは、 カスケーディングアボートは起こらない。しかし、その他の要因による アボートではカスケーディングアボートが無制限に起きる可能性がある。

これを防ぐには、2相ロックの解除を徐々にではなく、一度に行えば 良い。(狭義の2相ロック) しかし、この方法もアクセス頻度が大きい場合には並列度を 下げるので嫌われることが多い。





コミットとログ

アボートは、トランザクション自身が指示することもできる。SQL文では、 という操作によって行う。

普通、データベースシステムは、どのトランザクションがコミットしたか を記録している。これはログ(log)と呼ばれる。 何もしなければログは どんどん大きくなっていく。カスケーディングアボートに備えるために、 ずーとログを取っておかなくてはならないからである。

ある時点で、データベースの状態をとっておき、それを正規の状態 と決めれば、その時点より前のログを取っておく必要はない。それは バックアップ(backup)と呼ばれる。バックアップの後は、明示的に トランザクションログを消さなくてはならないデータベースも多い。

Unix には、sync というコミットに近い動作がある。これは、 アプリケーションからは書き込みは終わっているが、OSはディスクに データを書き込んでいない状態を解消する。したがって、Unix上の ファイルシステムをもとにしたデータベースは本当の意味で安全になることは ない。多くのデータベースではファイルシステムを経由せずに直接 ディスクを操作することでこれを回避している。



Unixでのロック



宿題

C で書いた flock_test.c または、 Perlで書いた, flock_test.pl を 用いて、この授業のデッドロックを2相ロックで再現してみよ。ま た、2相ロックでない場合に矛盾が生じる例を示せ。 (注) sleep などを使わないとうまくdead lock しないことがある。

宿題

情報工学実験Iの 4.4 相互排除4.5 プロセス間の同期問題を行うこと。 おこなうこと。この実験は nirai/kanai または、pw/nw でおこなうこと。 レポートはメールで

    Subject: Report Operating System Lecture 6/5
というように、課題を出した日付をサブジェクトに入れたメールで 提出して下さい。締め切りは来週です。