no-image

ファイルロックについて

少し前の勉強会で、ファイルシステム関連について発表しました。今回は、その中からファイルロックを取り上げてまとめます。

【参考】ファイルシステム周りのシステムコールとGoの関数について

ファイルロックとは

ファイルロックとは、複数のプロセス間で同じリソースを同時に変更しないようするために、使用中のプロセスが他のプロセスに「このリソース使ってるなう」と伝える手法の一つです。

一つのリソースに対して複数のプロセスが同時に更新処理を行い、ファイルの内容が壊れるのを防ぎます。

ファイルに限らずこういった二重アクセスを防ぐ機構のことを排他制御といいます。

ファイル銀行振込の例

では、なぜ複数のプロセスから操作されると良くないのでしょうか。
よく用いられる例に銀行の振込の処理があります。

AさんはB社とC社でバイトを掛け持ちしており、月末に給料が振り込まれます。
Aさんの口座残高を管理しているファイルに、これらの振込処理を行っていきます。

  1. B社が「Aさんの口座に5万円を振り込む」処理を実行
  2. B社の残高から5万円が引かれる
  3. C社がほぼ同時に「Aさんの口座に5万円を振り込む」処理を実行
  4. C社の残高から5万円が引かれる

このあと、Aさんの残高を管理するシステムの2つのプロセスは、残高ファイルに書き込む処理を行います。

  1. プロセスBはAさんの残高ファイルを読み込んで(Aの残高は1万円)B社の5万円を書きこむ
  2. 同時に、プロセスCはAさんの残高ファイルを読み込んで(Aの残高は1万円)C社の5万円を書きこむ
  3. プロセスBは書き込み処理をファイルに保存する
  4. プロセスCも書き込み処理をファイルに保存する

以上のような処理を行うと、プロセスCの処理はプロセスBの処理を反映していないファイルに書きこむため上書きしてしまいます。
結果、Aさんの残高は6万円になり、B社の振り込んだ5万円はどこかに消えてしまいました。

こういった問題を起こさないために排他制御が設けられています。

【参考】ファイルのロックに関する基礎知識

ロックファイルとは、目印になるファイル

つぎは「ロックファイル」についてです。
名前がややこしいですが、ファイルロックはロックで、ロックファイルはファイルです。

ロックファイルというのは、「今、僕は一つのプロセスに使用されています」というのを他のプロセスに伝える目印の役割を果たすファイルのことです。
ファイルの中身は関係なく、存在することが目印になります。

あるプロセスがリソースにアクセスしたタイミングでロックファイルを作成し、閉じるタイミングで破棄します。

逆に、あるプロセスがリソースにアクセスし、ロックファイルを作ろうとしたときに、すでに存在していれば作成せずに待機します。

こんなふうにして複数のアクセスによる更新を防ぎます。

完全なロックはしない、勧告ロック

 

しかしこのロックファイル、実は確実なものではありません。ロックファイルが存在しても、他のプロセスは更新することはできます。
つまり、あとからアクセスしてくるプロセスが「リソースのロックファイルの存在を確認する」という操作をしてくれないと、本来の機能を果たせません。

このようなロックの方法を「アドバイザリー(勧告)ロック」と言います。
例えば、rootは常にこの方法でファイルを作ることができるので、rootに対してはロックは効かないことになります。

逆に確実に他プロセスのアクセスを禁じるロックの方法を「強制ロック」と言います。
強制ロックを行うためにはシステムコールレベルでファイルにロックをかけます。
こうすると全てのアクセスが監視され、ロックを保持していないプロセスは、ロックが解除されるまでブロックされます。

一見、こちらのほうが便利そうですが、強制ロックが掛けられるとrootもそのファイルを操作することができなくなります。
ですので、システムが乗っ取られ、一部に強制ロックがかけられると誰も操作することができなくなってしまいます。

これらの理由から多くのUNIXでは、勧告ロックを用いてロックの管理をしています。

共有ロックと排他ロック

ロックの種類に共有ロックと排他ロックとあります。
これらの違いを見ていきます。

共有ロック

読み込むときに使用され、複数のプロセスから同じリソースに対して、いくつも同時にかけることができます。

あるリソースに共有ロックがかかっているとき、他のプロセスがロックをかけようとした場合は、上述の通り、共有ロックなら許可しますが、排他ロックの場合ブロックします。

排他ロック

書きこむときに使用され、同じリソースに対しては、排他ロックは一つのプロセスしかかけることができません。

あるリソースに排他ロックがかかっているとき、他のプロセスがロックをかけようとした場合は、共有ロックも排他ロックもブロックします。

つまり

少しわかりにくいですが、つまりこういうことです。

一つのリソースは複数のプロセスから同時に読み込まれることができます。
ですが、一つのプロセスが書きこんでいる間は、他のプロセスはそのリソースを読み込むことも書きこむこともできません。

他が書き込んでいる最中に、読み込むとそれが更新前のものなのか更新後のものなのかわからないからですね。

Goで実際に試してみる

ファイルロックをするシステムコールにflockというものがあります。

これをGo上から呼び出すsyscall.Flock()が用意されているので、これを少し触ってみます。

【参考】syscall – The Go Programming Language

 

【参考】Goならわかるシステムプログラミング

ファイルを作成するコンストラクタ関数、ファイルロックをするメソッド、アンロックするメソッドを定義しています。

これらを「go_flock.go」という名前で保存し、2つのターミナルを開いて、$ go run go_flock.goで実行します。

最初に実行したほうでは、

で、10秒スリープした後に、「unlock」と表示されますが、後から実行した方では「try locking…」のままロックが解除されるまで、待機します。

システムコールflock()

syscall.Flock(m.fd, syscall.LOCK_EX)の部分で、システムコールflock()を実行しています。

【参考】FLOCK(2) – システムコール – YOS OPENSONAR

第一引数でファイルディスクリプタ、第二引数にロックの種類を示すフラグを代入します。

LOCK_EXは排他ロックを示し、LOCK_SHは共有ロックを示します。

Unlockメソッドの中ではロックを解除するLOCK_UNフラグを用いています。

その他に、ノンブロッキングモードを示すLOCK_NBというフラグもあります。

ここでは、ロック時に排他ロックをかけているので、書き込みしているときと同じような状態になり、一つのプロセスがロックを掛けている間は他のプロセスがアクセスできないことが確認できます。

さきほどのコードを共有ロックにしてみる

ここで、syscall.Flock(m.fd, syscall.LOCK_SH)と書き換えて、共有ロックをかけて実行してみます。

どうなるでしょうか。
共有ロックは一つのリソースに同時に複数かけられるのでした。

2つとも以下の状態で同時にスリープに入りました。

同時にアクセスできていることがわかります。

まとめ

  • 同時に更新されて壊れるのを防ぐためにファイルロックがある
  • 使用中であることを示すためにロックファイルを用いる
  • 共有ロックは読み込むときに使い、排他ロックは書きこむときに使う

参考