Skip to content

Commit 66d5f6a

Browse files
zhytyTom Yang
andauthored
[lldb] fix parallel module loading deadlock for Linux DYLD (#166480)
Another attempt at resolving the deadlock issue @GeorgeHuyubo discovered (his previous [attempt](#160225)). This change can be summarized as the following: * Plumb through a boolean flag to force no preload in `GetOrCreateModules` all the way through to `LoadModuleAtAddress`. * Parallelize `Module::PreloadSymbols` separately from `Target::GetOrCreateModule` and its caller `LoadModuleAtAddress` (this is what avoids the deadlock). These changes roughly maintain the performance characteristics of the previous implementation of parallel module loading. Testing on targets with between 5000 and 14000 modules, I saw similar numbers as before, often more than 10% faster in the new implementation across multiple trials for these massive targets. I think it's because we have less lock contention with this approach. # The deadlock See [bt.txt](https://github.com/user-attachments/files/22524471/bt.txt) for a sample backtrace of LLDB when the deadlock occurs. As @GeorgeHuyubo explains in his [PR](#160225), the deadlock occurs from an ABBA deadlock that happens when a thread context-switches out of `Module::PreloadSymbols`, goes into `Target::GetOrCreateModule` for another module, possibly entering this block: ``` if (!module_sp) { // The platform is responsible for finding and caching an appropriate // module in the shared module cache. if (m_platform_sp) { error = m_platform_sp->GetSharedModule( module_spec, m_process_sp.get(), module_sp, &search_paths, &old_modules, &did_create_module); } else { error = Status::FromErrorString("no platform is currently set"); } } ``` `Module::PreloadSymbols` holds a module-level mutex, and then `GetSharedModule` *attempts* to hold the mutex of the global shared `ModuleList`. So, this thread holds the module mutex, and waits on the global shared `ModuleList` mutex. A competing thread may execute `Target::GetOrCreateModule`, enter the same block as above, grabbing the global shared `ModuleList` mutex. Then, in `ModuleList::GetSharedModule`, we eventually call `ModuleList::FindModules` which eventually waits for the `Module` mutex held by the first thread (via `Module::GetUUID`). Thus, we deadlock. ## Reproducing the deadlock It might be worth noting that I've never been able to observe this deadlock issue during live debugging (e.g. launching or attaching to processes), however we were able to consistently reproduce this issue with coredumps when using the following settings: ``` (lldb) settings set target.parallel-module-load true (lldb) settings set target.preload-symbols true (lldb) settings set symbols.load-on-demand false (lldb) target create --core /some/core/file/here # deadlock happens ``` ## How this change avoids this deadlock This change avoids concurrent executions of `Module::PreloadSymbols` with `Target::GetOrCreateModule` by waiting until after the `Target::GetOrCreateModule` executions to run `Module::PreloadSymbols` in parallel. This avoids the ordering of holding a Module lock *then* the ModuleList lock, as `Target::GetOrCreateModule` executions maintain the ordering of the shared ModuleList lock first (from what I've read and tested). ## Why not read-write lock? Some feedback in #160225 was to modify mutexes used in these components with read-write locks. This might be a good idea overall, but I don't think it would *easily* resolve this specific deadlock. `Module::PreloadSymbols` would probably need a write lock to Module, so even if we had a read lock in `Module::GetUUID` we would still contend. Maybe the `ModuleList` lock could be a read lock that converts to a write lock if it chooses to update the module, but it seems likely that some thread would try to update the shared module list and then the write lock would contend again. Perhaps with deeper architectural changes, we could fix this issue? # Other attempts One downside of this approach (and the former approach of parallel module loading) is that each DYLD would need to implement this pattern themselves. With @clayborg's help, I looked at a few other approaches: * In `Target::GetOrCreateModule`, backgrounding the `Module::PreloadSymbols` call by adding it directly to the thread pool via `Debugger::GetThreadPool().async()`. This required adding a lock to `Module::SetLoadAddress` (probably should be one there already) since `ObjectFileELF::SetLoadAddress` is not thread-safe (updates sections). Unfortunately, during execution, this causes the preload symbols to run synchronously with `Target::GetOrCreateModule`, preventing us from truly parallelizing the execution. * In `Module::PreloadSymbols`, backgrounding the `symtab` and `sym_file` `PreloadSymbols` calls individually, but similar issues as the above. * Passing a callback function like swiftlang#10746 instead of the boolean I use in this change. It's functionally the same change IMO, with some design tradeoffs: * Pro: the caller doesn't need to explicitly call `Module::PreloadSymbols` itself, and can instead call whatever function is passed into the callback. * Con: the caller needs to delay the execution of the callback such that it occurs after the `GetOrCreateModule` logic, otherwise we run into the same issue. I thought this would be trickier for the caller, requiring some kinda condition variable or otherwise storing the calls to execute afterwards. # Test Plan: ``` ninja check-lldb ``` --------- Co-authored-by: Tom Yang <[email protected]>
1 parent e02fdf0 commit 66d5f6a

File tree

7 files changed

+50
-9
lines changed

7 files changed

+50
-9
lines changed

lldb/include/lldb/Core/ModuleList.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,12 @@ class ModuleList {
511511
/// Atomically swaps the contents of this module list with \a other.
512512
void Swap(ModuleList &other);
513513

514+
/// For each module in this ModuleList, preload its symbols.
515+
///
516+
/// \param[in] parallelize
517+
/// If true, all modules will be preloaded in parallel.
518+
void PreloadSymbols(bool parallelize) const;
519+
514520
protected:
515521
// Class typedefs.
516522
typedef std::vector<lldb::ModuleSP>

lldb/include/lldb/Target/DynamicLoader.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ class DynamicLoader : public PluginInterface {
352352
protected:
353353
// Utility methods for derived classes
354354

355+
/// Find a module in the target that matches the given file.
355356
lldb::ModuleSP FindModuleViaTarget(const FileSpec &file);
356357

357358
/// Checks to see if the target module has changed, updates the target

lldb/include/lldb/Target/Target.h

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -629,13 +629,20 @@ class Target : public std::enable_shared_from_this<Target>,
629629
/// or identify a matching Module already present in the Target,
630630
/// and return a shared pointer to it.
631631
///
632+
/// Note that this function previously also preloaded the module's symbols
633+
/// depending on a setting. This function no longer does any module
634+
/// preloading because that can potentially cause deadlocks when called in
635+
/// parallel with this function.
636+
///
632637
/// \param[in] module_spec
633638
/// The criteria that must be matched for the binary being loaded.
634639
/// e.g. UUID, architecture, file path.
635640
///
636641
/// \param[in] notify
637642
/// If notify is true, and the Module is new to this Target,
638-
/// Target::ModulesDidLoad will be called.
643+
/// Target::ModulesDidLoad will be called. See note in
644+
/// Target::ModulesDidLoad about thread-safety with
645+
/// Target::GetOrCreateModule.
639646
/// If notify is false, it is assumed that the caller is adding
640647
/// multiple Modules and will call ModulesDidLoad with the
641648
/// full list at the end.
@@ -931,6 +938,13 @@ class Target : public std::enable_shared_from_this<Target>,
931938
// the address of its previous instruction and return that address.
932939
lldb::addr_t GetBreakableLoadAddress(lldb::addr_t addr);
933940

941+
/// This call may preload module symbols, and may do so in parallel depending
942+
/// on the following target settings:
943+
/// - TargetProperties::GetPreloadSymbols()
944+
/// - TargetProperties::GetParallelModuleLoad()
945+
///
946+
/// Warning: if preloading is active and this is called in parallel with
947+
/// Target::GetOrCreateModule, this may result in a ABBA deadlock situation.
934948
void ModulesDidLoad(ModuleList &module_list);
935949

936950
void ModulesDidUnload(ModuleList &module_list, bool delete_locations);

lldb/source/Core/DynamicLoader.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ ModuleSP DynamicLoader::FindModuleViaTarget(const FileSpec &file) {
165165
if (ModuleSP module_sp = target.GetImages().FindFirstModule(module_spec))
166166
return module_sp;
167167

168-
if (ModuleSP module_sp = target.GetOrCreateModule(module_spec, false))
168+
if (ModuleSP module_sp =
169+
target.GetOrCreateModule(module_spec, /*notify=*/false))
169170
return module_sp;
170171

171172
return nullptr;

lldb/source/Core/ModuleList.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//===----------------------------------------------------------------------===//
88

99
#include "lldb/Core/ModuleList.h"
10+
#include "lldb/Core/Debugger.h"
1011
#include "lldb/Core/Module.h"
1112
#include "lldb/Core/ModuleSpec.h"
1213
#include "lldb/Core/PluginManager.h"
@@ -28,6 +29,7 @@
2829
#include "lldb/Utility/Log.h"
2930
#include "lldb/Utility/UUID.h"
3031
#include "lldb/lldb-defines.h"
32+
#include "llvm/Support/ThreadPool.h"
3133

3234
#if defined(_WIN32)
3335
#include "lldb/Host/windows/PosixApi.h"
@@ -1381,3 +1383,21 @@ void ModuleList::Swap(ModuleList &other) {
13811383
m_modules_mutex, other.m_modules_mutex);
13821384
m_modules.swap(other.m_modules);
13831385
}
1386+
1387+
void ModuleList::PreloadSymbols(bool parallelize) const {
1388+
std::lock_guard<std::recursive_mutex> guard(m_modules_mutex);
1389+
1390+
if (!parallelize) {
1391+
for (const ModuleSP &module_sp : m_modules)
1392+
module_sp->PreloadSymbols();
1393+
return;
1394+
}
1395+
1396+
llvm::ThreadPoolTaskGroup task_group(Debugger::GetThreadPool());
1397+
for (const ModuleSP &module_sp : m_modules)
1398+
task_group.async([module_sp] {
1399+
if (module_sp)
1400+
module_sp->PreloadSymbols();
1401+
});
1402+
task_group.wait();
1403+
}

lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,8 @@ void DynamicLoaderPOSIXDYLD::RefreshModules() {
469469
}
470470

471471
ModuleSP module_sp = LoadModuleAtAddress(
472-
so_entry.file_spec, so_entry.link_addr, so_entry.base_addr, true);
472+
so_entry.file_spec, so_entry.link_addr, so_entry.base_addr,
473+
/*base_addr_is_offset=*/true);
473474
if (!module_sp.get())
474475
return;
475476

@@ -726,9 +727,8 @@ void DynamicLoaderPOSIXDYLD::LoadAllCurrentModules() {
726727
task_group.async(load_module_fn, *I);
727728
task_group.wait();
728729
} else {
729-
for (I = m_rendezvous.begin(), E = m_rendezvous.end(); I != E; ++I) {
730+
for (I = m_rendezvous.begin(), E = m_rendezvous.end(); I != E; ++I)
730731
load_module_fn(*I);
731-
}
732732
}
733733

734734
m_process->GetTarget().ModulesDidLoad(module_list);

lldb/source/Target/Target.cpp

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1855,6 +1855,9 @@ void Target::NotifyModulesRemoved(lldb_private::ModuleList &module_list) {
18551855
}
18561856

18571857
void Target::ModulesDidLoad(ModuleList &module_list) {
1858+
if (GetPreloadSymbols())
1859+
module_list.PreloadSymbols(GetParallelModuleLoad());
1860+
18581861
const size_t num_images = module_list.GetSize();
18591862
if (m_valid && num_images) {
18601863
for (size_t idx = 0; idx < num_images; ++idx) {
@@ -2509,10 +2512,6 @@ ModuleSP Target::GetOrCreateModule(const ModuleSpec &orig_module_spec,
25092512
if (symbol_file_spec)
25102513
module_sp->SetSymbolFileFileSpec(symbol_file_spec);
25112514

2512-
// Preload symbols outside of any lock, so hopefully we can do this for
2513-
// each library in parallel.
2514-
if (GetPreloadSymbols())
2515-
module_sp->PreloadSymbols();
25162515
llvm::SmallVector<ModuleSP, 1> replaced_modules;
25172516
for (ModuleSP &old_module_sp : old_modules) {
25182517
if (m_images.GetIndexForModule(old_module_sp.get()) !=

0 commit comments

Comments
 (0)