Per l'esclusione reciproca tra processi, è possibile utilizzare il blocco dei file. Con Linux, il codice è semplice come proteggere la sezione critica con una chiamata a flock
.
int fd_lock = open(LOCK_FILE, O_CREAT);
flock(fd_lock, LOCK_EX);
// do stuff
flock(fd_lock, LOCK_UN);
Se hai bisogno della compatibilità POSIX, puoi usare fcntl
.
Puoi utilizzare un semaforo denominato se riesci a far concordare tutti i processi su un nome comune.
Un semaforo con nome è identificato da un nome nella forma
/somename
; ovvero una stringa con terminazione null composta da un massimo di NAME_MAX-4 (ovvero 251) caratteri composta da una barra iniziale, seguita da uno o più caratteri, nessuno dei quali è una barra. Due processi possono operare sullo stesso semaforo con nome passando lo stesso nome asem_open(3)
.
Ho esaminato l'utilizzo della soluzione shared-pthread-mutex ma non mi è piaciuta la gara logica in essa contenuta. Quindi ho scritto una classe per farlo usando i built-in atomic
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
using std::string;
//from the command line - "ls /dev/shm" and "lsof /dev/shm/<name>" to see which process ID has access to it
template<typename PAYLOAD>
class InterprocessSharedVariable
{
protected:
int mSharedMemHandle;
string const mSharedMemoryName;
bool mOpenedMemory;
bool mHaveLock;
pid_t mPID;
// this is the shared memory structure
typedef struct
{
pid_t mutex;
PAYLOAD payload;
}
tsSharedPayload;
tsSharedPayload* mSharedData;
bool openSharedMem()
{
mPID = getpid();
// The following caters for the shared mem being created by root but opened by non-root,
// giving the shared-memory 777 permissions.
int openFlags = O_CREAT | O_RDWR;
int shareMode = S_IRWXU | S_IRWXG | S_IRWXO;
// see https://stackoverflow.com/questions/11909505/posix-shared-memory-and-semaphores-permissions-set-incorrectly-by-open-calls
// store old
mode_t old_umask = umask(0);
mSharedMemHandle = shm_open (mSharedMemoryName.c_str(), openFlags, shareMode);
// restore old
umask(old_umask);
if (mSharedMemHandle < 0)
{
std::cerr << "failed to open shared memory" << std::endl;
return false;
}
if (-1 == ftruncate(mSharedMemHandle, sizeof(tsSharedPayload)))
{
std::cerr << "failed to resize shared memory" << std::endl;
return false;
}
mSharedData = (tsSharedPayload*) mmap (NULL,
sizeof(tsSharedPayload),
PROT_READ | PROT_WRITE,
MAP_SHARED,
mSharedMemHandle,
0);
if (MAP_FAILED == mSharedData)
{
std::cerr << "failed to map shared memory" << std::endl;
return false;
}
return true;
}
void closeSharedMem()
{
if (mSharedMemHandle > 0)
{
mSharedMemHandle = 0;
shm_unlink (mSharedMemoryName.c_str());
}
}
public:
InterprocessSharedVariable () = delete;
InterprocessSharedVariable (string const&& sharedMemoryName) : mSharedMemoryName(sharedMemoryName)
{
mSharedMemHandle = 0;
mOpenedMemory = false;
mHaveLock = false;
mPID = 0;
}
virtual ~InterprocessSharedVariable ()
{
releaseSharedVariable ();
closeSharedMem ();
}
// no copying
InterprocessSharedVariable (InterprocessSharedVariable const&) = delete;
InterprocessSharedVariable& operator= (InterprocessSharedVariable const&) = delete;
bool tryLockSharedVariable (pid_t& ownerProcessID)
{
// Double-checked locking. See if a process has already grabbed the mutex. Note the process could be dead
__atomic_load (&mSharedData->mutex, &ownerProcessID, __ATOMIC_SEQ_CST);
if (0 != ownerProcessID)
{
// It is possible that we have started with the same PID as a previous process that terminated abnormally
if (ownerProcessID == mPID)
{
// ... in which case, we already "have ownership"
return (true);
}
// Another process may have the mutex. Check whether it is alive.
// We are specifically looking for an error returned with ESRCH
// Note that if the other process is owned by root, "kill 0" may return a permissions error (which indicates the process is running!)
int processCheckResult = kill (ownerProcessID, 0);
if ((0 == processCheckResult) || (ESRCH != errno))
{
// another process owns the shared memory and is running
return (false);
}
// Here: The other process does not exist ((0 != processCheckResult) && (ESRCH == errno))
// We could assume here that we can now take ownership, but be proper and fall into the compare-exchange
ownerProcessID = 0;
}
// It's possible that another process has snuck in here and taken ownership of the shared memory.
// If that has happened, the exchange will "fail" (and the existing PID is stored in ownerProcessID)
// ownerProcessID == 0 -> representing the "expected" value
mHaveLock = __atomic_compare_exchange_n (&mSharedData->mutex,
&ownerProcessID, //"expected"
mPID, //"desired"
false, //"weak"
__ATOMIC_SEQ_CST, //"success-memorder"
__ATOMIC_SEQ_CST); //"fail-memorder"
return (mHaveLock);
}
bool acquireSharedVariable (bool& failed, pid_t& ownerProcessID)
{
if (!mOpenedMemory)
{
mOpenedMemory = openSharedMem ();
if (!mOpenedMemory)
{
ownerProcessID = 0;
failed = true;
return false;
}
}
// infrastructure is working
failed = false;
bool gotLock = tryLockSharedVariable (ownerProcessID);
return (gotLock);
}
void releaseSharedVariable ()
{
if (mHaveLock)
{
__atomic_store_n (&mSharedData->mutex, 0, __ATOMIC_SEQ_CST);
mHaveLock = false;
}
}
};
Esempio di utilizzo:qui lo stiamo semplicemente usando per garantire che venga eseguita solo un'istanza dell'applicazione.
int main(int argc, char *argv[])
{
typedef struct { } tsEmpty;
InterprocessSharedVariable<tsEmpty> programMutex ("/run-once");
bool memOpenFailed;
pid_t ownerProcessID;
if (!programMutex.acquireSharedVariable (memOpenFailed, ownerProcessID))
{
if (memOpenFailed)
{
std::cerr << "Failed to open shared memory" << std::endl;
}
else
{
std::cerr << "Program already running - process ID " << ownerProcessID << std::endl;
}
return -1;
}
... do stuff ...
return 0;
}
Puoi far funzionare i mutex C++ oltre i limiti del processo su Linux. Tuttavia, è coinvolta della magia nera che lo rende meno appropriato per il codice di produzione.
Spiegazione:
Il std::mutex
della libreria standard e std::shared_mutex
usa struct pthread_mutex_s
di pthread e pthread_rwlock_t
sotto il cappuccio. L'native_handle()
Il metodo restituisce un puntatore a una di queste strutture.
Lo svantaggio è che alcuni dettagli vengono estratti dalla libreria standard e predefiniti nell'implementazione. Ad esempio, std::shared_mutex
crea il relativo pthread_rwlock_t
sottostante struttura passando NULL
come secondo parametro di pthread_rwlock_init()
. Questo dovrebbe essere un puntatore a un pthread_rwlockattr_t
struttura contenente un attributo che determina la politica di condivisione.
public:
__shared_mutex_pthread()
{
int __ret = pthread_rwlock_init(&_M_rwlock, NULL);
...
In teoria, dovrebbe ricevere attributi predefiniti. Secondo le pagine man per pthread_rwlockattr_getpshared()
:
Il valore predefinito dell'attributo process-shared è PTHREAD_PROCESS_PRIVATE.
Detto questo, entrambi std::shared_mutex
e std::mutex
lavorare comunque attraverso i processi. Sto usando Clang 6.0.1 (modello thread x86_64-unknown-linux-gnu / POSIX). Ecco una descrizione di quello che ho fatto per controllare:
-
Crea una regione di memoria condivisa con
shm_open
. -
Controlla la dimensione della regione con
fstat
per determinare la proprietà. Se.st_size
è zero, quindiftruncate()
esso e il chiamante sa che è il processo di creazione della regione. -
Chiama
mmap
su di esso.- Il processo di creazione utilizza il posizionamento -
new
per costruire unstd::mutex
ostd::shared_mutex
oggetto all'interno della regione condivisa. - I processi successivi usano
reinterpret_cast<>()
per ottenere un puntatore digitato allo stesso oggetto.
- Il processo di creazione utilizza il posizionamento -
-
I processi ora si ripetono chiamando
trylock()
eunlock()
ad intervalli. Puoi vederli bloccarsi a vicenda usandoprintf()
prima e dopotrylock()
e prima diunlock()
.
Ulteriori dettagli:ero interessato a sapere se le intestazioni c++ o l'implementazione di pthreads fossero in errore, quindi ho scavato in pthread_rwlock_arch_t
. Troverai un __shared
attributo che è zero e un __flags
attributo che è zero anche per il campo indicato da __PTHREAD_RWLOCK_INT_FLAGS_SHARED
. Quindi sembra che per impostazione predefinita questa struttura non sia destinata alla condivisione, anche se sembra fornire comunque questa struttura (a partire da luglio 2019).
Riepilogo
Sembra funzionare, anche se un po' per caso. Consiglierei cautela nello scrivere software di produzione che funzioni in contrasto con la documentazione.