Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion echoframe/cpp/libs/Storage/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,21 @@ endif ()
# with and without the deferred-write knob. Drives a real
# Handler through a real file, which is where the counting
# lives, so this one needs elevation on Windows.
# linux_alignment_check how a statx(STATX_DIOALIGN) result becomes the O_DIRECT
# offset/length and buffer-address alignments, including the
# no-STATX_DIOALIGN fallback. Linux only; no file I/O.
#
# Both exit non-zero on failure.
# All exit non-zero on failure.
include(CTest)

foreach (check write_stats_check write_count_check)
add_executable(${check} tests/${check}.cpp)
target_link_libraries(${check} PRIVATE echoframe_storage)
add_test(NAME ${check} COMMAND ${check})
endforeach ()

if (NOT WIN32)
add_executable(linux_alignment_check tests/linux_alignment_check.cpp)
target_link_libraries(linux_alignment_check PRIVATE echoframe_storage)
add_test(NAME linux_alignment_check COMMAND linux_alignment_check)
endif ()
5 changes: 4 additions & 1 deletion echoframe/cpp/libs/Storage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ than plain synchronous writes:
buffers and the header are sector-size aligned (padding is added when a buffer
is not a multiple of the sector size, and recorded in the header). The
alignment is queried at open time — `queryAlignment()` — from the physical
sector size on Windows and from `statx(STATX_DIOALIGN)` on Linux.
sector size on Windows and from `statx(STATX_DIOALIGN)` on Linux. A Linux
kernel that does not report `STATX_DIOALIGN` (pre-6.1, or a filesystem that
does not implement the query) falls back to a conservative page-sized
alignment and logs a warning.
- **Preallocation + privileged extension** — with `preallocateFullFile` the file
is sized up front. On Windows extension uses `SE_MANAGE_VOLUME_NAME`
(`SeManageVolumePrivilege`) to grow files without zero-filling; on Linux it
Expand Down
15 changes: 9 additions & 6 deletions echoframe/cpp/libs/Storage/src/storage/Linux/LinuxFileIO.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
//
// The sector size used for offset/length rounding (adjustToSectorSize()) and
// the buffer-address alignment O_DIRECT requires are queried at runtime via
// statx(STATX_DIOALIGN) in queryAlignment(); the constructor throws if the
// kernel/filesystem can't report a real value. The file header
// (writeToHeader()) is allocated against these queried values.
// statx(STATX_DIOALIGN) in queryAlignment(); a kernel that does not report it
// falls back to a conservative page-sized alignment, and the constructor throws
// only if the filesystem reports the query but does not enforce alignment. The
// file header (writeToHeader()) is allocated against these queried values.
//

#ifndef CUBE_STORAGE_LINUXFILEIO_H
Expand Down Expand Up @@ -44,7 +45,9 @@ class LinuxFileIO {
/**
* @brief Query the file's actual O_DIRECT offset/length alignment
* (mSectorSize) and buffer-address alignment (mMemAlign) via
* statx(STATX_DIOALIGN). Throws if either value can't be determined.
* statx(STATX_DIOALIGN). Falls back to the page size for both when the
* kernel does not report the query. Throws if the filesystem reports it but
* does not enforce alignment.
*/
void queryAlignment();

Expand Down Expand Up @@ -80,8 +83,8 @@ class LinuxFileIO {
std::vector<uint64_t> mBlockOffset; // element offset into buffer for each block

// O_DIRECT alignment requirements. Set by queryAlignment(), called
// early in the constructor (which throws if either can't be
// determined) -- never read at their zero-initialized value.
// early in the constructor, which either resolves them or throws --
// never read at their zero-initialized value.
uint64_t mSectorSize{0}; // required offset/length alignment
uint64_t mMemAlign{0}; // required buffer-address alignment

Expand Down
90 changes: 18 additions & 72 deletions echoframe/cpp/libs/Storage/src/storage/Linux/LinuxFileIO.t.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,58 +24,10 @@
#include <string>

#include "../error.h"
#include "linux_statx.h"

namespace Storage {

// The kernel's statx layout, transcribed. glibc's struct statx and
// <linux/stat.h> cannot be included together (same tag, different members),
// and neither reliably declares stx_dio_mem_align/stx_dio_offset_align. The
// raw syscall is used rather than glibc's wrapper, which is typed against
// glibc's own struct.
namespace {

struct EfStatxTimestamp {
int64_t tv_sec;
uint32_t tv_nsec;
int32_t __reserved;
};

struct EfStatx {
uint32_t stx_mask;
uint32_t stx_blksize;
uint64_t stx_attributes;
uint32_t stx_nlink;
uint32_t stx_uid;
uint32_t stx_gid;
uint16_t stx_mode;
uint16_t __spare0[1];
uint64_t stx_ino;
uint64_t stx_size;
uint64_t stx_blocks;
uint64_t stx_attributes_mask;
EfStatxTimestamp stx_atime;
EfStatxTimestamp stx_btime;
EfStatxTimestamp stx_ctime;
EfStatxTimestamp stx_mtime;
uint32_t stx_rdev_major;
uint32_t stx_rdev_minor;
uint32_t stx_dev_major;
uint32_t stx_dev_minor;
uint64_t stx_mnt_id;
uint32_t stx_dio_mem_align;
uint32_t stx_dio_offset_align;
uint64_t __spare3[12]; // reserved space for fields added after these
};

// statx(2) takes no buffer-length argument: the kernel always writes a fixed
// 256-byte struct. A smaller struct here would be overflowed by the syscall.
static_assert(sizeof(EfStatx) == 256, "EfStatx must match the kernel's fixed statx(2) ABI size");

constexpr uint32_t kEfStatxDioalign = 0x00002000U; // STATX_DIOALIGN
constexpr uint32_t kEfStatxBasicStats = 0x000007ffU; // STATX_BASIC_STATS (covers stx_blksize)

} // namespace

// ---------------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -315,41 +267,35 @@ void LinuxFileIO<bufferType_t>::extendFile(int nBuffers) {
// Block layout
// ---------------------------------------------------------------------------

// Query the file's O_DIRECT alignment via statx(STATX_DIOALIGN). Offset/length
// alignment (stx_dio_offset_align) and buffer-address alignment
// (stx_dio_mem_align) are reported separately and are not guaranteed equal.
// Throws if either is unavailable -- a kernel without STATX_DIOALIGN, or a
// filesystem that accepts O_DIRECT without enforcing alignment (e.g. tmpfs).
// Query the file's O_DIRECT alignment via statx(STATX_DIOALIGN) and store it.
// A kernel that does not report the query falls back to a conservative
// page-sized alignment (resolveDioAlignment) and warns. Throws if the
// filesystem reports the query but zeroes either value, i.e. accepts O_DIRECT
// without enforcing alignment (e.g. tmpfs).
template <typename bufferType_t>
void LinuxFileIO<bufferType_t>::queryAlignment() {
EfStatx stx {};
detail::EfStatx stx {};
if (syscall(SYS_statx, mFd, "", AT_EMPTY_PATH,
kEfStatxDioalign | kEfStatxBasicStats, &stx) != 0)
detail::kEfStatxDioalign | detail::kEfStatxBasicStats,
&stx) != 0)
throw storageException(
std::string("queryAlignment: statx failed: ") + strerror(errno) +
"\n");
if (!(stx.stx_mask & kEfStatxDioalign))
throw storageException(
"queryAlignment: kernel does not report STATX_DIOALIGN "
"(requires Linux 6.1+)\n");
if (stx.stx_dio_offset_align == 0 || stx.stx_dio_mem_align == 0)

const detail::DioAlignment align = detail::resolveDioAlignment(stx);
if (!align.reported && logAtLeast(kLogNormal))
std::cerr << "queryAlignment: kernel does not report STATX_DIOALIGN; "
"using conservative "
<< align.sectorSize << "-byte O_DIRECT alignment.\n";
if (align.sectorSize == 0 || align.memAlign == 0)
throw storageException(
"queryAlignment: filesystem does not support/enforce O_DIRECT "
"alignment (dio_offset_align/dio_mem_align reported as 0 -- "
"direct I/O is not actually honored on this filesystem, e.g. "
"tmpfs)\n");

// dio_offset_align is only the required minimum: on a 512e drive (512 B
// logical, 4096 B physical) it is 512, and writing at that alignment forces
// a device-side read-modify-write per block. Prefer stx_blksize (typically
// the physical sector) when it is a whole multiple of dio_offset_align, so
// writes land on physical-sector boundaries; fall back to dio_offset_align.
mSectorSize = stx.stx_dio_offset_align;
if ((stx.stx_mask & kEfStatxBasicStats) && stx.stx_blksize > mSectorSize &&
stx.stx_blksize % stx.stx_dio_offset_align == 0) {
mSectorSize = stx.stx_blksize;
}
mMemAlign = stx.stx_dio_mem_align;
mSectorSize = align.sectorSize;
mMemAlign = align.memAlign;

mHeaderSize = computeHeaderSize();
}
Expand Down
121 changes: 121 additions & 0 deletions echoframe/cpp/libs/Storage/src/storage/Linux/linux_statx.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//
// O_DIRECT alignment reported by statx(STATX_DIOALIGN), and how to resolve it
// into the two alignments EchoFrame needs.
//
// Split out of LinuxFileIO.t.hpp so the resolution can be exercised directly
// from tests/linux_alignment_check.cpp. It is header-only and safe to include
// from more than one translation unit.
//

#ifndef CUBE_STORAGE_LINUX_STATX_H
#define CUBE_STORAGE_LINUX_STATX_H

#include <unistd.h>

#include <cstdint>

namespace Storage {
namespace detail {

// The kernel's statx layout, transcribed. glibc's struct statx and
// <linux/stat.h> cannot be included together (same tag, different members),
// and neither reliably declares stx_dio_mem_align/stx_dio_offset_align. The
// raw syscall is used rather than glibc's wrapper, which is typed against
// glibc's own struct.
struct EfStatxTimestamp {
int64_t tv_sec;
uint32_t tv_nsec;
int32_t __reserved;
};

struct EfStatx {
uint32_t stx_mask;
uint32_t stx_blksize;
uint64_t stx_attributes;
uint32_t stx_nlink;
uint32_t stx_uid;
uint32_t stx_gid;
uint16_t stx_mode;
uint16_t __spare0[1];
uint64_t stx_ino;
uint64_t stx_size;
uint64_t stx_blocks;
uint64_t stx_attributes_mask;
EfStatxTimestamp stx_atime;
EfStatxTimestamp stx_btime;
EfStatxTimestamp stx_ctime;
EfStatxTimestamp stx_mtime;
uint32_t stx_rdev_major;
uint32_t stx_rdev_minor;
uint32_t stx_dev_major;
uint32_t stx_dev_minor;
uint64_t stx_mnt_id;
uint32_t stx_dio_mem_align;
uint32_t stx_dio_offset_align;
uint64_t __spare3[12]; // reserved space for fields added after these
};

// statx(2) takes no buffer-length argument: the kernel always writes a fixed
// 256-byte struct. A smaller struct here would be overflowed by the syscall.
static_assert(sizeof(EfStatx) == 256,
"EfStatx must match the kernel's fixed statx(2) ABI size");

constexpr uint32_t kEfStatxDioalign = 0x00002000U; // STATX_DIOALIGN
constexpr uint32_t kEfStatxBasicStats =
0x000007ffU; // STATX_BASIC_STATS (covers stx_blksize)

/// @return The runtime page size, which bounds any O_DIRECT buffer-address
/// alignment. Falls back to 4096 if the query fails, which is the smallest
/// page size Linux allows on the platforms EchoFrame targets.
inline uint64_t pageSize() {
const long ps = sysconf(_SC_PAGESIZE);
return ps > 0 ? static_cast<uint64_t>(ps) : 4096;
}

/// The two alignments an O_DIRECT write has to satisfy, and whether the
/// filesystem reported them.
struct DioAlignment {
uint64_t sectorSize; // required offset/length alignment
uint64_t memAlign; // required buffer-address alignment
bool reported; // false when the kernel did not report STATX_DIOALIGN
};

/// Resolve a statx result into the O_DIRECT offset/length and buffer-address
/// alignments.
///
/// Offset/length alignment (stx_dio_offset_align) and buffer-address alignment
/// (stx_dio_mem_align) are reported separately and are not guaranteed equal.
/// Both come back as 0 from a filesystem that accepts O_DIRECT without
/// enforcing it (e.g. tmpfs); the caller is expected to reject that, so they
/// are passed through rather than replaced by the conservative fallback.
///
/// A kernel that does not report STATX_DIOALIGN at all -- before Linux 6.1, or
/// a filesystem that does not implement the query -- yields `reported == false`
/// and a conservative page-sized alignment for both the file offset/length and
/// the buffer address.
inline DioAlignment resolveDioAlignment(const EfStatx &stx) {
if (!(stx.stx_mask & kEfStatxDioalign)) {
const uint64_t fallback = pageSize();
return {fallback, fallback, false};
}

if (stx.stx_dio_offset_align == 0 || stx.stx_dio_mem_align == 0)
return {0, 0, true};

// dio_offset_align is only the required minimum: on a 512e drive (512 B
// logical, 4096 B physical) it is 512, and writing at that alignment forces
// a device-side read-modify-write per block. Prefer stx_blksize (typically
// the physical sector) when it is a whole multiple of dio_offset_align, so
// writes land on physical-sector boundaries; fall back to dio_offset_align.
uint64_t sectorSize = stx.stx_dio_offset_align;
if ((stx.stx_mask & kEfStatxBasicStats) && stx.stx_blksize > sectorSize &&
stx.stx_blksize % stx.stx_dio_offset_align == 0) {
sectorSize = stx.stx_blksize;
}
return {sectorSize, stx.stx_dio_mem_align, true};
}

} // namespace detail
} // namespace Storage

#endif // CUBE_STORAGE_LINUX_STATX_H
Loading
Loading