木头云 37f16cea47 Fix mutex unlock timing to avoid interfering with other references
Problem: The previous fix unconditionally called pthread_mutex_unlock() at the
beginning of close(), which could interfere with other threads/processes that
still had valid references to the mutex. This caused test failures on FreeBSD
when running tests multiple times (ShmTest.RemoveByName would fail on the second run).

Root cause: Calling unlock() too early could affect the mutex state for other
references that are still using it, leading to unexpected behavior.

Solution: Move pthread_mutex_unlock() to only be called when we're about to
destroy the mutex (i.e., when we're the last reference: shm_->ref() <= 1 &&
self_ref <= 1). This ensures:
1. We don't interfere with other threads/processes using the mutex
2. We still unlock before destroying to avoid FreeBSD robust list issues
3. The unlock happens at the correct time - right before pthread_mutex_destroy()

This is the correct approach because:
- Only the last reference holder should clean up the mutex
- Unlocking should be paired with destroying for the final cleanup
- Other references should not be affected by one reference closing

Fixes the second-run test failure on FreeBSD while maintaining the segfault fix.
2025-12-06 06:53:29 +00:00

281 lines
9.5 KiB
C++

#pragma once
#include <cstring>
#include <cassert>
#include <cstdint>
#include <system_error>
#include <mutex>
#include <atomic>
#include <pthread.h>
#include "libipc/platform/detail.h"
#include "libipc/utility/log.h"
#include "libipc/utility/scope_guard.h"
#include "libipc/memory/resource.h"
#include "libipc/shm.h"
#include "get_wait_time.h"
namespace ipc {
namespace detail {
namespace sync {
class mutex {
ipc::shm::handle *shm_ = nullptr;
std::atomic<std::int32_t> *ref_ = nullptr;
pthread_mutex_t *mutex_ = nullptr;
struct curr_prog {
struct shm_data {
ipc::shm::handle shm;
std::atomic<std::int32_t> ref;
struct init {
char const *name;
std::size_t size;
};
shm_data(init arg)
: shm{arg.name, arg.size}, ref{0} {}
};
ipc::map<ipc::string, shm_data> mutex_handles;
std::mutex lock;
static curr_prog &get() {
static curr_prog info;
return info;
}
};
pthread_mutex_t *acquire_mutex(char const *name) {
if (name == nullptr) {
return nullptr;
}
auto &info = curr_prog::get();
IPC_UNUSED_ std::lock_guard<std::mutex> guard {info.lock};
auto it = info.mutex_handles.find(name);
if (it == info.mutex_handles.end()) {
it = info.mutex_handles
.emplace(std::piecewise_construct,
std::forward_as_tuple(name),
std::forward_as_tuple(curr_prog::shm_data::init{
name, sizeof(pthread_mutex_t)}))
.first;
}
shm_ = &it->second.shm;
ref_ = &it->second.ref;
if (shm_ == nullptr) {
return nullptr;
}
return static_cast<pthread_mutex_t *>(shm_->get());
}
template <typename F>
static void release_mutex(ipc::string const &name, F &&clear) {
if (name.empty()) return;
auto &info = curr_prog::get();
IPC_UNUSED_ std::lock_guard<std::mutex> guard {info.lock};
auto it = info.mutex_handles.find(name);
if (it == info.mutex_handles.end()) {
return;
}
if (clear()) {
info.mutex_handles.erase(it);
}
}
static pthread_mutex_t const &zero_mem() {
static const pthread_mutex_t tmp{};
return tmp;
}
public:
mutex() = default;
~mutex() = default;
static void init() {
// Avoid exception problems caused by static member initialization order.
zero_mem();
curr_prog::get();
}
pthread_mutex_t const *native() const noexcept {
return mutex_;
}
pthread_mutex_t *native() noexcept {
return mutex_;
}
bool valid() const noexcept {
return (shm_ != nullptr) && (ref_ != nullptr) && (mutex_ != nullptr)
&& (std::memcmp(&zero_mem(), mutex_, sizeof(pthread_mutex_t)) != 0);
}
bool open(char const *name) noexcept {
close();
if ((mutex_ = acquire_mutex(name)) == nullptr) {
return false;
}
auto self_ref = ref_->fetch_add(1, std::memory_order_relaxed);
if (shm_->ref() > 1 || self_ref > 0) {
return valid();
}
::pthread_mutex_destroy(mutex_);
auto finally = ipc::guard([this] { close(); }); // close when failed
// init mutex
int eno;
pthread_mutexattr_t mutex_attr;
if ((eno = ::pthread_mutexattr_init(&mutex_attr)) != 0) {
ipc::error("fail pthread_mutexattr_init[%d]\n", eno);
return false;
}
IPC_UNUSED_ auto guard_mutex_attr = guard([&mutex_attr] { ::pthread_mutexattr_destroy(&mutex_attr); });
if ((eno = ::pthread_mutexattr_setpshared(&mutex_attr, PTHREAD_PROCESS_SHARED)) != 0) {
ipc::error("fail pthread_mutexattr_setpshared[%d]\n", eno);
return false;
}
if ((eno = ::pthread_mutexattr_setrobust(&mutex_attr, PTHREAD_MUTEX_ROBUST)) != 0) {
ipc::error("fail pthread_mutexattr_setrobust[%d]\n", eno);
return false;
}
*mutex_ = PTHREAD_MUTEX_INITIALIZER;
if ((eno = ::pthread_mutex_init(mutex_, &mutex_attr)) != 0) {
ipc::error("fail pthread_mutex_init[%d]\n", eno);
return false;
}
finally.dismiss();
return valid();
}
void close() noexcept {
if ((ref_ != nullptr) && (shm_ != nullptr) && (mutex_ != nullptr)) {
if (shm_->name() != nullptr) {
release_mutex(shm_->name(), [this] {
auto self_ref = ref_->fetch_sub(1, std::memory_order_relaxed);
if ((shm_->ref() <= 1) && (self_ref <= 1)) {
// Before destroying the mutex, try to unlock it.
// This is important for robust mutexes on FreeBSD, which maintain
// a per-thread robust list. If we destroy a mutex while it's locked
// or still in the robust list, FreeBSD may encounter dangling pointers
// later, leading to segfaults.
// Only unlock here (when we're the last reference) to avoid
// interfering with other threads that might be using the mutex.
::pthread_mutex_unlock(mutex_);
int eno;
if ((eno = ::pthread_mutex_destroy(mutex_)) != 0) {
ipc::error("fail pthread_mutex_destroy[%d]\n", eno);
}
return true;
}
return false;
});
} else shm_->release();
}
shm_ = nullptr;
ref_ = nullptr;
mutex_ = nullptr;
}
void clear() noexcept {
if ((shm_ != nullptr) && (mutex_ != nullptr)) {
if (shm_->name() != nullptr) {
release_mutex(shm_->name(), [this] {
// Unlock before destroying, same reasoning as in close()
::pthread_mutex_unlock(mutex_);
int eno;
if ((eno = ::pthread_mutex_destroy(mutex_)) != 0) {
ipc::error("fail pthread_mutex_destroy[%d]\n", eno);
}
shm_->clear();
return true;
});
} else shm_->clear();
}
shm_ = nullptr;
ref_ = nullptr;
mutex_ = nullptr;
}
static void clear_storage(char const *name) noexcept {
if (name == nullptr) return;
release_mutex(name, [] { return true; });
ipc::shm::handle::clear_storage(name);
}
bool lock(std::uint64_t tm) noexcept {
if (!valid()) return false;
for (;;) {
auto ts = posix_::detail::make_timespec(tm);
int eno = (tm == invalid_value)
? ::pthread_mutex_lock(mutex_)
: ::pthread_mutex_timedlock(mutex_, &ts);
switch (eno) {
case 0:
return true;
case ETIMEDOUT:
return false;
case EOWNERDEAD: {
// EOWNERDEAD means we have successfully acquired the lock,
// but the previous owner died. We need to make it consistent.
int eno2 = ::pthread_mutex_consistent(mutex_);
if (eno2 != 0) {
ipc::error("fail pthread_mutex_lock[%d], pthread_mutex_consistent[%d]\n", eno, eno2);
return false;
}
// After calling pthread_mutex_consistent(), the mutex is now in a
// consistent state and we hold the lock. Return success.
return true;
}
default:
ipc::error("fail pthread_mutex_lock[%d]\n", eno);
return false;
}
}
}
bool try_lock() noexcept(false) {
if (!valid()) return false;
auto ts = posix_::detail::make_timespec(0);
int eno = ::pthread_mutex_timedlock(mutex_, &ts);
switch (eno) {
case 0:
return true;
case ETIMEDOUT:
return false;
case EOWNERDEAD: {
// EOWNERDEAD means we have successfully acquired the lock,
// but the previous owner died. We need to make it consistent.
int eno2 = ::pthread_mutex_consistent(mutex_);
if (eno2 != 0) {
ipc::error("fail pthread_mutex_timedlock[%d], pthread_mutex_consistent[%d]\n", eno, eno2);
throw std::system_error{eno2, std::system_category()};
}
// After calling pthread_mutex_consistent(), the mutex is now in a
// consistent state and we hold the lock. Return success.
return true;
}
default:
ipc::error("fail pthread_mutex_timedlock[%d]\n", eno);
break;
}
throw std::system_error{eno, std::system_category()};
}
bool unlock() noexcept {
if (!valid()) return false;
int eno;
if ((eno = ::pthread_mutex_unlock(mutex_)) != 0) {
ipc::error("fail pthread_mutex_unlock[%d]\n", eno);
return false;
}
return true;
}
};
} // namespace sync
} // namespace detail
} // namespace ipc