Skip to content
  • 需求

    正常情况下这种操作比较反设计,需要谨慎使用 有些时候我们使用docker的时候会需要用到宿主机命令操作,比如

    • 执行 netplan apply生效机器网络配置
    • 查看宿主机网络信息使用ifconfig或者 ip addr
    • 从容器发出命令重启宿主机

    具体实现

    docker run实现

    最简单的实现 使用docker运行一个ubuntu容器 进入容器之后执行nsenter命令查看宿主机网络配置信息

    $ sudo docker run -it --pid=host --privileged=true ubuntu /bin/bash
    # 进入容器内部之后执行
    
    /# nsenter -a -t 1 sh -c "ip addr"
    $ sudo docker run -it --pid=host --privileged=true ubuntu /bin/bash
    # 进入容器内部之后执行
    
    /# nsenter -a -t 1 sh -c "ip addr"

    docker compose实现

    新建一个compose.yaml文件,写入如下内容

    services:
        demo:
            image: python:3-bullseye
            pid: host
            privileged: true
            container_name: demo-exec
            command: /bin/sh -c "while true; do echo hello; sleep 3600;done"
    services:
        demo:
            image: python:3-bullseye
            pid: host
            privileged: true
            container_name: demo-exec
            command: /bin/sh -c "while true; do echo hello; sleep 3600;done"

    执行命令

    $ docker compose up -d
    $ docker compose up -d

    进入容器,并且创建写入demo.py文件代码

    $ docker exec -it demo-exec /bin/bash
    /# cat > demo.py <<EOF
    import subprocess
    
    
    if __name__ == '__main__':
        command = 'ls /var/lib/docker'
        resp = subprocess.run(
            f'nsenter -m -u -i -n -p -t 1 sh -c "{command}"',
            capture_output=True, check=True, text=True, shell=True)
        print(f'stdout: {resp.stdout}')
        print(f'stderr: {resp.stderr}')
    EOF
    $ docker exec -it demo-exec /bin/bash
    /# cat > demo.py <<EOF
    import subprocess
    
    
    if __name__ == '__main__':
        command = 'ls /var/lib/docker'
        resp = subprocess.run(
            f'nsenter -m -u -i -n -p -t 1 sh -c "{command}"',
            capture_output=True, check=True, text=True, shell=True)
        print(f'stdout: {resp.stdout}')
        print(f'stderr: {resp.stderr}')
    EOF

    在容器内部检查文件是否写入成功并且执行程序

    /# cat demo.py
    /# python demo.py
    /# cat demo.py
    /# python demo.py

    实现原理

    docker 参数

    --pid=host

    • 使用宿主机命名空间,方便容器获取到宿主机所有进程信息
    • 把宿主机的/proc文件夹挂载进入容器的/proc路径,其中/proc/1作为nsentertarget,作为容器向宿主机发送命令的关键部分

    --privileged=true

    • 使得docker容器有root权限执行宿主机命令,确保从容器执行命令的时候不会产生权限不足错误

    nsenter命令

    nsenter命令是一个可以在指定进程的命令空间下运行指定程序的命令

    $ nsenter --help
    
    用法:
     nsenter [选项] [<程序> [<参数>...]]
    
    以其他程序的名字空间运行某个程序。
    
    选项:
     -a, --all              enter all namespaces
     -t, --target <pid>     要获取名字空间的目标进程
     -m, --mount[=<文件>]   进入 mount 名字空间
     -u, --uts[=<文件>]     进入 UTS 名字空间(主机名等)
     -i, --ipc[=<文件>]     进入 System V IPC 名字空间
     -n, --net[=<文件>]     进入网络名字空间
     -p, --pid[=<文件>]     进入 pid 名字空间
     -C, --cgroup[=<文件>]  进入 cgroup 名字空间
     -U, --user[=<文件>]    进入用户名字空间
     -S, --setuid <uid>     设置进入空间中的 uid
     -G, --setgid <gid>     设置进入名字空间中的 gid
         --preserve-credentials 不干涉 uid 或 gid
     -r, --root[=<目录>]     设置根目录
     -w, --wd[=<dir>]       设置工作目录
     -F, --no-fork          执行 <程序> 前不 fork
     -Z, --follow-context  根据 --target PID 设置 SELinux 环境
    
     -h, --help             display this help
     -V, --version          display version
    
    更多信息请参阅 nsenter(1)。
    $ nsenter --help
    
    用法:
     nsenter [选项] [<程序> [<参数>...]]
    
    以其他程序的名字空间运行某个程序。
    
    选项:
     -a, --all              enter all namespaces
     -t, --target <pid>     要获取名字空间的目标进程
     -m, --mount[=<文件>]   进入 mount 名字空间
     -u, --uts[=<文件>]     进入 UTS 名字空间(主机名等)
     -i, --ipc[=<文件>]     进入 System V IPC 名字空间
     -n, --net[=<文件>]     进入网络名字空间
     -p, --pid[=<文件>]     进入 pid 名字空间
     -C, --cgroup[=<文件>]  进入 cgroup 名字空间
     -U, --user[=<文件>]    进入用户名字空间
     -S, --setuid <uid>     设置进入空间中的 uid
     -G, --setgid <gid>     设置进入名字空间中的 gid
         --preserve-credentials 不干涉 uid 或 gid
     -r, --root[=<目录>]     设置根目录
     -w, --wd[=<dir>]       设置工作目录
     -F, --no-fork          执行 <程序> 前不 fork
     -Z, --follow-context  根据 --target PID 设置 SELinux 环境
    
     -h, --help             display this help
     -V, --version          display version
    
    更多信息请参阅 nsenter(1)。

    具体执行

    $ nsenter -a -t 1 sh -c "ip addr"
    $ nsenter -a -t 1 sh -c "ip addr"
    • -a表示进入宿主机的所有命名空间
    • -t 1表示获取/proc/1进程,就是pid=1的进程,这个进程是docker使用--pid=host参数挂载进入容器内部的宿主机进程
    • sh -c "ip addr"就表示发送给宿主机的命令是ip addr

    实际使用过程中如果出现宿主机和容器命名空间不一致问题,主要产生原因是宿主机内核版本和容器 所默认的加载内核版本不一致

    比如cgroup是在Linux4.6版本加入的,如果使用Ubuntu20或者其他python3.10等比较新的镜像启动容器的时候,当nsenter使用参数-a,容器会加载所有命名空间,但是cgroup命名空间在旧版本的系统里面由于内核版本比较旧,所以该命名空间是没有的,最终nsenter命令就会报错

    需要按照宿主机有的命名空间来调整nsenter参数,可以调整如下

    $ nsenter -m -u -i -n -p  -t 1 sh -c "ip addr"
    $ nsenter -m -u -i -n -p  -t 1 sh -c "ip addr"

    比如可以把-a参数替换成-m -u -i -n -p,明确指定进入mount, UTS,System V IPC,网络,pid命名空间 这几个命名空间包含了绝大多数的空间环境,linux的大部分命令都可以正常执行

    命名空间说明

    namespaceLinux中一些进程的属性的作用域,使用命名空间,可以隔离不同的进程

    Linux在不断的添加命名空间,目前有

    • mount:挂载命名空间,使进程有一个独立的挂载文件系统,始于Linux 2.4.19
    • ipcipc命名空间,使进程有一个独立的ipc,包括消息队列,共享内存和信号量,始于Linux 2.6.19
    • utsuts命名空间,使进程有一个独立的hostnamedomainname,始于Linux 2.6.19
    • netnetwork命令空间,使进程有一个独立的网络栈,始于Linux 2.6.24
    • pidpid命名空间,使进程有一个独立的pid空间,始于Linux 2.6.24
    • useruser命名空间,是进程有一个独立的user空间,始于Linux 2.6.23,结束于Linux 3.8
    • cgroupcgroup命名空间,使进程有一个独立的cgroup控制组,始于Linux 4.6

    参考阅读

Last updated:

Released under the MIT License.