ui设计做兼职的网站有哪些,是短视频迅猛发展的非常重要的因素,网络营销推广公司找哪家,深圳小程序外包公司来源 | 多选参数责编 | 寇雪芹头图 | 下载于视觉中国NamespaceLinux Namespace 是 Linux 提供的一种内核级别环境隔离的方法。这种隔离机制和 chroot 很类似#xff0c;chroot 是把某个目录修改为根目录#xff0c;从而无法访问外部的内容。Linux Namesapce 在此基础之上chroot 是把某个目录修改为根目录从而无法访问外部的内容。Linux Namesapce 在此基础之上提供了对 UTS、IPC、Mount、PID、Network、User 等的隔离机制如下所示。分类系统调用参数相关内核版本Mount NamespacesCLONE_NEWNSLinux 2.4.19UTS NamespacesCLONE_NEWUTSLinux 2.6.19IPC NamespacesCLONE_NEWIPCLinux 2.6.19PID NamespacesCLONE_NEWPIDLinux 2.6.19Network NamespacesCLONE_NEWNET始于Linux 2.6.24 完成于 Linux 2.6.29User NamespacesCLONE_NEWUSER始于 Linux 2.6.23 完成于 Linux 3.8)Linux Namespace 官方文档Namespaces in operationnamespace 有三个系统调用可以使用clone() --- 实现线程的系统调用用来创建一个新的进程并可以通过设计上述参数达到隔离。unshare() --- 使某个进程脱离某个 namespacesetns(int fd, int nstype) --- 把某进程加入到某个 namespace下面使用这几个系统调用来演示 Namespace 的效果更加详细地可以看 DOCKER基础技术LINUX NAMESPACE上、 DOCKER基础技术LINUX NAMESPACE下。UTS NamespaceUTS Namespace 主要是用来隔离主机名的也就是每个容器都有自己的主机名。我们使用如下的代码来进行演示。注意假如在容器内部没有设置主机名的话会使用主机的主机名的假如在容器内部设置了主机名但是没有使用 CLONE_NEWUTS 的话那么改变的其实是主机的主机名。#define _GNU_SOURCE
#include sys/types.h
#include sys/wait.h
#include sys/mount.h
#include stdio.h
#include sched.h
#include signal.h
#include unistd.h#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];char* const container_args[] {/bin/bash,NULL
};int container_main(void* arg) {printf(Container [%5d] - inside the container!\n, getpid());sethostname(container_dawn, 15);execv(container_args[0], container_args);printf(Somethings wrong!\n);return 1;
}int main() {printf(Parent [%5d] - start a container!\n, getpid());int container_id clone(container_main, container_stack STACK_SIZE, CLONE_NEWUTS | SIGCHLD, NULL);waitpid(container_id, NULL, 0);printf(Parent - container stopped!\n);return 0;
}PID Namespace每个容器都有自己的进程环境中也就是相当于容器内进程的 PID 从 1 开始命名此时主机上的 PID 其实也还是从 1 开始命名的就相当于有两个进程环境一个主机上的从 1 开始另一个容器里的从 1 开始。为啥 PID 从 1 开始就相当于进程环境的隔离了呢因此在传统的 UNIX 系统中PID 为 1 的进程是 init地位特殊。它作为所有进程的父进程有很多特权。另外其还会检查所有进程的状态我们知道如果某个进程脱离了父进程父进程没有 wait 它那么 init 就会负责回收资源并结束这个子进程。所以要想做到进程的隔离首先需要创建出 PID 为 1 的进程。但是【kubernetes 里面的话】int container_main(void* arg) {printf(Container [%5d] - inside the container!\n, getpid());sethostname(container_dawn, 15);execv(container_args[0], container_args);printf(Somethings wrong!\n);return 1;
}int main() {printf(Parent [%5d] - start a container!\n, getpid());int container_id clone(container_main, container_stack STACK_SIZE, CLONE_NEWUTS | CLONE_NEWPID | SIGCHLD, NULL);waitpid(container_id, NULL, 0);printf(Parent - container stopped!\n);return 0;
}如果此时你在子进程的 shell 中输入 ps、top 等命令我们还是可以看到所有进程。这是因为ps、top 这些命令是去读 /proc 文件系统由于此时文件系统并没有隔离所以父进程和子进程通过命令看到的情况都是一样的。IPC Namespace常见的 IPC 有共享内存、信号量、消息队列等。当使用 IPC Namespace 把 IPC 隔离起来之后只有同一个 Namespace 下的进程才能相互通信因为主机的 IPC 和其他 Namespace 中的 IPC 都是看不到了的。而这个的隔离主要是因为创建出来的 IPC 都会有一个唯一的 ID那么主要对这个 ID 进行隔离就好了。想要启动 IPC 隔离只需要在调用 clone 的时候加上 CLONE_NEWIPC 参数就可以了。int container_main(void* arg) {printf(Container [%5d] - inside the container!\n, getpid());sethostname(container_dawn, 15);execv(container_args[0], container_args);printf(Somethings wrong!\n);return 1;
}int main() {printf(Parent [%5d] - start a container!\n, getpid());int container_id clone(container_main, container_stack STACK_SIZE, CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWIPC | SIGCHLD, NULL);waitpid(container_id, NULL, 0);printf(Parent - container stopped!\n);return 0;
}Mount NamespaceMount Namespace 可以让容器有自己的 root 文件系统。需要注意的是在通过 CLONE_NEWNS 创建 mount namespace 之后父进程会把自己的文件结构复制给子进程中。所以当子进程中不重新 mount 的话子进程和父进程的文件系统视图是一样的假如想要改变容器进程的视图一定需要重新 mount这个是 mount namespace 和其他 namespace 不同的地方。另外子进程中新的 namespace 中的所有 mount 操作都只影响自身的文件系统注意这边是 mount 操作而创建文件等操作都是会有所影响的而不对外界产生任何影响这样可以做到比较严格地隔离当然这边是除 share mount 之外的。下面我们重新挂载子进程的 /proc 目录从而可以使用 ps 来查看容器内部的情况。int container_main(void* arg) {printf(Container [%5d] - inside the container!\n, getpid());sethostname(container_dawn, 15);if (mount(proc, /proc, proc, 0, NULL) !0 ) {perror(proc);}execv(container_args[0], container_args);printf(Somethings wrong!\n);return 1;
}int main() {printf(Parent [%5d] - start a container!\n, getpid());int container_id clone(container_main, container_stack STACK_SIZE, CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);waitpid(container_id, NULL, 0);printf(Parent - container stopped!\n);return 0;
}这里会有个问题就是在退出子进程之后当再次使用 ps -elf 的时候会报错如下所示这是因为 /proc 是 share mount对它的操作会影响所有的 mount namespace可以看这里http://unix.stackexchange.com/questions/281844/why-does-child-with-mount-namespace-affect-parent-mounts上面仅仅重新 mount 了 /proc 这个目录其他的目录还是跟父进程一样视图的。一般来说容器创建之后容器进程需要看到的是一个独立的隔离环境而不是继承宿主机的文件系统。接下来演示一个山寨镜像来模仿 Docker 的 Mount Namespace。也就是给子进程实现一个较为完整的独立的 root 文件系统让这个进程只能访问自己构成的文件系统中的内容想想我们平常使用 Docker 容器的样子。首先我们使用 docker export 将 busybox 镜像导出成一个 rootfs 目录这个 rootfs 目录的情况如图所示已经包含了 /proc、/sys 等特殊的目录。之后我们在代码中将一些特殊目录重新挂载并使用 chroot() 系统调用将进程的根目录改成上文的 rootfs 目录。char* const container_args[] {/bin/sh,NULL
};int container_main(void* arg) {printf(Container [%5d] - inside the container!\n, getpid());sethostname(container_dawn, 15);if (mount(proc, rootfs/proc, proc, 0, NULL) ! 0) {perror(proc);}if (mount(sysfs, rootfs/sys, sysfs, 0, NULL)!0) {perror(sys);}if (mount(none, rootfs/tmp, tmpfs, 0, NULL)!0) {perror(tmp);}if (mount(udev, rootfs/dev, devtmpfs, 0, NULL)!0) {perror(dev);}if (mount(devpts, rootfs/dev/pts, devpts, 0, NULL)!0) {perror(dev/pts);}if (mount(shm, rootfs/dev/shm, tmpfs, 0, NULL)!0) {perror(dev/shm);}if (mount(tmpfs, rootfs/run, tmpfs, 0, NULL)!0) {perror(run);}if ( chdir(./rootfs) || chroot(./) ! 0 ){perror(chdir/chroot);}// 改变根目录之后那么 /bin/bash 是从改变之后的根目录中搜索了execv(container_args[0], container_args);perror(exec);printf(Somethings wrong!\n);return 1;
}int main() {printf(Parent [%5d] - start a container!\n, getpid());int container_id clone(container_main, container_stack STACK_SIZE, CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);waitpid(container_id, NULL, 0);printf(Parent - container stopped!\n);return 0;
}
最后查看实现效果如下图所示。实际上Mount Namespace 是基于 chroot 的不断改良才被发明出来的chroot 可以算是 Linux 中第一个 Namespace。那么上面被挂载在容器根目录上、用来为容器镜像提供隔离后执行环境的文件系统就是所谓的容器镜像也被叫做 rootfs根文件系统。需要明确的是rootfs 只是一个操作系统所包含的文件、配置和目录并不包括操作系统内核。User Namespace容器内部看到的 UID 和 GID 和外部是不同的了比如容器内部针对 dawn 这个用户显示的是 0但是实际上这个用户在主机上应该是 1000。要实现这样的效果需要把容器内部的 UID 和主机的 UID 进行映射需要修改的文件是 /proc/pid/uid_map 和 /proc/pid/gid_map这两个文件的格式是ID-INSIDE-NS ID-OUTSIDE-NS LENGTHID-INSIDE-NS 表示在容器内部显示的 UID 或 GIDID-OUTSIDE-NS表示容器外映射的真实的 UID 和 GIDLENGTH表示映射的范围一般为 1表示一一对应比如下面就是将真实的 uid1000 的映射为容器内的 uid 0$ cat /proc/8353/uid_map
0 1000 1再比如下面则表示把 namesapce 内部从 0 开始的 uid 映射到外部从 0 开始的 uid其最大范围是无符号 32 位整型下面这条命令是在主机环境中输入的。$ cat /proc/$$/uid_map
0 0 4294967295默认情况设置了 CLONE_NEWUSER 参数但是没有修改上述两个文件的话容器中默认情况下显示为 65534这是因为容器找不到真正的 UID所以就设置了最大的 UID。如下面的代码所示#define _GNU_SOURCE
#include stdio.h
#include stdlib.h
#include sys/types.h
#include sys/wait.h
#include sys/mount.h
#include sys/capability.h
#include stdio.h
#include sched.h
#include signal.h
#include unistd.h#define STACK_SIZE (1024 * 1024)static char container_stack[STACK_SIZE];
char* const container_args[] {/bin/bash,NULL
};int container_main(void* arg) {printf(Container [%5d] - inside the container!\n, getpid());printf(Container: eUID %ld; eGID %ld, UID%ld, GID%ld\n,(long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());printf(Container [%5d] - setup hostname!\n, getpid());//set hostnamesethostname(container,10);execv(container_args[0], container_args);printf(Somethings wrong!\n);return 1;
}int main() {const int gidgetgid(), uidgetuid();printf(Parent: eUID %ld; eGID %ld, UID%ld, GID%ld\n,(long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());printf(Parent [%5d] - start a container!\n, getpid());int container_pid clone(container_main, container_stackSTACK_SIZE, CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWUSER | SIGCHLD, NULL);printf(Parent [%5d] - Container [%5d]!\n, getpid(), container_pid);printf(Parent [%5d] - user/group mapping done!\n, getpid());waitpid(container_pid, NULL, 0);printf(Parent - container stopped!\n);return 0;
}当我以 dawn 这个用户执行的该程序的时候那么会显示如下图所示的效果。使用 root 用户的时候是同样的接下去我们要开始来实现映射的效果了也就是让 dawn 这个用户在容器中显示为 0。代码是几乎完全拿耗子叔的博客上的链接可见文末int pipefd[2];void set_map(char* file, int inside_id, int outside_id, int len) {FILE* mapfd fopen(file, w);if (NULL mapfd) {perror(open file error);return;}fprintf(mapfd, %d %d %d, inside_id, outside_id, len);fclose(mapfd);
}void set_uid_map(pid_t pid, int inside_id, int outside_id, int len) {char file[256];sprintf(file, /proc/%d/uid_map, pid);set_map(file, inside_id, outside_id, len);
}int container_main(void* arg) {printf(Container [%5d] - inside the container!\n, getpid());printf(Container: eUID %ld; eGID %ld, UID%ld, GID%ld\n,(long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());/* 等待父进程通知后再往下执行进程间的同步 */char ch;close(pipefd[1]);read(pipefd[0], ch, 1);printf(Container [%5d] - setup hostname!\n, getpid());//set hostnamesethostname(container,10);//remount /proc to make sure the top and ps show containers informationmount(proc, /proc, proc, 0, NULL);execv(container_args[0], container_args);printf(Somethings wrong!\n);return 1;
}int main() {const int gidgetgid(), uidgetuid();printf(Parent: eUID %ld; eGID %ld, UID%ld, GID%ld\n,(long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());pipe(pipefd);printf(Parent [%5d] - start a container!\n, getpid());int container_pid clone(container_main, container_stackSTACK_SIZE, CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL);printf(Parent [%5d] - Container [%5d]!\n, getpid(), container_pid);//To map the uid/gid, // we need edit the /proc/PID/uid_map (or /proc/PID/gid_map) in parentset_uid_map(container_pid, 0, uid, 1);printf(Parent [%5d] - user/group mapping done!\n, getpid());/* 通知子进程 */close(pipefd[1]);waitpid(container_pid, NULL, 0);printf(Parent - container stopped!\n);return 0;
}实现的最终效果如图所示可以看到在容器内部将 dawn 这个用户 UID 显示为了 0root但其实这个容器中的 /bin/bash 进程还是以一个普通用户也就是 dawn 来运行的只是显示出来的 UID 是 0所以当查看 /root 目录的时候还是没有权限。User Namespace 是以普通用户运行的但是别的 Namespace 需要 root 权限那么当使用多个 Namespace 该怎么办呢我们可以先用一般用户创建 User Namespace然后把这个一般用户映射成 root那么在容器内用 root 来创建其他的 Namespace。Network Namespace隔离容器中的网络每个容器都有自己的虚拟网络接口和 IP 地址。在 Linux 中可以使用 ip 命令创建 Network NamespaceDocker 的源码中它没有使用 ip 命令而是自己实现了 ip 命令内的一些功能。下面就使用 ip 命令来讲解一下 Network Namespace 的构建以 bridge 网络为例。bridge 网络的拓扑图一般如下图所示其中 br0 是 Linux 网桥。在使用 Docker 的时候如果启动一个 Docker 容器并使用 ip link show 查看当前宿主机上的网络情况那么你会看到有一个 docker0 还有一个 veth**** 的虚拟网卡这个 veth 的虚拟网卡就是上图中 veth而 docker0 就相当于上图中的 br0。那么我们可以使用下面这些命令即可创建跟 docker 类似的效果参考自耗子叔的博客链接见文末参考结合上图加了一些文字。## 1. 首先我们先增加一个网桥 lxcbr0模仿 docker0
brctl addbr lxcbr0
brctl stp lxcbr0 off
ifconfig lxcbr0 192.168.10.1/24 up #为网桥设置IP地址## 2. 接下来我们要创建一个 network namespace 命名为 ns1# 增加一个 namesapce 命令为 ns1 使用 ip netns add 命令
ip netns add ns1 # 激活 namespace 中的 loopback即127.0.0.1使用 ip netns exec ns1 相当于进入了 ns1 这个 namespace那么 ip link set dev lo up 相当于在 ns1 中执行的
ip netns exec ns1 ip link set dev lo up ## 3. 然后我们需要增加一对虚拟网卡# 增加一对虚拟网卡注意其中的 veth 类型。这里有两个虚拟网卡veth-ns1 和 lxcbr0.1veth-ns1 网卡是要被安到容器中的而 lxcbr0.1 则是要被安到网桥 lxcbr0 中的也就是上图中的 veth。
ip link add veth-ns1 type veth peer name lxcbr0.1# 把 veth-ns1 按到 namespace ns1 中这样容器中就会有一个新的网卡了
ip link set veth-ns1 netns ns1# 把容器里的 veth-ns1 改名为 eth0 容器外会冲突容器内就不会了
ip netns exec ns1 ip link set dev veth-ns1 name eth0 # 为容器中的网卡分配一个 IP 地址并激活它
ip netns exec ns1 ifconfig eth0 192.168.10.11/24 up# 上面我们把 veth-ns1 这个网卡按到了容器中然后我们要把 lxcbr0.1 添加上网桥上
brctl addif lxcbr0 lxcbr0.1# 为容器增加一个路由规则让容器可以访问外面的网络
ip netns exec ns1 ip route add default via 192.168.10.1## 4. 为这个 namespace 设置 resolv.conf这样容器内就可以访问域名了
echo nameserver 8.8.8.8 conf/resolv.conf上面基本上就相当于 docker 网络的原理只不过Docker 不使用 ip 命令而是自己实现了 ip 命令内的一些功能。Docker 的 resolv.conf 没有使用这样的方式而是将其写到指定的 resolv.conf 中之后在启动容器的时候将其和 hostname、host 一起以只读的方式加载到容器的文件系统中。docker 使用进程的 PID 来做 network namespace 的名称。同理我们还可以使用如下的方式为正在运行的 docker 容器增加一个新的网卡ip link add peerA type veth peer name peerB
brctl addif docker0 peerA
ip link set peerA up
ip link set peerB netns ${container-pid}
ip netns exec ${container-pid} ip link set dev peerB name eth1
ip netns exec ${container-pid} ip link set eth1 up
ip netns exec ${container-pid} ip addr add ${ROUTEABLE_IP} dev eth1Namespace 情况查看Cgroup 的操作接口是文件系统位于 /sys/fs/cgroup 中。假如想查看 namespace 的情况同样可以查看文件系统namespace 主要查看 /proc/pid/ns 目录。我们以上面的 [PID Namespace 程序](#PID Namespace) 为例当这个程序运行起来之后我们可以看到其 PID 为 11702。之后我们保持这个子进程运行然后打开另一个 shell查看这个程序创建的子进程的 PID也就是容器中运行的进程在主机中的 PID。最后我们分别查看 /proc/11702/ns 和 /proc/11703/ns 这两个目录的情况也就是查看这两个进程的 namespace 情况。可以看到其中 cgroup、ipc、mnt、net、user 都是同一个 ID而 pid、uts 是不同的 ID。如果两个进程的 namespace 编号相同那么表示这两个进程位于同一个 namespace 中否则位于不同 namespace 中。如果可以查看 ns 的情况之外这些文件一旦被打开只要 fd 被占用着即使 namespace 中所有进程都已经结束了那么创建的 namespace 也会一直存在。比如可以使用 mount --bind /proc/11703/ns/uts ~/uts让 11703 这个进程的 UTS Namespace 一直存在。总结Namespace 技术实际上修改了应用进程看待整个计算机“视图”即它的”视图“已经被操作系统做了限制只能”看到“某些指定的内容这仅仅对应用进程产生了影响。但是对宿主机来说这些被隔离了的进程其实还是进程跟宿主机上其他进程并无太大区别都由宿主机统一管理。只不过这些被隔离的进程拥有额外设置过的 Namespace 参数。那么 Docker 项目在这里扮演的更多是旁路式的辅助和管理工作。如下左图所示因此相比虚拟机的方式容器会更受欢迎。这是假如使用虚拟机的方式作为应用沙盒那么必须要由 Hypervisor 来负责创建虚拟机这个虚拟机是真实存在的并且里面必须要运行一个完整的 Guest OS 才能执行用户的应用进程。这样就导致了采用虚拟机的方式之后不可避免地带来额外的资源消耗和占用。根据实验一个运行着 CentOS 的 KVM 虚拟机启动后在不做优化的情况下虚拟机就需要占用 100-200 MB 内存。此外用户应用运行在虚拟机中它对宿主机操作系统的调用就不可避免地要经过虚拟机软件的拦截和处理这本身就是一层消耗尤其对资源、网络和磁盘 IO 的损耗非常大。而假如使用容器的方式容器化之后应用本质还是宿主机上的一个进程这也就意味着因为虚拟机化带来的性能损耗是不存在的而另一方面使用 Namespace 作为隔离手段的容器并不需要单独的 Guest OS这就使得容器额外的资源占用几乎可以忽略不计。总得来说“敏捷”和“高性能”是容器相对于虚拟机最大的优势也就是容器能在 PaaS 这种更加细粒度的资源管理平台上大行其道的重要原因。但是基于 Linux Namespace 的隔离机制相比于虚拟化技术也有很多不足之处其中最主要的问题就是隔离不彻底。首先容器只是运行在宿主机上的一种特殊进程那么容器之间使用的还是同一个宿主机上的操作系统。尽管可以在容器里面通过 mount namesapce 单独挂载其他不同版本的操作系统文件比如 centos、ubuntu但是这并不能改变共享宿主机内核的事实。这就意味着你要在 windows 上运行 Linux 容器或者在低版本的 Linux 宿主机上运行高版本的 Linux 容器都是行不通的。而拥有虚拟机技术和独立 Guest OS 的虚拟机就要方便多了。其次在 Linux 内核中有很多资源和对象都是不能被 namespace 化的比如时间。假如你的容器中的程序使用 settimeofday(2) 系统调用修改了时间整个宿主机的时间都会被随之修改。相比虚拟机里面可以随意折腾的自由度在容器里部署应用的时候“什么能做什么不能做” 是用户必须考虑的一个问题。之外容器给应用暴露出来的攻击面是相当大的应用“越狱”的难度也比虚拟机低很多。虽然实践中可以使用 Seccomp 等技术对容器内部发起的所有系统调用进行过滤和甄别来进行安全加固但这种方式因为多了一层对系统调用的过滤也会对容器的性能产生影响。因此在生产环境中没有人敢把运行在物理机上的 Linux 容器直接暴露到公网上。另外容器是一个“单进程”模型。容器的本质是一个进程用户的应用进程实际上就是容器里 PID1 的进程而这个进程也是后续创建的所有进程的父进程。这也就意味着在一个容器中你没办法同时运行两个不同的应用除非能事先找到一个公共的 PID1 的程序来充当两者的父进程比如使用 systemd 或者 supervisord。容器的设计更多是希望容器和应用同生命周期的而不是容器还在运行而里面的应用早已经挂了。上面这段话个人的理解是因为创建出子进程之后子进程需要运行的而此时父进程需要等待子进程运行结束相当于只有子进程在运行。比如容器中的第一个进程往往就是业务需要的进程也就是 entrypoint 指定的程序运行起来的进程。而创建出子进程会导致这个进程被暂停。即使将子进程改为后台执行但是由于容器中 PID1 的进程压根没有管理后台进程的能力所以还是会有进程无法管理。巨人的肩膀极客时间---《深入剖析 Kubernetes》---张磊老师DOCKER基础技术LINUX NAMESPACE上DOCKER基础技术LINUX NAMESPACE下更多阅读推荐都在说云原生它的技术图谱你真的了解吗SRE 是如何保障稳定性的如何写出让 CPU 跑得更快的代码Serverless 在 SaaS 领域的最佳实践云原生人物志|Pulsar翟佳社区的信任最重要阿里的 RocketMQ 如何让双十一峰值之下0故障