mirror of
https://github.com/mutouyun/cpp-ipc.git
synced 2025-12-07 01:06:45 +08:00
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.
281 lines
9.5 KiB
C++
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
|