容器之namespace

1. 介绍

简单玩了下Linux kernel为容器技术提供的基础设施之一namespace(另一个是cgroups),包括uts/user/pid/mnt/ipc/net六个(3.13.0的内核). 这东西主要用来做资源的隔离,我感觉本质上是全局资源的映射,映射之间独立了自然隔离了。主要涉及到的东西是:

  • clone
  • setns
  • unshare
  • /proc/pid/ns, /proc/pid/uid_map, /proc/pid/gid_map等

2. 测试流程及代码

下面是一些简单的例子,主要测试uts/pid/user/mnt四个namespace的效果,测试代码主要用到三个进程,一个是clone系统调用执行/bin/bash后的进程,也是生成新的子namespace的初始进程,然后是打开/proc/pid/ns下的namespace链接文件,用setns将第二个可执行文件的进程加入/bin/bash的进程的namespace(容器),并让其fork出一个子进程,测试pid namespace的差异。值得注意的几个点:

  • 不同版本的内核setns和unshare对namespace的支持不一样,较老的内核可能只支持ipc/net/uts三个namespace
  • 某个进程创建后其pid namespace就固定了,使用setns和unshare改变后,其本身的pid namespace不会改变,只有fork出的子进程的pid namespace改变
  • setns将进程加入的新namespace需是此进程的后代namespace
  • 用setns添加mnt namespace应该放在其他namespace之后,否则可能出现无法打开/proc/pid/ns/…的错误
// 代码1: 开一些新的namespace(启动新容器)
#define _GNU_SOURCE
#include <sys/wait.h>
#include <sched.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define errExit(msg)  do { perror(msg); exit(EXIT_FAILURE); \
} while (0)

/* Start function for cloned child */
static int childFunc(void *arg)
{
  const char *binary = "/bin/bash";
  char *const argv[] = {
    "/bin/bash",
    NULL
  };
  char *const envp[] = { NULL };

  /* wrappers for execve */
  // has const char * as argument list
  // execl
  // execle  => has envp
  // execlp  => need search PATH 

  // has char *const arr[] as argument list
  // execv
  // execvpe => need search PATH and has envp
  // execvp  => need search PATH 

  //int ret = execve(binary, argv, envp);
  int ret = execv(binary, argv);
  if (ret < 0) {
    errExit("execve error");
  }
  return ret;
}

#define STACK_SIZE (1024 * 1024)    /* Stack size for cloned child */

int main(int argc, char *argv[])
{
  char *stack;
  char *stackTop;
  pid_t pid;
  stack = malloc(STACK_SIZE);
  if (stack == NULL)
    errExit("malloc");
  stackTop = stack + STACK_SIZE;  /* Assume stack grows downward */

  //pid = clone(childFunc, stackTop, CLONE_NEWUTS | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUSER | SIGCHLD, NULL);
  pid = clone(childFunc, stackTop, CLONE_NEWUTS | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUSER | CLONE_NEWIPC | SIGCHLD, NULL);
//pid = clone(childFunc, stackTop, CLONE_NEWUTS | //CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUSER | CLONE_NEWIPC //| CLONE_NEWNET | SIGCHLD, NULL);
  if (pid == -1)
    errExit("clone");
  printf("clone() returned %ld\n", (long) pid);

  if (waitpid(pid, NULL, 0) == -1)
    errExit("waitpid");
  printf("child has terminated\n");

  exit(EXIT_SUCCESS);
}
// 代码2: 使用setns加入新进程
#define _GNU_SOURCE  // ?
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/utsname.h>
#include <unistd.h>
#include <sys/types.h>
#include <sched.h>
#include <fcntl.h>
#include <wait.h>

// mainly setns and unshare system calls

/* int setns(int fd, int nstype); */

// 不同版本内核/proc/pid/ns下namespace文件情况
/*
   CLONE_NEWCGROUP (since Linux 4.6)
   fd must refer to a cgroup namespace.

   CLONE_NEWIPC (since Linux 3.0)
   fd must refer to an IPC namespace.

   CLONE_NEWNET (since Linux 3.0)
   fd must refer to a network namespace.

   CLONE_NEWNS (since Linux 3.8)
   fd must refer to a mount namespace.

   CLONE_NEWPID (since Linux 3.8)
   fd must refer to a descendant PID namespace.

   CLONE_NEWUSER (since Linux 3.8)
   fd must refer to a user namespace.

   CLONE_NEWUTS (since Linux 3.0)
   fd must refer to a UTS namespace.
   */

/* // 特殊的pid namespace
   CLONE_NEWPID behaves somewhat differently from the other nstype
values: reassociating the calling thread with a PID namespace changes
only the PID namespace that child processes of the caller will be
created in; it does not change the PID namespace of the caller
itself.  Reassociating with a PID namespace is allowed only if the
PID namespace specified by fd is a descendant (child, grandchild,
etc.)  of the PID namespace of the caller.  For further details on
PID namespaces, see pid_namespaces(7).
*/

/*
int unshare(int flags);
CLONE_FILES | CLONE_FS | CLONE_NEWCGROUP | CLONE_NEWIPC | CLONE_NEWNET
| CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUSER | CLONE_NEWUTS | CLONE_SYSVSEM
*/

#define MAX_PROCPATH_LEN 1024

#define errorExit(msg) \
  do { fprintf(stderr, "%s in file %s in line %d\n", msg, __FILE__, __LINE__);    exit(EXIT_FAILURE); } while (0)

void printInfo();
int openAndSetns(const char *path);

int main(int argc, char *argv[])
{
  if (argc < 2) {
    fprintf(stdout, "usage : execname pid(find namespaces of this process)\n");
    return 0;
  }
  printInfo();

  fprintf(stdout, "---- setns for uts ----\n");
  char uts[MAX_PROCPATH_LEN];
  snprintf(uts, MAX_PROCPATH_LEN, "/proc/%s/ns/uts", argv[1]);
  openAndSetns(uts);
  printInfo();

  fprintf(stdout, "---- setns for user ----\n");
  char user[MAX_PROCPATH_LEN];
  snprintf(user, MAX_PROCPATH_LEN, "/proc/%s/ns/user", argv[1]);
  openAndSetns(user);
  printInfo();

  // 注意pid namespace的不同行为,只有后续创建的子进程进入setns设置
  // 的新的pid namespace,本进程不会改变
  fprintf(stdout, "---- setns for pid ----\n");
  char pidpath[MAX_PROCPATH_LEN];
  snprintf(pidpath, MAX_PROCPATH_LEN, "/proc/%s/ns/pid", argv[1]);
  openAndSetns(pidpath);
  printInfo();

  fprintf(stdout, "---- setns for ipc ----\n");
  char ipc[MAX_PROCPATH_LEN];
  snprintf(ipc, MAX_PROCPATH_LEN, "/proc/%s/ns/ipc", argv[1]);
  openAndSetns(ipc);
  printInfo();

  fprintf(stdout, "---- setns for net ----\n");
  char net[MAX_PROCPATH_LEN];
  snprintf(net, MAX_PROCPATH_LEN, "/proc/%s/ns/net", argv[1]);
  openAndSetns(net);
  printInfo();

  // 注意mnt namespace需要放在其他后面,避免mnt namespace改变后
  // 找不到/proc/pid/ns下的文件
  fprintf(stdout, "---- setns for mount ----\n");
  char mount[MAX_PROCPATH_LEN];
  snprintf(mount, MAX_PROCPATH_LEN, "/proc/%s/ns/mnt", argv[1]);
  openAndSetns(mount);
  printInfo();

  // 测试子进程的pid namespace
  int ret = fork();
  if (-1 == ret) {
    errorExit("failed to fork");
  } else if (ret == 0) {
    fprintf(stdout, "********\n");
    fprintf(stdout, "in child process\n");
    printInfo();
    fprintf(stdout, "********\n");
    for (;;) {
      sleep(5);
    }
  } else {
    fprintf(stdout, "child pid : %d\n", ret);
  }
  for (;;) {
    sleep(5);
  }
  waitpid(ret, NULL, 0);
  return 0;
}

void printInfo()
{
  pid_t pid;
  struct utsname uts;
  uid_t uid;
  gid_t gid;
  // pid namespace
  pid = getpid();
  // user namespace
  uid = getuid();
  gid = getgid();
  // uts namespace
  uname(&uts);
  fprintf(stdout, "pid : %d\n", pid);
  fprintf(stdout, "uid : %d\n", uid);
  fprintf(stdout, "gid : %d\n", gid);
  fprintf(stdout, "hostname : %s\n", uts.nodename);
}

int openAndSetns(const char *path)
{
  int ret = open(path, O_RDONLY, 0);
  if (-1 == ret) {
    fprintf(stderr, "%s\n", strerror(errno));
    errorExit("failed to open fd");
  }
  if (-1 == (ret = setns(ret, 0))) {
    fprintf(stderr, "%s\n", strerror(errno));
    errorExit("failed to setns");
  }
  return ret;
}

3. 测试效果

  • user的效果 : 通过/proc/pid/uid_map和/proc/pid/gid_map设置container外用户id和容器内用户id的映射关系(把这放前面是因为后面hostname和mount需要权限…)

  • uts的效果 : 改变container中的hostname不会影响container外面的hostname

  • pid和mnt的效果 : container中进程id被重新映射,在container中重新挂载/proc filesystem不会影响容器外的/proc

  • setns的测试
    • 依次为init进程,container init进程(6个namespace的flag都指定了),新加入container的进程以及其fork出的子进程的namespace情况,可以看到container init进程与init进程的namespace完全不同了,新加入container的进程除了pid与init相同外,其他namespace与container init进程相同,而新加入container的进程fork出的子进程的namespace则与container init进程完全相同

    • 新加入container init进程pid namespace的子进程

    • 程序2输出

时间: 2024-11-12 05:08:57

容器之namespace的相关文章

初探STL容器之Vector

vector 特点: 1.可变长的动态数组 2.使用时包含头文件 #include <vector> 3.支持随机访问迭代器 ? 根据下标随机访问某个元素时间为常数 ? 在尾部添加速度很快 ? 在中间插入慢 成员函数 初始化 vector(); 初始化成空 vector(int n); 初始化成有n个元素 vector(int n, const T & val); 初始化成有n个元素, 每个元素的值都是val,类型是T vector(iterator first, iterator l

初探STL容器之List

List 特点: 1.实质上是双向链表 2.使用时包含<list>头文件 #include<list> 3.不支持随机访问迭代器,只能使用双向迭代器  //因此不能使用一些算法和运算符操作 4.在任何位置的插入.删除操作都是常数时间 成员函数 初始化 list <int> intlist0; // 创建空的 intlist list <int> intlist1( 3 ); //包含3个元素 list <int> intlist2( 5, 2 )

STL 源码剖析读书笔记三:序列式容器之 vector、list

1. STL 中的容器 容器,置物之所也.STL 容器即是将运用最广的一些数据结构实现出来.如下图所示: 上图以内缩方式来表达基层和衍生层的关系.所谓衍生,并非派生关系,而是内含关系.例如 heap 内含一个 vector,priority-queue 内含一个 heap.stack.queue都内含一个 deque,set/map/multimap/multiset 都内含一个 RB-tree,hash_x 都内含一个 hashtable. 2. 序列式容器之 vector 所谓序列式容器,其

A022-列表容器之ExpandableListView

概述 本节课介绍Android中可实现二级可展开收缩列表的ExpandableListView容器,笔者感觉它非常难用并且难理解,很多时候我们可能需要对控件进行扩展和定制,然而它不太方便扩展,它使用难点主要在数据结构上和对控件的事件监听,其他的实现方式类似ListView,下面会提供笔者在实际开发中使用到的案例. 案例 上面实现的效果可展开的二级列表,每个组项都可能有若干个子项,默认的ExpandableListView不太美观,我们需要通过自定义布局类美化它,在使用过程中有一些需要我们去了解的

STL源码笔记(12)—序列式容器之deque(二)

STL源码笔记(12)-序列式容器之deque(二) 再谈deque数据结构 我们知道deque是通过map管理很多个互相独立连续空间,由于对deque_iterator的特殊设计,使得在使用的时候就好像连续一样.有了deque_iterator的基础(例如重载的操作符等),对于我们实现容器的一些方法就十分方便了.与vector一样,deque也维护一个start,和finish两个迭代器,start指向容器中的一个元素,finish指向最后一个元素的后一个位置(前闭后开),从微观上讲,star

Java并发编程:并发容器之ConcurrentHashMap(转)

本文转自:http://www.cnblogs.com/dolphin0520/p/3932905.html Java并发编程:并发容器之ConcurrentHashMap(转载) 下面这部分内容转载自: http://www.haogongju.net/art/2350374 JDK5中添加了新的concurrent包,相对同步容器而言,并发容器通过一些机制改进了并发性能.因为同步容器将所有对容器状态的访问都 串行化了,这样保证了线程的安全性,所以这种方法的代价就是严重降低了并发性,当多个线程

Java并发编程:并发容器之CopyOnWriteArrayList(转)

本文转自:http://www.cnblogs.com/dolphin0520/p/3938914.html Java并发编程:并发容器之CopyOnWriteArrayList(转载) 原文链接: http://ifeve.com/java-copy-on-write/ Copy-On-Write简称COW,是一种用于程序设计中的优化策略.其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略.从J

STL学习笔记--4、序列式容器之vector

常见的数据结构:array数组,list链表,tree树,stack栈,queue队列,hash table散列表,set集合,map映射-- 根据数据在容器中的排列分为:序列式sequence和关联式associative. 序列式容器之vector 1.vector VS array: array是静态空间,一旦配置则无法改变: vector是动态空间,随着元素的加入,内部机制会自动扩充新的空间来容纳新的元素. 实现技术:对大小的控制和重新配置时的数据移动效率. 扩充空间:配置新空间.数据移

Servlet容器之Jetty的安装和配置(Windows)

网上多说Jetty轻量级,好用.好吧,本着好奇就打算学习一下.不过这里还是要抱怨一句,关于Jetty的学习资料真心不多. 前提:必须安装jdk. 1.下载Jetty安装包:http://dist.codehaus.org/jetty/jetty-6.1.22/ 2.解压至任意目录 3.直接进入bin目录,双击Jetty-Service.exe.启动成功 4.地址栏直接输入http://localhost:8080 ps: 1.修改E:\jetty-6.1.22\etc\ jetty.xml文件中