并发架构设计与分析
本文档详细分析了 NewScanmem 在引入并发扫描时面临的挑战,并提出了基于 Map-Reduce 模型的解决方案。
1. 为什么目前的架构不支持并发?
目前的 Scanner 和 Engine 设计是单线程的。如果简单地尝试在 runScanInternal 中开启多线程(例如使用 OpenMP),程序将面临两个致命问题:数据竞争 (Data Race) 和 资源竞争 (Resource Contention)。
A. 数据竞争 (Data Race)
这是最致命的问题。请看 src/scan/engine.cppm 中的核心扫描循环:
// ❌ 伪代码:如果直接并行化
for (const auto& region : regions) {
// 假设这里是多线程并行执行...
if (auto swath = scanRegion(...)) {
// 💥 崩溃点!
// out 是一个 MatchesAndOldValuesArray,内部通常是 std::vector。
// 当多个线程同时调用 push_back() 时:
// 1. 可能会导致 vector 扩容(reallocate),旧指针失效,导致 Use-After-Free。
// 2. 可能会导致 size 计数器错误,数据覆盖。
out.addSwath(*swath);
}
}
结论:out (即 MatchesAndOldValuesArray) 是一个共享的可变状态 (Shared Mutable State)。在多线程环境下,必须对其加锁保护,或者消除共享。
B. 资源竞争 (Resource Contention)
ProcMemReader 类持有一个指向 /proc/pid/mem 的文件描述符 (m_fd)。
- 文件偏移量竞争:如果底层使用
lseek+read,这是绝对线程不安全的。线程 A 刚lseek到位置 X,线程 Blseek到位置 Y,紧接着线程 A 调用read就会读到 Y 的数据。 - 内核锁竞争:即使使用原子定位读
pread,在高并发下,多个线程争抢同一个内核文件对象锁,也会导致性能下降。
2. 解决方案:Map-Reduce 架构
为了实现高效且安全的并发,我们需要采用 Map-Reduce (映射-归约) 模式。
核心思想:让每个线程拥有自己的私有结果容器和私有资源,最后再合并。
架构演进
第一步:Map 阶段 (并行)
将扫描任务(regions 列表)切分给多个工作线程。每个线程拥有:
1. 私有结果容器 (local_out):线程只向自己的容器写入数据,完全避免了对全局 out 的锁竞争。
2. 私有 Reader (local_reader):每个线程打开自己的文件描述符,避免内核层面的锁竞争和文件偏移量混乱。
第二步:Scan 阶段 (无锁)
扫描函数保持为纯函数 (Pure Function) 风格。它不依赖也不修改任何全局状态,只根据输入产生输出。
// ✅ 线程安全的工作函数
auto worker = [&](std::span<Region> my_regions) -> MatchesAndOldValuesArray {
MatchesAndOldValuesArray local_out; // ⭐️ 线程私有变量
ProcMemReader local_reader(pid); // ⭐️ 线程私有 Reader (避免 FD 竞争)
for (const auto& region : my_regions) {
if (auto swath = scanRegion(region, local_reader, ...)) {
local_out.addSwath(*swath); // ✅ 安全:只写私有变量
}
}
return local_out;
};
第三步:Reduce 阶段 (合并)
当所有线程完成工作后,主线程负责将所有 local_out 合并到总的结果集中。
MatchesAndOldValuesArray global_out;
for (auto& f : futures) {
auto local_result = f.get();
// 串行合并,或者使用细粒度锁并行合并
global_out.merge(local_result);
}
// (可选) 排序:并发扫描会导致结果乱序,通常需要按地址重新排序
global_out.sortByAddress();
3. 实施路线图
为了平滑过渡到并发架构,我们将分阶段进行:
-
Phase 1 (当前阶段):
- 保持单线程执行。
- 关键目标:确保
scanRegion等核心函数保持无状态 (Stateless) 和纯函数特性。 - 完善单元测试,确保逻辑正确性。
-
Phase 2 (未来规划):
- 引入线程池 (
ThreadPool) 或std::async。 - 实现
MatchesAndOldValuesArray::merge()和sortByAddress()方法。 - 重构
runScanInternal为 Map-Reduce 模式。
- 引入线程池 (
通过这种设计,我们可以在不破坏现有功能的前提下,为未来支持大规模内存并发扫描打下坚实基础。