Skip to content
Draft
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
53 changes: 23 additions & 30 deletions filesystem/ext4/ext4.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,16 @@ func Create(b backend.Storage, size, start, sectorsize int64, p *Params) (*FileS

// recalculate if it was not user provided
if !userProvidedBlocksize {
sectorsPerBlockR, blocksizeR, numblocksR := recalculateBlocksize(numblocks, size)
sectorsPerBlockR, blocksizeR, numblocksR := recalculateBlocksize(size)
_, blocksize, numblocks = uint8(sectorsPerBlockR), blocksizeR, numblocksR
// resize_inode / reserved-GDT growth isn't yet supported for
// non-1 KiB block sizes (see TODO further down). When *we*
// picked the non-1 KiB default, drop the feature so Create
// doesn't try to lay out blocks it can't read back.
const oneKiB = uint32(SectorSize512) * 2
if blocksize != oneKiB {
fflags.reservedGDTBlocksForExpansion = false
}
}

// how many blocks in each block group (and therefore how many block groups)
Expand Down Expand Up @@ -1865,36 +1873,21 @@ func (fs *FileSystem) readBlock(blockNumber uint64) ([]byte, error) {
return blockBytes, nil
}

// recalculate blocksize based on the existing number of blocks
// - 0 <= blocks < 3MM : floppy - blocksize = 1024
// - 3MM <= blocks < 512MM : small - blocksize = 1024
// - 512MM <= blocks < 4*1024*1024MM : default - blocksize =
// - 4*1024*1024MM <= blocks < 16*1024*1024MM : big - blocksize =
// - 16*1024*1024MM <= blocks : huge - blocksize =
//
// the original code from e2fsprogs https://git.kernel.org/pub/scm/fs/ext2/e2fsprogs.git/tree/misc/mke2fs.c
func recalculateBlocksize(numblocks, size int64) (sectorsPerBlock int, blocksize uint32, numBlocksAdjusted int64) {
var (
million64 = int64(million)
sectorSize512 = uint32(SectorSize512)
)
switch {
case 0 <= numblocks && numblocks < 3*million64:
sectorsPerBlock = 2
blocksize = 2 * sectorSize512
case 3*million64 <= numblocks && numblocks < 512*million64:
sectorsPerBlock = 2
blocksize = 2 * sectorSize512
case 512*million64 <= numblocks && numblocks < 4*1024*1024*million64:
sectorsPerBlock = 2
blocksize = 2 * sectorSize512
case 4*1024*1024*million64 <= numblocks && numblocks < 16*1024*1024*million64:
sectorsPerBlock = 2
blocksize = 2 * sectorSize512
case numblocks > 16*1024*1024*million64:
sectorsPerBlock = 2
blocksize = 2 * sectorSize512
// recalculateBlocksize picks a default ext4 block size when the caller
// did not specify one. We follow mke2fs's "small" vs "default" split:
// 1 KiB blocks below 512 MiB, 4 KiB blocks at or above. The previous
// implementation hard-coded 1 KiB blocks for all sizes, which made the
// journal allocator (capped at 128 MiB) need >65535 1-KiB blocks at a
// few GiB — past both the per-extent cap and the inode's 4-extent root
// limit. 4 KiB blocks keep a typical journal in a single extent.
func recalculateBlocksize(size int64) (sectorsPerBlock int, blocksize uint32, numBlocksAdjusted int64) {
const smallFilesystemThreshold = 512 * 1024 * 1024 // 512 MiB
if size < smallFilesystemThreshold {
sectorsPerBlock = 2 // 1 KiB blocks
} else {
sectorsPerBlock = 8 // 4 KiB blocks
}
blocksize = uint32(sectorsPerBlock) * uint32(SectorSize512)
return sectorsPerBlock, blocksize, size / int64(blocksize)
}

Expand Down
63 changes: 63 additions & 0 deletions filesystem/ext4/multigb_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package ext4

import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"testing"

"github.com/diskfs/go-diskfs/backend/file"
)

// TestCreateMultiGB exercises Create on filesystems past the 512 MiB
// threshold at which recalculateBlocksize switches to 4 KiB blocks.
// 2 GiB is the size that exposed diskfs/go-diskfs#402 under the
// previous 1 KiB-blocks default (a 64 MiB journal then required 65536
// blocks, exceeding both the 32768-blocks-per-extent cap and the
// inode-root extent tree's 4-extent limit).
func TestCreateMultiGB(t *testing.T) {
for _, sizeGiB := range []int64{1, 2} {
sizeGiB := sizeGiB

Check failure on line 22 in filesystem/ext4/multigb_test.go

View workflow job for this annotation

GitHub Actions / Test (macos-latest)

The copy of the 'for' variable "sizeGiB" can be deleted (Go 1.22+) (copyloopvar)

Check failure on line 22 in filesystem/ext4/multigb_test.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

The copy of the 'for' variable "sizeGiB" can be deleted (Go 1.22+) (copyloopvar)
t.Run(fmt.Sprintf("%dGiB", sizeGiB), func(t *testing.T) {
tmp := t.TempDir()
imgPath := filepath.Join(tmp, "fs.img")
size := sizeGiB * 1024 * 1024 * 1024
f, err := os.Create(imgPath)
if err != nil {
t.Fatal(err)
}
if err := f.Truncate(size); err != nil {
_ = f.Close()
t.Fatal(err)
}
if err := f.Close(); err != nil {
t.Fatal(err)
}
f, err = os.OpenFile(imgPath, os.O_RDWR, 0)
if err != nil {
t.Fatal(err)
}
defer func() { _ = f.Close() }()
fs, err := Create(file.New(f, false), size, 0, 512, &Params{})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
if fs == nil {
t.Fatalf("Create returned nil filesystem")
}
if err := f.Sync(); err != nil {
t.Fatalf("Sync: %v", err)
}
cmd := exec.Command("e2fsck", "-f", "-n", imgPath)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
t.Fatalf("e2fsck failed: %v\nstdout:\n%s\nstderr:\n%s",
err, stdout.String(), stderr.String())
}
})
}
}
Loading