如何停止 chroot 中的所有进程?

如何停止 chroot 中的所有进程?

我有多个 LVM 分区,每个分区都包含一个 Ubuntu 安装。有时,我想执行apt-get dist-upgrade,以将安装更新为最新的软件包。我使用 chroot 执行此操作 - 过程通常类似于:

$ sudo mount /dev/local/chroot-0 /mnt/chroot-0
$ sudo chroot /mnt/chroot-0 sh -c 'apt-get update && apt-get dist-upgrade'
$ sudo umount /mnt/chroot-0

[ 未显示:我还将其/mnt/chroot-0/{dev,sys,proc}作为绑定挂载挂载到真正的和/dev,因为 dist-upgrade 似乎期望这些存在 ]/sys/proc

但是,升级到 precise 后,此过程不再有效 - 最终的 umount 将失败,因为/mnt/chroot-0文件系统上仍有打开的文件。lsof确认 chroot 中有打开文件的进程。这些进程已在 dist-upgrade 期间启动,我假设这是因为 chroot 中的某些服务需要在service postgresql restart软件包升级后重新启动(例如,通过)。

因此,我认为我需要告诉 upstart 停止此 chroot 中运行的所有服务。有没有可靠的方法可以做到这一点?

我试过了:

cat <<EOF | sudo chroot /mnt/chroot-0 /bin/sh
# stop 'initctl' services 
initctl list | awk '/start\/running/ {print \$1}' | xargs -n1 -r initctl stop
EOF

Whereinitctl list 似乎做了正确的事情,并且只列出了已在此特定根目录中启动的进程。我也尝试添加它,正如 Tuminoid 所建议的那样:

cat <<EOF | sudo chroot /mnt/chroot-0 /bin/sh
# stop 'service' services
service --status-all 2>/dev/null |
    awk '/^ \[ \+ \]/ { print \$4}' |
    while read s; do service \$s stop; done
EOF

但是,这些似乎并不能捕获所有情况;已守护进程并重新设置为 PID 1 的进程不会被停止。我还尝试过:

sudo chroot /mnt/chroot-0 telinit 0

但在这种情况下,init没有区分单独的根并关闭整个机器。

那么,有什么方法可以告诉 init 停止特定 chroot 中的所有进程,以便我可以安全地卸载文件系统? upstart 是否有任何工具可以在 chroot 中 SIGTERM/SIGKILL 所有子进程(就像在常规关闭期间所做的那样)?

答案1

除了内核,我不相信任何东西能在这里保持正常状态,所以我不会(滥用)使用 init 来完成这项工作,我也不指望自己真正知道哪些是挂载的,哪些是未挂载的(某些软件包可以挂载额外的文件系统,如 binfmt_misc)。因此,对于进程屠杀,我使用:

PREFIX=/mnt/chroot-0
FOUND=0

for ROOT in /proc/*/root; do
    LINK=$(readlink $ROOT)
    if [ "x$LINK" != "x" ]; then
        if [ "x${LINK:0:${#PREFIX}}" = "x$PREFIX" ]; then
            # this process is in the chroot...
            PID=$(basename $(dirname "$ROOT"))
            kill -9 "$PID"
            FOUND=1
        fi
    fi
done

if [ "x$FOUND" = "x1" ]; then
    # repeat the above, the script I'm cargo-culting this from just re-execs itself
fi

对于卸载 chroot,我使用:

PREFIX=/mnt/chroot-0
COUNT=0

while grep -q "$PREFIX" /proc/mounts; do
    COUNT=$(($COUNT+1))
    if [ $COUNT -ge 20 ]; then
        echo "failed to umount $PREFIX"
        if [ -x /usr/bin/lsof ]; then
            /usr/bin/lsof "$PREFIX"
        fi
        exit 1
    fi
    grep "$PREFIX" /proc/mounts | \
        cut -d\  -f2 | LANG=C sort -r | xargs -r -n 1 umount || sleep 1
done

作为补充,我想指出,将此视为 init 问题可能不是正确的看法,除非您实际上在 chroot 中有一个 init 和一个单独的进程空间(即:在 LXC 容器的情况下)。使用单个 init(在 chroot 之外)和一个共享进程空间,这不再是“init 的问题”,而只是由您自己来找到恰好具有有问题的路径的进程,因此才有上述 proc 遍历。

从您的初始帖子中无法确定这些是完全可启动的系统,您只是从外部进行升级(我是这样理解的),还是您用于软件包构建等操作的 chroot。如果是后者,您可能还需要一个 policy-rc.d(如 mk-sbuild 插入的那个),它首先禁止 init 作业启动。显然,如果这些系统也是可启动的系统,那么这不是一个明智的解决方案。

答案2

您自己已经发现了问题:有些东西service ...在 dist-upgrade 期间运行,它们service不是 Upstart 的一部分,而是 的一部分sysvinit。添加类似的 awk magic 来service --status-all停止 sysvinit 服务,就像您用于 Upstart 服务一样。

答案3

我知道这个问题已经很老了,但我认为它今天和 2012 年一样重要,希望有人觉得这段代码有用。我为我正在做的事情编写了代码,但我想分享一下。

我的代码不同,但想法与@infinity 非常相似(事实上 - 我现在知道 /proc/*/root 的唯一原因是因为他的回答 - 感谢@infinity!)。我还添加了一些很酷的附加功能

#Kills any PID passed to it
#At first it tries nicely with SIGTERM
#After a timeout, it uses SIGKILL
KILL_PID()
{
        PROC_TO_KILL=$1

        #Make sure we have an arg to work with
        if [[ "$PROC_TO_KILL" == "" ]]
        then
                echo "KILL_PID: \$1 cannot be empty"
                return 1
        fi

        #Try to kill it nicely
        kill -0 $PROC_TO_KILL &>/dev/null && kill -15 $PROC_TO_KILL

        #Check every second for 5 seconds to see if $PROC_TO_KILL is dead
        WAIT_TIME=5

        #Do a quick check to see if it's still running
        #It usually takes a second, so this often doesn't help
        kill -0 $PROC_TO_KILL &>/dev/null &&
        for SEC in $(seq 1 $WAIT_TIME)
        do
                sleep 1

                if [[ "$SEC" != $WAIT_TIME ]]
                then
                        #If it's dead, exit
                        kill -0 $PROC_TO_KILL &>/dev/null || break
                else
                        #If time's up, kill it
                        kill -0 $PROC_TO_KILL &>/dev/null && kill -9 $PROC_TO_KILL
                fi
        done
}

现在你需要做两件事来确保 chroot 可以被卸载:

终止可能在 chroot 中运行的所有进程:

CHROOT=/mnt/chroot/

#Find processes who's root folder is actually the chroot
for ROOT in $(find /proc/*/root)
do
        #Check where the symlink is pointing to
        LINK=$(readlink -f $ROOT)

        #If it's pointing to the $CHROOT you set above, kill the process
        if echo $LINK | grep -q ${CHROOT%/}
        then
                PID=$(basename $(dirname "$ROOT"))
                KILL_PID $PID
        fi
done

终止所有可能在 chroot 之外运行但会干扰它的进程(例如:如果您的 chroot 是 /mnt/chroot 并且 dd 正在写入 /mnt/chroot/testfile,则 /mnt/chroot 将无法卸载)

CHROOT=/mnt/chroot/

#Get a list of PIDs that are using $CHROOT for anything
PID_LIST=$(sudo lsof +D $CHROOT 2>/dev/null | tail -n+2 | tr -s ' ' | cut -d ' ' -f 2 | sort -nu)

#Kill all PIDs holding up unmounting $CHROOT
for PID in $PID_LIST
do
        KILL_PID $PID
done

注意:以 root 身份运行所有代码

另外,对于不太复杂的版本,可以使用以下任一方法替换 KILL_PID kill -SIGTERMkill -SIGKILL

答案4

只是所选答案的缩写版本

可以使用正则表达式来同时处理多个 chroot。

终止单个或多个 $CHROOTS 中的所有进程。

狂欢

for p in /proc/*/root; do [[ `readlink $p` =~ ^$CHROOTS ]] && kill ${p//[^0-9]/}; done

perl (更快)

perl -e "kill 15, map {/\d+/;\$&} grep {readlink =~ m:^$CHROOTS:} @ARGV" /proc/*/root

递归地卸载单个 $CHROOT。

狂欢

i=20; while ! umount -R "$CHROOT" && ((--i)); do sleep 1; done

递归地卸载多个 $CHROOTS。

狂欢

i=20; while ((i--))&&sleep 1&&a=`sed -nr "s:^\S+ ($CHROOTS) .*:\1:p" /etc/mtab`&&[[ $a ]]; do xargs umount -qR<<<"$a";done

相关内容