Armadilloフォーラム

ファイル書込みの排他処理について

shibata5

2018年11月18日 11時26分

はじめまして。
ファイル書込の排他処理について、ご質問があります。

書込のみのプロセスと、読込のみのプロセスがあります。
書込のみのプロセスは、Cプログラムです。
読込のみのプロセスは、WEBサーバーです。
書込と読込は非同期に行われます。
書込サイズが、512バイト未満であっても、ファイルロック機構は必要ですか。
CF(ブロックサイズ512バイト)の場合と、RAMディスク(tmpfs)の場合で教えてください。

ファイルサイズが大きい場合は、ファイルロック機構が必要だと思います。
もし、BUFSIZ未満(システムによるが、512バイト未満は少ないと思います)であった場合、fputs/fgetsやperl(cgi)のreadを使用していた場合、read/writeシステムコールは、バッファリング機能により、1回のみしか発生しないはずです。
システムコールレベルで排他してあれば、排他処理が必要ない気がします。
また、CFカードへの書込みであった場合、CFカードのブロックサイズが512バイトなので、512バイト未満は排他が必要ない気がします。

GPIOへの制御は、「echo 1 > /sys/class/gpio/CON9_2/value」と、echoコマンドが使われます。
もし、排他が必要なのであれば、echoコマンドの前に、ファイルロック処理が必要です。
上記のコマンドを実行すると、「open->write->close」が実行されるはずです。
確か、O_TRUNCオプション付きのopenです。すなわち、OPEN時に、ファイルサイズが0になります。何も記録されていないファイルになります。
それでも、正常に動作する理由は、受け取る側が不正値を無視するように実装してある気がしますが、それでも、少し気になります。

初歩的な質問で申し訳ありません。
どなたか、ご教示していただけませんか。
よろしくお願いします。

コメント

毎度お世話様、伊澤です。

質問のポイントを絞って、且つ具体的に例示しないと回答し難いかと。

一般論で言ってしまうと、何らかの形でファイルをロックするような仕組みが必要と言う回答になってしまいます。

例えば読み出すプロセスがファイルをオープンしたままの状態で書き込むプロセスがファイルを新規オープンすると、
読み手と書き手で違うファイルの実体を扱うことになります。
また、ファイルバッファからバッファサイズー1バイト読み込み済みのときにfread()などで2バイトの読み込みを
行なうと、以前読み込んでバッファに残っている1バイトと新たにシステムコールを行なって読み込んだ1バイトの
組み合わせと言う新旧混ざった事態になります。

伊澤様

ご回答、ありがとうございます。

> 質問のポイントを絞って、且つ具体的に例示しないと回答し難いかと。
>
> 一般論で言ってしまうと、何らかの形でファイルをロックするような仕組みが必要と言う回答になってしまいます。
>
私も必要だと思っています。
ただ、echoコマンドによる書込みが一般的に使用されているので、
サイズが小さいものは、意外にカーネル層の理由で不必要かも知れないと、考えています。
汎用性を考慮すると、当然、ロックするべきです。
この問題を抱えているプログラムは、既に実装してあります。
可能であれば、変更したくありません。
だから、問い合わせています。
本当に勝手な質問で申し訳ありません。

> 例えば読み出すプロセスがファイルをオープンしたままの状態で書き込むプロセスがファイルを新規オープンすると、
> 読み手と書き手で違うファイルの実体を扱うことになります。
>
実体が複数になるのは、問題ありません。

説明が下手からも知れませんが、例を示します。

書込み先は、CFとRAMディスクとあります。
writeとreadのシステムコールは、バッファリングにより、それぞれ一回しか発生しない(※)前提です。
※BUFSIZ未満のファイルなので、stdioのバッファリング機能により、一回しかシステムコールが発生しない。
書込データサイズは、512バイト未満です。追記書込みのケースも含みます。

書換え前のファイルデータは、「abcdefghijk」とアルファベットのみです。
書込データが「123456789」と数字のみのデータです。

writeとreadがほぼ同時に発行されたとします。
readで読込まれるデータは、アルファベットのみか、数字のみになりますか?
もし、アルファベットと数字が混合した状態が発生するようなら、ファイルロックが必要となります。

> また、ファイルバッファからバッファサイズー1バイト読み込み済みのときにfread()などで2バイトの読み込みを
> 行なうと、以前読み込んでバッファに残っている1バイトと新たにシステムコールを行なって読み込んだ1バイトの
> 組み合わせと言う新旧混ざった事態になります。
>
確かに、2回 readが発生したら、混ざると思います。
1回であれば、その時点のファイルデータを読込みます。

しかし、バッファはstdioだけではなく、カーネルバッファも存在します。
write(flush)でカーネルバッファを反映している途中にreadを実行したら、カーネルがバイトレベルの排他であった場合、新旧が混在します。
多分、システムコール単位で排他するか、CFカードの書込みシーケンス(※)が原因で発生する可能性がある排他がないと、新旧が混在することになると思います。
※CFは512バイト単位で書込む。

お手間をおかけします。
よろしくお願いします。

あまり調べずに記憶で書きます。確実なところはソースを読まないと断言できませんが。。。。

UNIX系ファイルシステムのレギュラーファイルの場合あくまでもカーネルのバッファキャッシュに対しての読み書き処理が完結するので、read/writeシステムコールの原子性を考えるときにデバイスのブロックサイズは関係ありません。

数10バイトならCプログラムレベルで排他処理しなくても書き込み前後のデータが混在することはありません。
というのもレギュラーファイルの1回のread/writeシステムコールでユーザーランドとやりとりするデータは
ユーザーランドとバッファキャッシュの間のコピーですから、ブロックサイズ(バッファキャッシュのブロックサイズ)未満なら必一気に行われる(すなわち、vnodeかバッファキャッシュの該当ブロックかどっちか(調べないと確実なところは言えませんが)をロックした状態で行われる)はずだからです。

WEBサーバが OPEN→read→CLOSEという動作ですよね?CプログラムはOPEN→write→closeだとします(open->write→lseek(0)→write→lseek(0)→writeではないですよね?また、書き込み前に一旦ファイルをrmしたりもしてませんよね?)。
ここで書き込みが O_TRUNCをつけてオープンしているとしてファイルの(論理的な=ユーザランドから見た)内容は、open->writeに従って、abcdefghijk→(空ファイル)→123456789と変化します。WEBプログラムがこのファイルを読んだ場合、タイミングによってabcdefghijkか、(空)か、123456789かのどれかが読み取られることになります。

ファイルサイズがBUFSIZ(か、ページサイズかそのあたり)を越えないなら、追記でも同様。(のはずです)

サイトウ様

ご教授、ありがとうございます。
とても助かりました。

> UNIX系ファイルシステムのレギュラーファイルの場合あくまでもカーネルのバッファキャッシュに対しての読み書き処理が完結するので、read/writeシステムコールの原子性を考えるときにデバイスのブロックサイズは関係ありません。
>
そうですか。残念です。
もともと質問内容は、期待も込めた仕様だったので、とても納得できます。

> 数10バイトならCプログラムレベルで排他処理しなくても書き込み前後のデータが混在することはありません。
> というのもレギュラーファイルの1回のread/writeシステムコールでユーザーランドとやりとりするデータは
> ユーザーランドとバッファキャッシュの間のコピーですから、ブロックサイズ(バッファキャッシュのブロックサイズ)未満なら必一気に行われる(すなわち、vnodeかバッファキャッシュの該当ブロックかどっちか(調べないと確実なところは言えませんが)をロックした状態で行われる)はずだからです。
>
だから、echoコマンドで、/sys内のファイル書換えや読込みが行えるのですね。
大変に勉強になりました。
ありがとうございます。

> WEBサーバが OPEN→read→CLOSEという動作ですよね?CプログラムはOPEN→write→closeだとします(open->write→lseek(0)→write→lseek(0)→writeではないですよね?また、書き込み前に一旦ファイルをrmしたりもしてませんよね?)。
> ここで書き込みが O_TRUNCをつけてオープンしているとしてファイルの(論理的な=ユーザランドから見た)内容は、open->writeに従って、abcdefghijk→(空ファイル)→123456789と変化します。WEBプログラムがこのファイルを読んだ場合、タイミングによってabcdefghijkか、(空)か、123456789かのどれかが読み取られることになります。
>
やはりopen時に空ファイルになりますか。残念です。
writeシステムコールが発行されるまでは、write用のバッファ内だけで空ファイルという形になっているだと嬉しかったのです。
すなわち、「echo 1 > /sys/class/gpio/CON9_2/value」実行時は、一度、空ファイルになるので、受け取る側が不正値を弾いていると考えればよいですね。
ありがとうございます。
今後の設計の参考にさせていただきます。

> ファイルサイズがBUFSIZ(か、ページサイズかそのあたり)を越えないなら、追記でも同様。(のはずです)
>
fopenの"a"をstraceでチェックすると、以下のように、openを読んでいます。

open("./test.csv", O_WRONLY|O_CREAT|O_APPEND, 0666) = 3

O_TRUNCオプションがないので、おそらく空ファイルにならないと思います。
間違っていますか。

ご教授、本当にありがとうございます。
本当に助かりました。

小さいファイルに小さいデータを1回のwriteで追記(O_APPEND)する場合、これも同じファイルを読んでいる別プロセスからは追記分は一気に増えたように見えるはずです。

あと、ファイルをサイズを一旦サイズゼロにして書き換える場合でサイズゼロのタイミングを他のプロセスに見せたくないなら、別名で書き込んでcloseしたあとmvコマンド(renameシステムコール)で名前を変えるという手段が使えます。
renameですでにあるファイルに名前を変えると元々あったファイルはunlinkされますしマニュアルにも「不可分で置き換えられる(但し書き有り)」とあります。

サイトウ様

本当に何度もありがとうございます。

> 小さいファイルに小さいデータを1回のwriteで追記(O_APPEND)する場合、これも同じファイルを読んでいる別プロセスからは追記分は一気に増えたように見えるはずです。
>
> あと、ファイルをサイズを一旦サイズゼロにして書き換える場合でサイズゼロのタイミングを他のプロセスに見せたくないなら、別名で書き込んでcloseしたあとmvコマンド(renameシステムコール)で名前を変えるという手段が使えます。
> renameですでにあるファイルに名前を変えると元々あったファイルはunlinkされますしマニュアルにも「不可分で置き換えられる(但し書き有り)」とあります。

排他対策として、renameと、mkdirによるロックの方法は、調査し、見つけることができました。
しかし、mkdirの排他は理解できましたが、renameは、rename中に操作が割り込まないかが心配でした。
これで、安心して使用できます。
大変に、ありがとうございました。