/* * Copyright (C) 2009 Karel Zak * * This file may be redistributed under the terms of the * GNU Lesser General Public License. */ /** * SECTION: lock * @title: Mtab locking * @short_description: locking methods for work with /etc/mtab * * The lock is backwardly compatible with the standard linux /etc/mtab locking. * Note, it's necessary to use the same locking schema in all application that * access the file. */ #include #include #include #include #include #include #include #include #include #include #include #include #include "pathnames.h" #include "nls.h" #include "mountP.h" /* * lock handler */ struct _mnt_lock { char *lockfile; /* path to lock file (e.g. /etc/mtab~) */ char *linkfile; /* path to link file (e.g. /etc/mtab~.) */ int lockfile_fd; /* lock file descriptor */ int locked; /* do we own the lock? */ }; /** * mnt_new_lock: * @dataname: the file that should be covered by the lock * @id: unique linkfile identifier or 0 (default is getpid()) * * Returns: newly allocated lock handler or NULL on case of error. */ mnt_lock *mnt_new_lock(const char *datafile, pid_t id) { mnt_lock *ml = NULL; char *lo = NULL, *ln = NULL; /* lockfile */ if (!datafile) return NULL; if (asprintf(&lo, "%s~", datafile) == -1) { lo = NULL; goto err; } if (asprintf(&ln, "%s~.%d", datafile, id ? : getpid()) == -1) { ln = NULL; goto err; } ml = calloc(1, sizeof(struct _mnt_lock) ); if (!ml) goto err; ml->lockfile_fd = -1; ml->linkfile = ln; ml->lockfile = lo; return ml; err: free(lo); free(ln); free(ml); return NULL; } /** * mnt_free_lock: * @ml: mnt_lock handler * * Deallocates mnt_lock. */ void mnt_free_lock(mnt_lock *ml) { if (!ml) return; free(ml->lockfile); free(ml->linkfile); free(ml); } /** * mnt_lock_get_lockfile: * @ml: mnt_lock handler * * Returns: path to lockfile. */ const char *mnt_lock_get_lockfile(mnt_lock *ml) { return ml ? ml->lockfile : NULL; } /** * mnt_lock_get_linkfile: * @ml: mnt_lock handler * * Note that the filename is generated by mnt_new_lock() and depends on * getpid() or 'id' argument of the mnt_new_lock() function. * * Returns: unique (per process/thread) path to linkfile. */ const char *mnt_lock_get_linkfile(mnt_lock *ml) { return ml ? ml->linkfile : NULL; } static void mnt_lockalrm_handler(int sig) { /* do nothing, say nothing, be nothing */ } /* * Waits for F_SETLKW, unfortunately we have to use SIGALRM here to interrupt * fcntl() to avoid never ending waiting. * * Returns: 0 on success, 1 on timeout, -errno on error. */ static int mnt_wait_lock(mnt_lock *ml, struct flock *fl, time_t maxtime) { struct timeval now; struct sigaction sa, osa; int ret = 0; gettimeofday(&now, NULL); if (now.tv_sec >= maxtime) return 1; /* timeout */ /* setup ALARM handler -- we don't want to wait forever */ sa.sa_flags = 0; sa.sa_handler = mnt_lockalrm_handler; sigfillset (&sa.sa_mask); sigaction(SIGALRM, &sa, &osa); DBG(DEBUG_LOCKS, fprintf(stderr, "LOCK: (%d) waiting for F_SETLKW.\n", getpid())); alarm(maxtime - now.tv_sec); if (fcntl(ml->lockfile_fd, F_SETLKW, fl) == -1) ret = errno == EINTR ? 1 : -errno; alarm(0); /* restore old sigaction */ sigaction(SIGALRM, &osa, NULL); DBG(DEBUG_LOCKS, fprintf(stderr, "LOCK: (%d) leaving mnt_wait_setlkw(), rc=%d.\n", getpid(), ret)); return ret; } /* * Create the lock file. * * The old code here used flock on a lock file /etc/mtab~ and deleted * this lock file afterwards. However, as rgooch remarks, that has a * race: a second mount may be waiting on the lock and proceed as * soon as the lock file is deleted by the first mount, and immediately * afterwards a third mount comes, creates a new /etc/mtab~, applies * flock to that, and also proceeds, so that the second and third mount * now both are scribbling in /etc/mtab. * * The new code uses a link() instead of a creat(), where we proceed * only if it was us that created the lock, and hence we always have * to delete the lock afterwards. Now the use of flock() is in principle * superfluous, but avoids an arbitrary sleep(). * * Where does the link point to? Obvious choices are mtab and mtab~~. * HJLu points out that the latter leads to races. Right now we use * mtab~. instead. * * * The original mount locking code has used sleep(1) between attempts and * maximal number of attempts has been 5. * * There was very small number of attempts and extremely long waiting (1s) * that is useless on machines with large number of mount processes. * * Now we wait few thousand microseconds between attempts and we have a global * time limit (30s) rather than limit for number of attempts. The advantage * is that this method also counts time which we spend in fcntl(F_SETLKW) and * number of attempts is not restricted. * -- kzak@redhat.com [Mar-2007] * * * This mtab locking code has been refactored and moved to libmount. The mtab * locking is really not perfect (e.g. SIGALRM), but it's stable, reliable and * backwardly compatible code. * * Don't forget that this code has to be compatible with 3rd party mounts * (/sbin/mount.) and has to work with NFS. * -- kzak@redhat.com [May-2009] */ /* maximum seconds between first and last attempt */ #define MOUNTLOCK_MAXTIME 30 /* sleep time (in microseconds, max=999999) between attempts */ #define MOUNTLOCK_WAITTIME 5000 /** * mnt_unlock_file: * @ml: lock struct * * Unlocks the file. The function could be called independently on the * lock status (for example from exit(3)). */ void mnt_unlock_file(mnt_lock *ml) { if (!ml) return; DBG(DEBUG_LOCKS, fprintf(stderr, "LOCK: (%d) unlocking/cleaning.\n", getpid())); if (ml->locked == 0 && ml->lockfile && ml->linkfile) { /* We have (probably) all files, but we don't own the lock, * Really? Check it! Maybe ml->locked wasn't set properly * because code was interrupted by signal. Paranoia? Yes. * * We own the lock when linkfile == lockfile. */ struct stat lo, li; if (!stat(ml->lockfile, &lo) && !stat(ml->linkfile, &li) && lo.st_dev == li.st_dev && lo.st_ino == li.st_ino) ml->locked = 1; } if (ml->linkfile) unlink(ml->linkfile); if (ml->lockfile_fd >= 0) close(ml->lockfile_fd); if (ml->locked == 1 && ml->lockfile) unlink(ml->lockfile); ml->locked = 0; ml->lockfile_fd = -1; } /** * mnt_lock_file * @ml: pointer to mnt_lock instance * * Creates lock file (e.g. /etc/mtab~). Note that this function uses * alarm(). * * Your application has to always call mnt_unlock_file() before exit. * * Locking scheme: * * 1. create linkfile (e.g. /etc/mtab~.$PID) * 2. link linkfile --> lockfile (e.g. /etc/mtab~.$PID --> /etc/mtab~) * * 3. a) link() success: setups F_SETLK lock (see fcnlt(2)) * b) link() failed: wait (max 30s) on F_SETLKW lock, goto 2. * * Example: * * * * mnt_lock *ml; * * void unlock_fallback(void) * { * if (!ml) * return; * mnt_unlock_file(ml); * mnt_free_lock(ml); * } * * int update_mtab() * { * int sig = 0; * const char *mtab; * * if (!(mtab = mnt_get_writable_mtab_path())) * return 0; // system without mtab * if (!(ml = mnt_new_lock(mtab, 0))) * return -1; // error * * atexit(unlock_fallback); * * if (mnt_lock_file(ml) != 0) { * printf(stderr, "cannot create %s lockfile\n", * mnt_lock_get_lockfile(ml)); * return -1; * } * * ... modify mtab ... * * mnt_unlock_file(ml); * mnt_free_lock(ml); * ml = NULL; * return 0; * } * * * * Returns: 0 on success or negative number in case of error (-ETIMEOUT is case * of stale lock file). */ int mnt_lock_file(mnt_lock *ml) { int i, rc = -1; struct timespec waittime; struct timeval maxtime; const char *lockfile, *linkfile; if (!ml) return -EINVAL; if (ml->locked) return 0; lockfile = mnt_lock_get_lockfile(ml); if (!lockfile) return -EINVAL; linkfile = mnt_lock_get_linkfile(ml); if (!linkfile) return -EINVAL; i = open(linkfile, O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR); if (i < 0) { /* linkfile does not exist (as a file) and we cannot create it. * Read-only or full filesystem? Too many files open in the system? */ if (errno > 0) rc = -errno; goto failed; } close(i); gettimeofday(&maxtime, NULL); maxtime.tv_sec += MOUNTLOCK_MAXTIME; waittime.tv_sec = 0; waittime.tv_nsec = (1000 * MOUNTLOCK_WAITTIME); /* Repeat until it was us who made the link */ while (ml->locked == 0) { struct timeval now; struct flock flock; int j; j = link(linkfile, lockfile); if (j == 0) ml->locked = 1; if (j < 0 && errno != EEXIST) { if (errno > 0) rc = -errno; goto failed; } ml->lockfile_fd = open(lockfile, O_WRONLY); if (ml->lockfile_fd < 0) { /* Strange... Maybe the file was just deleted? */ int errsv = errno; gettimeofday(&now, NULL); if (errsv == ENOENT && now.tv_sec < maxtime.tv_sec) { ml->locked = 0; continue; } if (errsv > 0) rc = -errsv; goto failed; } flock.l_type = F_WRLCK; flock.l_whence = SEEK_SET; flock.l_start = 0; flock.l_len = 0; if (ml->locked) { /* We made the link. Now claim the lock. */ if (fcntl (ml->lockfile_fd, F_SETLK, &flock) == -1) { DBG(DEBUG_LOCKS, fprintf(stderr, "%s: can't F_SETLK lockfile, errno=%d\n", lockfile, errno)); /* proceed, since it was us who created the lockfile anyway */ } break; } else { /* Someone else made the link. Wait. */ int err = mnt_wait_lock(ml, &flock, maxtime.tv_sec); if (err == 1) { DBG(DEBUG_LOCKS, fprintf(stderr, "%s: can't create link: time out (perhaps " "there is a stale lock file?)", lockfile)); rc = -ETIMEDOUT; goto failed; } else if (err < 0) { rc = err; goto failed; } nanosleep(&waittime, NULL); close(ml->lockfile_fd); ml->lockfile_fd = -1; } } DBG(DEBUG_LOCKS, fprintf(stderr, "LOCK: %s: (%d) successfully locked\n", lockfile, getpid())); unlink(linkfile); return 0; failed: mnt_unlock_file(ml); return rc; } #ifdef TEST_PROGRAM #include mnt_lock *lock; /* * read number from @filename, increment the number and * write the number back to the file */ void increment_data(const char *filename, int verbose, int loopno) { long num; FILE *f; char buf[256]; if (!(f = fopen(filename, "r"))) err(EXIT_FAILURE, "%d: failed to open: %s", getpid(), filename); if (!fgets(buf, sizeof(buf), f)) err(EXIT_FAILURE, "%d failed read: %s", getpid(), filename); fclose(f); num = atol(buf) + 1; if (!(f = fopen(filename, "w"))) err(EXIT_FAILURE, "%d: failed to open: %s", getpid(), filename); fprintf(f, "%ld", num); fclose(f); if (verbose) fprintf(stderr, "%d: %s: %ld --> %ld (loop=%d)\n", getpid(), filename, num - 1, num, loopno); } void clean_lock(void) { fprintf(stderr, "%d: cleaning\n", getpid()); if (!lock) return; mnt_unlock_file(lock); mnt_free_lock(lock); } void sig_handler(int sig) { errx(EXIT_FAILURE, "\n%d: catch signal: %s\n", getpid(), strsignal(sig)); } int test_lock(struct mtest *ts, int argc, char *argv[]) { const char *datafile; int verbose = 0, loops, l; if (argc < 3) return -1; datafile = argv[1]; loops = atoi(argv[2]); if (argc == 5 && strcmp(argv[4], "--verbose") == 0) verbose = 1; atexit(clean_lock); /* be paranoid and call exit() (=clean_lock()) for all signals */ { int sig = 0; struct sigaction sa; sa.sa_handler = sig_handler; sa.sa_flags = 0; sigfillset(&sa.sa_mask); while (sigismember(&sa.sa_mask, ++sig) != -1 && sig != SIGCHLD) sigaction (sig, &sa, (struct sigaction *) 0); } for (l = 0; l < loops; l++) { lock = mnt_new_lock(datafile, 0); if (!lock) return -1; if (mnt_lock_file(lock) != 0) { fprintf(stderr, "%d: failed to lock %s file\n", getpid(), datafile); return -1; } increment_data(datafile, verbose, l); mnt_unlock_file(lock); mnt_free_lock(lock); lock = NULL; } return 0; } /* * Note that this test should be executed from a script that creates many * parallel processes, otherwise this test does not make sense. */ int main(int argc, char *argv[]) { struct mtest tss[] = { { "--lock", test_lock, " [--verbose] increment number in datafile" }, { NULL } }; return mnt_run_test(tss, argc, argv); } #endif /* TEST_PROGRAM */