上篇我们用go语言体验了namespace,今天了解下namespace的实现。
namespace的目的是资源隔离,即资源都在,但不能让你看到。比如进程a只属于namespace A,则不能让a看到namespace B的资源,除非把a拉到namespace B中。
namespace隔离的资源有:
系统的hostname
网络资源(网卡信息,路由信息等)
进程信息(有哪些进程,父进程子进程间的关系等)
用户信息(有哪些用户,组,用户的权限是什么)
文件系统信息(有什么可用的文件系统等)
等等...
那Linux是如何实现隔离的呢?我理解如下:
namespace记录上述资源
每个namespace记录的资源都不一样,都有自己的ns号
把进程绑定到对应ns号中,实现进程的资源隔离(/proc/PID/ns,PID指对应的进程号)
通过clone(),unshare(),setns()系统调用来操作namespace
这三个系统调用的功能如下:
clone() : 创建一个新的进程,通过传namespace的flags参数(NEW*标识符)来指定对应的namespace类型。
unshare() : 把某进程加入到新的namespace。
setns() : 把某进程加入到某个namespace。
我们来看看这三个系统调用。
clone()
clone和fork,vfork都是可以用来创建进程的,但clone可以传对应的flags来指定子进程的资源,它的参数如下:
int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);
child_func : 传入子进程运行的程序主函数。
child_stack : 传入子进程使用的栈空间。
flags : 表示使用哪些 CLONE_* 标志位。
args : 用于传入用户参数。
使用clone创建进程时,一旦指定了flags标志位,相对应类型的 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就是我们说的CLONE_NEW*系列的标志位
ushare()
ushare()用于把某进程加入到新的namespace,参数如下:
int unshare(int flags);
unshare() 与 clone() 类似,但它运行在现有进程上,不需要创建一个新进程。
通过指定的 flags 参数 CLONE_NEW* 创建一个新的 namespace,
将调用者加入该 namespace。
最后实现的效果其实就是将调用者从当前的 namespace 分离,然后加入一个新的 namespace。
linux本身也提供了一个ushare命令,可以直接使用。
setns()
setns()用于加入某个已存在的namespace,参数如下:
int setns(int fd, int nstype);
fd 表示要加入的 namespace 的文件描述符,可以通过打开其中一个符号链接来获取,也可以通过打开 bind mount 到其中一个链接的文件来获取。
stype 让调用者可以去检查 fd 指向的 namespace 类型,值可以设置为 CLONE_NEW*和0。填 0 表示不检查,如果调用者已经明确知道自己要加入了 namespace 类型,或者不关心 namespace 类型,就可以使用该参数来自动校验。
UTS namespace 实现方式
以UTS为例,我们来看看namespace的实现。
我们知道,在linux中,只要符合task_struct的结构体,都能被系统调度。在该结构体中有个nsproxy字段,该字段保存着namespace的相关信息。
(/usr/src/linux-headers-4.15.0-88/include/linux/sched.h)
818 /* Namespaces: */
819 struct nsproxy *nsproxy;
该结构体定义如下
(/usr/src/linux-headers-4.15.0-88/include/linux/nsproxy.h):
31 struct nsproxy {
32 atomic_t count;
33 struct uts_namespace *uts_ns;
34 struct ipc_namespace *ipc_ns;
35 struct mnt_namespace *mnt_ns;
36 struct pid_namespace *pid_ns_for_children;
37 struct net *net_ns;
38 struct cgroup_namespace *cgroup_ns;
39 };
以hostname命令为例,hostname本质调用的是gethostname,它的实现如下(kernel/sys.c):
SYSCALL_DEFINE2(gethostname, char __user *, name, int, len)
{
int i, errno;
struct new_utsname *u;
if (len < 0)
return -EINVAL;
down_read(&uts_sem);
u = utsname();
i = 1 + strlen(u->nodename);
if (i > len)
i = len;
errno = 0;
if (copy_to_user(name, u->nodename, i))
errno = -EFAULT;
up_read(&uts_sem);
return errno;
}
utsname的关键实现如下:
(/usr/src/linux-headers-4.15.0-88/include/linux/utsname.h)
74 static inline struct new_utsname *utsname(void)
75 {
76 return ¤t->nsproxy->uts_ns->name;
77 }
可以看到最终还是落到了结构体nsproxy中,通过这样就实现了可以给不同的namespace设置不同的hostname。
谢谢阅读。
参考:
https://zhuanlan.zhihu.com/p/120136568
https://www.cnblogs.com/sparkdev/p/9377072.html
https://blog.csdn.net/21cnbao/article/details/60326419