Skip to main content

kernel_manager/
pager.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! This supports paging for ext4 files.  `zx_pager_supply_pages` requires is to transfer pages to
6//! the target, hence the need for a transfer VMO.  This also uses a static zeroed VMO to transfer
7//! pages that should be zeroed.
8
9use fidl_fuchsia_starnix_runner as fstarnixrunner;
10use futures::TryStreamExt;
11use starnix_logging::{log_debug, log_error, log_warn, with_zx_name};
12use starnix_sync::{LockDepMutex, PagerFilesByInodeLock, PagerFilesystemsLock};
13use starnix_uapi::errors::Errno;
14use starnix_uapi::{errno, error};
15use std::collections::HashMap;
16use std::collections::hash_map::Entry;
17use std::ops::Range;
18use std::sync::Arc;
19use std::sync::atomic::{AtomicU32, Ordering};
20use zx::sys::zx_page_request_command_t::{ZX_PAGER_VMO_COMPLETE, ZX_PAGER_VMO_READ};
21
22// N.B. At time of writing, no particular science has gone into picking these numbers; tweaking
23// these numbers might or might not give us better performance.
24const PAGER_THREADS: usize = 1;
25const TRANSFER_VMO_SIZE: u64 = 1 * 1024 * 1024;
26const ZERO_VMO_SIZE: u64 = 1 * 1024 * 1024;
27
28/// Tracing category used to trace the pager.
29const CATEGORY_STARNIX_PAGER: &'static str = "starnix:pager";
30
31pub async fn run_pager(
32    pager_request: fstarnixrunner::ManagerCreatePagerRequest,
33    pager: Arc<Pager>,
34) {
35    let fstarnixrunner::ManagerCreatePagerRequest {
36        backing_vmo: Some(backing_vmo),
37        block_size: Some(block_size),
38        pager: Some(pager_server),
39        ..
40    } = pager_request
41    else {
42        log_error!("Invalid create pager request");
43        return;
44    };
45
46    let filesystem = Arc::new(match Filesystem::new(pager.clone(), backing_vmo, block_size) {
47        Ok(filesystem) => filesystem,
48        Err(error) => {
49            log_error!("Unable to register filesystem {error}");
50            return;
51        }
52    });
53
54    pager.add_filesystem(filesystem.clone());
55
56    let mut stream = pager_server.into_stream();
57    'outer: while let Ok(Some(event)) = stream.try_next().await {
58        match event {
59            fstarnixrunner::PagerRequest::RegisterFile {
60                payload:
61                    fstarnixrunner::PagerRegisterFileRequest {
62                        name: Some(name),
63                        inode_num: Some(inode_num),
64                        size: Some(size),
65                        extents: Some(extents),
66                        ..
67                    },
68                responder,
69                ..
70            } => {
71                fuchsia_trace::instant!(
72                    CATEGORY_STARNIX_PAGER,
73                    "file_register",
74                    fuchsia_trace::Scope::Thread
75                );
76                match filesystem.register(
77                    &name,
78                    inode_num,
79                    size,
80                    extents
81                        .iter()
82                        .map(|e| PagerExtent {
83                            logical: e.logical_start..e.logical_end,
84                            physical_block: e.physical_block,
85                        })
86                        .collect(),
87                ) {
88                    Ok(vmo) => {
89                        match responder.send(Ok(fstarnixrunner::PagerRegisterFileResponse {
90                            vmo: Some(vmo),
91                            ..Default::default()
92                        })) {
93                            Ok(_) => {}
94                            Err(e) => {
95                                log_error!("Error sending pager response {:?}", e);
96                                break 'outer;
97                            }
98                        }
99                    }
100                    Err(e) => match responder.send(Err(e.into_raw())) {
101                        Ok(_) => {}
102                        Err(e) => {
103                            log_error!("Error sending pager error response {:?}", e);
104                            break 'outer;
105                        }
106                    },
107                };
108            }
109            fstarnixrunner::PagerRequest::RegisterFile { .. } => {
110                log_error!("Invalid RegisterFile request");
111                break 'outer;
112            }
113            _ => {}
114        }
115    }
116    pager.remove_filesystem(&*filesystem);
117}
118
119/// A simple pager implementation. One pager can serve multiple filesystems.
120pub struct Pager {
121    pager: zx::Pager,
122    port: zx::Port,
123    zero_vmo: zx::Vmo,
124    next_filesystem_id: AtomicU32,
125    filesystems: LockDepMutex<HashMap<u32, Arc<Filesystem>>, PagerFilesystemsLock>,
126}
127
128impl Pager {
129    pub fn new() -> Result<Self, Errno> {
130        Ok(Self {
131            pager: zx::Pager::create(zx::PagerOptions::empty()).map_err(|error| {
132                log_error!(error:?; "Pager::create failed");
133                errno!(EINVAL)
134            })?,
135            port: zx::Port::create(),
136            zero_vmo: with_zx_name(
137                zx::Vmo::create(ZERO_VMO_SIZE).map_err(|_| errno!(EINVAL))?,
138                b"starnix:ext4",
139            ),
140            next_filesystem_id: AtomicU32::new(1),
141            filesystems: Default::default(),
142        })
143    }
144
145    /// Starts the pager threads.
146    pub fn start_threads(self: &Arc<Self>) {
147        for i in 0..PAGER_THREADS {
148            let this = self.clone();
149            let _ = std::thread::Builder::new().name(format!("pager-{}", i)).spawn(move || {
150                this.run_pager_thread();
151            });
152        }
153    }
154
155    /// Dedicated thread responsible for listening on port and supplying pages as needed.
156    /// More than one pager thread can be running concurrently.
157    pub fn run_pager_thread(&self) {
158        let transfer_vmo = with_zx_name(
159            zx::Vmo::create(TRANSFER_VMO_SIZE).expect("unable to create transfer vmo"),
160            b"starnix:ext4",
161        );
162        let transfer_vmo_addr = fuchsia_runtime::vmar_root_self()
163            .map(
164                0,
165                &transfer_vmo,
166                0,
167                TRANSFER_VMO_SIZE as usize,
168                zx::VmarFlags::PERM_READ | zx::VmarFlags::PERM_WRITE | zx::VmarFlags::ALLOW_FAULTS,
169            )
170            .expect("unable to map transfer vmo");
171        scopeguard::defer!({
172            // SAFETY: We mapped the VMO above.
173            let _ = unsafe {
174                fuchsia_runtime::vmar_root_self()
175                    .unmap(transfer_vmo_addr, TRANSFER_VMO_SIZE as usize)
176            };
177        });
178        let split_key = |key: u64| -> (u32, u32) { ((key >> 32) as u32, key as u32) };
179        loop {
180            match self.port.wait(zx::MonotonicInstant::INFINITE) {
181                Ok(packet) => {
182                    match packet.contents() {
183                        zx::PacketContents::Pager(contents)
184                            if contents.command() == ZX_PAGER_VMO_READ =>
185                        {
186                            fuchsia_trace::duration!(CATEGORY_STARNIX_PAGER, "vmo_read");
187                            let (filesystem_num, inode_num) = split_key(packet.key());
188                            let filesystem = self
189                                .filesystems
190                                .lock()
191                                .get(&filesystem_num)
192                                .cloned()
193                                .expect("Unexpected packet key");
194                            filesystem.receive_pager_packet(
195                                inode_num,
196                                contents,
197                                &transfer_vmo,
198                                transfer_vmo_addr,
199                            );
200                        }
201                        zx::PacketContents::Pager(contents)
202                            if contents.command() == ZX_PAGER_VMO_COMPLETE =>
203                        {
204                            fuchsia_trace::duration!(CATEGORY_STARNIX_PAGER, "vmo_complete");
205                            // We don't care about this command, but we will receive them and we
206                            // don't want to log them as unexpected.
207                        }
208                        zx::PacketContents::SignalOne(signals)
209                            if signals.observed().contains(zx::Signals::VMO_ZERO_CHILDREN) =>
210                        {
211                            fuchsia_trace::duration!(
212                                CATEGORY_STARNIX_PAGER,
213                                "signal_zero_children"
214                            );
215                            let (filesystem_num, inode_num) = split_key(packet.key());
216                            // We may get VMO_ZERO_CHILDREN notifications for
217                            // files within a filesystem after it is unmounted,
218                            // ignore those.
219                            let filesystem = self.filesystems.lock().get(&filesystem_num).cloned();
220                            if let Some(filesystem) = filesystem {
221                                filesystem.on_zero_children(inode_num).expect("on_zero_children");
222                            }
223                        }
224                        zx::PacketContents::User(_) => break,
225                        _ => log_error!("Unexpected port packet: {:?}", packet.contents()),
226                    }
227                }
228                Err(error) => log_error!(error:?; "Port::wait failed"),
229            }
230        }
231        log_debug!("Pager thread terminating");
232    }
233
234    fn create_pager_vmo(&self, key: u64, size: u64) -> Result<zx::Vmo, zx::Status> {
235        self.pager.create_vmo(zx::VmoOptions::RESIZABLE, &self.port, key, size)
236    }
237
238    fn allocate_filesystem_id(&self) -> u32 {
239        self.next_filesystem_id.fetch_add(1, Ordering::Relaxed)
240    }
241
242    fn add_filesystem(&self, filesystem: Arc<Filesystem>) {
243        self.filesystems.lock().insert(filesystem.id(), filesystem);
244    }
245
246    fn remove_filesystem(&self, filesystem: &Filesystem) {
247        self.filesystems.lock().remove(&filesystem.id());
248    }
249
250    /// Terminates (asynchronously) the pager threads.
251    pub fn terminate(&self) {
252        let up = zx::UserPacket::from_u8_array([0; 32]);
253        let packet = zx::Packet::from_user_packet(0, 0, up);
254        for _ in 0..PAGER_THREADS {
255            self.port.queue(&packet).unwrap();
256        }
257    }
258}
259
260/// Filesystem registered with the pager.
261pub struct Filesystem {
262    pager: Arc<Pager>,
263    backing_vmo: zx::Vmo,
264    block_size: u64,
265    files_by_inode: LockDepMutex<HashMap<u32, Arc<PagedFile>>, PagerFilesByInodeLock>,
266    id: u32,
267}
268
269impl Filesystem {
270    /// Returns a new filesystem.  `block_size` shouldn't be too big (which might cause overflows) and it
271    /// should be a power of 2.
272    pub fn new(pager: Arc<Pager>, backing_vmo: zx::Vmo, block_size: u64) -> Result<Self, Errno> {
273        if block_size > 1024 * 1024 || !block_size.is_power_of_two() {
274            return error!(EINVAL, "Bad block size {block_size}");
275        }
276        let id = pager.allocate_filesystem_id();
277        Ok(Self { pager, backing_vmo, block_size, files_by_inode: Default::default(), id })
278    }
279
280    /// Registers the file with the pager.  Returns a child VMO.  `extents` should be sorted.
281    pub fn register(
282        &self,
283        name: &str,
284        inode_num: u32,
285        size: u64,
286        extents: Box<[PagerExtent]>,
287    ) -> Result<zx::Vmo, zx::Status> {
288        let (file, did_create) = {
289            match self.files_by_inode.lock().entry(inode_num) {
290                Entry::Occupied(o) => (o.get().clone(), false),
291                Entry::Vacant(v) => (
292                    v.insert(Arc::new(PagedFile {
293                        vmo: self
294                            .pager
295                            .create_pager_vmo(self.port_key_for_inode(inode_num), size)?,
296                        extents,
297                    }))
298                    .clone(),
299                    true,
300                ),
301            }
302        };
303        let child_vmo = file.vmo.create_child(zx::VmoChildOptions::REFERENCE, 0, 0);
304        if did_create {
305            let set_up_vmo = |vmo| -> Result<(), zx::Status> {
306                self.watch_for_zero_children(vmo, inode_num)?;
307                vmo.set_name(&zx::Name::new_lossy(&format!("ext4!{}", name)))?;
308                Ok(())
309            };
310
311            if let Err(e) = set_up_vmo(&file.vmo) {
312                self.files_by_inode.lock().remove(&inode_num);
313                return Err(e);
314            }
315        }
316        child_vmo
317    }
318
319    fn receive_pager_packet(
320        &self,
321        inode_num: u32,
322        contents: zx::PagerPacket,
323        transfer_vmo: &zx::Vmo,
324        transfer_vmo_addr: usize,
325    ) {
326        let Some(file) = self.files_by_inode.lock().get(&inode_num).cloned() else {
327            return;
328        };
329
330        let requested_range = contents.range();
331
332        // Align the read to 128 KiB slots (readahead).
333        const ALIGNMENT: u64 = 128 * 1024;
334        let readahead_start = (requested_range.start / ALIGNMENT) * ALIGNMENT;
335        let mut readahead_end = requested_range.end.next_multiple_of(ALIGNMENT);
336
337        // Clamp to VMO size to avoid supplying pages out of bounds.
338        let vmo_size = match file.vmo.get_size() {
339            Ok(size) => size,
340            Err(status) => {
341                log_error!("Failed to get VMO size: {:?}", status);
342                let _ = self.pager.pager.op_range(
343                    zx::PagerOp::Fail(zx::Status::IO),
344                    &file.vmo,
345                    contents.range(),
346                );
347                return;
348            }
349        };
350        readahead_end = std::cmp::min(readahead_end, vmo_size);
351
352        if readahead_end <= readahead_start {
353            return;
354        }
355
356        let start_block = (readahead_start / self.block_size) as u32;
357        let mut ix = file.extents.partition_point(|e| e.logical.end <= start_block);
358
359        // SAFETY: We know that `transfer_vmo` is mapped (and initialized) for `TRANSFER_VMO_SIZE`
360        // bytes and `len` must be less than or equal to that.
361        let buf = unsafe {
362            std::slice::from_raw_parts_mut(transfer_vmo_addr as *mut u8, TRANSFER_VMO_SIZE as usize)
363        };
364
365        let mut current_offset = readahead_start;
366        let mut supply_helper =
367            SupplyHelper::new(transfer_vmo, buf, &file.vmo, current_offset, &*self.pager);
368
369        while ix < file.extents.len() && current_offset < readahead_end {
370            let extent = &file.extents[ix];
371
372            let logical_start = extent.logical.start as u64 * self.block_size;
373
374            // Deal with holes.
375            if current_offset < logical_start {
376                if let Err(e) = supply_helper.zero(logical_start - current_offset) {
377                    supply_helper.fail_to(readahead_end, e);
378                    return;
379                }
380                current_offset = logical_start;
381            }
382
383            let end = std::cmp::min(extent.logical.end as u64 * self.block_size, readahead_end);
384
385            while current_offset < end {
386                let phys_offset =
387                    extent.physical_block * self.block_size + current_offset - logical_start;
388
389                match supply_helper.fill_buf(|buf| {
390                    let amount = std::cmp::min(buf.len() as u64, end - current_offset) as usize;
391                    self.backing_vmo.read(&mut buf[..amount], phys_offset)?;
392                    Ok(amount)
393                }) {
394                    Ok(amount) => {
395                        // We don't need the pages in the backing VMO any more.  Don't worry about
396                        // errors; this is purely a hint.
397                        let _ = self.backing_vmo.op_range(
398                            zx::VmoOp::DONT_NEED,
399                            phys_offset,
400                            amount as u64,
401                        );
402                        current_offset += amount as u64;
403                    }
404                    Err(e) => {
405                        supply_helper.fail_to(readahead_end, e);
406                        return;
407                    }
408                }
409            }
410
411            ix += 1;
412        }
413
414        if let Err(e) = supply_helper.finish(readahead_end) {
415            supply_helper.fail_to(readahead_end, e);
416        }
417    }
418
419    fn watch_for_zero_children(&self, vmo: &zx::Vmo, inode_num: u32) -> Result<(), zx::Status> {
420        vmo.wait_async(
421            &self.pager.port,
422            self.port_key_for_inode(inode_num),
423            zx::Signals::VMO_ZERO_CHILDREN,
424            zx::WaitAsyncOpts::empty(),
425        )
426    }
427
428    fn on_zero_children(&self, inode_num: u32) -> Result<(), Errno> {
429        let mut files = self.files_by_inode.lock();
430        let file = files.entry(inode_num);
431        if let Entry::Occupied(o) = file {
432            let vmo = &o.get().vmo;
433            match vmo.info() {
434                Ok(info) => {
435                    if info.num_children == 0 {
436                        // This is a true signal, so we can remove this entry.
437                        o.remove();
438                    } else {
439                        // This shouldn't fail, and there's not much we can do
440                        // if it does.
441                        if let Err(error) = self.watch_for_zero_children(vmo, inode_num) {
442                            log_error!(
443                                error:?;
444                                "watch_for_zero_children failed"
445                            );
446                        }
447                    }
448                }
449                Err(error) => log_error!(error:?; "Vmo::info failed"),
450            }
451        }
452        Ok(())
453    }
454
455    fn port_key_for_inode(&self, inode_num: u32) -> u64 {
456        (self.id as u64) << 32 | inode_num as u64
457    }
458
459    fn id(&self) -> u32 {
460        self.id
461    }
462}
463
464/// Per file state needed by the pager.
465struct PagedFile {
466    /// The main VMO.  We always hand out children of this VMO.
467    vmo: zx::Vmo,
468
469    /// The extents for the file, which will be sorted and not overlapping.  There can be holes i.e.
470    /// zeroed ranges within the file.
471    extents: Box<[PagerExtent]>,
472}
473
474/// A single extent.
475pub struct PagerExtent {
476    pub logical: Range<u32>,
477    pub physical_block: u64,
478}
479
480/// SupplyHelper exists to make dealing with misalignment easier.
481struct SupplyHelper<'a> {
482    transfer_vmo: &'a zx::Vmo,
483    buffer: &'a mut [u8],
484    target_vmo: &'a zx::Vmo,
485    offset: u64,
486    pager: &'a Pager,
487    page_size: u64,
488    buf_len: usize,
489}
490
491impl<'a> SupplyHelper<'a> {
492    fn new(
493        transfer_vmo: &'a zx::Vmo,
494        buffer: &'a mut [u8],
495        target_vmo: &'a zx::Vmo,
496        offset: u64,
497        pager: &'a Pager,
498    ) -> Self {
499        Self {
500            transfer_vmo,
501            buffer,
502            target_vmo,
503            offset,
504            pager,
505            page_size: *starnix_core::mm::PAGE_SIZE,
506            buf_len: 0,
507        }
508    }
509
510    /// Zeroes `len` bytes.
511    fn zero(&mut self, mut len: u64) -> Result<(), zx::Status> {
512        let unaligned = self.buf_len as u64 % self.page_size;
513        if unaligned > 0 {
514            let amount = std::cmp::min(self.page_size - unaligned, len);
515            self.buffer[self.buf_len..self.buf_len + amount as usize].fill(0);
516            self.buf_len += amount as usize;
517            len -= amount;
518            self.supply_pages()?;
519        }
520        // Zero whole pages by supplying pages from the zero VMO.
521        while len >= self.page_size {
522            let amount =
523                if len >= ZERO_VMO_SIZE { ZERO_VMO_SIZE } else { len - len % self.page_size };
524            self.pager.pager.supply_pages(
525                self.target_vmo,
526                self.offset..self.offset + amount,
527                &self.pager.zero_vmo,
528                0,
529            )?;
530            self.offset += amount;
531            len -= amount;
532        }
533        // And now the remaining partial page...
534        self.buffer[self.buf_len..self.buf_len + len as usize].fill(0);
535        self.buf_len += len as usize;
536        Ok(())
537    }
538
539    /// Flushes whole pages.
540    fn supply_pages(&mut self) -> Result<(), zx::Status> {
541        if self.buf_len as u64 >= self.page_size {
542            let len = self.buf_len - self.buf_len % self.page_size as usize;
543            self.pager.pager.supply_pages(
544                self.target_vmo,
545                self.offset..self.offset + len as u64,
546                self.transfer_vmo,
547                0,
548            )?;
549            // Move any remaining data to the beginning of the buffer.
550            self.buffer.copy_within(len..self.buf_len, 0);
551            self.buf_len -= len;
552            self.offset += len as u64;
553        }
554        Ok(())
555    }
556
557    /// Fills the buffer by calling the provided callback.  Returns the amount of data filled.
558    fn fill_buf(
559        &mut self,
560        f: impl FnOnce(&mut [u8]) -> Result<usize, zx::Status>,
561    ) -> Result<usize, zx::Status> {
562        let amount = f(&mut self.buffer[self.buf_len..])?;
563        self.buf_len += amount;
564        self.supply_pages()?;
565        Ok(amount)
566    }
567
568    /// Zeroes out to at least `end`, then pads to a page boundary and supplies those pages.
569    fn finish(&mut self, mut end: u64) -> Result<(), zx::Status> {
570        let byte_offset = self.offset + self.buf_len as u64;
571        end = std::cmp::max(end, byte_offset);
572        end = end + self.page_size - 1;
573        end -= end % self.page_size;
574        self.zero(end - byte_offset)
575    }
576
577    /// Fails the request up to the given offset with `error`.
578    fn fail_to(&mut self, end: u64, error: zx::Status) {
579        if self.offset < end {
580            log_warn!(error:?; "Failing page-in, range: {:?}", self.offset..end);
581            // The pager is fussy about what errors we can return here, so we always return IO.
582            match self.pager.pager.op_range(
583                zx::PagerOp::Fail(zx::Status::IO),
584                self.target_vmo,
585                self.offset..end,
586            ) {
587                Ok(()) => {}
588                Err(error) => log_error!(error:?; "Failed to report error"),
589            }
590            self.offset = end;
591            self.buf_len = 0;
592        }
593    }
594}
595
596#[cfg(test)]
597mod tests {
598    use super::{Filesystem, Pager, PagerExtent};
599
600    use std::sync::Arc;
601    use std::time::Duration;
602
603    #[::fuchsia::test]
604    async fn test_pager() {
605        let backing_vmo = zx::Vmo::create(1 * 1024 * 1024).expect("Vmo::craete failed");
606        let backing_vmo_clone =
607            backing_vmo.duplicate_handle(zx::Rights::SAME_RIGHTS).expect("failed handle dup");
608
609        let pager = Arc::new(Pager::new().expect("Pager::new failed"));
610        let filesystem = Arc::new(
611            Filesystem::new(pager.clone(), backing_vmo_clone, 1024)
612                .expect("Filesystem::new failed"),
613        );
614
615        pager.add_filesystem(filesystem.clone());
616
617        {
618            pager.start_threads();
619
620            // With no extent, we expect it to return zeroed data.
621            let vmo = filesystem.register("a".into(), 1, 5, Box::new([])).expect("register failed");
622
623            let mut buf = vec![1; 5];
624            vmo.read(&mut buf, 0).expect("read failed");
625
626            assert_eq!(&buf, &[0; 5]);
627
628            // A single extent:
629            let vmo = filesystem
630                .register(
631                    "b".into(),
632                    2,
633                    5,
634                    Box::new([PagerExtent { logical: 0..1, physical_block: 0 }]),
635                )
636                .expect("register failed");
637            backing_vmo.write(b"hello", 0).expect("write failed");
638            vmo.read(&mut buf, 0).expect("read failed");
639
640            assert_eq!(&buf, b"hello");
641
642            // A file with sparse ranges: 6 sparse, 1 extent, 5 more sparse, 1 extent, 4 sparse + a
643            // bit.
644            let file_size = (6 + 1 + 5 + 4) * 1024 + 100;
645            let vmo = filesystem
646                .register(
647                    "c".into(),
648                    3,
649                    file_size,
650                    Box::new([
651                        PagerExtent { logical: 6..7, physical_block: 0 },
652                        PagerExtent { logical: 12..13, physical_block: 1 },
653                    ]),
654                )
655                .expect("register failed");
656            backing_vmo.write(b"there", 1024).expect("write failed");
657            let mut buf = vec![1; file_size as usize];
658            vmo.read(&mut buf, 0).expect("read failed");
659
660            let mut expected = vec![0; file_size as usize];
661            expected[6 * 1024..6 * 1024 + 5].copy_from_slice(b"hello");
662            expected[12 * 1024..12 * 1024 + 5].copy_from_slice(b"there");
663            assert_eq!(&buf, &expected);
664
665            // Use the same file, but initiate a read that starts after the first extent.
666            let vmo = filesystem
667                .register(
668                    "d".into(),
669                    4,
670                    file_size,
671                    Box::new([
672                        PagerExtent { logical: 6..7, physical_block: 0 },
673                        PagerExtent { logical: 12..13, physical_block: 1 },
674                    ]),
675                )
676                .expect("register failed");
677
678            let offset = 9000;
679            let mut buf = vec![1; (file_size - offset) as usize];
680            vmo.read(&mut buf, offset).expect("read failed");
681
682            assert_eq!(&buf, &expected[offset as usize..]);
683        }
684
685        // After dropping all VMOs, we expect the pager to clean up.
686        loop {
687            if filesystem.files_by_inode.lock().is_empty() {
688                break;
689            }
690            // The pager is running on different threads, hence:
691            std::thread::sleep(Duration::from_millis(10));
692        }
693    }
694}