Skip to content

ProcessManager容量与回收

解析 Unified Exec 的 64 条目软上限、最近使用保护、已退出优先回收、锁竞争和终止清理。

基于rust-v0.150.0
CodexRustExecution

ProcessManager容量与回收 ​

Unified Exec 的进程 store 不是无限增长的 hashmap。manager 把 64 条目作为软上限,优先回收保护集之外的已退出进程;如果候选正在发布终态,则宁可暂时超限,也不误杀活跃进程。回收还必须结束网络审批、终止底层进程并清理 ID,同时不能抢走 exit watcher 持有的 plugin metrics 收尾资源。

本文承接Unified Exec数据结构和Unified Exec会话状态机,专门分析 ProcessManager 的资源治理,不重复字段定义。读完后,你应能根据 last_used、退出状态和 interaction_lock 预测候选条目,并解释单进程终止为何采用两阶段校验。

1. 软上限 ​

1.1 64个条目 ​

插入新 ProcessEntry 前,manager 调用 prune_processes_if_needed。数量小于 MAX_UNIFIED_EXEC_PROCESSES 时直接返回,不做扫描。

源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: prune_processes_if_needed

rust
fn prune_processes_if_needed(store: &mut ProcessStore) -> Option<ProcessEntry> {
    if store.processes.len() < MAX_UNIFIED_EXEC_PROCESSES {
        return None;
    }
    let mut meta: Vec<(i32, Instant, bool)> = store
        .processes
        .iter()
        .map(|(id, entry)| (*id, entry.last_used, entry.process.has_exited()))
        .collect();
    // choose a removable entry...
}

这是软上限而非硬断言:锁住的已退出条目可能使 store 暂时保持 64 个以上,算法不会因此立即驱逐 live process。

1.2 最近八个保护 ​

算法按 last_used 从新到旧排序,保护最近 8 个 ID;即使保护集中的条目已经退出,也不会优先回收它。

源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: process_id_to_prune_from_meta

rust
let mut by_recency = meta.to_vec();
by_recency.sort_by_key(|(_, last_used, _)| Reverse(*last_used));
let protected: HashSet<i32> = by_recency
    .iter()
    .take(8)
    .map(|(process_id, _, _)| *process_id)
    .collect();

保护集减少刚被用户交互过的会话被马上回收的概率,但不表示这些会话永久保留。

2. 候选顺序 ​

2.1 已退出优先 ​

保护集之外,算法先找 exited == true 的最久未使用条目;没有已退出条目时才回退到保护集外的 LRU。

源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: process_id_to_prune_from_meta

rust
let mut lru = meta.to_vec();
lru.sort_by_key(|(_, last_used, _)| *last_used);

if let Some((process_id, _, _)) = lru
    .iter()
    .find(|(process_id, _, exited)| !protected.contains(process_id) && *exited)
{
    return Some(*process_id);
}

lru.into_iter()
    .find(|(process_id, _, _)| !protected.contains(process_id))
    .map(|(process_id, _, _)| process_id)

退出优先减少终止活进程的代价;但当所有条目都活跃时,容量治理仍会回收最久未使用者。

2.2 交互锁竞争 ​

候选选出后还要尝试取得 interaction_lock。锁被 write_stdin 或终态事件占用时,候选会被跳过。

源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: prune_processes_if_needed

rust
if let Some(interaction_lock) = candidate_process
    .as_ref()
    .map(|process| process.interaction_lock())
    && let Ok(_interaction_guard) = interaction_lock.try_lock_owned()
{
    return store.remove(process_id);
}
found_locked_exited_process |= candidate_has_exited
    || candidate_process.is_some_and(|process| process.has_exited());
meta.retain(|(id, _, _)| *id != process_id);

如果已退出候选被锁住,后续发现活跃候选时函数会返回 None,避免用活进程填补暂时不可回收的退出条目。

3. 锁外清理 ​

3.1 插入与清理 ​

store_process 在持有 store 锁时完成候选移出和新条目插入;释放锁后才注销网络审批并终止被裁剪进程。

源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: store_process

rust
let pruned_entry = {
    let mut store = self.process_store.lock().await;
    let pruned_entry = Self::prune_processes_if_needed(&mut store);
    store.processes.insert(process_id, entry);
    pruned_entry
};
if let Some(pruned_entry) = pruned_entry {
    unregister_network_approval_for_entry(&pruned_entry).await;
    pruned_entry.process.terminate();
}

锁外 await 是必要的:网络审批注销可能调用 session service,进程终止也可能等待 backend;把它们放在 store 锁内会阻塞所有 ID 操作。

ProcessEntry 还保存 Option<SharedPluginMetricsSidecar>。store 移除条目只释放这一份 Arc;spawn_exit_watcher 在创建时已经取得另一份共享所有权。进程 terminate 后,watcher 仍能等待输出 drain,并通过 Option::take 唯一消费 sidecar。prune 路径因此不在这里直接完成插件测量。

源码位置:

  • codex-rs/core/src/unified_exec/mod.rs :: ProcessEntry、SharedPluginMetricsSidecar
  • codex-rs/core/src/unified_exec/process_manager.rs :: UnifiedExecProcessManager::store_process
  • codex-rs/core/src/unified_exec/async_watcher.rs :: spawn_exit_watcher

3.2 全部终止 ​

session 关闭时,manager drain 整个 map、清空 reserved IDs,再逐条注销审批和终止进程。先清空索引,后执行慢清理,保证关闭期间不会再找到旧条目。

源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: terminate_all_processes

rust
let entries: Vec<ProcessEntry> = {
    let mut processes = self.process_store.lock().await;
    let entries = processes.processes.drain().map(|(_, entry)| entry).collect();
    processes.reserved_process_ids.clear();
    entries
};
for entry in entries {
    unregister_network_approval_for_entry(&entry).await;
    entry.process.terminate();
}

4. 单进程终止 ​

4.1 两阶段校验 ​

terminate_process 先复制进程 Arc 并释放 store 锁,锁外等待 terminate_confirmed;随后重新加锁,确认 map 中仍是同一个 Arc,再决定是否移除。

源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: terminate_process

rust
let (process, already_exited) = {
    let store = self.process_store.lock().await;
    let Some(entry) = store.processes.get(&process_id) else {
        return false;
    };
    (Arc::clone(&entry.process), entry.process.has_exited())
};

if !already_exited && process.terminate_confirmed().await.is_err() {
    return false;
}

4.2 防止误删 ​

重新加锁后的 Arc::ptr_eq 防止相同数字 ID 已被替换后误删新进程;首轮响应保护位为真时保留条目。

源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: terminate_process

rust
let entry = {
    let mut store = self.process_store.lock().await;
    let Some(entry) = store.processes.get(&process_id) else {
        return true;
    };
    if !Arc::ptr_eq(&entry.process, &process) {
        return true;
    }
    if entry.initial_exec_command_active.load(Ordering::Acquire) {
        return true;
    }
    let Some(entry) = store.remove(process_id) else {
        return false;
    };
    entry
};
unregister_network_approval_for_entry(&entry).await;
true

5. 两个验证 ​

pruning_prefers_exited_processes_outside_recently_used、pruning_falls_back_to_lru_when_no_exited 和 pruning_protects_recent_processes_even_if_exited 分别证明已退出优先、无退出时 LRU、最近 8 个保护。pruning_does_not_evict_live_process_while_exited_process_is_finalizing 持有退出进程锁,断言活进程不被误杀。

源码位置:codex-rs/core/src/unified_exec/process_manager_tests.rs :: pruning_prefers_exited_processes_outside_recently_used、pruning_does_not_evict_live_process_while_exited_process_is_finalizing

text
cd codex-rs
cargo test -p codex-core --lib 'unified_exec::process_manager::tests::pruning_' -- --test-threads=1

这些测试证明候选选择和锁保护,不证明真实 OS 进程的终止延迟或网络审批服务的外部行为。

6. 源码排查 ​

text
rg -n "MAX_UNIFIED_EXEC_PROCESSES|prune_processes_if_needed|process_id_to_prune" codex-rs/core/src/unified_exec
rg -n "store_process|terminate_all_processes|terminate_process" codex-rs/core/src/unified_exec/process_manager.rs
rg -n "pruning_prefers|pruning_falls_back|pruning_protects|does_not_evict" codex-rs/core/src/unified_exec/process_manager_tests.rs

核心不变量是:容量治理优先回收已退出且不在最近保护集的条目;回收不能与同会话交互重叠;所有慢清理在释放 store 锁后进行;两阶段终止必须防止旧 Arc 误删新 ID。