Skip to main content

starnix_core/mm/
map_info_cache.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
5use crate::task::CurrentTask;
6use memory_pinning::ShadowProcess;
7use page_buf::PageBuf;
8use starnix_sync::{LockDepMutex, MapInfoCacheBufLock};
9use starnix_uapi::errors::Errno;
10use starnix_uapi::from_status_like_fdio;
11use std::sync::Arc;
12
13/// A singleton cache that can be used to share pinned pages across all map info queries. These
14/// queries often generate lock contention in Zircon on Starnix's shared address space when the
15/// buffer pages are faulted in. This type holds a long-running allocation in pinned memory to
16/// get reduced Zircon VmAspace lock contention in exchange for some extra memory usage and
17/// potential lock contention for /proc/pid/status and related files. Luckily it seems that
18/// in practice files like /proc/pid/status are not regularly accessed concurrently.
19pub struct MapInfoCache {
20    state: LockDepMutex<MapInfoCacheState, MapInfoCacheBufLock>,
21}
22
23struct MapInfoCacheState {
24    buf: PageBuf<zx::MapInfo>,
25    /// Pool of reusable heap buffers used to copy `zx::MapInfo` entries out of the shared
26    /// pinned `buf` before releasing the `MapInfoCache` mutex.
27    ///
28    /// Holding the `MapInfoCache` mutex while callers iterate over `zx::MapInfo` entries and
29    /// format `/proc/<pid>/{status,statm,smaps_rollup}` serializes all concurrent procfs
30    /// readers behind a single global lock. Copying the slice into a pooled `Vec<zx::MapInfo>`
31    /// allows dropping the mutex immediately after the `zx_object_get_info(ZX_INFO_VMAR_MAPS)`
32    /// syscall while avoiding repeated multi-hundred-kilobyte `Vec` allocations.
33    scratch_pool: Vec<Vec<zx::MapInfo>>,
34}
35
36const ZIRCON_NAME: zx::Name = zx::Name::new_lossy("starnix_zx_map_info_cache");
37const MAX_SCRATCH_POOL_LEN: usize = 4;
38
39impl MapInfoCache {
40    pub fn get_or_init(current_task: &CurrentTask) -> Result<Arc<Self>, Errno> {
41        let kernel = current_task.kernel();
42        kernel.expando.get_or_try_init(|| {
43            let pinned_shadow_process = kernel.expando.get_or_try_init(|| {
44                ShadowProcess::new(ZIRCON_NAME)
45                    .map(InfoCacheShadowProcess)
46                    .map_err(|e| from_status_like_fdio!(e))
47            })?;
48
49            let num_cache_elements = kernel.features.cached_zx_map_info_bytes as usize
50                / std::mem::size_of::<zx::MapInfo>();
51            Self::new(&pinned_shadow_process.0, num_cache_elements)
52        })
53    }
54
55    fn new(shadow_process: &ShadowProcess, num_cache_elements: usize) -> Result<Self, Errno> {
56        let buf = PageBuf::new_with_extra_vmar(num_cache_elements, shadow_process.vmar())
57            .map_err(|e| from_status_like_fdio!(e))?;
58        buf.set_name(&ZIRCON_NAME);
59
60        Ok(Self { state: MapInfoCacheState { buf, scratch_pool: Vec::new() }.into() })
61    }
62
63    pub fn with_map_infos<R>(
64        &self,
65        vmar: &zx::Vmar,
66        op: impl FnOnce(Result<&[zx::MapInfo], zx::Status>) -> R,
67    ) -> R {
68        let mut state = self.state.lock();
69        let mut scratch = state.scratch_pool.pop().unwrap_or_default();
70        match vmar.maps(state.buf.as_mut()) {
71            Ok((maps, _, avail)) if maps.len() == avail => {
72                scratch.clear();
73                scratch.extend_from_slice(maps);
74                drop(state);
75                let res = op(Ok(&scratch));
76                let mut state = self.state.lock();
77                if state.scratch_pool.len() < MAX_SCRATCH_POOL_LEN {
78                    state.scratch_pool.push(scratch);
79                }
80                return res;
81            }
82            Err(e) => {
83                if state.scratch_pool.len() < MAX_SCRATCH_POOL_LEN && scratch.capacity() > 0 {
84                    state.scratch_pool.push(scratch);
85                }
86                drop(state);
87                return op(Err(e));
88            }
89
90            // The call succeeded but the buffer wasn't big enough, fall back to a heap allocation.
91            Ok(_) => {
92                if state.scratch_pool.len() < MAX_SCRATCH_POOL_LEN && scratch.capacity() > 0 {
93                    state.scratch_pool.push(scratch);
94                }
95            }
96        }
97        // No need to hold this lock while we're using the heap instead.
98        drop(state);
99
100        match vmar.maps_vec() {
101            Ok(maps) => op(Ok(&maps)),
102            Err(e) => op(Err(e)),
103        }
104    }
105}
106
107/// The memory pinning shadow process used for zx::MapInfo buffers.
108///
109/// Uses its own distinct shadow process so that it doesn't interfere with other uses of memory
110/// pinning.
111pub struct InfoCacheShadowProcess(ShadowProcess);
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[fuchsia::test]
118    async fn basic_test() {
119        let shadow_process = ShadowProcess::new(zx::Name::new_lossy("testing123")).unwrap();
120        let cache = MapInfoCache::new(&shadow_process, 100).unwrap();
121        let maps_len = cache.with_map_infos(&shadow_process.vmar(), |maps| {
122            let maps = maps.unwrap();
123            assert_ne!(maps, &[]);
124            maps.len()
125        });
126        assert!(maps_len > 0);
127        assert_eq!(cache.state.lock().scratch_pool.len(), 1);
128    }
129
130    #[fuchsia::test]
131    async fn fall_back_to_heap_over_limit() {
132        let shadow_process = ShadowProcess::new(zx::Name::new_lossy("testing123")).unwrap();
133        let cache = MapInfoCache::new(&shadow_process, 1).unwrap();
134
135        // Ensure that the test process' root VMAR has more mappings than can fit in the single page
136        // of the cache's buffer.
137        let number_of_extra_mappings =
138            (zx::system_get_page_size() as usize / std::mem::size_of::<zx::MapInfo>()) * 2;
139        let vmo_to_map = zx::Vmo::create(4096).unwrap();
140        for _ in 0..number_of_extra_mappings {
141            fuchsia_runtime::vmar_root_self()
142                .map(0, &vmo_to_map, 0, 4096, zx::VmarFlags::PERM_READ)
143                .unwrap();
144        }
145
146        let maps_base_addr = cache.with_map_infos(&fuchsia_runtime::vmar_root_self(), |maps| {
147            let maps = maps.unwrap();
148            assert!(!maps.is_empty());
149            maps.as_ptr() as usize
150        });
151
152        let cache_base_addr = cache.state.lock().buf.as_mut().as_ptr() as usize;
153        assert_ne!(maps_base_addr, cache_base_addr, "map infos must not have been read into cache");
154    }
155}