/*
* Test the system robust mutex implementation
*
* Copyright (C) 2016 Amitay Isaacs
* Copyright (C) 2018 Red Hat Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
/*
* To run the test do the following:
*
* (a) Compile the test.
*
* gcc -O2 -g3 -o test-robust-mutex test-robust-mutex.c -lpthread
*
* (b) Start the "init" process.
*
* ./test-robust-mutex /tmp/shared-mutex init
*
* (c) Start any number of "worker" instances.
*
* ./test-robust-mutex worker <#>
*
* e.g. /tmp/shared-mutex.
*
* <#> : Number of children processes.
*
* : 0 - Normal, 1 - Realtime, 2 - Nice 20.
*
* For example:
*
* As non-root:
*
* $ while true ; do ./test-robust-mutex /tmp/foo worker 10 0 ; done;
*
* As root:
*
* while true ; do ./test-robust-mutex /tmp/foo worker 10 1 ; done;
*
* This creates 20 processes, 10 at normal priority and 10 at realtime
* priority, all taking the lock, being killed and recovering the lock.
*
* If while runnig (c) the processes block, it might mean that a futex wakeup
* was lost, or that the handoff of EOWNERDEAD did not happen correctly. In
* either case you can debug the resulting mutex like this:
*
* $ ./test-robust-mutex /tmp/shared-mutex debug
*
* This prints the PID of the process holding the mutex or nothing if
* the value was cleared by the kernel and now no process holds the mutex.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/* Define DEBUG to 1 to enable verbose debugging. */
#define DEBUG 0
/* Implement the worker. The worker has to do the following things:
* Succeed at locking the mutex, including possible recovery.
* Kill itself.
Other workers are attempting exactly the same thing in order to
test the loss and recovery of the robust mutex. */
static void worker (const char *filename)
{
pthread_mutex_t *mutex;
void *addr;
int ret, fd;
/* Open the file and map the shared robust mutex. */
fd = open(filename, O_RDWR, 0600);
if (fd == -1) {
perror ("FAIL: open");
exit(EXIT_FAILURE);
}
addr = mmap(NULL,
sizeof(pthread_mutex_t),
PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_FILE,
fd,
0);
if (addr == NULL) {
perror ("FAIL: mmap");
exit(EXIT_FAILURE);
}
mutex = (pthread_mutex_t *)addr;
/* Every process will lock once, and die once. */
printf("INFO: pid %u locking\n", getpid());
do {
ret = pthread_mutex_lock(mutex);
#if DEBUG
fprintf(stderr,
"DEBUG: pid %u lock attempt, ret=%d\n",
getpid(),
ret);
#endif
if (ret == EOWNERDEAD) {
int rc;
rc = pthread_mutex_consistent(mutex);
if (rc == 0) {
pthread_mutex_unlock(mutex);
} else {
fprintf(stderr,
"FAIL: pthread_mutex_consistent "
"failed\n");
exit(EXIT_FAILURE);
}
#if DEBUG
fprintf(stderr,
"DEBUG: pid %u recovery lock attempt, ret=%d\n",
getpid(),
ret);
#endif
/* Will loop and try to lock again. */
}
} while (ret != 0);
printf ("INFO: pid %u locked, now killing\n", getpid());
kill(getpid(), SIGKILL);
}
/* One of three priority modes. */
#define PRIO_NORMAL 0
#define PRIO_REALTIME 1
#define PRIO_NICE_20 2
/* One of three operation modes. */
#define MODE_INIT 0
#define MODE_WORKER 1
#define MODE_DEBUG 2
/* Print usage information and exit. */
static void usage (const char *name)
{
fprintf(stderr,
"Usage: %s [init|worker|debug] [#] [0|1|2]\n",
name);
exit(EXIT_FAILURE);
}
/* Set the process priority. */
static void set_priority (int priority)
{
struct sched_param p;
int ret;
switch (priority) {
case PRIO_REALTIME:
p.sched_priority = 1;
ret = sched_setscheduler(0, SCHED_FIFO, &p);
if (ret == -1)
perror("FAIL: sched_setscheduler");
break;
case PRIO_NICE_20:
ret = nice(-20);
if (ret == -1)
perror("FAIL: nice");
break;
case PRIO_NORMAL:
default:
/* Normal priority is the default. */
break;
}
}
int main(int argc, const char **argv)
{
int i, fd, ret, num_children, mode = -1, priority = PRIO_NORMAL;
const char *mode_str;
const char *file;
char *addr;
pthread_mutex_t *mutex;
pthread_mutexattr_t mattr;
pid_t pid;
/* One of three modes, init, worker, or debug. */
if (argc < 3 || argc > 5)
usage (argv[0]);
/*
* The shared memory file. Care should be taken here because if glibc
* is upgraded between runs the internals of the robust mutex could
* change. See this blog post about the dangers:
* https://developers.redhat.com/blog/2017/03/13/cc-library-upgrades-and-opaque-data-types-in-process-shared-memory/
* and how to avoid problems inherent in this.
*/
file = argv[1];
/* Set the mode. */
mode_str = argv[2];
if (strcmp ("init", mode_str) == 0) {
mode = MODE_INIT;
} else if (strcmp ("worker", mode_str) == 0) {
mode = MODE_WORKER;
} else if (strcmp ("debug", mode_str) == 0) {
mode = MODE_DEBUG;
} else {
usage (argv[0]);
}
/* This is "worker" mode, so set the priority. */
if (mode == MODE_WORKER) {
priority = atoi(argv[4]);
set_priority(priority);
}
/* All modes open the file. */
fd = open(argv[1], O_CREAT|O_RDWR, 0600);
if (fd == -1) {
perror("FAIL: open");
exit(EXIT_FAILURE);
}
ret = lseek(fd, 0, SEEK_SET);
if (ret != 0) {
perror("FAIL: lseek");
exit(EXIT_FAILURE);
}
/* Truncate the file backing the mutex only in the init phase. */
if (mode == MODE_INIT) {
ret = ftruncate(fd, sizeof(pthread_mutex_t));
if (ret != 0) {
perror("FAIL: ftruncate");
exit(EXIT_FAILURE);
}
}
/* Map the robust mutex. */
addr = mmap(NULL,
sizeof(pthread_mutex_t),
PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_FILE,
fd,
0);
if (addr == NULL) {
perror("FAIL: mmap");
exit(EXIT_FAILURE);
}
mutex = (pthread_mutex_t *)(void *)addr;
/*
* In the debug mode we try to recover the mutex and print it.
* WARNING: All other processes should be stuck, otherwise they may
* change the value of the lock between trylock and the printing after
* EBUSY.
*/
if (mode == MODE_DEBUG) {
ret = pthread_mutex_trylock(mutex);
if (ret == EOWNERDEAD) {
ret = pthread_mutex_consistent(mutex);
if (ret == 0) {
pthread_mutex_unlock(mutex);
} else {
fprintf(stderr,
"FAIL: pthread_mutex_consistent "
"failed\n");
exit (EXIT_FAILURE);
}
} else if (ret == EBUSY) {
printf("INFO: pid=%u\n", mutex->__data.__owner);
} else if (ret == 0) {
pthread_mutex_unlock(mutex);
}
exit(EXIT_SUCCESS);
}
/*
* Only the initializing process does initialization because it is
* undefined behaviour to re-initialize an already initialized mutex
* that was not destroyed.
*/
if (mode == MODE_INIT) {
ret = pthread_mutexattr_init(&mattr);
if (ret != 0) {
fprintf(stderr,
"FAIL: pthread_mutexattr_init failed\n");
exit(EXIT_FAILURE);
}
ret = pthread_mutexattr_settype(&mattr,
PTHREAD_MUTEX_ERRORCHECK);
if (ret != 0) {
fprintf(stderr,
"FAIL: pthread_mutexattr_settype failed\n");
exit(EXIT_FAILURE);
}
ret = pthread_mutexattr_setpshared(&mattr,
PTHREAD_PROCESS_SHARED);
if (ret != 0) {
fprintf(stderr,
"FAIL: pthread_mutexattr_setpshared failed\n");
exit(EXIT_FAILURE);
}
ret = pthread_mutexattr_setrobust(&mattr, PTHREAD_MUTEX_ROBUST);
if (ret != 0) {
fprintf(stderr,
"FAIL: pthread_mutexattr_setrobust failed\n");
exit(EXIT_FAILURE);
}
ret = pthread_mutex_init(mutex, &mattr);
if (ret != 0) {
fprintf(stderr, "FAIL: pthread_mutex_init failed\n");
exit(EXIT_FAILURE);
}
printf ("INFO: init: Mutex initialization complete.\n");
/* Never exit. */
for (;;)
sleep (1);
}
/* Acquire the mutext for the first time. Might be dead.
Might also be concurrent with the high-priority threads. */
fprintf(stderr,
"INFO: parent: Acquiring mutex (pid = %d).\n",
getpid());
do {
ret = pthread_mutex_lock(mutex);
/* Not consistent? Try to make it so. */
if (ret == EOWNERDEAD) {
int rc;
rc = pthread_mutex_consistent(mutex);
if (rc == 0) {
pthread_mutex_unlock (mutex);
} else {
fprintf(stderr,
"FAIL: pthread_mutex_consistent "
"failed\n");
exit (EXIT_FAILURE);
}
/* Will loop and try to lock again. */
fprintf(stderr,
"INFO: parent: Unlock recovery ret = %d\n",
ret);
}
} while (ret != 0);
/*
* Set the parent process into it's own process group (hides the
* children).
*/
setpgid(0, 0);
/* Create # of children. */
fprintf(stderr, "INFO: parent: Creating children\n");
num_children = atoi(argv[3]);
for (i = 0; i < num_children; i++) {
pid = fork();
if (pid < 0) {
fprintf(stderr, "FAIL: fork() failed\n");
exit(EXIT_FAILURE);
}
if (pid == 0) {
close(fd);
worker(file);
exit(EXIT_FAILURE);
}
}
fprintf(stderr, "INFO: parent: Waiting for children\n");
/* Unlock the recently acquired mutex or the old lost mutex. */
ret = pthread_mutex_unlock(mutex);
if (ret != 0) {
fprintf(stderr, "FAIL: pthread_mutex_unlock failed\n");
exit(EXIT_FAILURE);
}
/*
* All threads are running now, and each will take the lock and
* die in turn. When they are all dead we will exit and be started
* again by the caller.
*/
for (i = 0; i < num_children; i++) {
int status;
pid = waitpid(-1, &status, 0);
if (pid <= 0) {
fprintf(stderr, "FAIL: waitpid() failed\n");
exit(EXIT_FAILURE);
}
fprintf(stderr,
"INFO: parent: Reaped %u\n",
(unsigned int) pid);
}
/* We never unlink fd. The file must be cleaned up by the caller. */
close(fd);
exit(EXIT_SUCCESS);
}