Pour l'exclusion mutuelle interprocessus, vous pouvez utiliser le verrouillage de fichiers. Avec Linux, le code est aussi simple que de protéger la section critique avec un appel à flock
.
int fd_lock = open(LOCK_FILE, O_CREAT);
flock(fd_lock, LOCK_EX);
// do stuff
flock(fd_lock, LOCK_UN);
Si vous avez besoin de la compatibilité POSIX, vous pouvez utiliser fcntl
.
Vous pouvez utiliser un sémaphore nommé si vous pouvez obtenir que tous les processus s'accordent sur un nom commun.
Un sémaphore nommé est identifié par un nom de la forme
/somename
; c'est-à-dire une chaîne terminée par un caractère nul contenant jusqu'à NAME_MAX-4 (c'est-à-dire 251) caractères composé d'une barre oblique initiale, suivie d'un ou plusieurs caractères, dont aucun n'est une barre oblique. Deux processus peuvent fonctionner sur le même sémaphore nommé en passant le même nom àsem_open(3)
.
J'ai envisagé d'utiliser la solution shared-pthread-mutex mais je n'ai pas aimé la course logique qu'elle contient. J'ai donc écrit une classe pour faire cela en utilisant les fonctions intégrées atomiques
#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;
}
}
};
Exemple d'utilisation - ici, nous l'utilisons simplement pour nous assurer qu'une seule instance de l'application s'exécute.
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;
}
Vous pouvez faire fonctionner les mutex C++ au-delà des limites de processus sous Linux. Cependant, il y a une certaine magie noire impliquée qui le rend moins approprié pour le code de production.
Explication :
std::mutex
de la bibliothèque standard et std::shared_mutex
utiliser le struct pthread_mutex_s
de pthread et pthread_rwlock_t
sous la capuche. Le native_handle()
renvoie un pointeur vers l'une de ces structures.
L'inconvénient est que certains détails sont extraits de la bibliothèque standard et mis par défaut dans l'implémentation. Par exemple, std::shared_mutex
crée son pthread_rwlock_t
sous-jacent structure en passant NULL
comme deuxième paramètre à pthread_rwlock_init()
. Ceci est censé être un pointeur vers un pthread_rwlockattr_t
structure contenant un attribut qui détermine la politique de partage.
public:
__shared_mutex_pthread()
{
int __ret = pthread_rwlock_init(&_M_rwlock, NULL);
...
En théorie, il devrait recevoir des attributs par défaut. Selon les pages de manuel pour pthread_rwlockattr_getpshared()
:
La valeur par défaut de l'attribut partagé par processus est PTHREAD_PROCESS_PRIVATE.
Cela dit, les deux std::shared_mutex
et std::mutex
travailler à travers les processus de toute façon. J'utilise Clang 6.0.1 (modèle de thread x86_64-unknown-linux-gnu / POSIX). Voici une description de ce que j'ai fait pour vérifier :
-
Créer une région de mémoire partagée avec
shm_open
. -
Vérifiez la taille de la région avec
fstat
pour déterminer la propriété. Si.st_size
est zéro, alorsftruncate()
et l'appelant sait qu'il s'agit du processus de création de la région. -
Appelez le
mmap
dessus.- Le processus de création utilise le placement -
new
pour construire unstd::mutex
oustd::shared_mutex
objet dans la région partagée. - Les processus ultérieurs utilisent
reinterpret_cast<>()
pour obtenir un pointeur typé vers le même objet.
- Le processus de création utilise le placement -
-
Les processus bouclent maintenant sur l'appel de
trylock()
etunlock()
à intervalles. Vous pouvez les voir se bloquer en utilisantprintf()
avant et aprèstrylock()
et avantunlock()
.
Détail supplémentaire :je voulais savoir si les en-têtes c++ ou l'implémentation de pthreads étaient en cause, alors j'ai creusé dans pthread_rwlock_arch_t
. Vous trouverez un __shared
attribut qui vaut zéro et un __flags
attribut qui vaut aussi zéro pour le champ noté __PTHREAD_RWLOCK_INT_FLAGS_SHARED
. Il semble donc que par défaut cette structure ne soit pas destinée à être partagée, bien qu'elle semble fournir cette fonctionnalité de toute façon (à partir de juillet 2019).
Résumé
Cela semble fonctionner, bien qu'un peu par hasard. Je conseillerais la prudence lors de l'écriture d'un logiciel de production qui fonctionne contrairement à la documentation.