memory: allow concurrent backing reads

This commit is contained in:
nmzik 2026-10-07 23:10:14 +04:00
parent 4d10ea17a0
commit 7e011fa565
4 changed files with 202 additions and 49 deletions

View File

@ -21,6 +21,7 @@
#include <map>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <vector>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
@ -168,6 +169,7 @@ static bool VirtualRangesOverlap(uint64_t left_start, uint64_t left_size, uint64
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
static uint32_t g_test_backing_store_unmaps_before_failure = UINT32_MAX;
static callback_func_t g_test_before_backing_map = nullptr;
static callback_func_t g_test_backing_read = nullptr;
#endif
#include "memoryAddressSpace.inc"
@ -3569,6 +3571,10 @@ void TestBeforeNextBackingMap(callback_func_t callback) {
g_test_before_backing_map = callback;
}
void TestSetBackingReadCallback(callback_func_t callback) {
g_test_backing_read = callback;
}
void TestFailNextPhysicalMemoryUnmap() {
TestFailPhysicalMemoryUnmapAfter(0);
}

View File

@ -192,6 +192,7 @@ bool FreeGuestMemory(uint64_t vaddr, uint64_t size);
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
void TestBeforeNextBackingMap(callback_func_t callback);
void TestSetBackingReadCallback(callback_func_t callback);
void TestFailNextPhysicalMemoryUnmap();
void TestFailPhysicalMemoryUnmapAfter(uint32_t successful_unmaps);
void TestFailGuestBackingStoreUnmapAfter(uint32_t successful_unmaps);

View File

@ -161,30 +161,90 @@ public:
if (!IsAvailable() || !RangeOk(backing_offset, size)) {
return false;
}
std::lock_guard lock(m_mutex);
std::memset(m_backing_base + backing_offset, 0, size);
return true;
}
bool TryWriteBacking(uint64_t vaddr, const void* data, uint64_t size) {
if (data == nullptr) {
if (!IsAvailable() || data == nullptr || size == 0 || UINT64_MAX - vaddr < size) {
return false;
}
std::lock_guard lock(m_mutex);
const auto end = vaddr + size;
auto first = m_maps.upper_bound(vaddr);
if (first == m_maps.begin()) {
return false;
}
--first;
auto current = vaddr;
for (auto mapping = first; current < end; ++mapping) {
if (mapping == m_maps.end() || current < mapping->second.vaddr ||
current >= mapping->second.vaddr + mapping->second.size) {
return false;
}
const auto& entry = mapping->second;
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
if (!RangeOk(entry.backing_offset + current - entry.vaddr, bytes)) {
return false;
}
current += bytes;
}
const auto* source = static_cast<const uint8_t*>(data);
return TryTransferBacking(vaddr, size,
[source](uint8_t* backing, uint64_t data_offset, size_t bytes) {
std::memcpy(backing, source + data_offset, bytes);
});
current = vaddr;
for (auto mapping = first; current < end; ++mapping) {
const auto& entry = mapping->second;
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
const auto offset = entry.backing_offset + current - entry.vaddr;
std::memcpy(m_backing_base + offset, source + (current - vaddr), bytes);
current += bytes;
}
return true;
}
bool TryReadBacking(uint64_t vaddr, void* data, uint64_t size) {
if (data == nullptr) {
if (!IsAvailable() || data == nullptr || size == 0 || UINT64_MAX - vaddr < size) {
return false;
}
std::shared_lock lock(m_mutex);
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
if (g_test_backing_read != nullptr) {
g_test_backing_read(vaddr, size);
}
#endif
const auto end = vaddr + size;
auto first = m_maps.upper_bound(vaddr);
if (first == m_maps.begin()) {
return false;
}
--first;
auto current = vaddr;
for (auto mapping = first; current < end; ++mapping) {
if (mapping == m_maps.end() || current < mapping->second.vaddr ||
current >= mapping->second.vaddr + mapping->second.size) {
return false;
}
const auto& entry = mapping->second;
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
if (!RangeOk(entry.backing_offset + current - entry.vaddr, bytes)) {
return false;
}
current += bytes;
}
auto* destination = static_cast<uint8_t*>(data);
return TryTransferBacking(
vaddr, size, [destination](uint8_t* backing, uint64_t data_offset, size_t bytes) {
std::memcpy(destination + data_offset, backing, bytes);
});
current = vaddr;
for (auto mapping = first; current < end; ++mapping) {
const auto& entry = mapping->second;
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
const auto offset = entry.backing_offset + current - entry.vaddr;
std::memcpy(destination + (current - vaddr), m_backing_base + offset, bytes);
current += bytes;
}
return true;
}
bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size) {
@ -192,7 +252,12 @@ public:
return false;
}
std::lock_guard lock(m_mutex);
std::shared_lock lock(m_mutex);
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
if (g_test_backing_read != nullptr) {
g_test_backing_read(vaddr, size);
}
#endif
const auto end = vaddr + size;
auto first = m_maps.lower_bound(vaddr);
if (first != m_maps.begin()) {
@ -437,7 +502,7 @@ public:
return false;
}
std::lock_guard lock(m_mutex);
std::shared_lock lock(m_mutex);
const auto end = vaddr + size;
auto current = vaddr;
@ -457,39 +522,6 @@ public:
}
private:
template <typename Transfer>
bool TryTransferBacking(uint64_t vaddr, uint64_t size, Transfer transfer) {
if (!IsAvailable() || size == 0 || UINT64_MAX - vaddr < size) {
return false;
}
std::lock_guard lock(m_mutex);
const auto end = vaddr + size;
auto current = vaddr;
while (current < end) {
MapEntry entry {};
if (!FindContainingUnlocked(current, 1, &entry)) {
return false;
}
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
if (!RangeOk(entry.backing_offset + current - entry.vaddr, bytes)) {
return false;
}
current += bytes;
}
current = vaddr;
while (current < end) {
MapEntry entry {};
EXIT_IF(!FindContainingUnlocked(current, 1, &entry));
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
const auto offset = entry.backing_offset + current - entry.vaddr;
transfer(m_backing_base + offset, current - vaddr, static_cast<size_t>(bytes));
current += bytes;
}
return true;
}
struct MapEntry {
uint64_t vaddr = 0;
uint64_t size = 0;
@ -656,7 +688,7 @@ private:
uint8_t* m_backing_base = nullptr;
uint64_t m_size = 0;
std::mutex m_mutex;
std::shared_mutex m_mutex;
std::map<uint64_t, MapEntry> m_maps;
};
@ -727,7 +759,7 @@ public:
if (size == 0 || UINT64_MAX - vaddr < size) {
return false;
}
std::lock_guard lock(m_mutex);
std::shared_lock lock(m_mutex);
return std::any_of(m_owned.begin(), m_owned.end(), [vaddr, size](const auto& range) {
return vaddr >= range.first && vaddr + size <= range.first + range.second;
});
@ -755,7 +787,7 @@ public:
if (!Owns(vaddr, size)) {
return false;
}
std::lock_guard lock(m_mutex);
std::shared_lock lock(m_mutex);
auto mapping = m_mapped.upper_bound(vaddr);
if (mapping != m_mapped.begin()) {
--mapping;
@ -1381,7 +1413,7 @@ private:
#endif
}
std::mutex m_mutex;
std::shared_mutex m_mutex;
std::map<uint64_t, uint64_t> m_free;
std::map<uint64_t, MappedRegion> m_mapped;
std::vector<std::pair<uint64_t, uint64_t>> m_owned;

View File

@ -24,6 +24,7 @@
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <semaphore>
#include <string>
#include <thread>
#include <utility>
@ -586,6 +587,107 @@ void TestSparseReadDuringDirectCommit() {
std::printf("[host] %-48s ok\n", test);
}
std::counting_semaphore<2>* g_backing_reads_entered = nullptr;
std::counting_semaphore<2>* g_continue_backing_reads = nullptr;
void ParkBackingRead(uintptr_t, size_t) {
g_backing_reads_entered->release();
g_continue_backing_reads->acquire();
}
void TestConcurrentBackingReads() {
using namespace Libs::LibKernel::Memory;
const char* test = "ConcurrentBackingReads";
constexpr uint64_t original = 0x123456789abcdef0ull;
for (const auto& readers: {std::pair {TryReadBacking, TryReadBacking},
std::pair {TryReadBacking, TryReadSparseBacking},
std::pair {TryReadSparseBacking, TryReadSparseBacking}}) {
const auto base = MapNamedFlexible(test, SceKernelPageSize, SceKernelProtCpuRw,
"concurrent_backing_reads");
std::memcpy(reinterpret_cast<void*>(base), &original, sizeof(original));
std::counting_semaphore<2> entered {0};
std::counting_semaphore<2> resume {0};
g_backing_reads_entered = &entered;
g_continue_backing_reads = &resume;
TestSetBackingReadCallback(ParkBackingRead);
std::array<uint64_t, 2> values {};
std::array<bool, 2> results {};
std::thread first([&] { results[0] = readers.first(base, &values[0], sizeof(values[0])); });
std::thread second(
[&] { results[1] = readers.second(base, &values[1], sizeof(values[1])); });
const bool concurrent = entered.try_acquire_for(std::chrono::seconds(5)) &&
entered.try_acquire_for(std::chrono::seconds(5));
// Release both threads even if an exclusive read lock prevents the second
// entry.
resume.release(2);
first.join();
second.join();
TestSetBackingReadCallback(nullptr);
CheckOk(test, KernelMunmap(base, SceKernelPageSize), "KernelMunmap");
Check(test, concurrent, "backing reads could not hold their locks concurrently");
Check(test, results[0] && results[1] && values[0] == original && values[1] == original,
"concurrent backing reads returned different contents");
}
std::printf("[host] %-48s ok\n", test);
}
void TestBackingReadExcludesWritesAndUnmap() {
using namespace Libs::LibKernel::Memory;
const char* test = "BackingReadExcludesWritesAndUnmap";
constexpr uint64_t original = 0x123456789abcdef0ull;
constexpr uint64_t replacement = 0xfedcba9876543210ull;
for (const auto read: {TryReadBacking, TryReadSparseBacking}) {
for (const bool unmap: {false, true}) {
const auto base = MapNamedFlexible(test, SceKernelPageSize, SceKernelProtCpuRw,
"backing_read_exclusion");
std::memcpy(reinterpret_cast<void*>(base), &original, sizeof(original));
std::counting_semaphore<2> entered {0};
std::counting_semaphore<2> resume {0};
g_backing_reads_entered = &entered;
g_continue_backing_reads = &resume;
TestSetBackingReadCallback(ParkBackingRead);
uint64_t snapshot = 0;
bool read_ok = false;
std::thread reader([&] { read_ok = read(base, &snapshot, sizeof(snapshot)); });
const bool reader_parked = entered.try_acquire_for(std::chrono::seconds(5));
std::binary_semaphore started {0};
std::binary_semaphore finished {0};
bool write_ok = false;
int unmap_result = OK;
std::thread mutation([&] {
started.release();
if (unmap) {
unmap_result = KernelMunmap(base, SceKernelPageSize);
} else {
write_ok = TryWriteBacking(base, &replacement, sizeof(replacement));
}
finished.release();
});
started.acquire();
const bool excluded = !finished.try_acquire_for(std::chrono::milliseconds(100));
resume.release();
reader.join();
mutation.join();
TestSetBackingReadCallback(nullptr);
uint64_t after = 0;
const bool still_backed = TryReadBacking(base, &after, sizeof(after));
if (!unmap) {
CheckOk(test, KernelMunmap(base, SceKernelPageSize), "KernelMunmap");
}
Check(test, reader_parked && excluded,
"backing write or unmap completed while a backing read held its "
"lock");
Check(test, read_ok && snapshot == original,
"backing mutation changed an in-progress read");
Check(test,
unmap ? unmap_result == OK && !still_backed
: write_ok && still_backed && after == replacement,
"backing mutation failed after the reader released its lock");
}
}
std::printf("[host] %-48s ok\n", test);
}
void TestGuestAddressSpaceHasNoFixedFallback() {
const char* test = "GuestAddressSpaceHasNoFixedFallback";
const auto unowned_address = reinterpret_cast<void*>(0x10000);
@ -4296,6 +4398,16 @@ void TestSmallFiberStacksAndMigration() {
int main(int argc, char** argv) {
InitSubsystems();
if (argc == 2 && std::strcmp(argv[1], "--backing-transfers-only") == 0) {
RunTest(TestConcurrentBackingReads);
RunTest(TestBackingReadExcludesWritesAndUnmap);
RunTest(TestSparseBackingReadPreservesResidency);
RunTest(TestSparseReadDuringDirectCommit);
RunTest(TestDirectMapQueryOffsetAndPartialMunmap);
RunTest(TestFlexibleMemoryUsesSharedBacking);
RunTest(TestFlexibleMemoryReuseIsZeroFilled);
return g_failed_tests == 0 ? 0 : 1;
}
#if defined(__linux__)
if (argc == 2 && std::strcmp(argv[1], "--fixed-direct-replacement-only") == 0) {
RunTest(TestFixedDirectReplacementPreservesAccess);
@ -4344,6 +4456,8 @@ int main(int argc, char** argv) {
RunTest(TestGuestAddressSpaceOwnsReservationsBeforeBacking);
RunTest(TestSparseBackingReadPreservesResidency);
RunTest(TestSparseReadDuringDirectCommit);
RunTest(TestConcurrentBackingReads);
RunTest(TestBackingReadExcludesWritesAndUnmap);
RunTest(TestGuestAddressSpaceHasNoFixedFallback);
RunTest(TestGuestFreeRangeSearchDoesNotUnderflow);
RunTest(TestFlexibleMemoryCapacityIsBootFixed);