一、namespace 是什么?
Linux namespaces 是对全局系统资源的一种封装隔离,使得处于不同 namespace 的进程拥有独立的全局系统资源,改变一个 namespace 中的系统资源只会影响当前 namespace 里的进程,对其他 namespace 中的进程没有影响。
二、namespace 解决了什么问题?
Linux 内核出现 namespace 的一个主要目的就是实现轻量级虚拟化(容器)服务。在同一个 namespace 下的进程可以感知彼此的变化,而对外界的进程一无所知,从而达到隔离的目的。
其实换个说法,Linux 内核所提供的 namespace 技术为 docker 等容器技术的出现和发展提供了基础条件。没有 Linux 内核中的 namespace 的出现,可能 docker 容器化技术还不会那么快出现。
三、namespace 具体有哪些呢?
1. Mount Namespace
文件系统隔离。隔离了一组进程所看到的文件系统挂载点的集合,在不同的Mount Namespace 的进程所看到的文件是不同的。
2. UTS Namespace
隔离主机和域名信息。隔离了 uname() 系统调用返回的两个系统标识符 nodename 和 domainname. 在容器的上下文中,UTS namespace 允许每个容器拥有自己的hostname 和 UNIX domainname,这对于初始化和配置脚本是十分有用的,这些脚本根据这些名称来定制他们的操作。
3. IPC Namespace
隔离进程间通信。隔离了某些IPC资源(interprocess community,进程间通信)使划分到不同IPC Namespace 的进程组通信上隔离,无法通过消息队列、共享内存、信号量方式通信,这样,只有在同一个Namespace下的进程才能相互通信。如果你熟悉IPC的原理的话,你会知道,IPC需要有一个全局的ID,即然是全局的,那么就意味着我们的Namespace需要对这个ID隔离,不能让别的Namespace的进程看到。 要启动IPC隔离,我们只需要在调用clone时加上CLONE_NEWIPC参数就可以了。 int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWUTS | CLONE_NEWIPC | SIGCHLD, NULL);
4. PID Namespace
进程隔离。隔离了进程ID空间,不同的 PID Namespace 中的进程可以拥有相同的 PID。PID Namespace 的好处之一是,容器可以在主机之间迁移,同时容器内的进程保持相同的进程ID。PID Namespace 空间还允许每个容器拥有自己的 init (PID 1),它是“所有进程的祖先”,负责管理各种系统初始化任务,并在子进程终止时收割孤儿进程。
5. Network Namespace
网络资源隔离。每个 Network Namespace 都有自己的网络设备、IP地址、IP路由表、/proc/net目录、端口号等。
6. User Namespace
用户和用户组隔离。一个进程的用户和组ID在 User Namespace 空间外可以是不同的,一个进程可以在用户命名空间外拥有一个正常的无权限用户 ID,同时在命名空间内拥有一个(root 权限) 的用户ID。
上面说到了一句,要启动IPC隔离,我们只需要在调用clone时加上CLONE_NEWIPC参数就可以了。
这主要是因为 Linux 的 namespace 主要是利用下面三个系统调用:
clone
() – 实现线程的系统调用,用来创建一个新的进程,并可以通过设计上述参数达到隔离。unshare
() – 使某进程脱离某个namespacesetns
() – 把某进程加入到某个namespace
深处我也不会~
四、通过实践来证明
理论结合实践才能更好的理解这些到底在说什么,我用一个简单的例子来简单阐述一下吧。
docker run -it busybox /bin/sh
1、我们以交互式的方式进入容器,执行 ls
命令,可以看到常规的 Linux 系统目录结构,但这并非是宿主机的文件系统。
有了文件系统隔离,我们在当前容器内对文件所做的操作并不会影响到外部宿主机的文件,另外我们启动不同的容器,我们所看到的文件系统也是隔离的。
2、隔离主机和域名信息。这点其实很好验证,当执行 hostname 命令时,控制台会返回当前主机名称给我们。
前者是在容器内部执行,返回的是容器ID,后者是在宿主机的控制台执行,输出的是ubuntu。
3、当我们执行 ifconfig 命令,我们会看到和我们宿主机不同的网卡和网关信息等
除了上面的方式,我们也可以通过 docker inspect <容器名|容器ID>
来看容器的相关信息,其中也包含了容器网络相关信息。
4、进程隔离也是同样如此,我们在宿主机和容器中分别执行 ps -ef | grep sh
命令,看看结果就知道啦
在容器内明显看不到宿主机其他进程情况,并且容器内的 /bin/sh
PID 为1,PID =1 的进程是系统启动时的第一个进程,也称 init 进程。其他的进程,都是由它管理产生的。而此时,PID=1 却是 /bin/sh 进程。而它在宿主机上所展示时,它的PID变成了 2456。
这一点也可以换成下面这条命令来准确定位:
docker inspect -f '{{.State.Pid}}' <容器名称或ID> # 查看容器的PID
这个结论就非常好说了,在容器中,它确确实实是完成了 PID 的隔离,明明宿主机上是 2456 的进程,变成了容器内的 1 号进程,同时在容器中还看不到宿主机其他进程。
五、Docker 中的 User Namespace 详解
Docker 中引入的 user namespace 可以让容器中有一个 “假”的 root 用户,它在容器内是 root,在容器外是一个非 root 用户。也就是 user namespace 实现了 host user 和 container user 之间的映射。
我们拿一个简单的例子来说明:
docker run -it -v /bin:/host/bin busybox /bin/sh
先来解释下这条命令,现在是 dj(uid=1000,gid=1000)的用户,将本机上的 /bin
目录,挂载到容器中 /host/bin
目录下啦,并以交互式的方式启动了容器 busybox ,进入到容器内部。
那么现在有个问题:我在容器中的 /host/bin
目录下可以修改文件或者增加文件吗? 见下图。
答案是可以的。为什么呢?我一个非root用户,为什么可以操作root权限的文件呢?
原因:Docker容器运行的时候,如果没有专门指定user, 默认以root用户运行。它并不是说按照你现在的登录的用户去分配权限的,而是没有指定就默认使用root用户运行。
另外有没有觉得这是非常恐怖的一件事情,我明明没有 root 权限,却突然之间通过 docker 给容器挂载一个文件目录,虽然我一下没想起来可以做什么,但还是有点恐怖的哈。
因为在 Docker中默认并没有开启 user namespace,这并不是说 Linux 机器没有支持 user namespace ,而是 docker 中没有开启。
在说如何让 Docker 启用 user namespace 之前,我们先用另一种方式来曲线救国一下。
我之前使用 id 命令, 看到了我当前用户 dj
的(uid=1001),我们在执行 docker run
记得指定一下即可。
上面的命令改为:
docker run -it -u 1001:1001 -v /bin:/host/bin busybox /bin/sh
我们使用了 -u 1001:1001
来指定了容器内所使用的用户。我们再重复一下上面的操作。
六、如何让 Docker启用 User namespace 呢?
1、备份 docker 配置文件
因为我之前没有配置文件,所以我直接新建了一个文件
2、修改 docker 配置文件
添加 User Namespace 配置: 在配置文件中添加以下内容以启用 User Namespace。这将告诉 Docker 守护进程使用 User Namespace 功能。
{
"userns-remap": "default"
}
3、保存文件,重启启动 Docker 服务,以使配置文件生效
sudo systemctl restart docker
4、验证配置
cat /etc/subuid
cat /etc/subgid
dockermap 是默认的映射名称。
补充: /etc/subuid
是一个系统配置文件,用于管理用户命名空间中用户的子用户标识(sub-IDs)。不多占篇幅介绍了,/etc/subgid
相应就是用户组的标识。具体感兴趣可以去了解。
现在我们再执行上面之前测试的命令。
docker run -it -v /bin:/host/bin busybox /bin/sh
我们还是使用 dj
这个用户,但是它已经没有权限修改从宿主机 /bin
映射到容器的/host/bin
的目录了。
并且使用 id
命令,可以看到容器内部是 root 用户,但实际上它在容器外并不是root 用户。
另外还可以查找容器的PID(进程号),通过容器的进程号,来查看它的命名空间。
docker inspect -f '{{.State.Pid}}' <容器名称或ID> # 查看容器的PID
查看进程命名空间:
cat /proc/${容器PID}/uid_map
/proc/5517/uid_map
它表示了容器内外用户的映射关系即将host 上的 231072 用户映射为容器内的 0 即 root 用户。
这说明通过使用 user namespace 使得容器内的进程运行在非 root 用户上,我们成功地限制了容器内进程的权限。
七、Docker 为什么不默认启用 User namespace呢?
编写这一小章节的时候,我原打算去找资料啦,因为我已经猜测到一些原因,但需要验证一下,但是看到还没关闭的 GPT窗口,就打算拿它试一下。答案如下:
结论也很容易得出,Docker 希望降低复杂性,获取更强的兼容性,降低故障排查难度,也希望降低普通开发人员的使用门槛,更好的推广。
八、Namespace 存在的问题
我们都知道 Namespace 的隔离是轻量化的,比起虚拟化的隔离,它的缺陷也很明显,就是无法进行彻底的隔离。
因为不管如何隔离,它都是依赖于同一个内核的,那么此时内核就成了所有容器的共享变量,改动了一次,就会影响到全部。
九、检查 linux 操作系统是否启用了 namespace
运行下面的命令即可检查是否启用了:
root@ubuntu:/home# uname -a
Linux ubuntu 5.15.0-78-generic #85~20.04.1-Ubuntu SMP Mon Jul 17 09:42:39 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
root@ubuntu:/home# cat /boot/config-5.15.0-7
config-5.15.0-76-generic config-5.15.0-78-generic
root@ubuntu:/home# cat /boot/config-5.15.0-78-generic | grep CONFIG_USER_NS
CONFIG_USER_NS=y
root@ubuntu:/home#
如果是 「y」,则启用了,否则未启用。同样地,可以查看其它 namespace:
CONFIG_UTS_NS=y
CONFIG_IPC_NS=y
CONFIG_USER_NS=y
CONFIG_PID_NS=y
CONFIG_NET_NS=y