ファイルロック機構(flock・fcntl・リース)
なぜforkでfcntlロックが消えるのか、なぜ同一プロセスの二重openでロックが自滅するのか。flock・fcntl・OFDロック・リースの所有権モデルを押さえれば、ファイルロックの落とし穴を原理から回避できます。
- 1.Linuxの古典的ロックは大半が勧告的(advisory)で、協調するプロセスがロックを取りに来た時だけ効く。強制的(mandatory)ロックは事実上廃止され、当てにできない。
- 2.flockはオープンファイル記述(struct file)単位、伝統的fcntlは(プロセス,inode)単位。後者はforkで継承されず二重openで自滅するため、その欠陥を直したのがOFDロック(F_OFD_SETLK)。
- 3.fcntl/OFDロックはカーネルがロック保持者の有向グラフを走査してデッドロックを検出しEDEADLKを返す。リースはファイルを開く第三者を検知してSIGIOで持ち主に通知する別系統の仕組み。
勧告的ロックと強制的ロック:合意がなければ効かない
ファイルロックの最初の落とし穴は、ほとんどのロックが「勧告的(advisory)」であることです。勧告的ロックは、ロックを取得した事実をカーネルに登録するだけで、他プロセスの read/write を物理的に止めません。協調する側が「書く前に必ずロックを取りに行く」という規約を守って初めて排他が成立します。規約を無視して直接 write するプロセスがいれば、ロックは何の防御にもなりません。鍵ではなく、共有黒板に貼る「使用中」の付箋に近い仕組みです。
これに対し 強制的(mandatory)ロック は、ロック中のバイト範囲への read/write をカーネルが自動でブロックまたはエラー化する、本物の鍵です。POSIXは規定するものの、Linuxの実装は危険なレースを抱え、対象ファイルシステムを mand オプションでマウントし、ファイルに setgid かつグループ実行ビットなしという特殊なモード設定が要るという扱いづらさもあり、長く非推奨でした。Linux 5.15で CONFIG_MANDATORY_FILE_LOCKING 自体が削除され、事実上廃止されています。実務では「強制ロックは存在しないもの」と考え、勧告的ロックの規約を全員で守る前提で設計します。
勧告的ロックの保証は「ロックを取りに来た協調プロセス同士の排他」だけです。ロックを確認しないプロセスや、別のシステムから来た書き込みは素通りします。したがって信頼できないアクターからのデータ保護にはロックを使えません。ロックはあくまで協調する仲間内の調停であり、アクセス制御(権限・VFS層のパーミッション)とは役割が別です。
flockとfcntl:所有権モデルが根本的に違う
Linuxには伝統的に2系統の勧告的ロックAPIがあり、「ロックが誰のものか」という所有権の単位が決定的に異なります。ここを混同すると、forkや二重openでロックが意図せず消えたり自滅したりします。
| 観点 | flock(2) | fcntl(2) POSIXロック |
|---|---|---|
| 粒度 | ファイル全体のみ | 任意のバイト範囲(start,len指定) |
| 所有者の単位 | オープンファイル記述(struct file) | (プロセス, inode)の組 |
| forkでの継承 | 同一記述を共有→子も同じロックを保持 | 継承されない(プロセス境界で別物) |
| 同一inodeを2回open | 別記述なので独立に競合 | 同じ所有者と見なされロックが融合・上書き |
| close時の解放 | その記述への最後のfdが閉じた時 | そのプロセスのどのfdを閉じても全解放 |
| デッドロック検出 | なし | あり(EDEADLK) |
flock のロックは**オープンファイル記述(struct file)**に紐づきます。open 1回で生まれる記述が所有者なので、dup や fork で同じ記述を共有するfdは同じロックを指し、別々に open した2つのfdは別の所有者として互いに競合します。この「記述単位」という性質は、fdの三層構造(ファイルディスクリプタとオープンファイルテーブルの構造)を押さえているとそのまま理解できます。
一方、伝統的 fcntl(F_SETLK/F_SETLKW)のロックは**(プロセス, inode)の組**に紐づきます。これが2つの悪名高い罠を生みます。
(1)二重openの自滅:同じファイルをライブラリAとBが別々に open し、Aがロック取得・Bが片方のfdを close すると、所有者が同じ(プロセス, inode)なのでAのロックまで解放されます。POSIXが「そのinodeに対するいずれかのfdをcloseすると、そのプロセスの当該ロックが全て消える」と規定しているためです。(2)fork非継承:ロックは(プロセス,inode)所有なので、fork した子はfdを引き継いでもロックを引き継ぎません。さらにマルチスレッドでは全スレッドが同一プロセスなので、スレッド間でロックが区別されず排他になりません。
flock と fcntl のロックは別系統で互いに見えない点も重要です(一部の古いシステムやNFS実装を除く)。同じファイルに flock と fcntl を混在させても排他できないため、システム全体でどちらか一方に統一します。
OFDロック:fcntlの欠陥を所有権モデルから修正する
二重openの自滅とfork非継承は、いずれも「所有者が(プロセス, inode)である」ことに起因します。そこでLinux 3.15(POSIX.1-2024で標準化)が導入したのが**OFDロック(Open File Description lock)です。F_OFD_SETLK/F_OFD_SETLKW/F_OFD_GETLK を使うと、ロックの所有者が flock と同じくオープンファイル記述(struct file)**になり、バイト範囲指定という fcntl の利点はそのまま残ります。
| 欲しい性質 | flock | fcntl(伝統) | OFDロック(F_OFD_*) |
|---|---|---|---|
| バイト範囲ロック | 不可 | 可 | 可 |
| 所有者の単位 | 記述(struct file) | (プロセス,inode) | 記述(struct file) |
| スレッド間で排他できる | 別openなら可 | 不可 | 別openなら可 |
| 二重openの自滅を回避 | 回避できる | 起きる | 回避できる |
| forkで子と共有 | 共有する(同一記述) | 継承しない | 共有する(同一記述) |
所有者が記述になることで、別々に open した2つのfd(=別記述)はスレッド間でも独立した所有者となり、互いに正しく競合します。これでマルチスレッドのファイル間排他が安全に書けます。逆に fork 後は親子が同一記述を共有するため、子もロックを保持し続けます。新規コードでバイト範囲ロックが要るなら、伝統的 fcntl ではなく OFDロックが第一選択です。
所有権モデルの違い(同じファイルを2回 open した場合)
flock / OFDロック: fcntl(伝統):
open#1 → 記述A → ロックA open#1 ┐
open#2 → 記述B → ロックB open#2 ┴→ (プロセス,inode) で1つの所有者
AとBは別所有者 → 正しく競合 → どちらかを close すると両方のロックが消える
fcntl/OFDのバイト範囲は、ファイルの現在サイズと無関係に指定できます。len に0を渡すと「start からファイル末尾まで(将来の拡張も含む)」を意味する特別な指定です。レコード単位の排他は、各レコードのオフセット範囲をロックすることで実現します。範囲が重なる読みロック同士は共存でき(共有ロック)、書きロックは他のいかなるロックとも排他になります(排他ロック)。
デッドロック検出とリース:カーネルが見るグラフと逆方向の通知
F_SETLKW/F_OFD_SETLKW(待機付き取得)では、ロック待ちのプロセスが循環するデッドロックが起こり得ます。AがレコードR1を持ってR2を待ち、BがR2を持ってR1を待つ典型です。fcntl/OFDロックではカーネルがロック保持者と待機者の有向グラフを内部に持ち、新たな待機を登録する直前にそのグラフを走査して循環を探します。循環が見つかれば待機を成立させず、即座に EDEADLK を返してデッドロックを未然に防ぎます。これは排他制御とデッドロックで扱う検出アルゴリズムを、カーネルがファイルロックに対して実装したものです。
カーネルのデッドロック検出は同一カーネルが管理するロック集合の中だけで完結します。NFS越しのロック、ロックと別資源(メモリ・パイプ)が絡む循環、グラフ走査コストを抑えるための探索打ち切りなどにより、検出されない循環や偽陽性の余地があります。EDEADLKに頼り切るのではなく、ロック取得順序を全プロセスで統一するという基本のデッドロック回避策を併用すべきです。
リース(lease) は、これまでのロックとは方向が逆の仕組みです。fcntl(fd, F_SETLEASE, F_RDLCK or F_WRLCK) でファイルにリースを設定したプロセスは、他のプロセスがそのファイルを open したり truncate しようとした瞬間に通知(既定で SIGIO)を受け取ります(シグナル)。リース保持者はその通知を受けてキャッシュを書き戻すなどの後始末を行い、リースを降格・解除します。一定時間(/proc/sys/fs/lease-break-time、既定45秒)内に応じないと、カーネルが強制的にリースを破棄して相手のopenを進めます。
リースの典型用途は、ユーザ空間のファイルサーバ(Samba等)が「自分がファイルをキャッシュ中だが、他者がアクセスし始めたら即座に知りたい」というキャッシュ整合性の通知です。ロックが「自分がアクセスする間、他者を待たせる」のに対し、リースは「他者がアクセスし始めたら自分が動く」という非対称な役割分担になっています。
(1)Linuxの古典ロックは原則勧告的、強制ロックは事実上廃止。(2)flockは記述(struct file)単位、伝統的fcntlは(プロセス,inode)単位で、後者はfork非継承・二重openで自滅。(3)その欠陥を所有権モデルから直したのがOFDロック(範囲指定可・記述単位)。(4)fcntl/OFDはEDEADLKでデッドロック検出。(5)リースはopen/truncateを検知してSIGIOで持ち主に通知する逆方向の仕組み。この5点が頻出です。
まとめ
- Linuxの古典的ファイルロックは大半が勧告的で、協調プロセス同士の規約として働く。強制ロックは事実上廃止され、信頼できないアクセスの防御には使えない。
- flockはオープンファイル記述(struct file)単位、伝統的fcntlは(プロセス, inode)単位。所有権モデルの違いから、fcntlはforkで継承されず、同一inodeの二重open+片方closeで自滅する。
- **OFDロック(
F_OFD_SETLK系)**は所有者を記述単位に変え、fcntlの欠陥を直しつつバイト範囲指定を残す。新規コードでの範囲ロックの第一選択。 - fcntl/OFDロックはカーネルが保持者・待機者の有向グラフを走査して循環を検出し**
EDEADLK**を返す。リースは他者のopen/truncateを検知してSIGIOで持ち主に通知する、方向が逆のキャッシュ整合性向け機構。
所有権モデルの土台はファイルディスクリプタとオープンファイルテーブルの構造、検出と回避の原理は排他制御とデッドロックとデッドロック、通知に使うSIGIOはシグナルを合わせて読むと、ファイルロックという地味だが事故の多い領域の全体像が原理から見渡せます。
OS Article
ファイルロック機構(flock・fcntl・リース)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ファイルロック
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
flockはオープンファイル記述(struct file)単位、伝統的fcntlは(プロセス,inode)単位。後者はforkで継承されず二重openで自滅するため、その欠陥を直したのがOFDロック(F_OFD_SETLK)。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「ファイルロック / flock」に近いか確認する。
- 強みである「Linuxの古典的ロックは大半が勧告的(advisory)で、協調するプロセスがロックを取りに来た時だけ効く。強制的(mandatory)ロックは事実上廃止され、当てにできない。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。