Linux Namespace 是 Linux 提供的一种内核级别环境隔离的方法。用官方的话来说,Linux Namespace 将全局系统资源封装在一个抽象中,从而使 namespace 内的进程认为自己具有独立的资源实例。这项技术本来没有掀起多大的波澜,是容器技术的崛起让他重新引起了大家的注意。

Linux Namespace 有如下 6 个种类:

分类 系统调用参数 相关内核版本
Mount namespaces CLONE_NEWNS Linux 2.4.19
UTS namespaces CLONE_NEWUTS Linux 2.6.19
IPC namespaces CLONE_NEWIPC Linux 2.6.19
PID namespaces CLONE_NEWPID Linux 2.6.24
Network namespaces CLONE_NEWNET 始于Linux 2.6.24 完成于 Linux 2.6.29
User namespaces CLONE_NEWUSER 始于 Linux 2.6.23 完成于 Linux 3.8

namespace 的 API 由三个系统调用和一系列 /proc 文件组成,本文将会详细介绍这些系统调用和 /proc 文件。为了指定要操作的 namespace 类型,需要在系统调用的 flag 中通过常量 CLONE_NEW* 指定(包括 CLONE_NEWIPCCLONE_NEWNSCLONE_NEWNETCLONE_NEWPIDCLONE_NEWUSER 和 `CLONE_NEWUTS),可以指定多个常量,通过 |(位或)操作来实现。

简单描述一下三个系统调用的功能:

  • clone() : 实现线程的系统调用,用来创建一个新的进程,并可以通过设计上述系统调用参数达到隔离的目的。
  • unshare() : 使某进程脱离某个 namespace。
  • setns() : 把某进程加入到某个 namespace。

具体的实现原理请往下看。

1. clone()


clone() 的原型如下:

int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);
  • child_func : 传入子进程运行的程序主函数。
  • child_stack : 传入子进程使用的栈空间。
  • flags : 表示使用哪些 CLONE_* 标志位。
  • args : 用于传入用户参数。

clone()fork() 类似,都相当于把当前进程复制了一份,但 clone() 可以更细粒度地控制与子进程共享的资源(其实就是通过 flags 来控制),包括虚拟内存、打开的文件描述符和信号量等等。一旦指定了标志位 CLONE_NEW*,相对应类型的 namespace 就会被创建,新创建的进程也会成为该 namespace 中的一员。

clone() 的原型并不是最底层的系统调用,而是封装过的,真正的系统调用内核实现函数为 do_fork(),形式如下:

long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)

其中 clone_flags 可以赋值为上面提到的标志。

下面来看一个例子:

/* demo_uts_namespaces.c

   Copyright 2013, Michael Kerrisk
Licensed under GNU General Public License v2 or later Demonstrate the operation of UTS namespaces.
*/
#define _GNU_SOURCE
#include <sys/wait.h>
#include <sys/utsname.h>
#include <sched.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> /* A simple error-handling function: print an error message based
on the value in 'errno' and terminate the calling process */ #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
} while (0) static int /* Start function for cloned child */
childFunc(void *arg)
{
struct utsname uts; /* 在新的 UTS namespace 中修改主机名 */ if (sethostname(arg, strlen(arg)) == -1)
errExit("sethostname"); /* 获取并显示主机名 */ if (uname(&uts) == -1)
errExit("uname");
printf("uts.nodename in child: %s\n", uts.nodename); /* Keep the namespace open for a while, by sleeping.
This allows some experimentation--for example, another
process might join the namespace. */ sleep(100); return 0; /* Terminates child */
} /* 定义一个给 clone 用的栈,栈大小1M */
#define STACK_SIZE (1024 * 1024) static char child_stack[STACK_SIZE]; int
main(int argc, char *argv[])
{
pid_t child_pid;
struct utsname uts; if (argc < 2) {
fprintf(stderr, "Usage: %s <child-hostname>\n", argv[0]);
exit(EXIT_FAILURE);
} /* 调用 clone 函数创建一个新的 UTS namespace,其中传出一个函数,还有一个栈空间(为什么传尾指针,因为栈是反着的);
新的进程将在用户定义的函数 childFunc() 中执行 */ child_pid = clone(childFunc,
child_stack + STACK_SIZE, /* 因为栈是反着的,
所以传尾指针 */
CLONE_NEWUTS | SIGCHLD, argv[1]);
if (child_pid == -1)
errExit("clone");
printf("PID of child created by clone() is %ld\n", (long) child_pid); /* Parent falls through to here */ sleep(1); /* 给子进程预留一定的时间来改变主机名 */ /* 显示当前 UTS namespace 中的主机名,和
子进程所在的 UTS namespace 中的主机名不同 */ if (uname(&uts) == -1)
errExit("uname");
printf("uts.nodename in parent: %s\n", uts.nodename); if (waitpid(child_pid, NULL, 0) == -1) /* 等待子进程结束 */
errExit("waitpid");
printf("child has terminated\n"); exit(EXIT_SUCCESS);
}

该程序通过标志位 CLONE_NEWUTS 调用 clone() 函数创建一个 UTS namespace。UTS namespace 隔离了两个系统标识符 — 主机名NIS 域名 —它们分别通过 sethostname()setdomainname() 这两个系统调用来设置,并通过系统调用 uname() 来获取。

下面将对程序中的一些关键部分进行解读(为了简单起见,我们将省略其中的错误检查)。

程序运行时后面需要跟上一个命令行参数,它将会创建一个在新的 UTS namespace 中执行的子进程,该子进程会在新的 UTS namespace 中将主机名改为命令行参数中提供的值。

主程序的第一个关键部分是通过系统调用 clone() 来创建子进程:

child_pid = clone(childFunc,
child_stack + STACK_SIZE, /* Points to start of
downwardly growing stack */
CLONE_NEWUTS | SIGCHLD, argv[1]); printf("PID of child created by clone() is %ld\n", (long) child_pid);

子进程将会在用户定义的函数 childFunc() 中开始执行,该函数将会接收 clone() 最后的参数(argv[1])作为自己的参数,并且标志位包含了 CLONE_NEWUTS,所以子进程会在新创建的 UTS namespace 中执行。

接下来主进程睡眠一段时间,让子进程能够有时间更改其 UTS namespace 中的主机名。然后调用 uname() 来检索当前 UTS namespace 中的主机名,并显示该主机名:

sleep(1);           /* Give child time to change its hostname */

uname(&uts);
printf("uts.nodename in parent: %s\n", uts.nodename);

与此同时,由 clone() 创建的子进程执行的函数 childFunc() 首先将主机名改为命令行参数中提供的值,然后检索并显示修改后的主机名:

sethostname(arg, strlen(arg);

uname(&uts);
printf("uts.nodename in child: %s\n", uts.nodename);

子进程退出之前也睡眠了一段时间,这样可以防止新的 UTS namespace 不会被关闭,让我们能够有机会进行后续的实验。

执行程序,观察父进程和子进程是否处于不同的 UTS namespace 中:

$ su                   # 需要特权才能创建 UTS namespace
Password:
# uname -n
antero
# ./demo_uts_namespaces bizarro
PID of child created by clone() is 27514
uts.nodename in child: bizarro
uts.nodename in parent: antero

除了 User namespace 之外,创建其他的 namespace 都需要特权,更确切地说,是需要相应的 Linux Capabilities,即 CAP_SYS_ADMIN。这样就可以避免设置了 SUID(Set User ID on execution)的程序因为主机名不同而做出一些愚蠢的行为。如果对 Linux Capabilities 不是很熟悉,可以参考我之前的文章:Linux Capabilities 入门教程:概念篇

2. proc 文件


每个进程都有一个 /proc/PID/ns 目录,其下面的文件依次表示每个 namespace, 例如 user 就表示 user namespace。从 3.8 版本的内核开始,该目录下的每个文件都是一个特殊的符号链接,链接指向 $namespace:[$namespace-inode-number],前半部份为 namespace 的名称,后半部份的数字表示这个 namespace 的句柄号。句柄号用来对进程所关联的 namespace 执行某些操作。

$ ls -l /proc/$$/ns         # $$ 表示当前所在的 shell 的 PID
total 0
lrwxrwxrwx. 1 mtk mtk 0 Jan 8 04:12 ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 mtk mtk 0 Jan 8 04:12 mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 mtk mtk 0 Jan 8 04:12 net -> net:[4026531956]
lrwxrwxrwx. 1 mtk mtk 0 Jan 8 04:12 pid -> pid:[4026531836]
lrwxrwxrwx. 1 mtk mtk 0 Jan 8 04:12 user -> user:[4026531837]
lrwxrwxrwx. 1 mtk mtk 0 Jan 8 04:12 uts -> uts:[4026531838]

这些符号链接的用途之一是用来确认两个不同的进程是否处于同一 namespace 中。如果两个进程指向的 namespace inode number 相同,就说明他们在同一个 namespace 下,否则就在不同的 namespace 下。这些符号链接指向的文件比较特殊,不能直接访问,事实上指向的文件存放在被称为 nsfs 的文件系统中,该文件系统用户不可见,可以使用系统调用 stat() 在返回的结构体的 st_ino 字段中获取 inode number。在 shell 终端中可以用命令(实际上就是调用了 stat())看到指向文件的 inode 信息:

$ stat -L /proc/$$/ns/net
File: /proc/3232/ns/net
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: 4h/4d Inode: 4026531956 Links: 1
Access: (0444/-r--r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2020-01-17 15:45:23.783304900 +0800
Modify: 2020-01-17 15:45:23.783304900 +0800
Change: 2020-01-17 15:45:23.783304900 +0800
Birth: -

除了上述用途之外,这些符号链接还有其他的用途,如果我们打开了其中一个文件,那么只要与该文件相关联的文件描述符处于打开状态,即使该 namespace 中的所有进程都终止了,该 namespace 依然不会被删除。通过 bind mount 将符号链接挂载到系统的其他位置,也可以获得相同的效果:

$ touch ~/uts
$ mount --bind /proc/27514/ns/uts ~/uts

3. setns()


加入一个已经存在的 namespace 可以通过系统调用 setns() 来完成。它的原型如下:

int setns(int fd, int nstype);

更确切的说法是:setns() 将调用的进程与特定类型 namespace 的一个实例分离,并将该进程与该类型 namespace 的另一个实例重新关联。

  • fd 表示要加入的 namespace 的文件描述符,可以通过打开其中一个符号链接来获取,也可以通过打开 bind mount 到其中一个链接的文件来获取。
  • nstype 让调用者可以去检查 fd 指向的 namespace 类型,值可以设置为前文提到的常量 CLONE_NEW*,填 0 表示不检查。如果调用者已经明确知道自己要加入了 namespace 类型,或者不关心 namespace 类型,就可以使用该参数来自动校验。

结合 setns()execve() 可以实现一个简单但非常有用的功能:将某个进程加入某个特定的 namespace,然后在该 namespace 中执行命令。直接来看例子:

/* ns_exec.c 

   Copyright 2013, Michael Kerrisk
Licensed under GNU General Public License v2 or later Join a namespace and execute a command in the namespace
*/
#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h> /* A simple error-handling function: print an error message based
on the value in 'errno' and terminate the calling process */ #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
} while (0) int
main(int argc, char *argv[])
{
int fd; if (argc < 3) {
fprintf(stderr, "%s /proc/PID/ns/FILE cmd [arg...]\n", argv[0]);
exit(EXIT_FAILURE);
} fd = open(argv[1], O_RDONLY); /* 获取想要加入的 namespace 的文件描述符 */
if (fd == -1)
errExit("open"); if (setns(fd, 0) == -1) /* 加入该 namespace */
errExit("setns"); execvp(argv[2], &argv[2]); /* 在加入的 namespace 中执行相应的命令 */
errExit("execvp");
}

该程序运行需要两个或两个以上的命令行参数,第一个参数表示特定的 namespace 符号链接的路径(或者 bind mount 到这些符号链接的文件路径);第二个参数表示要在该符号链接相对应的 namespace 中执行的程序名称,以及执行这个程序所需的命令行参数。关键步骤如下:

fd = open(argv[1], O_RDONLY);   /* 获取想要加入的 namespace 的文件描述符 */

setns(fd, 0);                   /* 加入该 namespace */

execvp(argv[2], &argv[2]);      /* 在加入的 namespace 中执行相应的命令 */

还记得我们之前已经通过 bind mount 将 demo_uts_namespaces 创建的 UTS namespace 挂载到 ~/uts 中了吗?可以将本例中的程序与之结合,让新进程可以在该 UTS namespace 中执行 shell:

    $ ./ns_exec ~/uts /bin/bash     # ~/uts 被 bind mount 到了 /proc/27514/ns/uts
My PID is: 28788

验证新的 shell 是否与 demo_uts_namespaces 创建的子进程处于同一个 UTS namespace:

$ hostname
bizarro
$ readlink /proc/27514/ns/uts
uts:[4026532338]
$ readlink /proc/$$/ns/uts # $$ 表示当前 shell 的 PID
uts:[4026532338]

在早期的内核版本中,不能使用 setns() 来加入 mount namespace、PID namespace 和 user namespace,从 3.8 版本的内核开始,setns() 支持加入所有的 namespace。

util-linux 包里提供了nsenter 命令,其提供了一种方式将新创建的进程运行在指定的 namespace 里面,它的实现很简单,就是通过命令行(-t 参数)指定要进入的 namespace 的符号链接,然后利用 setns() 将当前的进程放到指定的 namespace 里面,再调用 clone() 运行指定的执行文件。我们可以用 strace 来看看它的运行情况:

# strace nsenter -t 27242 -i -m -n -p -u /bin/bash
execve("/usr/bin/nsenter", ["nsenter", "-t", "27242", "-i", "-m", "-n", "-p", "-u", "/bin/bash"], [/* 21 vars */]) = 0
…………
…………
pen("/proc/27242/ns/ipc", O_RDONLY) = 3
open("/proc/27242/ns/uts", O_RDONLY) = 4
open("/proc/27242/ns/net", O_RDONLY) = 5
open("/proc/27242/ns/pid", O_RDONLY) = 6
open("/proc/27242/ns/mnt", O_RDONLY) = 7
setns(3, CLONE_NEWIPC) = 0
close(3) = 0
setns(4, CLONE_NEWUTS) = 0
close(4) = 0
setns(5, CLONE_NEWNET) = 0
close(5) = 0
setns(6, CLONE_NEWPID) = 0
close(6) = 0
setns(7, CLONE_NEWNS) = 0
close(7) = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f4deb1faad0) = 4968

4. unshare()


最后一个要介绍的系统调用是 unshare(),它的原型如下:

int unshare(int flags);

unshare()clone() 类似,但它运行在原先的进程上,不需要创建一个新进程,即:先通过指定的 flags 参数 CLONE_NEW* 创建一个新的 namespace,然后将调用者加入该 namespace。最后实现的效果其实就是将调用者从当前的 namespace 分离,然后加入一个新的 namespace。

Linux 中自带的 unshare 命令,就是通过 unshare() 系统调用实现的,使用方法如下:

$ unshare [options] program [arguments]

options 指定要创建的 namespace 类型。

unshare 命令的主要实现如下:

/* 通过提供的命令行参数初始化 'flags' */

unshare(flags);

/* Now execute 'program' with 'arguments'; 'optind' is the index
of the next command-line argument after options */ execvp(argv[optind], &argv[optind]);

unshare 命令的完整实现如下:

/* unshare.c 

   Copyright 2013, Michael Kerrisk
Licensed under GNU General Public License v2 or later A simple implementation of the unshare(1) command: unshare
namespaces and execute a command.
*/ #define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h> /* A simple error-handling function: print an error message based
on the value in 'errno' and terminate the calling process */ #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
} while (0) static void
usage(char *pname)
{
fprintf(stderr, "Usage: %s [options] program [arg...]\n", pname);
fprintf(stderr, "Options can be:\n");
fprintf(stderr, " -i unshare IPC namespace\n");
fprintf(stderr, " -m unshare mount namespace\n");
fprintf(stderr, " -n unshare network namespace\n");
fprintf(stderr, " -p unshare PID namespace\n");
fprintf(stderr, " -u unshare UTS namespace\n");
fprintf(stderr, " -U unshare user namespace\n");
exit(EXIT_FAILURE);
} int
main(int argc, char *argv[])
{
int flags, opt; flags = 0; while ((opt = getopt(argc, argv, "imnpuU")) != -1) {
switch (opt) {
case 'i': flags |= CLONE_NEWIPC; break;
case 'm': flags |= CLONE_NEWNS; break;
case 'n': flags |= CLONE_NEWNET; break;
case 'p': flags |= CLONE_NEWPID; break;
case 'u': flags |= CLONE_NEWUTS; break;
case 'U': flags |= CLONE_NEWUSER; break;
default: usage(argv[0]);
}
} if (optind >= argc)
usage(argv[0]); if (unshare(flags) == -1)
errExit("unshare"); execvp(argv[optind], &argv[optind]);
errExit("execvp");
}

下面我们执行 unshare.c 程序在一个新的 mount namespace 中执行 shell:

$ echo $$                             # 显示当前 shell 的 PID
8490
$ cat /proc/8490/mounts | grep mq # 显示当前 namespace 中的某个挂载点
mqueue /dev/mqueue mqueue rw,seclabel,relatime 0 0
$ readlink /proc/8490/ns/mnt # 显示当前 namespace 的 ID
mnt:[4026531840]
$ ./unshare -m /bin/bash # 在新创建的 mount namespace 中执行新的 shell
$ readlink /proc/$$/ns/mnt # 显示新 namespace 的 ID
mnt:[4026532325]

对比两个 readlink 命令的输出,可以知道两个shell 处于不同的 mount namespace 中。改变新的 namespace 中的某个挂载点,然后观察两个 namespace 的挂载点是否有变化:

$ umount /dev/mqueue                  # 移除新 namespace 中的挂载点
$ cat /proc/$$/mounts | grep mq # 检查是否生效
$ cat /proc/8490/mounts | grep mq # 查看原来的 namespace 中的挂载点是否依然存在?
mqueue /dev/mqueue mqueue rw,seclabel,relatime 0 0

可以看出,新的 namespace 中的挂载点 /dev/mqueue 已经消失了,但在原来的 namespace 中依然存在。

5. 总结


本文仔细研究了 namespace API 的每个组成部分,并将它们结合起来一起使用。后续的文章将会继续深入研究每个单独的 namespace,尤其是 PID namespace 和 user namespace。

参考链接

微信公众号

扫一扫下面的二维码关注微信公众号,在公众号中回复◉加群◉即可加入我们的云原生交流群,和孙宏亮、张馆长、阳明等大佬一起探讨云原生技术

Linux Namespace 入门系列:Namespace API的更多相关文章

  1. Web API 入门系列 - RESTful API 设计指南

    参考:https://developer.github.com/v3/  https://github.com/bolasblack/http-api-guide HTTP 协议 目前使用HTTP1. ...

  2. Linux ns 6. Network Namespace 详解

    文章目录 1. 简介 1.1 Docker Network 桥接模式配置 2. 代码解析 2.1 copy_net_ns() 2.2 pernet_list 2.2.1 loopback_net_op ...

  3. linux入门系列18--Web服务之Apache服务1

    前面系列文章讲解了Linux下通过文件传输.文件共享.邮件系统来分享和获取资源,本文讲解网络资源获取和共享的另外一种形式,通过Apache服务程序来提供Web服务. 本文先讲解目前主流的Web服务程序 ...

  4. Hadoop MapReduce编程 API入门系列之压缩和计数器(三十)

    不多说,直接上代码. Hadoop MapReduce编程 API入门系列之小文件合并(二十九) 生成的结果,作为输入源. 代码 package zhouls.bigdata.myMapReduce. ...

  5. 爬虫入门系列(三):用 requests 构建知乎 API

    爬虫入门系列目录: 爬虫入门系列(一):快速理解HTTP协议 爬虫入门系列(二):优雅的HTTP库requests 爬虫入门系列(三):用 requests 构建知乎 API 在爬虫系列文章 优雅的H ...

  6. HBase编程 API入门系列之create(管理端而言)(8)

    大家,若是看过我前期的这篇博客的话,则 HBase编程 API入门系列之put(客户端而言)(1) 就知道,在这篇博文里,我是在HBase Shell里创建HBase表的. 这里,我带领大家,学习更高 ...

  7. HBase编程 API入门系列之delete(客户端而言)(3)

    心得,写在前面的话,也许,中间会要多次执行,连接超时,多试试就好了. 前面的基础,如下 HBase编程 API入门系列之put(客户端而言)(1) HBase编程 API入门系列之get(客户端而言) ...

  8. HBase编程 API入门系列之get(客户端而言)(2)

    心得,写在前面的话,也许,中间会要多次执行,连接超时,多试试就好了. 前面是基础,如下 HBase编程 API入门系列之put(客户端而言)(1) package zhouls.bigdata.Hba ...

  9. HBase编程 API入门系列之HTable pool(6)

    HTable是一个比较重的对此,比如加载配置文件,连接ZK,查询meta表等等,高并发的时候影响系统的性能,因此引入了“池”的概念. 引入“HBase里的连接池”的目的是: 为了更高的,提高程序的并发 ...

随机推荐

  1. Selenium2自动化——初体验

    一.Windows下的环境搭建 1.安装Python 访问Python官网:https://www.python.org/ 2.安装setuptools与pip setuptools是Python e ...

  2. R (Ani Katchova) · Eric

    首先介绍一下Ani Katchova的R教程,然后再继续总结Advanced R. R introduction setwd("path")设置工作路径 mydata<-re ...

  3. ubuntu16.04安装mysql5.6

    apt-get install software-properties-commonsudo add-apt-repository 'deb http://archive.ubuntu.com/ubu ...

  4. LoraLU

    一.定义动画过程中形成的状态用transform transform 分为2D 和 3D,主要包含以下几种变换:旋转rotate.扭曲skew.缩放scale和移动 translate以及矩阵变形ma ...

  5. 《和莎莫的 500 天》中为什么 Summer 最终没有和 Tom 在一起?

    好的电影总是需要仔细赏味几次,每次也都会有不同的收获.就像我钟爱的[500 days of summer]. 彪悍的大胡子导演MarcWebb实在是太有趣,把自己的亲身经历搬上大荧幕,因为" ...

  6. css进阶之二:flex弹性布局

    布局模式是指一个盒子与其兄弟.祖先盒的关系决定其尺寸与位置的算法.css2.1中定义了四种布局模式,分别是块布局.行内布局.表格布局.以及定位布局.css3引入了新的布局模式Flexbox布局,灵活度 ...

  7. Spark ML机器学习库评估指标示例

    本文主要对 Spark ML库下模型评估指标的讲解,以下代码均以Jupyter Notebook进行讲解,Spark版本为2.4.5.模型评估指标位于包org.apache.spark.ml.eval ...

  8. Docker 安装 Nginx 负载均衡配置

    Docker 安装 # 1)安装依赖包 yum install -y yum-utils device-mapper-persistent-data lvm2 # 2)添加Docker软件包源(否则d ...

  9. Jstorm执行task报错windows CONFIG SET protected-mode no

    windows  CONFIG SET protected-mode no报错说redis受保护模式,redis使用的是Redis-x64-3.2.100,参考博文说是redis3.2之后加入的特性, ...

  10. 开发项目是Integer 与int 什么时候用

    什么时候用Integer : 如果该属性所对应的数据库的字段是主键或者是外键时,用Integer:因为Integer的默认值为null,数据库的主键或者外键不能为空,但是可以为null 什么时候用in ...