Skip to content

Commit 673dcd2

Browse files
authored
perf(ext4): batch nojournal range allocation (#2135)
* perf(ext4): batch nojournal range allocation CubeSandbox tmp writes on nojournal ext4 allocated and published every missing 4 KiB block independently. Each block synchronously rewrote allocation metadata, zeroed data, and updated the inode, keeping virtio-blk at queue depth one and amplifying fixed request latency. Add a bounded DirectRangeStage for depth-zero contiguous appends. Plan allocation metadata in transaction-private images, bulk-zero the complete range, enforce a reliable pre-publication flush, and publish bitmap, group descriptor, superblock, and inode homes in semantic order. Runtime failures restore allocation homes when safe and poison the direct backend when inode state or rollback durability is uncertain. Add fallible DMA/BIO construction, exact block I/O completion handling, bounded contiguous ext4 block transfers, and compile-time-disabled block/ext4 diagnostic counters. Preserve journaled, fragmented, sparse, small-write, unsupported-flush, and resource-pressure behavior through explicit legacy fallbacks. Cover extent validation, unwritten tails, allocation bounds, zero and flush failures, every allocation-home failure, rollback failures, inode uncertainty, and transaction snapshot ownership. The nojournal crash contract remains unchanged: abnormal power loss still requires fsck. Validation: make fmt; cargo test --manifest-path kernel/crates/another_ext4/Cargo.toml --lib; RUST_MIN_STACK=33554432 make kernel; local nojournal ext4 hash and e2fsck; CubeSandbox fresh-volume A1/B/A2 performance and hash checks. Signed-off-by: longjin <longjin@dragonos.org> * fix(ext4): address direct range review findings Make nojournal direct-range publication flush allocation metadata before writing the inode home block. If the durability barrier fails, restore and flush every allocation home; poison the backend when rollback durability cannot be established. Add fault-injection coverage for the new ordering and failure states. Keep coherent DMA memory in the normal write-back direct mapping instead of changing RAM cache attributes without architecture cache maintenance. Reject unsupported non-coherent cache policies explicitly, removing partial multi-page remaps and TLB shootdowns from allocation and interrupt-context release paths. Prune expired ext4 statistics registry entries during mount registration so repeated mount/unmount cycles do not retain dead Weak control blocks. Signed-off-by: longjin <longjin@dragonos.org> * fix(io): address direct range review findings Classify nojournal direct-range candidates under the compatible metadata gate before requesting an exclusive transaction snapshot. Unsupported small, overwrite, sparse, and oversized writes now continue directly through the legacy allocator, while real candidates are re-read and revalidated after exclusive acquisition to avoid stale plans. Restore zero-length virtio-blk synchronous I/O semantics by returning Ok(0) before buffer validation or BIO submission. Keep zero-length read/write BIOs invalid as an internal submission invariant. Add direct-range eligibility coverage for the optimized and legacy request shapes. Signed-off-by: longjin <longjin@dragonos.org> --------- Signed-off-by: longjin <longjin@dragonos.org>
1 parent 7383e9a commit 673dcd2

21 files changed

Lines changed: 2139 additions & 150 deletions

File tree

kernel/crates/another_ext4/src/ext4/alloc.rs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ use crate::ext4_defs::*;
44
use crate::format_error;
55
use crate::prelude::*;
66
use crate::return_error;
7+
use core::cmp::min;
8+
9+
pub(super) struct DirectRangeAllocation {
10+
pub first: PBlockId,
11+
pub allocation_homes: [PBlockId; 3],
12+
}
13+
14+
const DIRECT_RANGE_MAX_GROUP_PROBES: u32 = 4;
715

816
fn extent_tail_batch_limit(
917
first_data_block: PBlockId,
@@ -71,6 +79,135 @@ impl Ext4 {
7179
core::cmp::min(sb.blocks_per_group() as u64, total - first) as usize
7280
}
7381

82+
/// Stage one group-local contiguous data allocation without publishing
83+
/// any cache or disk metadata. The caller owns the filesystem-wide
84+
/// transactional metadata gate, so no direct allocator can race the
85+
/// transaction-private bitmap snapshot while zero I/O is in progress.
86+
pub(super) fn transaction_alloc_direct_range(
87+
&self,
88+
transaction: &mut super::journal_transaction::Transaction<'_>,
89+
inode_id: InodeId,
90+
preferred_first: Option<PBlockId>,
91+
require_preferred: bool,
92+
count: u32,
93+
) -> Result<DirectRangeAllocation> {
94+
if count == 0 {
95+
return_error!(ErrCode::EINVAL, "Cannot allocate an empty block range");
96+
}
97+
let mut sb = self.transaction_read_super_block(transaction)?;
98+
self.prepare_stats.record_superblock_io();
99+
let bg_count = sb.block_group_count();
100+
let preferred_inode_group = ((inode_id - 1) / sb.inodes_per_group()) as BlockGroupId;
101+
let preferred_group = preferred_first
102+
.filter(|block| *block >= sb.first_data_block() as PBlockId)
103+
.map(|block| {
104+
((block - sb.first_data_block() as PBlockId) / sb.blocks_per_group() as PBlockId)
105+
as BlockGroupId
106+
})
107+
.filter(|group| *group < bg_count)
108+
.unwrap_or(preferred_inode_group);
109+
let count_usize = count as usize;
110+
111+
for offset in 0..min(bg_count, DIRECT_RANGE_MAX_GROUP_PROBES) {
112+
let bgid = ((preferred_group as u64 + offset as u64) % bg_count as u64) as BlockGroupId;
113+
let blocks_in_group = Self::block_group_block_count(&sb, bgid);
114+
if blocks_in_group < count_usize {
115+
continue;
116+
}
117+
let mut bg = self.transaction_read_block_group(transaction, bgid)?;
118+
self.prepare_stats.record_gdt_io();
119+
if bg.desc.get_free_blocks_count() < count as u64 {
120+
continue;
121+
}
122+
let bitmap_home = bg.desc.block_bitmap_block();
123+
let bitmap_block = transaction.read(self.block_device.as_ref(), bitmap_home)?;
124+
self.prepare_stats.record_bitmap_io();
125+
let checksum_bytes = (sb.clusters_per_group() as usize) / 8;
126+
if sb.has_read_only_compatible_feature(SuperBlock::FEATURE_RO_COMPAT_METADATA_CSUM) {
127+
if !bg.verify_checksum(sb.metadata_checksum_seed()) {
128+
return_error!(ErrCode::EIO, "Corrupt block-group descriptor checksum");
129+
}
130+
if !bg.desc.verify_block_bitmap_csum(
131+
sb.metadata_checksum_seed(),
132+
&*bitmap_block,
133+
checksum_bytes,
134+
) {
135+
return_error!(ErrCode::EIO, "Corrupt block bitmap checksum");
136+
}
137+
}
138+
139+
let group_first = Self::block_group_first_block(&sb, bgid);
140+
let exact_hint = preferred_first
141+
.filter(|_| offset == 0)
142+
.and_then(|block| block.checked_sub(group_first))
143+
.and_then(|bit| usize::try_from(bit).ok())
144+
.filter(|bit| {
145+
bit.checked_add(count_usize)
146+
.is_some_and(|end| end <= blocks_in_group)
147+
&& (*bit..*bit + count_usize)
148+
.all(|index| bitmap_block[index / 8] & (1 << (index % 8)) == 0)
149+
});
150+
let bit = exact_hint.or_else(|| {
151+
(!require_preferred)
152+
.then(|| {
153+
Bitmap::first_clear_run_in(
154+
&*bitmap_block,
155+
blocks_in_group,
156+
0,
157+
blocks_in_group,
158+
count_usize,
159+
)
160+
})
161+
.flatten()
162+
});
163+
let Some(bit) = bit else { continue };
164+
let first = group_first
165+
.checked_add(bit as PBlockId)
166+
.ok_or_else(|| Ext4Error::new(ErrCode::EFBIG))?;
167+
self.validate_data_blocks(first, count as u64)?;
168+
let new_bg_free = bg
169+
.desc
170+
.get_free_blocks_count()
171+
.checked_sub(count as u64)
172+
.ok_or_else(|| Ext4Error::new(ErrCode::EIO))?;
173+
let new_sb_free = sb
174+
.free_blocks_count()
175+
.checked_sub(count as u64)
176+
.ok_or_else(|| Ext4Error::new(ErrCode::EIO))?;
177+
178+
{
179+
let image = self.transaction_block_for_update(transaction, bitmap_home)?;
180+
self.prepare_stats.record_bitmap_io();
181+
let mut bitmap = Bitmap::new(image, blocks_in_group);
182+
if (bit..bit + count_usize).any(|index| !bitmap.is_bit_clear(index)) {
183+
return_error!(ErrCode::EIO, "Direct range changed during planning");
184+
}
185+
for index in bit..bit + count_usize {
186+
bitmap.set_bit(index);
187+
}
188+
if !bg.desc.update_block_bitmap_csum(
189+
sb.metadata_checksum_seed(),
190+
image,
191+
checksum_bytes,
192+
) {
193+
return_error!(ErrCode::EIO, "Invalid block bitmap checksum length");
194+
}
195+
}
196+
bg.desc.set_free_blocks_count(new_bg_free);
197+
self.transaction_stage_block_group_with_csum(transaction, &mut bg)?;
198+
self.prepare_stats.record_gdt_io();
199+
sb.set_free_blocks_count(new_sb_free);
200+
self.transaction_stage_super_block(transaction, &sb)?;
201+
self.prepare_stats.record_superblock_io();
202+
let (gdt_home, _) = self.block_group_disk_pos(bgid)?;
203+
return Ok(DirectRangeAllocation {
204+
first,
205+
allocation_homes: [bitmap_home, gdt_home, 0],
206+
});
207+
}
208+
return_error!(ErrCode::ENOSPC, "No contiguous direct range available");
209+
}
210+
74211
/// Stage the release of one contiguous physical-block range.
75212
///
76213
/// This changes allocation metadata only. In particular, freed data is
@@ -764,6 +901,7 @@ impl Ext4 {
764901
// extent physical block numbers are absolute filesystem block numbers.
765902
let bitmap_block_id = bg.desc.block_bitmap_block();
766903
let mut bitmap_block = self.read_block(bitmap_block_id)?;
904+
self.prepare_stats.record_bitmap_io();
767905
let old_bitmap_block = bitmap_block.clone();
768906
let old_bg = BlockGroupRef::new(bg.id, bg.desc);
769907
let old_sb = sb;
@@ -785,6 +923,7 @@ impl Ext4 {
785923
return_error!(ErrCode::EIO, "Invalid block bitmap checksum length");
786924
}
787925
self.write_block(&bitmap_block)?;
926+
self.prepare_stats.record_bitmap_io();
788927

789928
// Update block group counters
790929
bg.desc
@@ -844,6 +983,7 @@ impl Ext4 {
844983
}
845984
return Err(init_error);
846985
}
986+
self.prepare_stats.record_zero_io();
847987
Ok(pblock)
848988
}
849989

kernel/crates/another_ext4/src/ext4/extent.rs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,104 @@ pub(super) struct ExtentTail {
2626
pub unwritten: bool,
2727
}
2828

29+
pub(super) struct DirectAppendShape {
30+
pub preferred_first: Option<PBlockId>,
31+
pub requires_merge: bool,
32+
}
33+
2934
impl Ext4 {
35+
pub(super) fn direct_append_shape(
36+
&self,
37+
inode: &InodeRef,
38+
start_lblock: LBlockId,
39+
count: u32,
40+
) -> Result<Option<DirectAppendShape>> {
41+
if count == 0 || count > u16::MAX as u32 || !inode.inode.uses_extents() {
42+
return Ok(None);
43+
}
44+
let root = inode.inode.extent_root();
45+
let header = root.header();
46+
self.validate_extent_node(inode.id, &root)?;
47+
if header.depth() != 0
48+
|| header.max_entries_count() as usize != root.entry_capacity()
49+
|| header.entries_count() as usize > root.entry_capacity()
50+
{
51+
return Ok(None);
52+
}
53+
let entries = header.entries_count() as usize;
54+
if entries == 0 {
55+
return Ok((start_lblock == 0).then_some(DirectAppendShape {
56+
preferred_first: None,
57+
requires_merge: false,
58+
}));
59+
}
60+
let last = root.extent_at(entries - 1);
61+
if last.is_unwritten() {
62+
return Ok(None);
63+
}
64+
for index in 0..entries {
65+
let extent = root.extent_at(index);
66+
self.validate_data_blocks(extent.start_pblock(), extent.block_count() as u64)?;
67+
}
68+
let next_lblock = last
69+
.start_lblock()
70+
.checked_add(last.block_count())
71+
.ok_or_else(|| Ext4Error::new(ErrCode::EFBIG))?;
72+
if next_lblock != start_lblock {
73+
return Ok(None);
74+
}
75+
let preferred_first = last
76+
.start_pblock()
77+
.checked_add(last.block_count() as PBlockId)
78+
.ok_or_else(|| Ext4Error::new(ErrCode::EFBIG))?;
79+
let candidate = Extent::new(start_lblock, preferred_first, count as u16);
80+
let can_merge = Extent::can_append(last, &candidate);
81+
let root_full = entries == header.max_entries_count() as usize;
82+
if root_full && !can_merge {
83+
return Ok(None);
84+
}
85+
Ok(Some(DirectAppendShape {
86+
preferred_first: Some(preferred_first),
87+
requires_merge: root_full,
88+
}))
89+
}
90+
91+
pub(super) fn stage_direct_append_extent(
92+
&self,
93+
inode: &mut InodeRef,
94+
start_lblock: LBlockId,
95+
start_pblock: PBlockId,
96+
count: u32,
97+
) -> Result<()> {
98+
let new_extent = Extent::new(start_lblock, start_pblock, count as u16);
99+
let mut root = inode.inode.extent_root_mut();
100+
let entries = root.header().entries_count() as usize;
101+
if entries > 0 {
102+
let last = *root.extent_at(entries - 1);
103+
if Extent::can_append(&last, &new_extent) {
104+
root.extent_mut_at(entries - 1)
105+
.set_block_count(last.block_count() + count);
106+
} else if root.insert_extent(&new_extent, entries).is_err() {
107+
return Err(format_error!(
108+
ErrCode::ENOTSUP,
109+
"Inline extent root requires a split"
110+
));
111+
}
112+
} else if root.insert_extent(&new_extent, 0).is_err() {
113+
return Err(format_error!(
114+
ErrCode::ENOTSUP,
115+
"Inline extent root requires a split"
116+
));
117+
}
118+
let blocks = inode
119+
.inode
120+
.fs_block_count()
121+
.checked_add(count as u64)
122+
.ok_or_else(|| Ext4Error::new(ErrCode::EFBIG))?;
123+
inode.inode.set_fs_block_count(blocks);
124+
Ok(())
125+
}
126+
30127
fn verify_extent_block_checksum(&self, inode_ref: &InodeRef, image: &[u8]) -> Result<()> {
31128
let sb = self.read_super_block_cached();
32129
if !sb.has_read_only_compatible_feature(SuperBlock::FEATURE_RO_COMPAT_METADATA_CSUM) {
@@ -74,6 +171,7 @@ impl Ext4 {
74171
self.ensure_valid_pblock(inode_ref.id, pblock, "extent tree node")?;
75172
self.validate_data_blocks(pblock, 1)?;
76173
let block = self.read_block(pblock)?;
174+
self.prepare_stats.record_extent_io();
77175
self.verify_extent_block_checksum(inode_ref, &block.data[..])?;
78176
Ok(block)
79177
}
@@ -345,6 +443,7 @@ impl Ext4 {
345443
// Write checksum into the tail
346444
block.data[tail_offset..tail_offset + 4].copy_from_slice(&csum.to_le_bytes());
347445
self.write_block(block)
446+
.inspect(|_| self.prepare_stats.record_extent_io())
348447
}
349448
}
350449

@@ -862,6 +961,15 @@ impl Ext4 {
862961
inode_id
863962
));
864963
}
964+
if header.max_entries_count() as usize > ex_node.entry_capacity()
965+
|| header.entries_count() as usize > ex_node.entry_capacity()
966+
{
967+
return Err(format_error!(
968+
ErrCode::EIO,
969+
"extent header exceeds node capacity on inode {}",
970+
inode_id
971+
));
972+
}
865973
let entries = header.entries_count() as usize;
866974
if header.depth() > 0 {
867975
if entries == 0 {
@@ -1000,6 +1108,7 @@ mod tests {
10001108
inode_mutation_locks: (0..crate::ext4::INODE_MUTATION_LOCK_SHARDS)
10011109
.map(|_| spin::Mutex::new(()))
10021110
.collect(),
1111+
prepare_stats: crate::ext4::PrepareStats::new(),
10031112
}
10041113
}
10051114

@@ -1031,6 +1140,7 @@ mod tests {
10311140
inode_mutation_locks: (0..crate::ext4::INODE_MUTATION_LOCK_SHARDS)
10321141
.map(|_| spin::Mutex::new(()))
10331142
.collect(),
1143+
prepare_stats: crate::ext4::PrepareStats::new(),
10341144
}
10351145
}
10361146

@@ -1092,6 +1202,36 @@ mod tests {
10921202
assert_eq!(err.code(), ErrCode::EIO);
10931203
}
10941204

1205+
#[test]
1206+
fn validate_extent_node_rejects_header_larger_than_inline_storage() {
1207+
let fs = make_test_fs(1024);
1208+
let mut raw = [0u8; 60];
1209+
ExtentNodeMut::from_bytes(&mut raw).init(0, 0);
1210+
raw[2..4].copy_from_slice(&5u16.to_le_bytes());
1211+
raw[4..6].copy_from_slice(&5u16.to_le_bytes());
1212+
let node = ExtentNode::from_bytes(&raw);
1213+
1214+
let error = fs.validate_extent_node(5, &node).unwrap_err();
1215+
1216+
assert_eq!(error.code(), ErrCode::EIO);
1217+
}
1218+
1219+
#[test]
1220+
fn direct_append_rejects_unwritten_tail() {
1221+
let fs = make_test_fs(1024);
1222+
let mut inode = InodeRef::new(6, Box::new(Inode::default()));
1223+
inode.inode.extent_init();
1224+
let mut extent = Extent::new(0, 100, 16);
1225+
extent.mark_unwritten();
1226+
inode
1227+
.inode
1228+
.extent_root_mut()
1229+
.insert_extent(&extent, 0)
1230+
.unwrap();
1231+
1232+
assert!(fs.direct_append_shape(&inode, 16, 16).unwrap().is_none());
1233+
}
1234+
10951235
#[test]
10961236
fn trim_depth_zero_partial_extent_preserves_prefix() {
10971237
let mut raw = [0u8; 60];

kernel/crates/another_ext4/src/ext4/journal.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,15 @@ impl Ext4 {
146146
Ok(())
147147
}
148148

149-
pub(super) fn uses_journal(&self) -> bool {
149+
pub fn uses_journal(&self) -> bool {
150150
matches!(self.metadata_mode, MetadataMutationMode::Journal(_))
151151
}
152152

153+
pub(super) fn supports_direct_range_stage(&self) -> bool {
154+
matches!(self.metadata_mode, MetadataMutationMode::Direct(_))
155+
&& self.block_device.supports_reliable_flush()
156+
}
157+
153158
pub fn shutdown_writable(&self) -> Result<()> {
154159
// Clearing RECOVER is a metadata state transition and must not race a
155160
// direct writer even if the VFS normally excludes writers at umount.
@@ -199,6 +204,17 @@ impl Ext4 {
199204
}
200205
}
201206

207+
pub(super) fn transaction_start_direct_range(
208+
&self,
209+
credits: usize,
210+
) -> Result<journal_transaction::Transaction<'_>> {
211+
match &self.metadata_mode {
212+
MetadataMutationMode::Direct(core) => core.start_direct_range(credits),
213+
MetadataMutationMode::ReadOnly => Err(Ext4Error::new(ErrCode::EROFS)),
214+
MetadataMutationMode::Journal(_) => Err(Ext4Error::new(ErrCode::ENOTSUP)),
215+
}
216+
}
217+
202218
pub(super) fn initialize_journal(&mut self) -> Result<()> {
203219
if !self.block_device.supports_reliable_flush() {
204220
return Err(Ext4Error::new(ErrCode::ENOTSUP));

0 commit comments

Comments
 (0)