Skip to content

Commit e374b45

Browse files
authored
fix(kernel): stabilize apt update under sustained I/O (#2157)
* fix(kernel): stabilize apt update under sustained I/O Rework NAPI scheduling around an exact-once SCHED/MISSED state machine with bounded, fair polling. Move virtio-net to deeper raw queues with preallocated DMA buffers, asynchronous TX completion, and race-closing EVENT_IDX callback handling. Track actual cross-CPU task ownership independently from runqueue placement and serialize stop wakeups, affinity changes, and migration tails so a task cannot execute or enqueue twice during network wakeups. Batch ext4 sequential range allocation and orphan extent reclamation, preserve journal credit and checksum invariants, and avoid read-before-write for complete blocks. This prevents apt package-store writes and interrupted-download cleanup from synchronously amplifying metadata I/O. Treat disappearing proc fd and fdinfo tables as normal exit races, refresh the pinned virtio-drivers revision, and add host-testable NAPI transition coverage. Validated with cargo test -p napi-state, cargo test -p another_ext4, and make kernel. Signed-off-by: longjin <longjin@dragonos.org> * fix(kernel): refresh dynamic proc and block state Refresh whole-disk capacity from the backing block device so loop devices expose their configured size after LOOP_SET_FD. Keep the static start LBA on I/O hot paths, safely fall back after device teardown, and report only complete loop sectors. Revalidate cached proc fd and fdinfo entries against the live descriptor table. Return ENOENT across zombie and reaped-task paths, including reads from an already-open fdinfo file, matching Linux 6.6 semantics. Add a dunitest covering cached fd entries across process exit. Signed-off-by: longjin <longjin@dragonos.org> * fix(net): defer virtio response TX reservation Consume completed RX buffers independently of TX queue availability so NAPI can continue making receive progress under transmit pressure. Use a lazy response token for receive-side replies and reserve DMA capacity only when smoltcp actually emits a frame. Keep explicitly reserved tokens for standalone transmit calls and split RX/TX token types to preserve their ownership invariants. Signed-off-by: longjin <longjin@dragonos.org> * fix(net): report standard MTU without blocking boot Keep the smoltcp Ethernet frame limit separate from the user-visible IP MTU so rtnetlink reports 1500 bytes instead of 1514. Run the existing DHCP acquisition window in a kernel worker, allowing SystemState::Running and userspace startup to proceed when no DHCP server is available. Add a strict rtnetlink MTU regression and teach dunitest to parse singular GoogleTest summaries so the one-case suite is enforced by CI. Signed-off-by: longjin <longjin@dragonos.org> * fix(fat): serialize concurrent cluster allocation Signed-off-by: longjin <longjin@dragonos.org> * fix(ext4): bound transactional range probes Signed-off-by: longjin <longjin@dragonos.org> --------- Signed-off-by: longjin <longjin@dragonos.org>
1 parent 94d4a8d commit e374b45

34 files changed

Lines changed: 1947 additions & 476 deletions

File tree

kernel/Cargo.lock

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

kernel/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ atomic_enum = "=0.2.0"
4343
bit_field = "=0.10"
4444
bitfield-struct = "=0.5.3"
4545
bitflags = "=1.3.2"
46+
bitflags2 = { package = "bitflags", version = "=2.9.1" }
4647
bitmap = { path = "crates/bitmap" }
4748
driver_base_macros = { "path" = "crates/driver_base_macros" }
4849
elf = { version = "=0.7.2", default-features = false }
@@ -55,6 +56,7 @@ intertrait = { path = "crates/intertrait" }
5556
kcmdline_macros = { path = "crates/kcmdline_macros" }
5657
kdepends = { path = "crates/kdepends" }
5758
klog_types = { path = "crates/klog_types" }
59+
napi-state = { path = "crates/napi-state" }
5860
linkme = "=0.3.27"
5961
num = { version = "=0.4.0", default-features = false }
6062
num-derive = "=0.3"
@@ -82,7 +84,7 @@ smoltcp = { version = "=0.12.0", git = "https://github.com/DragonOS-Community/sm
8284
syscall_table_macros = { path = "crates/syscall_table_macros" }
8385
system_error = { path = "crates/system_error" }
8486
unified-init = { path = "crates/unified-init" }
85-
virtio-drivers = { git = "https://git.mirrors.dragonos.org.cn/DragonOS-Community/virtio-drivers", rev = "89f6a402d494cf615cadfd851c9f017f31acac81" }
87+
virtio-drivers = { git = "https://git.mirrors.dragonos.org.cn/DragonOS-Community/virtio-drivers", rev = "82d783c935084a67c2e0a3bead0266a66aed80fd" }
8688
wait_queue_macros = { path = "crates/wait_queue_macros" }
8789
paste = "=1.0.14"
8890
slabmalloc = { path = "crates/rust-slabmalloc" }

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

Lines changed: 132 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,28 @@ use crate::ext4_defs::*;
44
use crate::format_error;
55
use crate::prelude::*;
66
use crate::return_error;
7-
use core::cmp::min;
87

9-
pub(super) struct DirectRangeAllocation {
8+
pub(super) struct RangeAllocation {
109
pub first: PBlockId,
1110
pub allocation_homes: [PBlockId; 3],
1211
}
1312

14-
const DIRECT_RANGE_MAX_GROUP_PROBES: u32 = 4;
13+
// Range allocation runs under the transaction writer gate. Keep the
14+
// speculative search short so a fragmented filesystem cannot turn a single
15+
// sequential write into a volume-wide metadata read.
16+
const TRANSACTION_RANGE_MAX_GROUP_PROBES: u32 = 4;
17+
18+
fn transaction_range_probe_limit(block_group_count: u32, require_preferred: bool) -> u32 {
19+
// An exact-adjacency request can only succeed in its preferred group. A
20+
// failed probe is therefore a fast-path miss, not a reason to scan other
21+
// groups while holding the transaction gate.
22+
let limit = if require_preferred {
23+
1
24+
} else {
25+
TRANSACTION_RANGE_MAX_GROUP_PROBES
26+
};
27+
core::cmp::min(block_group_count, limit)
28+
}
1529

1630
fn extent_tail_batch_limit(
1731
first_data_block: PBlockId,
@@ -83,14 +97,14 @@ impl Ext4 {
8397
/// any cache or disk metadata. The caller owns the filesystem-wide
8498
/// transactional metadata gate, so no direct allocator can race the
8599
/// transaction-private bitmap snapshot while zero I/O is in progress.
86-
pub(super) fn transaction_alloc_direct_range(
100+
pub(super) fn transaction_alloc_range(
87101
&self,
88102
transaction: &mut super::journal_transaction::Transaction<'_>,
89103
inode_id: InodeId,
90104
preferred_first: Option<PBlockId>,
91105
require_preferred: bool,
92106
count: u32,
93-
) -> Result<DirectRangeAllocation> {
107+
) -> Result<RangeAllocation> {
94108
if count == 0 {
95109
return_error!(ErrCode::EINVAL, "Cannot allocate an empty block range");
96110
}
@@ -108,7 +122,11 @@ impl Ext4 {
108122
.unwrap_or(preferred_inode_group);
109123
let count_usize = count as usize;
110124

111-
for offset in 0..min(bg_count, DIRECT_RANGE_MAX_GROUP_PROBES) {
125+
// This is a speculative fast path. Bound its metadata I/O under the
126+
// transaction writer gate; on a miss the caller aborts the
127+
// transaction and uses the legacy allocator. Exact-adjacency probes
128+
// need only inspect the preferred group.
129+
for offset in 0..transaction_range_probe_limit(bg_count, require_preferred) {
112130
let bgid = ((preferred_group as u64 + offset as u64) % bg_count as u64) as BlockGroupId;
113131
let blocks_in_group = Self::block_group_block_count(&sb, bgid);
114132
if blocks_in_group < count_usize {
@@ -180,7 +198,7 @@ impl Ext4 {
180198
self.prepare_stats.record_bitmap_io();
181199
let mut bitmap = Bitmap::new(image, blocks_in_group);
182200
if (bit..bit + count_usize).any(|index| !bitmap.is_bit_clear(index)) {
183-
return_error!(ErrCode::EIO, "Direct range changed during planning");
201+
return_error!(ErrCode::EIO, "Range allocation changed during planning");
184202
}
185203
for index in bit..bit + count_usize {
186204
bitmap.set_bit(index);
@@ -200,7 +218,7 @@ impl Ext4 {
200218
self.transaction_stage_super_block(transaction, &sb)?;
201219
self.prepare_stats.record_superblock_io();
202220
let (gdt_home, _) = self.block_group_disk_pos(bgid)?;
203-
return Ok(DirectRangeAllocation {
221+
return Ok(RangeAllocation {
204222
first,
205223
allocation_homes: [bitmap_home, gdt_home, 0],
206224
});
@@ -706,57 +724,96 @@ impl Ext4 {
706724
break;
707725
}
708726

709-
let mut transaction = self.transaction_start(32)?;
710-
let Some(tail) = self.extent_tail(&transaction, &inode)? else {
711-
break;
712-
};
713-
let extent_end = tail
714-
.start_pblock
715-
.checked_add(tail.block_count as PBlockId)
716-
.ok_or_else(|| format_error!(ErrCode::EIO, "Invalid extent physical range"))?;
717-
if tail.start_pblock == 0
718-
|| extent_end > self.read_super_block_cached().block_count()
719-
|| self.journal_owns_block_range(tail.start_pblock, extent_end)
720-
{
721-
return_error!(
722-
ErrCode::EIO,
723-
"Orphan extent overlaps invalid or journal-owned blocks"
724-
);
725-
}
726-
// transaction_dealloc_block_range deliberately accepts one block
727-
// group only. Trim the right edge at that boundary so bitmap and
728-
// counters stay a compact, independently restartable unit.
729727
let sb = self.read_super_block_cached();
730728
let blocks_per_group = sb.blocks_per_group() as PBlockId;
731-
let remove_limit = extent_tail_batch_limit(
732-
sb.first_data_block() as PBlockId,
733-
blocks_per_group,
734-
tail.start_pblock,
735-
tail.block_count,
736-
)
737-
.ok_or_else(|| format_error!(ErrCode::EIO, "Invalid extent tail"))?;
738-
let removed = self
739-
.extent_remove_tail_in_transaction(&mut transaction, &mut inode, remove_limit)?
740-
.ok_or_else(|| format_error!(ErrCode::EIO, "Extent tail disappeared"))?;
741-
self.transaction_dealloc_block_range(
742-
&mut transaction,
743-
removed.start_pblock,
744-
removed.block_count,
745-
)?;
746-
for metadata in removed.metadata_blocks.iter().copied() {
747-
self.transaction_dealloc_block_range(&mut transaction, metadata, 1)?;
729+
let first_data_block = sb.first_data_block() as PBlockId;
730+
let mut transaction = self.transaction_start(32)?;
731+
let mut batch_group = None;
732+
let mut removals = 0usize;
733+
734+
// Sequential range allocation can create hundreds of extents in
735+
// one block group. Committing each tail entry independently turns
736+
// unlink/failed-download cleanup into hundreds of synchronous JBD2
737+
// barriers. Bitmap/GDT/superblock and the right-most extent leaf
738+
// are read-your-writes transaction images, so remove all adjacent
739+
// tail entries from the same allocation group in one commit.
740+
while removals < 64 {
741+
// One removal can detach an extent-tree right spine of at most
742+
// five blocks. In the adversarial layout every detached
743+
// metadata block belongs to a different allocation group:
744+
// data bitmap/GDT (2), five metadata bitmap/GDT pairs (10),
745+
// one surviving parent (1), the shared superblock (1), and
746+
// the final inode-table image (1). Do not begin another
747+
// mutation unless all 15 possible new homes still fit. This
748+
// avoids a repeatable E2BIG orphan-reclaim failure while
749+
// retaining large batches for the common shared-home case.
750+
const NEXT_REMOVAL_CREDIT_RESERVE: usize = 15;
751+
if removals != 0 && transaction.remaining_credits() < NEXT_REMOVAL_CREDIT_RESERVE {
752+
break;
753+
}
754+
let Some(tail) = self.extent_tail(&transaction, &inode)? else {
755+
break;
756+
};
757+
let extent_end = tail
758+
.start_pblock
759+
.checked_add(tail.block_count as PBlockId)
760+
.ok_or_else(|| format_error!(ErrCode::EIO, "Invalid extent physical range"))?;
761+
if tail.start_pblock == 0
762+
|| extent_end > sb.block_count()
763+
|| self.journal_owns_block_range(tail.start_pblock, extent_end)
764+
{
765+
return_error!(
766+
ErrCode::EIO,
767+
"Orphan extent overlaps invalid or journal-owned blocks"
768+
);
769+
}
770+
let remove_limit = extent_tail_batch_limit(
771+
first_data_block,
772+
blocks_per_group,
773+
tail.start_pblock,
774+
tail.block_count,
775+
)
776+
.ok_or_else(|| format_error!(ErrCode::EIO, "Invalid extent tail"))?;
777+
let removed_first = extent_end
778+
.checked_sub(remove_limit as PBlockId)
779+
.ok_or_else(|| format_error!(ErrCode::EIO, "Invalid extent tail range"))?;
780+
let group = removed_first
781+
.checked_sub(first_data_block)
782+
.ok_or_else(|| format_error!(ErrCode::EIO, "Invalid extent block group"))?
783+
/ blocks_per_group;
784+
if batch_group.is_some_and(|current| current != group) {
785+
break;
786+
}
787+
batch_group = Some(group);
788+
789+
let removed = self
790+
.extent_remove_tail_in_transaction(&mut transaction, &mut inode, remove_limit)?
791+
.ok_or_else(|| format_error!(ErrCode::EIO, "Extent tail disappeared"))?;
792+
self.transaction_dealloc_block_range(
793+
&mut transaction,
794+
removed.start_pblock,
795+
removed.block_count,
796+
)?;
797+
for metadata in removed.metadata_blocks.iter().copied() {
798+
self.transaction_dealloc_block_range(&mut transaction, metadata, 1)?;
799+
}
800+
let released = removed.block_count as u64 + removed.metadata_blocks.len() as u64;
801+
let remaining = inode
802+
.inode
803+
.fs_block_count()
804+
.checked_sub(released)
805+
.ok_or_else(|| format_error!(ErrCode::EIO, "Invalid inode block count"))?;
806+
inode.inode.set_fs_block_count(remaining);
807+
inode.inode.set_size(core::cmp::min(
808+
inode.inode.size(),
809+
removed.start_lblock as u64 * BLOCK_SIZE as u64,
810+
));
811+
removals += 1;
812+
}
813+
if removals == 0 {
814+
transaction.abort();
815+
break;
748816
}
749-
let released = removed.block_count as u64 + removed.metadata_blocks.len() as u64;
750-
let remaining = inode
751-
.inode
752-
.fs_block_count()
753-
.checked_sub(released)
754-
.ok_or_else(|| format_error!(ErrCode::EIO, "Invalid inode block count"))?;
755-
inode.inode.set_fs_block_count(remaining);
756-
inode.inode.set_size(core::cmp::min(
757-
inode.inode.size(),
758-
removed.start_lblock as u64 * BLOCK_SIZE as u64,
759-
));
760817
self.transaction_stage_inode_with_csum(&mut transaction, &mut inode)?;
761818
self.commit_reclaim_transaction(transaction)?;
762819
}
@@ -1236,8 +1293,25 @@ impl Ext4 {
12361293
}
12371294

12381295
#[cfg(test)]
1239-
mod reclaim_tests {
1240-
use super::{extent_tail_batch_limit, linked_orphan_tail_remove_limit};
1296+
mod allocation_tests {
1297+
use super::{
1298+
extent_tail_batch_limit, linked_orphan_tail_remove_limit, transaction_range_probe_limit,
1299+
TRANSACTION_RANGE_MAX_GROUP_PROBES,
1300+
};
1301+
1302+
#[test]
1303+
fn transactional_range_probes_are_bounded_and_exact_requests_are_local() {
1304+
assert_eq!(transaction_range_probe_limit(0, false), 0);
1305+
assert_eq!(transaction_range_probe_limit(1, false), 1);
1306+
assert_eq!(
1307+
transaction_range_probe_limit(128, false),
1308+
TRANSACTION_RANGE_MAX_GROUP_PROBES
1309+
);
1310+
1311+
assert_eq!(transaction_range_probe_limit(0, true), 0);
1312+
assert_eq!(transaction_range_probe_limit(1, true), 1);
1313+
assert_eq!(transaction_range_probe_limit(128, true), 1);
1314+
}
12411315

12421316
#[test]
12431317
fn extent_reclaim_batch_never_crosses_a_block_group() {

0 commit comments

Comments
 (0)