Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

XFS Filesystem

Introduction

XFS is a high-performance, 64-bit journaling filesystem originally developed by SGI (Silicon Graphics) for IRIX in 1993 and ported to Linux in 2001. It is the default filesystem for Red Hat Enterprise Linux (starting with RHEL 7) and is widely used for large-scale storage, databases, and media workloads. XFS excels at parallel I/O, large file handling, and filesystem scalability.

XFS was designed from the ground up for parallelism. Its allocation group architecture allows multiple threads to perform concurrent metadata operations without contention on global structures. This makes it particularly well-suited for multi-core systems with high-throughput storage.

Architecture Overview

Allocation Groups (AGs)

The defining architectural feature of XFS is the allocation group (AG). The filesystem is divided into AGs, each of which is essentially an independent filesystem with its own:

  • Free space management (B+ trees)
  • Inode allocation (B+ trees)
  • Inode B+ tree
  • Internal locks
graph TB
    subgraph "XFS Filesystem"
        AG0["AG 0<br>Blocks 0 - 1B<br>Own superblock copy<br>Own free-space B+ trees<br>Own inode B+ trees"]
        AG1["AG 1<br>Blocks 1B - 2B<br>Own free-space B+ trees<br>Own inode B+ trees"]
        AG2["AG 2<br>Blocks 2B - 3B<br>Own free-space B+ trees<br>Own inode B+ trees"]
        AG3["AG N...<br>..."]
    end

Each AG has its own locks, meaning multiple threads can allocate blocks and inodes from different AGs simultaneously without any contention:

sequenceDiagram
    participant T1 as Thread 1 (AG 0)
    participant T2 as Thread 2 (AG 1)
    participant T3 as Thread 3 (AG 2)
    participant AG0 as AG 0 Lock
    participant AG1 as AG 1 Lock
    participant AG2 as AG 2 Lock

    T1->>AG0: Lock AG 0
    T2->>AG1: Lock AG 1
    T3->>AG2: Lock AG 2
    Note over T1,T3: All three threads allocating simultaneously!
    AG0-->>T1: Blocks allocated
    AG1-->>T2: Blocks allocated
    AG2-->>T3: Blocks allocated

AG Size

Each AG is typically 1 GiB to 16 GiB in size. The number of AGs determines the degree of parallelism:

# View AG information
$ sudo xfs_info /dev/sda1
meta-data=/dev/sda1       isize=512    agcount=16, agsize=65536 blks
         =                sectsz=512   attr=2, projid32bit=1
         =                crc=1        finobt=1, sparse=1, rmapbt=0
         =                reflink=1    bigtime=1 inobtcount=1
data     =                bsize=4096   blocks=1048576, imaxpct=25
         =                sunit=0      swidth=0 blks
naming   =version 2       bsize=4096   ascii-ci=0, ftype=1
log      =internal        bsize=4096   blocks=16384, version=2
         =                sectsz=512   sunit=0 blks, lazy-count=1
realtime =none            rtextents=0  rblocks=0

In this example: agcount=16, agsize=65536 blks = 16 AGs, each 65536 × 4096 = 256 MiB.

B+ Trees

XFS uses B+ trees extensively for metadata management. There are several key B+ trees:

1. Free Space B+ Trees

Each AG maintains two B+ trees for free space:

  • By-block B+ tree: Maps physical block ranges to free extents (for allocation)
  • By-size B+ tree: Maps free extent sizes to their locations (for finding best-fit)
graph TD
    subgraph "Free Space B+ Tree (by block)"
        R["Root Node"]
        N1["Internal: blocks 0-500K"]
        N2["Internal: blocks 500K-1M"]
        L1["Leaf: (0, 1000) (1000, 500) (1500, 3000)"]
        L2["Leaf: (500000, 2000) (502000, 8000)"]
        R --> N1
        R --> N2
        N1 --> L1
        N2 --> L2
    end

2. Inode B+ Tree (inobt)

Each AG maintains a B+ tree mapping inode numbers to their on-disk locations:

/* XFS inode B+ tree record */
struct xfs_inobt_rec {
    __be32  ir_startino;     /* Starting inode number */
    __be16  ir_freecount;    /* Number of free inodes in chunk */
    __be16  ir_free;         /* Free inode bitmask (16-bit) */
    __be64  ir_free_hi;      /* Free inode bitmask (upper 48 bits) */
};

3. Reverse-Mapping B+ Tree (rmapbt)

Added in recent XFS, this maps physical blocks back to their owners (inodes + offsets):

struct xfs_rmap_rec {
    __be32  rm_owner;     /* Inode number (or special owner code) */
    __be64  rm_offset;    /* Logical offset within the owner */
    __be32  rm_blockno;   /* Physical block number */
    __be32  rm_len;       /* Length in blocks */
};

This enables:

  • Online filesystem repair
  • Reflink (shared extents)
  • Quota accounting improvements

For reflinked (shared) extents, XFS maintains a reference count B+ tree:

struct xfs_refcount_rec {
    __be32  rc_startblock;  /* Physical start block */
    __be32  rc_blockcount;  /* Number of blocks */
    __be32  rc_refcount;    /* Reference count */
};

Journaling

XFS uses an internal log (journal) for metadata consistency. The log is a circular buffer that records all metadata changes before they are committed to their final locations.

Delayed Logging

XFS uses a technique called “delayed logging” (also called “logical logging”) to batch metadata changes:

sequenceDiagram
    participant App as Application
    participant CIL as CIL (Committed Item List)
    participant Log as XFS Log
    participant AIL as AIL (Active Item List)
    participant Disk as Data/Metadata

    App->>CIL: Metadata change
    CIL->>CIL: Buffer changes in memory
    Note over CIL: Batch multiple changes

    CIL->>Log: Push: write CIL to log
    Log->>Log: Format and write log record

    CIL->>AIL: Move items to AIL
    AIL->>Disk: Writeback: flush dirty metadata to final location
    AIL->>Log: Advance log grant space

Key concepts:

  • CIL (Committed Item List): In-memory buffer that accumulates dirty metadata items
  • AIL (Active Item List): Tracks items whose log entries have been flushed but not yet written to their final location
  • Log ticket: A reservation for log space, ensuring transactions don’t overflow the log

Log Structure

/* XFS log record header */
struct xlog_rec_header {
    __be32  h_magicno;       /* XLOG_HEADER_MAGIC_NUM */
    __be32  h_cycle;         /* Log cycle number */
    __be32  h_version;       /* Log format version */
    __be32  h_len;           /* Length of data in record */
    __be64  h_lsn;           /* Log sequence number */
    __be64  h_tail_lsn;      /* Tail of log (oldest dirty) */
    __be32  h_fmt;           /* Format of data */
    uuid_t  h_fs_uuid;       /* Filesystem UUID */
    __be32  h_size;          /* Total record length including data */
    /* ... checksums, padding ... */
};

Log Placement

The log can be internal (stored within the filesystem) or external (on a separate device):

# Internal log (default)
$ sudo mkfs.xfs /dev/sda1

# External log on faster device
$ sudo mkfs.xfs -l logdev=/dev/nvme0n1p2,size=2g /dev/sda1

# View log location
$ sudo xfs_info /dev/sda1 | grep log
log      =internal        bsize=4096   blocks=16384, version=2

Inode Structure

XFS inodes are 512 bytes by default (configurable at mkfs time from 256 to 2048 bytes):

/* XFS on-disk inode (512 bytes) */
struct xfs_dinode {
    __be16  di_magic;        /* XFS_DINODE_MAGIC */
    __be16  di_mode;         /* File type and permissions */
    __be8   di_version;      /* Inode version (3 for v3) */
    __be8   di_format;       /* Data format (local/extents/btree) */
    __be16  di_onlink;       /* Old number of links */
    __be32  di_uid;          /* Owner UID */
    __be32  di_gid;          /* Owner GID */
    __be16  di_flushiter;    /* Flush iteration count */
    __be16  di_afi;          /* Attribute fork inline */
    __be32  di_projid_lo;    /* Project ID (lower) */
    __be16  di_projid_hi;    /* Project ID (upper) */
    __be8   di_pad[6];       /* Padding */
    __be16  di_flushiter;    /* Incremented on flush */
    __be64  di_size;         /* File size in bytes */
    __be64  di_nblocks;      /* Number of blocks */
    __be32  di_extsize;      /* Extent size hint */
    __be32  di_nextents;     /* Number of data extents */
    __be16  di_anextents;    /* Number of attribute extents */
    __be8   di_forkoff;      /* Fork offset (attr/data split) */
    __be8   di_aformat;      /* Attribute fork format */
    __be32  di_dmevmask;     /* DMAPI event mask */
    __be16  di_dmstate;      /* DMAPI state */
    __be16  di_flags;        /* Flags */
    __be32  di_gen;          /* Inode generation */
    /* Version 3 additions: */
    __be32  di_crc;          /* CRC of inode */
    __be64  di_changecount;  /* Change count */
    __be64  di_lsn;          /* Last log sequence number */
    __be64  di_flags2;       /* Additional flags */
    __be32  di_cowextsz;     /* CoW extent size */
    __u8    di_pad2[12];     /* Padding */
    xfs_timestamp_t di_crtime; /* Creation time */
    __be64  di_ino;          /* Inode number (64-bit) */
    uuid_t  di_uuid;         /* Filesystem UUID */
    /* Data/attr fork data follows */
};

Data Fork Formats

XFS inodes store data in “forks” — the data fork holds file data, the attribute fork holds extended attributes:

FormatWhen UsedDescription
XFS_DINODE_FMT_LOCALSmall files/dirsData stored inline in the inode
XFS_DINODE_FMT_EXTENTSMedium filesArray of extent descriptors
XFS_DINODE_FMT_BTREELarge/fragmented filesB+ tree of extents
XFS_DINODE_FMT_DEVDevice nodesDevice number stored
XFS_DINODE_FMT_UUID(deprecated)UUID reference

Extent Mapping

/* Extent descriptor */
struct xfs_bmbt_rec {
    __be64  l0, l1;  /* Packed extent info */
};

/* Unpacked extent */
struct xfs_bmbt_irec {
    xfs_fileoff_t br_startoff;    /* Logical offset */
    xfs_fsblock_t br_startblock;  /* Physical block */
    xfs_filblks_t br_blockcount;  /* Length in blocks */
    xfs_exntst_t  br_state;       /* Allocated or unwritten */
};

XFS supports “unwritten extents” — extents that are allocated but contain no data. When written to, they are converted to normal extents. This is critical for preallocation (fallocate()) and delayed allocation.

Directory Structure

XFS uses a hash-based B+ tree for directories:

Short Form (Inline)

For very small directories, entries are stored inline in the inode’s data fork.

Block Form

For small-medium directories, entries are stored in directory blocks with a linear scan.

Leaf/Node Form (B+ Tree)

For large directories, XFS uses a B+ tree indexed by a hash of the filename:

graph TD
    subgraph "XFS Large Directory"
        R["B+ Tree Root<br>Hash → Directory Block"]
        L1["Leaf: hash entries A-F"]
        L2["Leaf: hash entries G-M"]
        L3["Leaf: hash entries N-Z"]
        B1["Data Block: actual directory entries"]
        R --> L1
        R --> L2
        R --> L3
        L1 --> B1
    end

Realtime Subsystem

XFS has an optional “realtime” subsystem for guaranteed-bandwidth I/O. The realtime device is typically a separate, fast device (SSD, NVMe):

# Create XFS with realtime device
$ sudo mkfs.xfs -r rtdev=/dev/nvme0n1p2,rtsize=100g /dev/sda1

# Mark a file for realtime storage
$ sudo xfs_io -c "chattr +t" /path/to/realtime/file

# View realtime extent size
$ sudo xfs_info /dev/sda1 | grep realtime
realtime =/dev/nvme0n1p2   rtextents=25600 rblocks=2560000

The realtime device is divided into fixed-size “realtime extents” (typically 4 MiB). Files on the realtime device get contiguous allocations, ensuring consistent I/O performance.

Online Management

Online Resize

# Grow XFS to fill the device
$ sudo xfs_growfs /mount/point

# The filesystem must be mounted; XFS only supports growing, not shrinking

Online Repair (xfs_repair)

# Unmount and repair
$ sudo umount /dev/sda1
$ sudo xfs_repair /dev/sda1
Phase 1 - find and verify superblock...
Phase 2 - using internal log
        - zero log...
        - scan filesystem freespace and inode maps...
        - found root inode chunk
Phase 3 - for each AG...
        - scan (but don't clear) agi unlinked lists...
        - process known inodes and perform inode discovery...
        - process newly discovered inodes...
Phase 4 - check for duplicate blocks...
        - setting up duplicate extent list...
        - check for inodes claiming duplicate blocks...
        - agno = 0 ...
Phase 5 - rebuild AG headers and trees...
Phase 6 - check inode connectivity...
Phase 7 - verify and correct link counts...
done

# Dry run (no changes)
$ sudo xfs_repair -n /dev/sda1

Online Scrub (xfs_scrub)

Modern XFS supports online filesystem checking:

# Online scrub (filesystem stays mounted)
$ sudo xfs_scrub /mount/point

# Verbose mode
$ sudo xfs_scrub -v /mount/point

# Repair errors found by scrub
$ sudo xfs_scrub -r /mount/point

XFS supports reflinks (copy-on-write sharing of data blocks) since kernel 4.9:

# Enable reflink (default on recent mkfs.xfs)
$ sudo mkfs.xfs -m reflink=1 /dev/sdb1

# Copy a file using reflink (instant, shares blocks)
$ cp --reflink=always /data/large_file /data/clone

# Both files share the same physical blocks until one is modified
$ filefrag /data/large_file /data/clone
/data/large_file: 1 extent found
/data/clone: 1 extent found

# Check extent sharing via xfs_bmap
$ sudo xfs_bmap -v /data/large_file
$ sudo xfs_bmap -v /data/clone
# Same physical blocks!

Quota Management

XFS supports user, group, and project quotas:

# Enable quotas at mount time
$ sudo mount -o uquota,gquota,pquota /dev/sda1 /data

# Or via /etc/fstab
# /dev/sda1  /data  xfs  defaults,uquota,gquota,pquota  0  0

# Set user quota
$ sudo xfs_quota -x -c "limit bsoft=5g bhard=6g user1" /data

# Set project quota
$ sudo xfs_quota -x -c "project -s myproject" /data
$ sudo xfs_quota -x -c "limit -p bhard=100g myproject" /data

# Report quotas
$ sudo xfs_quota -x -c "report -h" /data

Extended Attributes

XFS stores extended attributes (xattrs) in the attribute fork of the inode:

# Set xattr
$ setfattr -n user.comment -v "important file" /data/file

# Get xattr
$ getfattr -n user.comment /data/file
# file: data/file
user.comment="important file"

# List all xattrs
$ getfattr -d /data/file

For large xattrs, XFS uses a B+ tree in the attribute fork (similar to the data fork for files).

Performance Tuning

Mount Options

# Common performance options
$ sudo mount -o noatime,logbufs=8,logbsize=256k,allocsize=64m /dev/sda1 /data
OptionEffect
noatimeDon’t update access timestamps
logbufs=8Number of log buffers (default: 8)
logbsize=256kLog buffer size
allocsize=64mPreallocation size for delayed allocation
nobarrierDisable write barriers (dangerous, SSD-only)
inode64Allow inodes beyond the first 1 TiB
swallocAlign allocations to stripe width
delaylogUse delayed logging (default)

Benchmarking

# Sequential write
$ fio --name=xfs-seq-write --filename=/data/fio-test \
    --size=10G --bs=1M --rw=write --direct=1 --ioengine=libaio \
    --iodepth=32 --numjobs=4

# Metadata-heavy workload (many small files)
$ fio --name=xfs-meta --filename=/data/fio-meta \
    --size=1G --bs=4k --rw=randwrite --direct=0 --ioengine=libaio \
    --iodepth=1 --numjobs=1 --create_on_open=1 --nrfiles=100000 \
    --openfiles=10000 --directory=/data/fio-meta-dir

Comparison with ext4

FeatureXFSext4
Max filesystem size8 EiB1 EiB
Max file size8 EiB16 TiB
Allocation parallelismPer-AG lockingPer-group
Directory structureB+ tree hashhtree
Delayed loggingYes (CIL)No
Online repairxfs_scrube2fsck (offline)
ReflinkYes (4.9+)No
Shrink filesystemNoYes
Realtime volumeYesNo
Inline dataYesYes

XFS Internal Design Documents (from docs.kernel.org)

The kernel documentation at docs.kernel.org/filesystems/xfs/ provides deep technical details on several XFS subsystems:

Delayed Logging Design

XFS’s delayed logging (logical logging) is a critical performance optimization. The design document covers:

  • Transactions in XFS: How operations are wrapped in transactions with reservation-based space accounting
  • Asynchronous nature: Transactions are not immediately written to disk — they are batched in the CIL
  • Transaction Reservations: Each transaction type has a pre-calculated log space reservation
  • Re-logging: Dirty items can be re-logged multiple times within a single CIL checkpoint interval, reducing log I/O
  • CIL (Committed Item List): The in-memory buffer that accumulates dirty metadata items across multiple transactions
  • AIL (Active Item List): Tracks items whose log entries have been flushed but metadata hasn’t reached its final location

Self-Describing Metadata

Modern XFS (v5 superblock, metadata CRCs) uses self-describing metadata structures that include:

  • Magic numbers: Each metadata block starts with a magic number identifying its type
  • UUID: Each block contains the filesystem UUID for cross-device validation
  • Block address: The expected location of the block on disk
  • Log sequence number (LSN): For cache validity checking
  • CRC32c checksum: For integrity verification

This enables runtime validation — the kernel can verify metadata consistency on every read, catching corruption early.

Online Fsck Design

XFS is developing online filesystem checking (xfs_scrub), which can verify and repair filesystem consistency while the filesystem remains mounted. The design involves:

  • Scan-and-fix approach: Scan metadata structures and fix inconsistencies in-place
  • Cross-referencing: Compare data structures (e.g., reverse-mapping B+ tree vs. inode extent maps) to detect discrepancies
  • Locking strategy: Use existing filesystem locks to ensure consistency during scrub operations
  • Repair: Fix detected problems by rebuilding corrupted data structures from redundant information

Further Reading

  • VFS — The virtual filesystem layer
  • Inode — Inode structure used by XFS
  • Journaling — XFS delayed logging and journal concepts
  • ext4 — Comparison filesystem
  • Btrfs — Another modern Linux filesystem