如何停止 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/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

在这个特定的 root 中,initctl 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信号终止所有子进程(就像在正常关机时一样)的功能?

这不是对你实际问题的回答,但可能会有帮助:我建议看一下lxc软件包。lxc提供了简单的工具来启动和干净地关闭容器中的实例。 - ion
6个回答

我不信任除了内核以外的任何东西来保持这里的一个健全状态,因此我不(滥用)使用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

而对于卸载chroots,我使用以下命令:
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

作为补充,我想指出以初始化问题来处理可能是错误的方式,除非你实际上在chroot中有一个init和一个单独的进程空间(例如LXC容器的情况)。如果只有一个初始化程序(在chroot之外)和一个共享的进程空间,这就不再是“初始化问题”,而是由你自己找到具有有问题路径的进程,因此需要进行上述proc遍历。
从你最初的帖子中并不清楚这些是否是可完全启动的系统,你只是在外部升级它们(我是这样理解的),还是它们是你用于诸如软件包构建之类的chroot。如果是后者,你可能还希望放置一个policy-rc.d(类似于mk-sbuild提供的那个),它只是禁止初始化任务的启动。显然,如果这些也被设计成可引导的系统,那么这不是一个明智的解决方案。

它们是可引导的系统,但policy-rc.d看起来是一种有趣的方法(在与chroot交互后,我可以简单地将其删除)。这会影响/etc/rc*.d/etc/init/*.conf两种类型的任务吗? - Jeremy Kerr
嗯,显然不是:https://bugs.launchpad.net/ubuntu/+source/upstart/+bug/939105 - Jeremy Kerr
既不是upstart也不是sysvinit的"consult policy-rc.d",而是invoke-rc.d来执行此操作,所有postinst脚本都应该使用它与init作业进行交互。实际上,它似乎能正常工作,除非出现了损坏的软件包(应该修复)。尽管如此,上述的"purge with fire"脚本可以完成任务,无论问题是绕过策略、没有设置策略,还是某种其他类型的长时间运行进程被保留下来(在这里,buildds的主要用例是在构建过程中后台运行或从sbuild中解除父子关系的事物)。 - infinity
1尝试绕过utpstart的chroot支持存在一个问题。我相当确定使用kill -9命令无法阻止upstart在有respawn指定的情况下重新启动upstart作业。因此,你确实仍然需要从chroot内部询问upstart以确定事务是否仍在运行。我认为这非常不幸,我们应该有一种方法可以从chroot外部终止这些作业。话虽如此,我确实能看到initctl list/awk/grep方法加上你的方法应该是完整的。 - SpamapS
1@SpamapS:说得好 - 手动杀死 init 作业确实会导致它们重新启动。如果能够告诉 upstart 执行特定于 chroot 的关机操作,停止定义的作业,然后杀死任何在 chroot 中具有根目录的剩余重新父进程,那将是很棒的。 - Jeremy Kerr

您已经自己找到了问题所在:某些事情在 dist-upgrade 过程中运行service ...,而service不是 Upstart 的一部分,而是sysvinit的一部分。请在service --status-all周围添加类似的 awk 魔法来停止 sysvinit 服务,就像您用于停止 Upstart 服务一样。

3啊,谢谢。情况好了一些,但这还不包括所有的服务。我已经运行了 sudo chroot /mnt/chroot-0 service --list-allsudo chroot /mnt/chroot-0 initctl list,两者都报告没有正在运行的服务。然而,/usr/bin/epmd(来自 erlang-base)仍在运行。 - Jeremy Kerr

我知道这个问题已经很久了,但我认为它在今天仍然和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:
1. 结束所有可能在chroot中运行的进程:
2.
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


jchroot: 带有更多隔离性的 chroot。

在执行完您的命令之后,通过执行此命令启动的任何进程都将被终止,任何 IPC 将被释放,任何挂载点也将被卸载。一切都干净利落!

schroot 目前还无法做到这一点,但计划中。

我已经在 OpenVZ VPS 上成功测试过,该 VPS 无法使用 docker 或者 lxc。

请阅读作者的博客以获取详细信息:

https://vincent.bernat.im/en/blog/2011-jchroot-isolation.html


只是一个已选择答案的简化版本
正则表达式可用于同时处理多个chroot。
杀死单个或多个$CHROOTS中的所有进程。
bash
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并递归删除。

bash

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

卸载多个$CHROOTS的递归方式。
bash
i=20; while ((i--))&&sleep 1&&a=`sed -nr "s:^\S+ ($CHROOTS) .*:\1:p" /etc/mtab`&&[[ $a ]]; do xargs umount -qR<<<"$a";done


使用schroot而不是chroot可能有助于解决原帖作者的问题,这一点可能需要解释清楚。 - please delete me