メモリマッピングとコピーオンライト
同じファイルを共有・私有・読取り専用で開き、書込みが届く範囲と失敗条件を確かめます。
説明
メモリマッピングはファイルなどを仮想アドレス空間へ対応付け、メモリの読書きとして内容へアクセスする仕組みです。共有マッピングでは同じ対象への変更を共有し、私有マッピングでは書込みを自分のコピーへ分離します。
ABCDという4バイトの一時ファイルを使います。同じファイルを二つの共有マッピングで開いて変更を確認し、私有コピーと読取り専用も追加します。その後、COWで物理ページの対応が分かれる過程を小さな表で確認します。
1ファイルと二つの共有マッピング
一時ファイルへABCDを書き、二つの書込み可能なマッピングa・bを作ります。flushでファイル側のバッファを反映してからマップします。
import tempfile, mmap一時ファイルとマッピング; file = tempfile.TemporaryFile()今回のファイル; file.write(b'ABCD'); file.flush()4バイトを準備; a = mmap.mmap(file.fileno(),4,access=mmap.ACCESS_WRITE)共有マッピングa; b = mmap.mmap(file.fileno(),4,access=mmap.ACCESS_WRITE)共有マッピングb; print(a[:],b[:])両方の内容先頭の4はfile.writeが書いたバイト数です。二つのマッピングはどちらもABCDで、同じファイルの先頭4バイトへ対応しています。Pythonではスライスa[:]でその範囲のバイトを取り出せます。
2一方の書込みを他方から読む
aの位置1のBをZへ置き換えます。続いてbの内容を読みます。
a[1:2] = b'Z'位置1へ書込み; print(a[:],b[:])共有した変更両方AZCDです。この操作は同じスレッドで順に実行しているため、書込みの後に読んでいます。別プロセスや別スレッドが同時に読む場合は、変更通知や排他など、アクセス順序を定める仕組みも必要です。
3ファイル側の内容を確認
共有マッピングをflushし、通常のファイル読取りでも内容を確かめます。
a.flush()マッピングの変更を反映; file.seek(0)ファイル先頭へ; print(file.read(4))通常の読取り先頭の0はfile.seekが返した移動後の位置です。ファイルからもAZCDが読めます。共有マッピングを他のマッピングから見られることと、電源断後にも保存が残ることは別の性質です。永続化が必要なアプリではOS・ファイルシステムの同期APIの保証を確認します。
4私有マッピングで書き換える
同じファイルをACCESS_COPYで開き、位置0のAをPへ変更します。cと共有側aを比べます。
c = mmap.mmap(file.fileno(),4,access=mmap.ACCESS_COPY)私有マッピング; c[0:1] = b'P'先頭を変更; print(c[:],a[:])私有側と共有側cはPZCD、aはAZCDです。私有マッピングへの書込みはコピー側だけを変えます。最初から全範囲を物理コピーする必要はなく、書込み時に分離するCOWという仕組みで実装できます。
5私有側を閉じても元ファイルは変わらない
私有マッピングを閉じ、ファイルを再度読みます。
c.close()私有マッピングを閉じる; file.seek(0)ファイル先頭へ; print(file.read(4))元の内容先頭の0はfile.seekが返した移動後の位置です。AZCDのままです。ACCESS_COPYへのPの書込みをファイルへ保存する操作としては扱えません。元ファイルへ反映したい処理と、変更を自分だけに留めたい処理でマッピング方式を選びます。
6読取り専用マッピングへの書込み
読取り専用のroを作ります。Pythonのmmapオブジェクトは書込み操作を検査し、TypeErrorにします。
ro = mmap.mmap(file.fileno(),4,access=mmap.ACCESS_READ)読取り専用; print(ro[:])許可された読取り読取りは成功します。次は同じ位置を書き換え、私有コピーが作られる場合との違いを確かめます。読取り専用は「書くと自動的にコピーができる」という指定ではありません。
7保護違反はCOWとは違う
読取り専用へXを書き込み、Pythonの検査で拒否されたことを表示します。
try:書込みを試す
ro[0:1] = b'X'先頭の変更要求
except TypeError:APIの書込み禁止
print('read-only: write rejected')拒否を表示書込みは拒否され、COWのような私有コピーは作られません。ここで観測したのはPython APIのTypeErrorです。ネイティブコードがOSの読取り専用ページへ直接書く場合は、メモリ保護例外を経てSIGSEGVなどの対象になります。
8COWでページの対応が分かれる過程
最後にCOWのページ対応を表すモデルを操作します。PとQがフレーム7を共有し、Qの書込み時にフレーム8を複製してから、Qの対応だけを変えます。
frames = {7:bytearray(b'AZCD')}; mapping = {'P':7,'Q':7}共有状態; frames[8] = frames[7].copy()新フレームへコピー; mapping['Q'] = 8Qだけ対応を変更; frames[8][0] = ord('P')コピー側へ書込み; print(mapping)ページの対応; print(bytes(frames[mapping['P']]),bytes(frames[mapping['Q']]))分離した内容対応はP→7、Q→8となり、内容はAZCDとPZCDです。実際のOSは私有書込みが許可された領域かを管理情報で判断し、必要なコピーとページ表更新を行います。単に書込み禁止ビットが立っているという情報だけでは、COWか本当の保護違反かを決められません。
9マッピングとファイルを閉じる
読取り専用と二つの共有マッピングを閉じ、最後に一時ファイルを閉じます。
ro.close(); a.close(); b.close()各マッピングを解除; file.close()ファイルを閉じる; print(a.closed,b.closed,ro.closed,file.closed)解放状態すべてTrueです。マッピングの解除とファイル記述子の閉鎖は別の資源操作です。この例では、使い終わった対応とファイルの両方を明示的に解放しています。
まとめ
共有マッピングへの変更は同じ対象へ対応するマッピングから参照できます。
私有マッピングの書込みはコピー側へ分離され、元ファイルへの保存にはなりません。
COWで許された私有書込みと、本当に禁止された書込みは、OSの領域管理情報も含めて判断されます。
操作履歴と解説を振り返れます。
Python 3
import tempfile, mmap; file = tempfile.TemporaryFile(); file.write(b'ABCD'); file.flush(); a = mmap.mmap(file.fileno(),4,access=mmap.ACCESS_WRITE); b = mmap.mmap(file.fileno(),4,access=mmap.ACCESS_WRITE); print(a[:],b[:])4 b'ABCD' b'ABCD'
a[1:2] = b'Z'; print(a[:],b[:])b'AZCD' b'AZCD'
a.flush(); file.seek(0); print(file.read(4))0 b'AZCD'
c = mmap.mmap(file.fileno(),4,access=mmap.ACCESS_COPY); c[0:1] = b'P'; print(c[:],a[:])b'PZCD' b'AZCD'
c.close(); file.seek(0); print(file.read(4))0 b'AZCD'
ro = mmap.mmap(file.fileno(),4,access=mmap.ACCESS_READ); print(ro[:])b'AZCD'
try:
ro[0:1] = b'X'
except TypeError:
print('read-only: write rejected')read-only: write rejected
frames = {7:bytearray(b'AZCD')}; mapping = {'P':7,'Q':7}; frames[8] = frames[7].copy(); mapping['Q'] = 8; frames[8][0] = ord('P'); print(mapping); print(bytes(frames[mapping['P']]),bytes(frames[mapping['Q']])){'P': 7, 'Q': 8}
b'AZCD' b'PZCD'ro.close(); a.close(); b.close(); file.close(); print(a.closed,b.closed,ro.closed,file.closed)True True True True
演習完了