Linux的namespace功能为我们将全局资源进行切分隔离提供了便利的手段。一个/一组进程被指定到新的namespace下,它们只会看到自己所处的namespace中关联的资源,在上一篇中已经介绍了network namespace。接下来本节内容轮到讲解的另一个重要的命名空间:pid namespace.
本节内容主要分为以下几个部分:
- 1.介绍linux的pid 和linux pid namespace的基本概念和一些相关知识;
- 2.通过一个案例,来演示在linux中如何进入到一个新的命名空间,进行一些实验性的操作。
- 3.总结docker使用的pid namespace;
本节内容会涉及较多的linux命令,我们假设读者有一定的linux命令行基础,这样可以帮助读者较好,较快地理解整个过程。
1.linux pid namespace
从《计算机操作系统原理》一课中,我们可以知道,操作系统通过数据结构进程控制块(process control block/PCB)来管理进程的生命周期。PCB中会包含该进程在整个系统中唯一的全局id,该id字段我们习惯称为pid。
在linux操作系统中,进程之间是存在依赖关系的,除了init进程之外,所有的其他进程都存在父进程(parent process)。因此整个操作系统的进程依赖关系最终会形成一个树状的视图。我们可以通过命令pstree
来查看当前系统的整个进程树状依赖关系:
[root@redis2 ~]# pstree
systemd─┬─NetworkManager───2*[{NetworkManager}]
├─agetty
├─auditd───{auditd}
├─chronyd
├─containerd───41*[{containerd}]
├─crond
├─dbus-daemon
├─dockerd───38*[{dockerd}]
├─irqbalance
├─java───71*[{java}]
├─java─┬─controller───2*[{controller}]
│ └─161*[{java}]
├─java─┬─controller───2*[{controller}]
│ └─148*[{java}]
├─java─┬─controller───2*[{controller}]
│ └─121*[{java}]
├─node───10*[{node}]
├─polkitd───5*[{polkitd}]
├─redis-server───3*[{redis-server}]
├─rsyslogd───2*[{rsyslogd}]
├─sshd─┬─sshd───bash───redis-cli
│ └─sshd───bash───pstree
├─systemd-journal
├─systemd-logind
├─systemd-udevd
└─tuned───4*[{tuned}]
复制代码
从上面的输出可以看到,所有的进程不断往上追溯寻找parent process,最终的结果都是指向了systemd进程,我也习惯将它称为操作系统的init进程,如果我们对命令行pstree加上参数 -ap: pstree -ap,返回的结果会给每一个进程都附带上其pid,而init进程的pid是1.
因此,在linux中,pid表示进程id,pid标识了该进程在操作系统中的唯一性,pid=1的进程地位很特殊,它是所有进程的父进程。
pid=1的进程,有以下特点:
- 如果一个子进程的parent process退出了,那么它会变成孤儿进程,孤儿进程会被pid=1的init进程接管。
- 如果init进程退出了,那么整个操作系统的进程都会退出,资源被回收(联想关机操作)。
Linux pid namespace用于隔离进程之间的进程号,在不同namespace中进程的pid是可以相同的。在一个操作系统中,起码会存在一个pid namespace。每一个pid_namespace中pid是从1开始编号的,pid=1 的进程表示它是在pid namepace下第一个启动的进程,它在该pid namespace下处于特殊地位;
然而,在整个操作系统层面其pid没有发生改变,仅仅是因为目标进程被隔离了,看不到操作系统的进程视图,认为自己就是一个init进程,pid=1。
打一个不恰当的比喻,将整个操作系统比作我国古代的周朝代,周天子就是整个操作系统的init进程,pid=1(身份证号是001)。每个诸侯国都是一个pid namespace,诸侯在自己的领土里,就是init进程,pid=1。但是诸侯在整个周朝下,他们的身份证号并不是001;
在一个pid namespace中,init process有以下特点:
-
当init process 进程退出时,操作系统会向linux内核发送SIGKILL信号,结束该namespace所有的进程,回收资源,最后该 pid namespace也会回收。
-
init process可以屏蔽信号,防止在同一个pid namespace下的其他进程杀死该init进程,导致整个pid namespace下的其他进程被销毁。(试想一下我们自写的代码启动的进程中含有一个
kill -9 1
的指令,如果init进程不屏蔽掉这个信号,就会导致整个操作系统关机了)
其他关于linux pid namespace详细文档,请参考 Linux man pid_namespace
我们可以通过命令获取当前系统的所有 namespace 列表,并通过-t参数指定的namespace:
#获取全部类型的namespace
[root@redis2 ~]# lsns -l
NS TYPE NPROCS PID USER COMMAND
4026531836 pid 229 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize 21
4026531837 user 229 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize 21
4026531838 uts 229 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize 21
4026531839 ipc 229 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize 21
4026531840 mnt 225 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize 21
4026531856 mnt 1 90 root kdevtmpfs
4026531956 net 229 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize 21
4026532400 mnt 1 477 root /usr/lib/systemd/systemd-udevd
4026532423 mnt 1 689 chrony /usr/sbin/chronyd
4026532424 mnt 1 699 root /usr/sbin/NetworkManager --no-daemon
# 只读取指定类型的namespace
# 这里演示读取pid namespace,可以看到当前系统只有默认的一个pid namespace
[root@redis2 ~]# lsns -l -t pid
NS TYPE NPROCS PID USER COMMAND
4026531836 pid 229 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize 21
复制代码
另外,如果我们想知道当前系统运行的进程的pid namespace是什么,可以通过下面的命令获取:
# -o 参数指定希望显示那些内容
[root@redis2 ~]# ps -e -o ppid,pid,pidns,args
PPID PID PIDNS COMMAND
0 1 4026531836 /usr/lib/systemd/systemd --switched-root --system --deserialize 21
0 2 4026531836 [kthreadd]
2 3 4026531836 [ksoftirqd/0]
2 5 4026531836 [kworker/0:0H]
......
复制代码
- 加入到一个存在的pid namespace中。
可通过命令 nsenter --mount --pid -t $pid $command
来将指定的进程( pid 的pid namespace中。
2.举例验证。
本小节通过一个案例,来演示linux pid namespace的。
-
- 读取当前终端进程所处的pid namespace:
# 当前的shell终端进程的pid namespace 为 4026531836
[root@hdp3 ~]# readlink /proc/self/ns/pid
pid:[4026531836]
复制代码
-
- 开启一个新的pid namespace,并且启动第一个进程 /bin/bash,运行两次sh命令,在该pid namespace下形成进程树。
# 开启新的pid namespace,并启动一个shell
unshare -p -f --mount-proc /bin/bash
#重新读取新的pid namespace,结果为 4026532426
[root@redis2 ~]# readlink /proc/self/ns/pid
pid:[4026532426]
#运行两次sh命令
[root@redis2]# sh
sh-4.2# sh
复制代码
- 最后我们打印出该pid namespace下的进程树
sh-4.2# pstree -ag
bash,1
└─bash,14
└─sh,25
└─pstree,29 -ag
复制代码
现在,我们打开另一个终端,并将启动一个shell加入到上面的pid namespace中并打印进程树:
# 找到pid namespace= [4026532426]的进程
# ps -ef -o ppid,pid,pidns,args
[root@redis2 ~]# ps -ef -o ppid,pid,pidns,args
PPID PID PIDNS COMMAND
237610 237612 4026531836 -bash USER=root LOGNAME=root HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin MAIL=/var/mail/root SHELL=/bin/bash SSH_CLIENT=10.11
237612 237647 4026531836 \_ ps -ef -o ppid,pid,pidns,args XDG_SESSION_ID=7100 HOSTNAME=redis2 TERM=xterm SHELL=/bin/bash HISTSIZE=1000 SSH_CLIENT=10.11.111.31 58018 22 SSH
236698 236701 4026531836 -bash USER=root LOGNAME=root HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin MAIL=/var/mail/root SHELL=/bin/bash SSH_CLIENT=10.11
236701 237579 4026531836 \_ unshare -p -f --mount-proc /bin/bash XDG_SESSION_ID=7077 HOSTNAME=redis2 TERM=xterm SHELL=/bin/bash HISTSIZE=1000 SSH_CLIENT=10.11.111.31 57006
237579 237580 4026532426 \_ /bin/bash XDG_SESSION_ID=7077 HOSTNAME=redis2 TERM=xterm SHELL=/bin/bash HISTSIZE=1000 SSH_CLIENT=10.11.111.31 57006 22 SSH_TTY=/dev/pts/3
237580 237593 4026532426 \_ bash XDG_SESSION_ID=7077 HOSTNAME=redis2 SHELL=/bin/bash TERM=xterm HISTSIZE=1000 SSH_CLIENT=10.11.111.31 57006 22 SSH_TTY=/dev/pts/3 U
237593 237604 4026532426 \_ sh XDG_SESSION_ID=7077 HOSTNAME=redis2 TERM=xterm SHELL=/bin/bash HISTSIZE=1000 SSH_CLIENT=10.11.111.31 57006 22 SSH_TTY=/dev/pts/3
236614 236628 4026531836 -bash USER=root LOGNAME=root HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin MAIL=/var/mail/root SHELL=/bin/bash SSH_CLIENT=10.11
236628 237077 4026531836 \_ ssh hdp3 XDG_SESSION_ID=7076 HOSTNAME=redis2 TERM=xterm SHELL=/bin/bash HISTSIZE=1000 SSH_CLIENT=10.11.111.31 56894 22 SSH_TTY=/dev/pts/2 USER=
188991 188993 4026531836 -bash LANG=zh_CN.UTF-8 USER=root LOGNAME=root HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin MAIL=/var/mail/root SHELL=/bin/bash
188993 189010 4026531836 \_ redis-cli -p 6380 XDG_SESSION_ID=5363 HOSTNAME=redis2 TERM=xterm SHELL=/bin/bash HISTSIZE=1000 SSH_CLIENT=192.168.59.145 10044 22 SSH_TTY=/dev/
1 950 4026531836 /sbin/agetty --noclear tty1 linux LANG= PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin TERM=linux LANGUAGE= LC_CTYPE= LC_NUMERIC= LC_TIME=
# 进入pid=237580 的 pid namespace中
[root@redis2 /]#senter --mount --pid -t 237580 /bin/bash
# 打印当前的进程树:
[root@redis2 /]# pstree -ag
bash,1
└─bash,14
└─sh,25
# 尝试杀死 bash,14 . 看看进程树如何变化
[root@redis2 /]# kill -9 14
[root@redis2 /]# pstree -ag
bash,1
└─sh,25
复制代码
可以发现,在pid namespace[4026532426] 中: sh,14 进程被杀死后,sh,25进程成为了孤儿进程,孤儿会被 bash,1 的init进程接管。
最后,我们在重新开一个终端,将pid namespace[4026532426] 的init(在操作系统中pid=237580)进程杀死。
kill -9 237580
复制代码
然后重新查看整个操作系统下的进程列表,发现 pid namespace[4026532426]下的进程全部都被回收了,pid namespace[4026532426]也不存在了:
[root@redis2 opt]# ps -ef -o ppid,pid,pidns,args
PPID PID PIDNS COMMAND
237744 237749 4026531836 -bash USER=root LOGNAME=root HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin MAIL=/var/mail/root SHELL=/bin/bash SSH_CLIENT=10.11
237749 237835 4026531836 \_ tail -200f /var/log/messages XDG_SESSION_ID=7103 HOSTNAME=redis2 TERM=xterm SHELL=/bin/bash HISTSIZE=1000 SSH_CLIENT=10.11.111.31 58147 22 SSH_
237610 237612 4026531836 -bash USER=root LOGNAME=root HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin MAIL=/var/mail/root SHELL=/bin/bash SSH_CLIENT=10.11
236698 236701 4026531836 -bash USER=root LOGNAME=root HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin MAIL=/var/mail/root SHELL=/bin/bash SSH_CLIENT=10.11
236701 237840 4026531836 \_ ps -ef -o ppid,pid,pidns,args XDG_SESSION_ID=7077 HOSTNAME=redis2 TERM=xterm SHELL=/bin/bash HISTSIZE=1000 SSH_CLIENT=10.11.111.31 57006 22 SSH
236614 236628 4026531836 -bash USER=root LOGNAME=root HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin MAIL=/var/mail/root SHELL=/bin/bash SSH_CLIENT=10.11
236628 237077 4026531836 \_ ssh hdp3 XDG_SESSION_ID=7076 HOSTNAME=redis2 TERM=xterm SHELL=/bin/bash HISTSIZE=1000 SSH_CLIENT=10.11.111.31 56894 22 SSH_TTY=/dev/pts/2 USER=
188991 188993 4026531836 -bash LANG=zh_CN.UTF-8 USER=root LOGNAME=root HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin MAIL=/var/mail/root SHELL=/bin/bash
188993 189010 4026531836 \_ redis-cli -p 6380 XDG_SESSION_ID=5363 HOSTNAME=redis2 TERM=xterm SHELL=/bin/bash HISTSIZE=1000 SSH_CLIENT=192.168.59.145 10044 22 SSH_TTY=/dev/
1 950 4026531836 /sbin/agetty --noclear tty1 linux LANG= PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin TERM=linux LANGUAGE= LC_CTYPE= LC_NUMERIC= LC_TIME=
[root@redis2 opt]# ps -ef -o ppid,pid,pidns,args | grep 4026532426 | grep -v auto
[root@redis2 opt]#
复制代码
3.docker隔离之 pid namespace
从上面的两节可以知道,pid namespace提供了进程id隔离机制。使得某些进程能够在一个新的pid namespace中成为一个 “init 进程”。
下面总结一下docker对pid namespace的应用:
- 每启动一个容器,都会建立一个与之对应的pid namespace。init进程为docker容器的entrypoint。(联想docker启动容器的时候都要填写一个command或者–entrypoint command):
[root@hdp3 ~]# docker run --rm -it ef799f0cd30c --entrypoint sh
sh-4.2# ps
PID TTY TIME CMD
1 pts/0 00:00:00 sh
6 pts/0 00:00:00 ps
//在docker容器内连续执行两个sh命令然后打印进程依赖关系
sh-4.2# sh
sh-4.2# sh
sh-4.2# ps -ef -o ppid,pid,pidns,args
PPID PID PIDNS COMMAND
0 1 4026533347 sh HOSTNAME=f038b7fae0a3 TERM=xterm PATH=/usr/local/jdk1.8.0_112/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin PWD=/workspace JAVA_HOME=/usr/loca
1 36 4026533347 sh HOSTNAME=f038b7fae0a3 TERM=xterm PATH=/usr/local/jdk1.8.0_112/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin PWD=/workspace JAVA_HOME=/usr/loca
36 37 4026533347 \_ sh HOSTNAME=f038b7fae0a3 TERM=xterm PATH=/usr/local/jdk1.8.0_112/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin _=/usr/bin/sh PWD=/workspace J
37 39 4026533347 \_ ps -ef -o ppid,pid,pidns,args HOSTNAME=f038b7fae0a3 TERM=xterm PATH=/usr/local/jdk1.8.0_112/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
复制代码
- 进入容器命令 docker exec -it $container_id /bin/bash 实际上就是加入知道一个已经存在的pid namespace中。实际上我们可以不通过docker exec命令也能进入到docker容器中。
为了模拟docker exec命令,我们可以通过lsns -t pid 找到docker容器的pid namespace,然后通过nsenter命令
来进入到指定的docker容器中。
# 启动docker,打印其进程树
[root@redis2 opt]# docker run -it 8d0faed9a42f /bin/bash
[root@62e30728e537 workspace]# sh
sh-4.2# sh
sh-4.2# ps -ef -o ppid,pid,args
PPID PID COMMAND
0 1 /bin/bash PATH=/usr/local/jdk1.8.0_112/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=62e30728e537 TERM=xterm JAVA_HOME=/usr/local/
1 14 sh HOSTNAME=62e30728e537 TERM=xterm LC_ALL=zh_CN.utf8 LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi
14 15 \_ sh HOSTNAME=62e30728e537 TERM=xterm LC_ALL=zh_CN.utf8 LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;0
15 16 \_ ps -ef -o ppid,pid,args HOSTNAME=62e30728e537 TERM=xterm LC_ALL=zh_CN.utf8 LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;
sh-4.2#
#新开一个终端获取刚启动的docker的pid namespace
[root@redis2 ~]# lsns -t pid
NS TYPE NPROCS PID USER COMMAND
4026531836 pid 242 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize 21
4026532430 pid 3 239178 root /bin/bash
#通过nsenter命令进去到容器中,打印容器的进程树,观察进程 1,14,15三个,和上面结果是一致的。
[root@redis2 ~]# nsenter --mount --pid -t 239178
[root@redis2 /]# ps -ef -o ppid,pid,args
PPID PID COMMAND
0 17 -bash XDG_SESSION_ID=7100 HOSTNAME=redis2 TERM=xterm SHELL=/bin/bash HISTSIZE=1000 SSH_CLIENT=10.11.111.31 58018 22 SSH_TTY=/dev/pts/4 USER=root LS_COLORS=rs=
17 30 \_ ps -ef -o ppid,pid,args XDG_SESSION_ID=7100 HOSTNAME=redis2 SHELL=/bin/bash TERM=xterm HISTSIZE=1000 SSH_CLIENT=10.11.111.31 58018 22 SSH_TTY=/dev/pts/4 U
0 1 /bin/bash PATH=/usr/local/jdk1.8.0_112/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=62e30728e537 TERM=xterm JAVA_HOME=/usr/local/
1 14 sh HOSTNAME=62e30728e537 TERM=xterm LC_ALL=zh_CN.utf8 LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi
14 15 \_ sh HOSTNAME=62e30728e537 TERM=xterm LC_ALL=zh_CN.utf8 LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;0
复制代码
4.总结
本章内容介绍了linux的pid namespace特性。并且剖析了docker在启动一个容器的时候是如何使用pid namespace这种特性来实现进程id隔离的,并模拟了如何通过shell 命令来实现docker exec命令。