O_DIRECTのアライメント要件は推測せずstatxで問い合わせる

Linux O_DIRECTのバッファアドレス・オフセット・長さのアライメントはファイルシステムとカーネルで異なります。Linux 6.1のSTATX_DIOALIGNと6.14のSTATX_DIO_READ_ALIGNで値を取得し、stx_maskを確認する方法をまとめます。

linuxdirect-iostatxsystem-calls

O_DIRECTで開いたファイルにpread()やpwrite()を発行するとEINVALが返る。同じコードが別のサーバーでは問題なく動く。こういうときにまず疑うべきはアライメントです。そして「512バイトに揃えれば大丈夫」「4096なら安全」といった経験則は、どこでも通用するわけではありません。

Linux 6.1以降は、この値をカーネルに直接問い合わせられます。この記事ではopen(2)とstatx(2)のマニュアルページ(Linux man-pages 6.19)をもとに、何を揃える必要があるのか、その値をどう取得するのかを整理します。

O_DIRECTがすること、しないこと

open(2)はO_DIRECTを「このファイルに対するI/Oのキャッシュの影響を最小限にしようとする」フラグと説明しています。ファイルI/Oはユーザー空間のバッファとの間で直接行われます。同じページによれば、一般には性能が下がり、アプリケーションが独自にキャッシュを持つ場合のような特殊な状況で役に立つものです。

O_DIRECTは永続性の保証ではありません。マニュアルには、O_DIRECTはデータを同期的に転送しようと努めるが、データと必要なメタデータが転送されるというO_SYNCの保証は与えない、とあります。同期I/Oを保証するにはO_SYNCを併用する必要があります。書き込みがいつディスクに残るのかはwrite()とfsync()の記事で別に扱いました。

マニュアル自身のまとめも慎重です。O_DIRECTは強力になりうる道具だが注意して使うべきもので、デフォルトでは無効にしておく性能オプションとして扱うよう勧めています。

何を揃える必要があるか

O_DIRECTは次の3つにアライメントの制約を課すことがあります。

  • ユーザー空間バッファのメモリアドレス
  • I/Oの長さ(ベクタI/Oなら各セグメントの長さ)
  • ファイルオフセット

厄介なのは、この制約が固定されていないことです。open(2)のNOTESによると、Linuxのアライメント制約はファイルシステムとカーネルバージョンによって異なり、まったく存在しない場合もあります。アライメントがずれたI/Oの扱いもまちまちで、EINVALで失敗することもあれば、バッファードI/Oにフォールバックすることもあります。

危ないのは後者です。エラーにならないので、アライメントの誤りに気づかないままページキャッシュ経由のI/Oをしていることになります。

EINVALが出る場所はもう一つあります。ファイルシステムがO_DIRECTをサポートしていなければ、open()そのものがEINVALで失敗します。

Linux 6.1: STATX_DIOALIGN

Linux 6.1以降、statx()のmaskにSTATX_DIOALIGNを指定すると、次の2つのフィールドを要求できます。

フィールド 意味
stx_dio_mem_align ユーザーメモリバッファに必要なアライメント(バイト)
stx_dio_offset_align ファイルオフセットとI/Oセグメント長に必要なアライメント(バイト)

値が0なら、そのファイルではdirect I/Oがサポートされていないという意味です。片方が0以外なら、もう片方も0以外になります。

サポート範囲は広くありません。statx(2)によると、ブロックデバイスはLinux 6.1からサポートされ、通常ファイルはファイルシステムによって異なります。マニュアルが名前を挙げているのは、Linux 6.1以降のext4、f2fs、xfsです。それ以外のファイルシステムについてはこのページからは分からないので、サポートを前提にせず、後述のように実行時に確認します。

stx_maskは必ず確認する

statx()は、要求したフィールドをすべて埋めるとは約束していません。マニュアルの説明は次のとおりです。

  • カーネルは未知のmaskビットを基本的に拒否しません。代わりに、このカーネルとファイルシステムが何をサポートしているかをstx_maskで知らせます。
  • ファイルシステムがフィールドをサポートしていなければ、要求していてもstx_maskの該当ビットはクリアされ、互換性のためのダミー値が入ることがあります。
  • 要求していないフィールドが埋められることもあり、その場合は該当ビットがセットされます。

つまり実行中のカーネルやファイルシステムがSTATX_DIOALIGNで要求したフィールドをサポートしていなければ、stx_maskにそのビットは立たず、フィールドにはダミー値が入っている可能性があります。確認せずにstx_dio_mem_alignを読むと、意味のない値をアライメントの基準にしてしまうおそれがあります。判断は常にstx_maskの確認から始めます。

同じ理由で、maskをUINT_MAXで埋めないようにという注意もあります。将来、一部のビットが構造体の拡張を示すために使われる可能性があるからです。

Linux 6.14: 読み込み専用のオフセットアライメント

Linux 6.14ではSTATX_DIO_READ_ALIGNが追加されました。これが要求するのはstx_dio_read_offset_align、つまりdirect I/Oの読み込みに必要なファイルオフセットとセグメント長のアライメントです。

  • 0なら、読み込みにもstx_dio_offset_alignがそのまま適用されます。
  • 0以外ならstx_dio_offset_align以下の値です。読み込み側の制約のほうが緩い場合があるということです。
  • メモリアライメント(stx_dio_mem_align)には影響しません。

マニュアルに記載されたサポート範囲は、Linux 6.14以降のxfsの通常ファイルです。この値はstx_dio_offset_alignと比べて初めて意味を持つので、STATX_DIOALIGNと一緒に要求するのが自然です。書き込みには引き続きstx_dio_offset_alignを使います。

Cのコード例

開いているファイルディスクリプタについてアライメントの値を取得し、予定しているI/Oがそれに合っているかを確認するコードです。statx()のラッパーはglibc 2.28から提供されています。STATX_DIOALIGNや新しいフィールドが定義されていない古いヘッダーではビルドできません。STATX_DIO_READ_ALIGNの部分は、ヘッダーにある場合だけコンパイルされるよう#ifdefで囲んでいます。

#define _GNU_SOURCE
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <sys/stat.h>

struct dio_align {
    uint32_t mem;          /* バッファアドレス */
    uint32_t offset;       /* 書き込み(と既定では読み込み)のオフセット・長さ */
    uint32_t read_offset;  /* 読み込みのオフセット・長さ */
};

/* 0: 値を取得, 1: 報告されなかった(別の方法が必要), -1: statx失敗 */
static int query_dio_align(int fd, struct dio_align *a)
{
    unsigned int want = STATX_DIOALIGN;
#ifdef STATX_DIO_READ_ALIGN
    want |= STATX_DIO_READ_ALIGN;
#endif
    struct statx stx;

    if (statx(fd, "", AT_EMPTY_PATH, want, &stx) == -1)
        return -1;
    if (!(stx.stx_mask & STATX_DIOALIGN))
        return 1;

    a->mem = stx.stx_dio_mem_align;
    a->offset = stx.stx_dio_offset_align;
    a->read_offset = a->offset;
#ifdef STATX_DIO_READ_ALIGN
    if ((stx.stx_mask & STATX_DIO_READ_ALIGN) &&
        stx.stx_dio_read_offset_align != 0)
        a->read_offset = stx.stx_dio_read_offset_align;
#endif
    return 0;
}

static bool dio_ok(const struct dio_align *a, const void *buf,
                   uint64_t off, uint64_t len, bool is_read)
{
    uint32_t step = is_read ? a->read_offset : a->offset;

    if (a->mem == 0)   /* このファイルはdirect I/O非対応 */
        return false;
    return (uintptr_t)buf % a->mem == 0 && off % step == 0 && len % step == 0;
}

statx()に空のパスとAT_EMPTY_PATHを渡すと、dirfdが指すファイルそのものが対象になります。パス名で問い合わせることもできますが、こうすればI/Oに使うまさにそのディスクリプタのファイルを調べられます。

query_dio_align()が1を返した場合、それは「アライメントが不明」という意味であって「制約がない」という意味ではありません。その場合は次の節の方法に進みます。

statxで分からないとき

open(2)には次の手段も書かれています。

  1. ファイルシステム固有のインターフェース。たとえばxfsにはxfsctl(3)のXFS_IOC_DIOINFOがあります。ただしSTATX_DIOALIGNが使えるならそちらを使うように、とされています。
  2. それもなければ、ファイルシステム、個々のファイル、下位のストレージデバイス、カーネルバージョンの既知の特性から推測するしかありません。

推測の出発点として、マニュアルは過去の基準を紹介しています。Linux 2.4では、ブロックデバイスベースのファイルシステムの多くが、ファイルオフセットとすべてのI/Oセグメントの長さ・メモリアドレスをファイルシステムのブロックサイズ(通常4096バイト)の倍数にすることを求めていました。Linux 2.6.0でこれはブロックデバイスの論理ブロックサイズ(通常512バイト)に緩和されました。論理ブロックサイズはioctl(2)のBLKSSZGETか、シェルのblockdev --getssで確認できます。

「通常」という言葉を読み飛ばしてはいけません。これらの値は推測の根拠であって、保証ではありません。

あわせて知っておきたい注意点

fork()と並行して実行しない

メモリバッファがプライベートマッピング(MAP_PRIVATEで作られたマッピング。ヒープや静的に確保したバッファも含む)なら、O_DIRECTのI/Oをfork(2)と並行して実行してはいけません。非同期I/Oインターフェースで発行したものでも別スレッドから発行したものでも、fork()を呼ぶ前に完了させる必要があります。そうしないと、親と子の両方でデータ破損や未定義動作が起こりえます。バッファをshmat(2)やMAP_SHAREDのmmap(2)で作成した場合や、madvise(2)でMADV_DONTFORKを指定して子に引き継がれないようにした場合は、この制約は当てはまりません。

バッファードI/Oやmmapと混ぜない

同じファイル、特に重なるバイト範囲に対してO_DIRECTと通常のI/Oを混在させないよう、マニュアルは勧めています。ファイルシステムが一貫性を正しく処理する場合でも、全体のスループットはどちらか一方だけを使う場合より遅くなりがちだとしています。同じファイルに対してmmap(2)とdirect I/Oを混ぜることも避けるべきだとされています。

NFSでは挙動が異なる

NFSプロトコルはこのフラグをサーバーに伝えられないため、ページキャッシュを迂回するのはクライアント側だけで、サーバーは引き続きキャッシュする可能性があります。LinuxのNFSクライアントはO_DIRECTのI/Oにアライメントの制約を課しません。

まとめ

アライメントの値はハードコードせず、ファイルごとにstatx()で問い合わせます。結果はstx_maskを確認してから信用し、報告されなかったときは「制約なし」ではなく「不明」として扱います。読み込み用の値が別に得られるのは6.14以降の一部のファイルシステムだけです。そしてO_DIRECT自体は永続性のための仕組みではなく、デフォルトでは無効にしておき必要なところで有効にする性能オプションです。

参考資料

ブログへ