namespaces提供了一种虚拟化的一种轻量级形式,用来将全局的资源隔离,docker容器就是利用了这一大特性来实现资源隔离。容器和虚拟机相比,容器是一种轻量级的虚拟化技术,直接使用宿主机的内核,使用命名空间隔离资源。
Linux namespaces 是对全局系统资源的一种封装隔离,使得处于不同 namespace 的进程拥有独立的全局系统资源,改变一个 namespace 中的系统资源只会影响当前 namespace 里的进程,对其他 namespace 中的进程没有影响。
每个任务都与一个选定的命名空间相关联:
struct task_struct {
......
/* namespaces */
struct nsproxy *nsproxy;
......
}
这里使用的是指针,这样多个任务就可以共享一组命名空间,修改指定的命名空间,对所有属于该命名空间的任务都是可见的。
/*
* A structure to contain pointers to all per-process
* namespaces - fs (mount), uts, network, sysvipc, etc.
*
* The pid namespace is an exception -- it's accessed using
* task_active_pid_ns. The pid namespace here is the
* namespace that children will use.
*
* 'count' is the number of tasks holding a reference.
* The count for each namespace, then, will be the number
* of nsproxies pointing to it, not the number of tasks.
*
* The nsproxy is shared by tasks which share all namespaces.
* As soon as a single namespace is cloned or unshared, the
* nsproxy is copied.
*/
struct nsproxy {
atomic_t count;
struct uts_namespace *uts_ns;
struct ipc_namespace *ipc_ns;
struct mnt_namespace *mnt_ns;
struct pid_namespace *pid_ns_for_children;
struct net *net_ns;
struct cgroup_namespace *cgroup_ns;
};
nsproxy 由共享所有命名空间的任务共享。 一旦cloned or unshared共享单个命名空间,就会复制 nsproxy。
clone系统调用有参数选项选择是否可以控制是与父进程共享命名空间还是建立新的命名空间。
unshare系统调用可以将进程的命名空间从父进程分离。
除此之外还有setns系统调用,可以将当前进程加入到已有的 namespace 中。
Linux中namespace提供了6种隔离功能:
namespace | 隔离资源 |
---|---|
uts_namespace | 主机名和域名信息 ,UTS:UNIX Timesharing System |
ipc_namespace | 进程间通信(信号量,消息队列和共享内存) |
mnt_namespace | (文件系统)挂载点 |
pid_namespace | 进程号PID |
net | 网络协议栈 |
cgroup_namespace | 控制组根目录 |
查看当前进程的的命名空间,可以看到这些 namespace 文件都是链接文件。链接文件的内容的格式为 xxx:[inode number]。其中的 xxx 为 namespace 的类型,inode number 则用来标识一个 namespace,我们也可以把它理解为 namespace 的 ID。如果两个进程的某个 namespace 文件指向同一个链接文件,即两个进程的namespace ID相同,说明其相关资源在同一个 namespace 中。
static struct kmem_cache *nsproxy_cachep;
int __init nsproxy_cache_init(void)
{
nsproxy_cachep = KMEM_CACHE(nsproxy, SLAB_PANIC);
return 0;
}
定义一个nsproxy_cachep高速缓存全局变量,采用slab分配器。
/*
* Please use this macro to create slab caches. Simply specify the
* name of the structure and maybe some flags that are listed above.
*
* The alignment of the struct determines object alignment. If you
* f.e. add ____cacheline_aligned_in_smp to the struct declaration
* then the objects will be properly aligned in SMP configurations.
*/
#define KMEM_CACHE(__struct, __flags) kmem_cache_create(#__struct,\
sizeof(struct __struct), __alignof__(struct __struct),\
(__flags), NULL)
KMEM_CACHE宏用来创建slab caches,等价于:
kmem_cache_create("nsproxy", sizeof(struct nsproxy), __alignof__(struct nsproxy), (0x00040000UL), NULL)
这样就创建一个名为nsproxy的高速缓存,其中放的对象类型就是struct nsproxy,该高速缓存存放的都是struct nsproxy对象,
然后需要struct nsproxy对象调用kmem_cache_alloc分配struct nsproxy对象即可:
static inline struct nsproxy *create_nsproxy(void)
{
struct nsproxy *nsproxy;
nsproxy = kmem_cache_alloc(nsproxy_cachep, GFP_KERNEL);
if (nsproxy)
atomic_set(&nsproxy->count, 1);
return nsproxy;
}
chroot(change root directory):也是一种简单的资源隔离技术,允许把当前目录变成根目录一样,用于限定用户使用真正的根目录,可以将进程限制到文件系统的某一部分。
chroot增加了系统的安全性,限制了用户的权力,在经过 chroot 之后,在新根下将访问不到旧系统的根目录结构和文件,这样就增强了系统的安全性。这个一般是在登录 (login) 前使用 chroot,以此达到用户不能访问一些特定的文件。使用 chroot 后,系统读取的是新根下的目录和文件,这是一个与原系统根下文件不相关的目录结构。
SYSCALL_DEFINE1(chroot, const char __user *, filename)
{
struct path path;
int error;
unsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_DIRECTORY;
error = user_path_at(AT_FDCWD, filename, lookup_flags, &path);
......
error = inode_permission(path.dentry->d_inode, MAY_EXEC | MAY_CHDIR);
......
error = security_path_chroot(&path);
set_fs_root(current->fs, &path);
.....
}
(1)
user_path_at函数根据文件名filename从当前目录开始查找获取 struct path 结构体。
(2)
inode_permission函数根据 struct path 结构体 找到 d_inode,检查给定 inode 的访问权限(检查 inode 的读/写/执行权限)
// linux-4.10.1/include/linux/path.h
struct path {
struct vfsmount *mnt;
struct dentry *dentry;
};
// linux-4.10.1/include/linux/dcache.h
struct dentry {
......
struct inode *d_inode; /* Where the name belongs to - NULL is * negative */
......
};
(3)
set_fs_root就是最关键的函数,把当前进程的文件系统的root设置为path。
set_fs_root(current->fs, &path);
/*
* Replace the fs->{rootmnt,root} with {mnt,dentry}. Put the old values.
* It can block.
*/
void set_fs_root(struct fs_struct *fs, const struct path *path)
{
struct path old_root;
path_get(path);
spin_lock(&fs->lock);
write_seqcount_begin(&fs->seq);
old_root = fs->root;
fs->root = *path;
write_seqcount_end(&fs->seq);
spin_unlock(&fs->lock);
if (old_root.dentry)
path_put(&old_root);
}
struct fs_struct {
......
struct path root, pwd;
......
};
其中:
root:根目录的目录项。
pwd:当前工作目录的目录项。
(4)
chroot 修改了进程的 root 目录的核心操作就是修改了 当前进程(struct task_struct) 的 current->fs->root = 当前文件名filename的struct path。
这样当前进程就认为 filename 是根目录,因为 fs->root 存的是 filename 目录的 path 结构。
chroot仅仅是在访问文件系统目录的时候,限定了用户的根目录。
假如不指定ns,那么默认所有进程在创建的时候,都会指定一个默认的ns:init_nsproxy。
比如 init_pid_ns :
进程号命名空间用来隔离进程号,对应的结构体是pid_namespace。每个进程号命名空间独立分配进程号。进程号命名空间按层次组织成一棵树,初始进程号命名空间是树的根,对应全局变量init_pid_ns,所有进程默认属于初始进程号命名空间。
extern struct nsproxy init_nsproxy;
struct nsproxy init_nsproxy = {
.count = ATOMIC_INIT(1),
.uts_ns = &init_uts_ns,
#if defined(CONFIG_POSIX_MQUEUE) || defined(CONFIG_SYSVIPC)
.ipc_ns = &init_ipc_ns,
#endif
.mnt_ns = NULL,
.pid_ns_for_children = &init_pid_ns,
#ifdef CONFIG_NET
.net_ns = &init_net,
#endif
#ifdef CONFIG_CGROUPS
.cgroup_ns = &init_cgroup_ns,
#endif
};
init_nsproxy定义了初始的全局命名空间,其中维护了指向各个子系统初始化的命名空间对象的指针。
struct new_utsname {
char sysname[__NEW_UTS_LEN + 1];
char nodename[__NEW_UTS_LEN + 1];
char release[__NEW_UTS_LEN + 1];
char version[__NEW_UTS_LEN + 1];
char machine[__NEW_UTS_LEN + 1];
char domainname[__NEW_UTS_LEN + 1];
};
这些字符串存储了系统名Linux,发行版,内核版本,机器等等,可以通过uname查看,也可以在/proc/sys/kernel/下查看:
struct uts_namespace {
struct kref kref;
struct new_utsname name;
struct user_namespace *user_ns;
struct ucounts *ucounts;
struct ns_common ns;
};
extern struct uts_namespace init_uts_ns;
struct uts_namespace init_uts_ns = {
.kref = {
.refcount = ATOMIC_INIT(2),
},
.name = {
.sysname = UTS_SYSNAME,
.nodename = UTS_NODENAME,
.release = UTS_RELEASE,
.version = UTS_VERSION,
.machine = UTS_MACHINE,
.domainname = UTS_DOMAINNAME,
},
.user_ns = &init_user_ns,
.ns.inum = PROC_UTS_INIT_INO,
#ifdef CONFIG_UTS_NS
.ns.ops = &utsns_operations,
#endif
};
EXPORT_SYMBOL_GPL(init_uts_ns);
init_uts_ns是uts_ns的初始化配置,相关的预处理器常数在内核中各处定义,通过编译内核顶层Makefile动态生成。
系统名是固定的:
#define UTS_SYSNAME "Linux"
以fork为例:
SYSCALL_DEFINE0(fork)
-->_do_fork()
-->copy_process()
-->copy_namespaces()
-->create_new_namespaces()
/*
* called from clone. This now handles copy for nsproxy and all
* namespaces therein.
*/
int copy_namespaces(unsigned long flags, struct task_struct *tsk)
{
struct nsproxy *old_ns = tsk->nsproxy;
struct user_namespace *user_ns = task_cred_xxx(tsk, user_ns);
struct nsproxy *new_ns;
if (likely(!(flags & (CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC |
CLONE_NEWPID | CLONE_NEWNET |
CLONE_NEWCGROUP)))) {
get_nsproxy(old_ns);
return 0;
}
if (!ns_capable(user_ns, CAP_SYS_ADMIN))
return -EPERM;
/*
* CLONE_NEWIPC must detach from the undolist: after switching
* to a new ipc namespace, the semaphore arrays from the old
* namespace are unreachable. In clone parlance, CLONE_SYSVSEM
* means share undolist with parent, so we must forbid using
* it along with CLONE_NEWIPC.
*/
if ((flags & (CLONE_NEWIPC | CLONE_SYSVSEM)) ==
(CLONE_NEWIPC | CLONE_SYSVSEM))
return -EINVAL;
new_ns = create_new_namespaces(flags, tsk, user_ns, tsk->fs);
if (IS_ERR(new_ns))
return PTR_ERR(new_ns);
tsk->nsproxy = new_ns;
return 0;
}
如果 clone 的参数里面没有 CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWCGROUP,就返回原来的 namespace,调用 get_nsproxy。
get_nsproxy函数只是将struct nsproxy成员的count加1。
static inline void get_nsproxy(struct nsproxy *ns)
{
atomic_inc(&ns->count);
}
相对应有个函数put_nsproxy将struct nsproxy成员的count减1,这个函数每次减1时会判断count是否等于0,等于0就释放掉调用free_nsproxy释放掉命名空间资源。
static inline void put_nsproxy(struct nsproxy *ns)
{
if (atomic_dec_and_test(&ns->count)) {
free_nsproxy(ns);
}
}
get_nsproxy 和 put_nsproxy 操作类似于C++中的智能指针操作。
如果 clone 的参数里面有 CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWCGROUP标志位,调用create_new_namespaces
/*
* Create new nsproxy and all of its the associated namespaces.
* Return the newly created nsproxy. Do not attach this to the task,
* leave it to the caller to do proper locking and attach it to task.
*/
static struct nsproxy *create_new_namespaces(unsigned long flags,
struct task_struct *tsk, struct user_namespace *user_ns,
struct fs_struct *new_fs)
{
struct nsproxy *new_nsp;
int err;
new_nsp = create_nsproxy();
if (!new_nsp)
return ERR_PTR(-ENOMEM);
new_nsp->mnt_ns = copy_mnt_ns(flags, tsk->nsproxy->mnt_ns, user_ns, new_fs);
if (IS_ERR(new_nsp->mnt_ns)) {
err = PTR_ERR(new_nsp->mnt_ns);
goto out_ns;
}
new_nsp->uts_ns = copy_utsname(flags, user_ns, tsk->nsproxy->uts_ns);
if (IS_ERR(new_nsp->uts_ns)) {
err = PTR_ERR(new_nsp->uts_ns);
goto out_uts;
}
new_nsp->ipc_ns = copy_ipcs(flags, user_ns, tsk->nsproxy->ipc_ns);
if (IS_ERR(new_nsp->ipc_ns)) {
err = PTR_ERR(new_nsp->ipc_ns);
goto out_ipc;
}
new_nsp->pid_ns_for_children =
copy_pid_ns(flags, user_ns, tsk->nsproxy->pid_ns_for_children);
if (IS_ERR(new_nsp->pid_ns_for_children)) {
err = PTR_ERR(new_nsp->pid_ns_for_children);
goto out_pid;
}
new_nsp->cgroup_ns = copy_cgroup_ns(flags, user_ns,
tsk->nsproxy->cgroup_ns);
if (IS_ERR(new_nsp->cgroup_ns)) {
err = PTR_ERR(new_nsp->cgroup_ns);
goto out_cgroup;
}
new_nsp->net_ns = copy_net_ns(flags, user_ns, tsk->nsproxy->net_ns);
if (IS_ERR(new_nsp->net_ns)) {
err = PTR_ERR(new_nsp->net_ns);
goto out_net;
}
return new_nsp;
......
}
创建新的nsproxy及其所有关联的名称空间,返回新创建的nsproxy。
比如copy_utsname:如果没有标志位CLONE_NEWUTS,则返回old_ns,如果设置了创建新的new_ns。
// linux-4.10.1/kernel/utsname.c
/*
* Copy task tsk's utsname namespace, or clone it if flags
* specifies CLONE_NEWUTS. In latter case, changes to the
* utsname of this process won't be seen by parent, and vice
* versa.
*/
struct uts_namespace *copy_utsname(unsigned long flags,
struct user_namespace *user_ns, struct uts_namespace *old_ns)
{
struct uts_namespace *new_ns;
BUG_ON(!old_ns);
get_uts_ns(old_ns);
if (!(flags & CLONE_NEWUTS))
return old_ns;
new_ns = clone_uts_ns(user_ns, old_ns);
put_uts_ns(old_ns);
return new_ns;
}
比如copy_ipcs:
struct ipc_namespace *copy_ipcs(unsigned long flags,
struct user_namespace *user_ns, struct ipc_namespace *ns)
{
if (!(flags & CLONE_NEWIPC))
return get_ipc_ns(ns);
return create_ipc_ns(user_ns, ns);
}
如果没有设置标志位CLONE_NEWIPC,调用get_ipc_ns将tsk->nsproxy->ipc_ns->count加1。
static inline struct ipc_namespace *get_ipc_ns(struct ipc_namespace *ns)
{
if (ns)
atomic_inc(&ns->count);
return ns;
}
如果有标志位CLONE_NEWIPC,调用 create_ipc_ns创建新的ipc_namespace命名空间。
如果没有设置标志位 CLONE_NEWPID,则返回老的 pid namespace。
// linux-4.10.1/kernel/pid_namespace.c
struct pid_namespace *copy_pid_ns(unsigned long flags,
struct user_namespace *user_ns, struct pid_namespace *old_ns)
{
if (!(flags & CLONE_NEWPID))
return get_pid_ns(old_ns);
if (task_active_pid_ns(current) != old_ns)
return ERR_PTR(-EINVAL);
return create_pid_namespace(user_ns, old_ns);
}
如果设置了,就调用 create_pid_namespace,创建新的 pid namespace。采用slab分配器分配struct pid_namespace对象。
// linux-4.10.1/kernel/pid_namespace.c
static struct pid_namespace *create_pid_namespace(struct user_namespace *user_ns,
struct pid_namespace *parent_pid_ns)
{
......
struct pid_namespace *ns = kmem_cache_zalloc(pid_ns_cachep, GFP_KERNEL);
......
}
static inline void *kmem_cache_zalloc(struct kmem_cache *k, gfp_t flags)
{
return kmem_cache_alloc(k, flags | __GFP_ZERO);
}
如果没有设置标志位 CLONE_NEWCGROUP,则返回老的 struct cgroup_namespace,old_ns。
// linux-4.10.1/kernel/cgroup.c
struct cgroup_namespace *copy_cgroup_ns(unsigned long flags,
struct user_namespace *user_ns,
struct cgroup_namespace *old_ns)
{
struct cgroup_namespace *new_ns;
struct ucounts *ucounts;
struct css_set *cset;
BUG_ON(!old_ns);
if (!(flags & CLONE_NEWCGROUP)) {
get_cgroup_ns(old_ns);
return old_ns;
}
/* Allow only sysadmin to create cgroup namespace. */
if (!ns_capable(user_ns, CAP_SYS_ADMIN))
return ERR_PTR(-EPERM);
ucounts = inc_cgroup_namespaces(user_ns);
if (!ucounts)
return ERR_PTR(-ENOSPC);
/* It is not safe to take cgroup_mutex here */
spin_lock_irq(&css_set_lock);
cset = task_css_set(current);
get_css_set(cset);
spin_unlock_irq(&css_set_lock);
new_ns = alloc_cgroup_ns();
if (IS_ERR(new_ns)) {
put_css_set(cset);
dec_cgroup_namespaces(ucounts);
return new_ns;
}
new_ns->user_ns = get_user_ns(user_ns);
new_ns->ucounts = ucounts;
new_ns->root_cset = cset;
return new_ns;
}
如果设置标志位 CLONE_NEWCGROUP,则调用alloc_cgroup_ns创建新的struct cgroup_namespace,new_ns。
// linux-4.10.1/kernel/cgroup.c
static struct cgroup_namespace *alloc_cgroup_ns(void)
{
struct cgroup_namespace *new_ns;
int ret;
new_ns = kzalloc(sizeof(struct cgroup_namespace), GFP_KERNEL);
if (!new_ns)
return ERR_PTR(-ENOMEM);
ret = ns_alloc_inum(&new_ns->ns);
if (ret) {
kfree(new_ns);
return ERR_PTR(ret);
}
atomic_set(&new_ns->count, 1);
new_ns->ns.ops = &cgroupns_operations;
return new_ns;
}
如果没有设置标志位 CLONE_NEWNET,则返回老的 old_net。
// linux-4.10.1/net/core/net_namespace.c
struct net *copy_net_ns(unsigned long flags,
struct user_namespace *user_ns, struct net *old_net)
{
struct ucounts *ucounts;
struct net *net;
int rv;
//返回老的 old_net
if (!(flags & CLONE_NEWNET))
return get_net(old_net);
ucounts = inc_net_namespaces(user_ns);
if (!ucounts)
return ERR_PTR(-ENOSPC);
//分配一个新的 struct net 结构
net = net_alloc();
if (!net) {
dec_net_namespaces(ucounts);
return ERR_PTR(-ENOMEM);
}
get_user_ns(user_ns);
......
//setup_net runs the initializers for the network namespace object.
rv = setup_net(net, user_ns);
if (rv == 0) {
rtnl_lock();
list_add_tail_rcu(&net->list, &net_namespace_list);
rtnl_unlock();
}
......
return net;
}
如果设置标志位 CLONE_NEWNET了,新建一个 network namespace。
copy_net_ns 会调用 net = net_alloc(),分配一个新的 struct net 结构:
// linux-4.10.1/net/core/net_namespace.c
static struct kmem_cache *net_cachep;
static struct net *net_alloc(void)
{
struct net *net = NULL;
......
net = kmem_cache_zalloc(net_cachep, GFP_KERNEL);
......
}
然后调用 setup_net 对新分配的 net 结构进行初始化,之后调用 list_add_tail_rcu,将新建的 network namespace,添加到全局的 network namespace 列表 net_namespace_list 中。
// linux-4.10.1/net/core/net_namespace.c
LIST_HEAD(net_namespace_list);
EXPORT_SYMBOL_GPL(net_namespace_list);
rv = setup_net(net, user_ns);
if (rv == 0) {
rtnl_lock();
list_add_tail_rcu(&net->list, &net_namespace_list);
rtnl_unlock();
}
Linux Namespace是Kernel的一个功能,它可以隔离一系列的系统资源,比如PID(Process ID)、User ID、Network等。
Linux 4.10.0
极客时间:趣谈操作系统
深入Linux 内核架构
Linux 内核分析和应用
Linux 内核深度解析
自己动手写docker
https://www.jianshu.com/p/5466c3d8d07b
https://zhuanlan.zhihu.com/p/47571649
https://www.cnblogs.com/crybaby/p/Linux_kernel_chroot.html
https://cloud.tencent.com/developer/article/1722181
https://rumenz.com/rumenbiji/linux-chroot.html