Para la exclusión mutua entre procesos, puede utilizar el bloqueo de archivos. Con Linux, el código es tan simple como proteger la sección crítica con una llamada a flock
.
int fd_lock = open(LOCK_FILE, O_CREAT);
flock(fd_lock, LOCK_EX);
// do stuff
flock(fd_lock, LOCK_UN);
Si necesita compatibilidad con POSIX, puede usar fcntl
.
Puede usar un semáforo con nombre si puede hacer que todos los procesos acuerden un nombre común.
Un semáforo con nombre se identifica por un nombre de la forma /somename
; es decir, una cadena terminada en nulo de hasta NOMBRE_MAX-4 (es decir, 251) caracteres que consta de una barra diagonal inicial, seguida de uno o más caracteres, ninguno de los cuales son barras diagonales. Dos procesos pueden operar en el mismo semáforo con nombre pasando el mismo nombre a sem_open(3)
.
Miré el uso de la solución shared-pthread-mutex pero no me gustó la carrera lógica en ella. Así que escribí una clase para hacer esto usando los componentes atómicos
#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;
}
}
};
Ejemplo de uso:aquí simplemente lo estamos usando para garantizar que solo se ejecute una instancia de la aplicación.
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;
}
Puede hacer que los mutexes de C++ funcionen a través de los límites del proceso en Linux. Sin embargo, hay algo de magia negra involucrada que lo hace menos apropiado para el código de producción.
Explicación:
std::mutex
de la biblioteca estándar y std::shared_mutex
usa el struct pthread_mutex_s
de pthread y pthread_rwlock_t
bajo el capó. El native_handle()
El método devuelve un puntero a una de estas estructuras.
El inconveniente es que ciertos detalles se abstraen de la biblioteca estándar y se establecen de forma predeterminada en la implementación. Por ejemplo, std::shared_mutex
crea su pthread_rwlock_t
subyacente estructura pasando NULL
como segundo parámetro para pthread_rwlock_init()
. Se supone que esto es un puntero a un pthread_rwlockattr_t
estructura que contiene un atributo que determina la política de uso compartido.
public:
__shared_mutex_pthread()
{
int __ret = pthread_rwlock_init(&_M_rwlock, NULL);
...
En teoría, debería recibir atributos predeterminados. De acuerdo con las páginas man para pthread_rwlockattr_getpshared()
:
El valor predeterminado del atributo de proceso compartido es PTHREAD_PROCESS_PRIVATE.
Dicho esto, ambos std::shared_mutex
y std::mutex
trabajar a través de los procesos de todos modos. Estoy usando Clang 6.0.1 (x86_64-unknown-linux-gnu/modelo de hilo POSIX). Aquí hay una descripción de lo que hice para verificar:
-
Crea una región de memoria compartida con
shm_open
. -
Compruebe el tamaño de la región con
fstat
para determinar la propiedad. Si.st_size
es cero, entoncesftruncate()
y la persona que llama sabe que es el proceso de creación de la región. -
Llama al
mmap
en él.- El proceso de creación utiliza ubicación -
new
para construir unstd::mutex
ostd::shared_mutex
objeto dentro de la región compartida. - Los procesos posteriores usan
reinterpret_cast<>()
para obtener un puntero escrito al mismo objeto.
- El proceso de creación utiliza ubicación -
-
Los procesos ahora se repiten al llamar a
trylock()
yunlock()
a intervalos. Puedes verlos bloqueándose unos a otros usandoprintf()
antes y después detrylock()
y antes deunlock()
.
Detalle adicional:me interesaba saber si los encabezados de C++ o la implementación de pthreads tenían la culpa, así que busqué en pthread_rwlock_arch_t
. Encontrarás un __shared
atributo que es cero y un __flags
atributo que también es cero para el campo indicado por __PTHREAD_RWLOCK_INT_FLAGS_SHARED
. Por lo tanto, parece que, de forma predeterminada, esta estructura no está destinada a ser compartida, aunque parece proporcionar esta función de todos modos (a partir de julio de 2019).
Resumen
Parece funcionar, aunque un poco por casualidad. Aconsejaría precaución al escribir software de producción que funcione de forma contraria a la documentación.