linux container (LXC)内核知识碎记
linux namespace资源隔离
在linux内核中一共有下面六种namespace
namespace | 系统调用参数 | 隔离内容 |
---|---|---|
UTS | CLONE_NEWUTS | 主机名与域名 |
IPC | CLONE_NEWIPC | 信号量、消息队列和贡享内存 |
PID | CLONE_NEWPID | 进程编号 |
NETWORK | CLONE_NEWNET | 网络设备、网络栈 |
Mount | CLONE_NEWNS | 挂载点(文件系统) |
USER | CLONE_NEWUSER | 用户和用户组 |
在linux 3.8以后的内核版本中,就对这六种namespace有了全面支持,还提供了相关的api函数,一共有四种方式进行namespace API操作。
- 通过clone()在创建新进程时创建namespace
int clone(int (*child_func)(void * args),void *child_stack,int flags,void * arg);
//clone()实际上是linux系统调用fork()的一种更通用的实现方式,他可以通过flags来控制使用了多少功能,一共有20多种CLONE_*标志位来控制clone进程的方方面面。
/**
四个参数说明:
child_func 传入子进程运行的程序主函数
child_stack 子进程使用栈空间的大小
flags 表示使用了哪些CLONE_*标志位。
args 用于传入主函数的参数.
*/
- 查看/proc/[pid]/ns文件
从linux内核3.8起,用户就可以通过查看/proc/[pid]/ns文件下看到指向不同namespace号的文件,如果两个进程的编号相同,就说明他们在同一个namespace下。/proc/[pid]/ns里设置这些link的目的还有一个就是,一旦上述link文件被打开,只要打开的文件描述符在,那么就算该namespace下所有的进程都已经结束,这个namespace也会存在,这样后续的进程依然可以加进来。
- 通过setns()加入一个已经存在的namespace
int setns(int fd,int nstype);
//参数fd表示要加入namespace的文件描述符,
//参数nstype让调用者可以检查fd指向的namespace是否符合实际要求,参数设为0表示不检查。
- 通过unshare()在原进程中进行namespace隔离
该系统调用与clone类似,但是不创建新进程,而是在原进程中进行namespace隔离
int unshare(int flags);
//fllags参数同clone系统调用的flags参数
UTS namespace
UTS namespace 地宫了主机名和域名的隔离,这样每个容器就有了独立的主机名和域名,在网络上被视为一个独立的节点,而非宿主机上一个进程。
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#define STACK_SIZE (1024*1024)
static char child_stack[STACK_SIZE];
char * const child_args[] = {
"/bin/bash",
NULL
};
int child_main(void * args){
printf("在子进程中\n");
sethostname("NewNamespace",12);
execv(child_args[0],child_args);
return 1;
}
int main(){
printf("程序开始\n");
int child_id = clone(child_main,child_stack+STACK_SIZE,CLONE_NEWIPC|CLONE_NEWUTS|SIGCHLD,NULL);
waitpid(child_id,NULL,0);
printf("已退出\n");
return 0;
}
IPC Namespace
进程将通信涉及的ipc资源通常包括常见的信号量,消息队列和贡享内存。申请IPC资源就申请了一个去哪聚唯一的32位ID,所以IPC namespace中实际上包含了系统IPC标识符以及实现POSIX消息队列的文件系统。在同一个IPC namespace下的进程彼此可见,不同的IPC namespace下的进程互不可见。
PID namespace
pid namespace隔离非常实用,他对进程pid进行重新编号,即两个不同namespace下的进程可以有相同的pid。有四点需要记住:
- 每个pid namespace中的第一个进程“PID 1”,都会像传统的linux中的init进程一样拥有特权,起特殊作用。
- 一个namespace中的进程,不可能通过kill或者ptrace影响父节点或者兄弟节点中的进程,因为其它节点的pid在这个namespace中没有意义。
- 如果你在新的pid namespace重新挂载/proc文件系统,会发现其下只显示同属于一个pid namespace中的进程。
- 在root namespace中可以看到所有的进程,并且递归包含所有的子节点中的进程。
pid namespace 中的init进程
传统的init进程是所有的进程的父进程,维护一张进程表,不断检查进程的状态,一旦有某个子进程因为父进程退而变成孤儿进程,init就会负责收养这个子进程并最终回收资源,结束进程。所以在容器中启动的第一个进程也需要实现类似与init的功能。
信号和init进程
内核为pid namespace 中的init进程赋予了其他的特权-屏蔽信号。如果init进程中没有编写处理某个信号的代码逻辑。,那么与init在同一个pid namespace 中的其他进程向其发送的该信号都会被屏蔽。这个功能主要是防止init进程被杀死。.
父节点pid namespace中的进程发送除了SIGKILL或者SIGSTOP同样也会被忽略。.
一旦pid namespace中的init进程被杀死,那么其他的子进程也随之收到SIGKILL信号而被销毁。
挂载/proc
我们知道在linux中ps,top等进程控制命令,实际上是读取的/proc/[pid]中的文件信息。如果不挂载新的/proc文件系统,我们看到依然是宿主机上的进程信息
在使用setns和unshare系统调用时PID Namespace的特殊性。
setns和unshare允许用户在原有的进程中建立namespace进程隔离,但创建了PID namespace后,原来的调用者并不进入namespace,因为一旦pid被创建后,它的pid namespace也就确定了,进入新的pid namespace会使进程pid发生变化,将导致进程出错。而创建其他的namespace后,调用者会进入新的namespace
mount namespace
mount namespace通过隔离文件系统挂载点对隔离文件系统进行支持,他时历史上第一个linux namespace。不同的mount namespace文件发生变化也不会相互影响。可以通过/proc/[pid]/mounts查看到所有挂载到当前namespacce下的文件系统.
进程在执行mount namespace时会把当前的文件结构复制给新的namespace。新的namespace中的所有操作都只会影响当前的文件系统。
mount propagation(挂载传播)
挂载传播定义了挂载对象之间的关系。这样的关系包括共享关系和从属关系,系统通过这些关系确定任何挂载对象中的挂载事件会如何传播到其他的挂载对象。
- 共享关系。如果两个对象之间是共享关系,那么一个挂载对象中的挂载事件会传播到另一个挂载对象,反之依然。
- 从属关系。如果两个挂载对象形成从属关系,那么一个挂载对象中的挂载事件会传播到另外一个挂载对象,但是反之不行;在这种关系中,从属对象是事件的接受者。
一个挂载状态可能为以下的一种:
- 共享挂载
- 从属挂载
- 共享/从属挂载
- 私有挂载
- 不可绑定挂载
具体的相关信息,可以查看mount的manual
network namespcae
network namespace 主要提供了关于网络资源的隔离,包括网络设备、网络协议栈,ip路由表,防火墙,/proc/net目录,/sys/class/net目录,套接字等。一个网络设备一般只存在与一个network namespace。
如果宿主机存在多块网卡,可以将其中一块或者多块分配给新创建的network namespace。而一般而言如果没有多余的网卡,我们常用的方法是创建一个虚拟网桥,将网络桥接到一块物理网卡中,同时通过虚拟的veth pair实现虚拟容器的网络通信。关于这部分,后续会有新的笔记来介绍。
user namespace
user namespace主要隔离了安全相关的表示符和属性,包括用户id,用户组id,root目录,key(密钥)以及特殊权限。通俗的讲,一个普通用户的进程通过clone创建的新进程在新的user namespace中可以拥有不同的用户和用户组。需要记住一下几点:
- user namespace被创建后,第一个进程被赋予了该namespace中的全部权限,这样该init进程就可以完成必要的初始化工作,而不会出现权限不足而出错。
- 从namespace内部观察到的uid和gid已经和外部不同了,默认显示为65534,表示尚未与外部namespace用户映射。此时需要对user namespace内部的这个初始化user和他外部namespace的某个用户建立映射关系,这样可以保证当涉及到一些对外部namespace操作时,系统可以检查其权限。同样用户组也需要建立起映射关系。
- 用户在新的namespace中具有所有权限,但是它创建的父namespace中不含任何权限,就算条用和创建他的进程具有全部权限,也是如此。
- user namespace的创建其实是一个层层嵌套的树状结构,最上面就是root namespace,新创建的namespace都有一个父节点user namespace,以及零个或者多个子节点user namespace,这一点与pid namespac非常类似。