do_unmount: add parallel unmounting

this turns mouninfo into a multicall binary which can also do
parallel unmounting. most mountinfo code is unchanged; only
notable change is find_mounts() now returns the number of mounts
found and also takes the "process" function as an argument; so
that is_mounted() can be implemented on top of it.

do_unmount mostly follows the logic of previous code. some
notable changes:

- do_unmount is now "lazy" when it comes to retrying failed
  unmounts. it will greedily keep running unmount as long as it
  can before it looks at the "waiting" queue.
- it will check if the mountpoint is still mounted or not when
  umount returns non-zero exit code. this is due to the fact that
  multiple umount calls might race to unmount a shared mount. so
  if umount fails _but_ the mountpoint is no longer mounted, we
  assume success.
- do_unmount used to fail if fuser did not find any pids using
  the mount. the new code tries one more time; the rationale being
  that there's a gap between the umount call and the fuser call,
  and so whatever was using the mount before might have stopped
  using it now and so it's worth another attempt.

Fixes: https://github.com/OpenRC/openrc/issues/662
Closes: https://github.com/OpenRC/openrc/pull/698
This commit is contained in:
NRK 2025-03-18 04:31:35 +00:00 committed by navi
parent a477d469eb
commit 303bc524e8
8 changed files with 419 additions and 116 deletions

View File

@ -112,7 +112,7 @@ stop()
# Umount loop devices
einfo "Unmounting loop devices"
eindent
do_unmount "umount -d" --skip-point-regex "$no_umounts_r" \
do_unmount -d -- --skip-point-regex "$no_umounts_r" \
--node-regex "^/dev/loop"
eoutdent
@ -125,7 +125,7 @@ stop()
fs="$fs${fs:+|}$x"
done
[ -n "$fs" ] && fs="^($fs)$"
do_unmount umount --skip-point-regex "$no_umounts_r" \
do_unmount -- --skip-point-regex "$no_umounts_r" \
"${fs:+--skip-fstype-regex}" $fs --nonetdev
eoutdent

View File

@ -25,8 +25,6 @@ start()
sync
ebegin "Remounting remaining filesystems read-only"
# We need the do_unmount function
. "$RC_LIBEXECDIR"/sh/rc-mount.sh
eindent
# Bug 381783
@ -48,7 +46,7 @@ start()
fs="$fs${fs:+|}$x"
done
[ -n "$fs" ] && fs="^($fs)$"
do_unmount "umount -r" \
do_unmount -r -- \
--skip-point-regex "$m" \
"${fs:+--skip-fstype-regex}" $fs --nonetdev
ret=$?

View File

@ -64,7 +64,6 @@ stop()
local x= fs=
ebegin "Unmounting network filesystems"
. "$RC_LIBEXECDIR"/sh/rc-mount.sh
for x in $net_fs_list $extra_net_fs_list; do
fs="$fs${fs:+,}$x"
@ -79,7 +78,7 @@ stop()
fs="$fs${fs:+|}$x"
done
[ -n "$fs" ] && fs="^($fs)$"
do_unmount umount ${fs:+--fstype-regex} $fs --netdev
do_unmount -- ${fs:+--fstype-regex} $fs --netdev
retval=$?
eoutdent

View File

@ -10,7 +10,6 @@ sh_conf_data.set('UUCP_GROUP', get_option('uucp_group'))
sh = [
'rc-functions.sh',
'rc-mount.sh',
'runit.sh',
's6.sh',
'start-stop-daemon.sh',

View File

@ -1,87 +0,0 @@
# Copyright (c) 2007-2015 The OpenRC Authors.
# See the Authors file at the top-level directory of this distribution and
# https://github.com/OpenRC/openrc/blob/HEAD/AUTHORS
#
# This file is part of OpenRC. It is subject to the license terms in
# the LICENSE file found in the top-level directory of this
# distribution and at https://github.com/OpenRC/openrc/blob/HEAD/LICENSE
# This file may not be copied, modified, propagated, or distributed
# except according to the terms contained in the LICENSE file.
# Declare this here so that no formatting doesn't affect the embedded newline
__IFS="
"
# Handy function to handle all our unmounting needs
# mountinfo is a C program to actually find our mounts on our supported OS's
# We rely on fuser being present, so if it's not then don't unmount anything.
# This isn't a real issue for the BSD's, but it is for Linux.
do_unmount()
{
local cmd="$1" retval=0 retry= pids=-
local f_opts="-m -c" f_kill="-s " mnt=
if [ "$RC_UNAME" = "Linux" ]; then
f_opts="-m"
f_kill="-"
fi
shift
local IFS="$__IFS"
set -- $(mountinfo "$@")
unset IFS
for mnt; do
# Unmounting a shared mount can unmount other mounts, so
# we need to check the mount is still valid
mountinfo --quiet "$mnt" || continue
# Ensure we interpret all characters properly.
mnt=$(printf "$mnt")
case "$cmd" in
umount)
ebegin "Unmounting $mnt"
;;
*)
ebegin "Remounting $mnt read only"
;;
esac
retry=4 # Effectively TERM, sleep 1, TERM, sleep 1, KILL, sleep 1
while ! LC_ALL=C $cmd "$mnt" 2>/dev/null; do
if command -v fuser >/dev/null 2>&1; then
pids="$(timeout -s KILL "${rc_fuser_timeout:-60}" \
fuser $f_opts "$mnt" 2>/dev/null)"
fi
case " $pids " in
*" $$ "*)
eend 1 "failed because we are using" \
"$mnt"
retry=0;;
" - ")
eend 1
retry=0;;
" ")
eend 1 "in use but fuser finds nothing"
retry=0;;
*)
if [ $retry -le 0 ]; then
eend 1
else
local sig="TERM"
: $(( retry -= 1 ))
[ $retry = 1 ] && sig="KILL"
fuser $f_kill$sig -k $f_opts \
"$mnt" >/dev/null 2>&1
sleep 1
fi
;;
esac
[ $retry -le 0 ] && break
done
if [ $retry -le 0 ]; then
retval=1
else
eend 0
fi
done
return $retval
}

View File

@ -1,5 +1,7 @@
executable('mountinfo', 'mountinfo.c',
include_directories: incdir,
dependencies: [rc, einfo, shared],
install: true,
install_dir: rc_bindir)
foreach exec : ['mountinfo', 'do_unmount']
executable(exec, 'mountinfo.c',
include_directories: incdir,
dependencies: [rc, einfo, shared],
install: true,
install_dir: rc_bindir)
endforeach

View File

@ -48,6 +48,8 @@
#include "einfo.h"
#include "queue.h"
#include "rc.h"
#include "rc_exec.h"
#include "timeutils.h"
#include "_usage.h"
#include "helpers.h"
@ -89,6 +91,21 @@ const char * const longopts_help[] = {
};
const char *usagestring = NULL;
#define UMOUNT_ARGS_MAX 16
#define RUN_MAX 32
#define TRY_MAX 3
#define TRY_DELAY_MS 1000
struct run_queue {
const char *mntpath;
int64_t last_exec_time;
int64_t fuser_exec_time;
pid_t pid;
pid_t fuser_pid;
int try_count;
int fuser_stdoutfd;
};
typedef enum {
mount_from,
mount_to,
@ -102,7 +119,12 @@ typedef enum {
net_no
} net_opts;
struct args;
typedef int process_func_t(RC_STRINGLIST *, struct args *,
char *, char *, char *, char *, int);
struct args {
process_func_t *process;
regex_t *node_regex;
regex_t *skip_node_regex;
regex_t *fstype_regex;
@ -112,6 +134,7 @@ struct args {
RC_STRINGLIST *mounts;
mount_type mount_type;
net_opts netdev;
const char *check_mntpath;
};
static int
@ -243,7 +266,7 @@ static struct opt {
};
static RC_STRINGLIST *
find_mounts(struct args *args)
find_mounts(struct args *args, size_t *num_mounts)
{
struct statfs *mnts;
int nmnts;
@ -277,7 +300,7 @@ find_mounts(struct args *args)
flags &= ~o->o_opt;
}
process_mount(list, args,
*num_mounts += 0 == args->process(list, args,
mnts[i].f_mntfromname,
mnts[i].f_mntonname,
mnts[i].f_fstypename,
@ -310,7 +333,7 @@ getmntfile(const char *file)
}
static RC_STRINGLIST *
find_mounts(struct args *args)
find_mounts(struct args *args, size_t *num_mounts)
{
FILE *fp;
char *buffer;
@ -345,7 +368,9 @@ find_mounts(struct args *args)
netdev = 1;
}
process_mount(list, args, from, to, fst, opts, netdev);
*num_mounts += 0 == args->process(list, args,
from, to, fst, opts, netdev);
free(buffer);
buffer = NULL;
}
@ -359,6 +384,157 @@ find_mounts(struct args *args)
# error "Operating system not supported!"
#endif
static int is_prefix(const char *needle, const char *hay)
{
size_t nlen = strlen(needle);
if (strncmp(needle, hay, nlen) == 0 && hay[nlen] == '/')
return true;
return false;
}
static char *unescape_octal(char *beg)
{
int n, i;
char *w = beg, *r = beg;
while (*r) {
if (*r != '\\' || *++r == '\\') {
*w++ = *r++;
} else {
/* octal. should have at least 3 bytes,
* but don't choke on malformed input
*/
for (i = n = 0; i < 3; ++i) {
if (*r >= '0' && *r <= '7') {
n <<= 3;
n |= *r++ - '0';
} else {
break;
}
}
if (n)
*w++ = n;
}
}
*w = '\0';
return beg;
}
static int check_is_mounted(RC_STRINGLIST *list RC_UNUSED, struct args *args,
char *from RC_UNUSED, char *to, char *fstype RC_UNUSED,
char *options RC_UNUSED, int netdev RC_UNUSED)
{
return strcmp(args->check_mntpath, unescape_octal(to)) == 0 ? 0 : -1;
}
static int is_mounted(const char *mntpath)
{
size_t num_mounts = 0;
struct args args = { .process = check_is_mounted, .check_mntpath = mntpath };
RC_STRINGLIST *l = find_mounts(&args, &num_mounts);
rc_stringlist_free(l);
return num_mounts > 0;
}
static pid_t run_umount(const char *mntpath,
const char **umount_args, int umount_args_num)
{
struct exec_args args;
struct exec_result res;
const char *argv[UMOUNT_ARGS_MAX + 3];
int k, i = 0;
argv[i++] = "umount";
for (k = 0; k < umount_args_num; ++k)
argv[i++] = umount_args[k];
argv[i++] = mntpath;
argv[i++] = NULL;
args = exec_init(argv);
args.redirect_stdout = args.redirect_stderr = EXEC_DEVNULL;
res = do_exec(&args);
if (res.pid < 0)
eerrorx("%s: failed to run umount: %s", applet, strerror(errno));
return res.pid;
}
static void fuser_run(struct run_queue *rp, const char *fuser_opt)
{
static int fuser_exec_failed = 0;
const char *argv[] = { "fuser", fuser_opt, rp->mntpath, NULL };
struct exec_result res;
struct exec_args args;
/* if exec failed, fuser likely doesn't exist. so don't retry */
if (fuser_exec_failed)
return;
args = exec_init(argv);
args.redirect_stdout = EXEC_MKPIPE;
args.redirect_stderr = EXEC_DEVNULL;
res = do_exec(&args);
if (res.pid < 0) {
fuser_exec_failed = 1;
} else {
rp->fuser_pid = res.pid;
rp->fuser_stdoutfd = res.proc_stdout;
rp->fuser_exec_time = tm_now();
}
}
static int fuser_decide(struct run_queue *rp,
const char *fuser_opt, const char *fuser_kill_prefix)
{
char buf[1<<12];
char selfpid[64];
int read_maybe_truncated;
ssize_t n;
if (rp->fuser_stdoutfd < 0)
return 0;
buf[0] = ' ';
n = read(rp->fuser_stdoutfd, buf + 1, sizeof buf - 3);
close(rp->fuser_stdoutfd);
rp->fuser_stdoutfd = -1;
read_maybe_truncated = (n == sizeof buf - 3);
if (n < 0 || read_maybe_truncated)
return 0;
while (n > 0 && buf[n] == '\n')
--n;
buf[n+1] = ' ';
buf[n+2] = '\0';
snprintf(selfpid, sizeof selfpid, " %lld ", (long long)getpid());
if (strstr(buf, selfpid)) {
/* lets not kill ourselves */
eerror("Unmounting %s failed because we are using it", rp->mntpath);
return -1;
} else if (strcmp(buf, " ") == 0) {
if (rp->try_count >= TRY_MAX) {
eerror("Unmounting %s failed but fuser finds no one using it", rp->mntpath);
return -1;
}
/* it's possible that whatever was using the mount stopped
* using it now, so allow 1 more retry */
rp->try_count = TRY_MAX;
return 0;
} else {
char sig[32];
const char *argv[] = {
"fuser", sig, "-k", fuser_opt, rp->mntpath, NULL
};
struct exec_result res;
struct exec_args args = exec_init(argv);
args.redirect_stdout = args.redirect_stderr = EXEC_DEVNULL;
snprintf(sig, sizeof sig, "%s%s", fuser_kill_prefix,
rp->try_count == TRY_MAX ? "KILL" : "TERM");
res = do_exec(&args);
if (res.pid > 0)
rc_waitpid(res.pid);
return 0;
}
}
static regex_t *
get_regex(const char *string)
{
@ -385,7 +561,23 @@ int main(int argc, char **argv)
char *real_path = NULL;
int opt;
int result;
char *this_path;
char *this_path, *argv0;
const char *tmps;
const char *fuser_opt, *fuser_kill_prefix;
size_t num_mounts = 0;
size_t unmount_index;
int doing_unmount = 0;
pid_t pid;
int status, flags;
int64_t tmp, next_retry, now;
int64_t rc_fuser_timeout = -1;
const char *umount_args[UMOUNT_ARGS_MAX];
int umount_args_num = 0;
const char **mounts = NULL;
struct run_queue running[RUN_MAX] = {0};
struct run_queue *rp;
size_t num_running = 0, num_waiting = 0;
enum { STATE_RUN, STATE_REAP, STATE_RETRY, STATE_END } state;
#define DO_REG(_var) \
if (_var) free(_var); \
@ -393,11 +585,46 @@ int main(int argc, char **argv)
#define REG_FREE(_var) \
if (_var) { regfree(_var); free(_var); }
argv0 = argv[0];
applet = basename_c(argv[0]);
memset (&args, 0, sizeof(args));
args.mount_type = mount_to;
args.netdev = net_ignore;
args.mounts = rc_stringlist_new();
args.process = process_mount;
if (strcmp(applet, "do_unmount") == 0) {
doing_unmount = 1;
while (argv[1]) {
/* shift over */
tmps = argv[1];
argv[1] = argv0;
++argv;
--argc;
if (strcmp(tmps, "--") == 0)
break;
if (umount_args_num >= (int)ARRAY_SIZE(umount_args))
eerrorx("%s: Too many umount arguments", applet);
umount_args[umount_args_num++] = tmps;
}
tmps = rc_conf_value("rc_fuser_timeout");
if (tmps && (rc_fuser_timeout = parse_duration(tmps)) < 0)
ewarn("%s: Invalid rc_fuser_timeout value: `%s`. "
"Defaulting to 20", applet, tmps);
if (rc_fuser_timeout < 0)
rc_fuser_timeout = 20 * 1000;
tmps = getenv("RC_UNAME");
if (!tmps || strcmp(tmps, "Linux") == 0) {
fuser_opt = "-m";
fuser_kill_prefix = "-";
} else {
fuser_opt = "-cm";
fuser_kill_prefix = "-s";
}
}
while ((opt = getopt_long(argc, argv, getoptstring,
longopts, (int *) 0)) != -1)
@ -459,18 +686,13 @@ int main(int argc, char **argv)
free(real_path);
real_path = NULL;
}
nodes = find_mounts(&args);
nodes = find_mounts(&args, &num_mounts);
rc_stringlist_free(args.mounts);
REG_FREE(args.fstype_regex);
REG_FREE(args.skip_fstype_regex);
REG_FREE(args.node_regex);
REG_FREE(args.skip_node_regex);
REG_FREE(args.options_regex);
REG_FREE(args.skip_options_regex);
if (doing_unmount)
mounts = xmalloc(num_mounts * sizeof(*mounts));
num_mounts = 0;
result = EXIT_FAILURE;
/* We should report the mounts in reverse order to ease unmounting */
TAILQ_FOREACH_REVERSE(s, nodes, rc_stringlist, entries) {
if (point_regex &&
@ -479,12 +701,162 @@ int main(int argc, char **argv)
if (skip_point_regex &&
regexec(skip_point_regex, s->value, 0, NULL, 0) == 0)
continue;
if (!rc_yesno(getenv("EINFO_QUIET")))
if (doing_unmount)
mounts[num_mounts++] = unescape_octal(s->value);
else if (!rc_yesno(getenv("EINFO_QUIET")))
printf("%s\n", s->value);
result = EXIT_SUCCESS;
}
rc_stringlist_free(nodes);
if (!doing_unmount)
goto exit;
/* STATE_RUN:
* can unmount => stays in STATE_RUN
* cannot unmount (for any of the reasons below) => STATE_REAP
* (a) nothing left to unmount
* (b) running queue is full
* (c) conflicts with running queue
*
* STATE_REAP:
* successful reap => STATE_RUN
* couldn't reap with WNOHANG and there are retries pending => STATE_RETRY
* nothing left to reap => STATE_RETRY
*
* STATE_RETRY:
* successfully launched a retry => STATE_REAP
* need to wait before retring -> sleep
* sleep successful => STATE_RETRY
* sleep interrupted via SIGCHILD (EINTR) => STATE_REAP
* nothing left to retry, reap
* and nothing to run either => STATE_END
* otherwise => STATE_RUN
*/
result = EXIT_SUCCESS;
state = STATE_RUN;
while (state != STATE_END) switch (state) {
case STATE_RUN:
for (unmount_index = 0; unmount_index < num_mounts; ++unmount_index) {
const char *candidate = mounts[unmount_index];
int safe_to_unmount = 1;
for (size_t k = 0; safe_to_unmount && k < num_running; ++k)
safe_to_unmount = !is_prefix(candidate, running[k].mntpath);
for (size_t k = 0; safe_to_unmount && k < num_mounts; ++k)
safe_to_unmount = !is_prefix(candidate, mounts[k]);
if (!safe_to_unmount)
continue;
if (!is_mounted(candidate)) {
/* probably a shared mount and got unmounted, remove */
mounts[unmount_index--] = mounts[--num_mounts];
} else {
break;
}
}
if (num_running == RUN_MAX || unmount_index >= num_mounts) {
state = STATE_REAP;
break;
}
rp = running + num_running++;
rp->mntpath = mounts[unmount_index];
rp->last_exec_time = tm_now();
rp->try_count = 0;
rp->pid = run_umount(rp->mntpath, umount_args, umount_args_num);
rp->fuser_pid = -1;
rp->fuser_stdoutfd = -1;
rp->fuser_exec_time = -1;
mounts[unmount_index] = mounts[--num_mounts];
break;
case STATE_REAP:
flags = (num_waiting > 0) ? WNOHANG : 0;
pid = waitpid(-1, &status, flags);
rp = NULL;
for (size_t i = 0; i < num_running; ++i) {
rp = running + i;
if (rp->fuser_pid == pid)
rp->fuser_pid = -1;
if (rp->pid == pid && pid > 0)
break;
rp = NULL;
}
if (rp) {
if ((WIFEXITED(status) && WEXITSTATUS(status) == 0) ||
!is_mounted(rp->mntpath)) {
einfo("Unmounted %s", rp->mntpath);
*rp = running[--num_running];
state = STATE_RUN;
} else if (rp->try_count >= TRY_MAX) {
eerror("Failed to unmount %s", rp->mntpath);
*rp = running[--num_running];
result = EXIT_FAILURE;
} else { /* put into waiting queue */
rp->pid = -1;
rp->try_count += 1;
num_waiting += 1;
fuser_run(rp, fuser_opt);
}
} else {
state = STATE_RETRY;
}
break;
case STATE_RETRY:
rp = NULL;
next_retry = INT64_MAX;
for (size_t i = 0; i < num_running; ++i) {
if (running[i].pid > 0)
continue;
if (running[i].fuser_pid > 0)
tmp = running[i].fuser_exec_time + rc_fuser_timeout;
else
tmp = running[i].last_exec_time + TRY_DELAY_MS;
if (tmp < next_retry) {
rp = running + i;
next_retry = tmp;
}
}
if (!rp) {
state = (num_mounts > 0) ? STATE_RUN : STATE_END;
break;
}
now = tm_now();
if (next_retry > now) {
int64_t sleep_for = next_retry - now;
/* a child may become available for reaping *before* we
* enter sleep. cap the timeout to stay responsive. */
if (sleep_for > 500)
sleep_for = 500;
if (tm_sleep(sleep_for, 0) != 0 && errno == EINTR)
state = STATE_REAP;
now = tm_now();
}
if (next_retry <= now) {
if (rp->fuser_pid > 0) {
kill(rp->fuser_pid, SIGKILL);
waitpid(rp->fuser_pid, NULL, 0);
rp->fuser_pid = -1;
}
if (fuser_decide(rp, fuser_opt, fuser_kill_prefix) < 0) { /* abort */
*rp = running[--num_running];
result = EXIT_FAILURE;
} else { /* retry */
rp->last_exec_time = tm_now();
rp->pid = run_umount(rp->mntpath,
umount_args, umount_args_num);
}
num_waiting -= 1;
state = STATE_REAP;
}
break;
default: break;
}
exit:
free(mounts);
rc_stringlist_free(nodes);
REG_FREE(args.fstype_regex);
REG_FREE(args.skip_fstype_regex);
REG_FREE(args.node_regex);
REG_FREE(args.skip_node_regex);
REG_FREE(args.options_regex);
REG_FREE(args.skip_options_regex);
REG_FREE(point_regex);
REG_FREE(skip_point_regex);

20
tools/manymounts.sh Executable file
View File

@ -0,0 +1,20 @@
#!/bin/sh
# can be used for testing do_unmount:
# # ./tools/manymounts.sh
# # do_unmount -- -p '^/tmp/manymounts.*'
set -- "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "A" "B" "C" "D" "E" "F"
mntdir="/tmp/manymounts"
for a in "$@"; do
mkdir -p "${mntdir}/${a}"
mount -t tmpfs -o size=256K tmpfs "${mntdir}/${a}"
for b in "$@"; do
mkdir -p "${mntdir}/${a}/${b}"
mount -t tmpfs -o size=256K tmpfs "${mntdir}/${a}/${b}"
for c in "$@"; do
mkdir -p "${mntdir}/${a}/${b}/${c}"
mount -t tmpfs -o size=256K tmpfs "${mntdir}/${a}/${b}/${c}"
done
done
done