nsenter 套娃
在 Linux 系统里,nsenter
是一个命令行工具,用于进入到另一个 namespace
。譬如,nsenter -n -t 1 bash
就是进入到 pid
为 1 的进程所在的网络 namespace
里。
如果不断的执行 nsenter
命令,就形成了一个 nsenter
套娃。
而在 Linux 系统里,会不会形成一个 namespace
栈呢?以便在退出里层的 namespace
后,就回到外层的 namespace
。
答案:不会。
本篇博客后面的内容,以 Go 代码的 nsenter
的实现为例,分析一下在 nsenter
背后 Linux 系统的实现。
Go 代码 nsenter 示例
需要实现的需求是,在一个 Go 进程里去对接处于不同 namespace
的 iptables-nfqueue
。
使用 Go 对接 iptables-nfqueue
,请参考:使用 Go 对接 iptables NFQUEUE 的例子。
对 namespace
的一个误解
在找到 kata-containers/nsenter 之前,一直以为 namespace
是 Go 程序运行的外部环境,导致一个 Go 程序无法处理跨 namespace
的事情。
因为这个错误的理解,导致在某个项目中,将原本可以写成一个 Go 程序的方案、变成了两个 Go 程序,每个 Go 程序各自在一个 namespace
里,进而导致了复杂的方案带来的性能损耗。非常遗憾采用了比较复杂的方案。
nsenter 带来的希望
kata-containers/nsenter 解决了一个 Go 程序对接处于不同 namespace
的 iptables-nfqueue
的需求,使用的代码如下:
1
2
3
4
5
6
|
import knsenter "github.com/kata-containers/runtime/virtcontainers/pkg/nsenter"
func nsenter(pid int, fn func() error) error {
netns := knsenter.Namespace{PID: pid, Type: knsenter.NSTypeNet}
return knsenter.NsEnter([]knsenter.Namespace{netns}, fn)
}
|
对接指定 PID 的网络 namespace
的 iptables-nfqueue
时,即可使用如下代码:
1
2
3
4
5
6
7
8
9
10
|
import "github.com/subgraph/go-nfnetlink/nfqueue"
func openNfqueue(pid, nfqueueNum int) (ch <-chan *nfqueue.NFQPacket, err error) {
err = nsenter(pid, func() error {
nfq := nfqueue.NewNFQueue(nfqueueNum)
ch, err = nfq.Open()
return err
})
return
}
|
这段代码的作用是:
- 进入到指定 PID 的网络
namespace
里
- 监听指定序号的 NFQUEUE
- 退出网络
namespace
- 返回一个网络包
chan
调用 openNfqueue
函数后,从 chan
取到的网络包就是从指定 PID 的网络 namespace
里 NFQUEUE 发过来的网络包。
这里产生一个疑问:为什么退出网络 namespace
后,用户程序的 NFQUEUE 不会从网络 namespace
里退出呢?
nsenter 背后的原理
在阅读内核源代码之前,完全想不明白内核怎么实现 namespace
切换。
iptables-nfqueue
是基于 netlink
跟用户程序进行通信;则在用户程序中,会创建一个 netlink
的 socket fd
去接收 NFQUEUE 发送过来的网络包;则该 socket fd
是怎么跟网络 namespace
绑定到一起的?
内核是怎么看网络 namespace
的?
其实在内核中,一个网络 namespace
就是一个 struct net
,并采用引用计数来管理 struct net
的生命周期。所以,在创建网络 namespace
后,需要有某个地方引用这个网络 namespace
;譬如,使用命令 ip netns add nstest
后,会创建一个文件 /var/run/netns/nstest
,并将这个文件的 fd
引用这个网络 namespace
。
在内核中,进程和线程都是使用 struct task_struct
来管理,并使用 struct nsproxy
去引用 namespace
。
1
2
3
4
5
6
7
8
9
10
11
12
|
// https://github.com/torvalds/linux/blob/master/include/linux/nsproxy.h#L31
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 time_namespace *time_ns;
struct time_namespace *time_ns_for_children;
struct cgroup_namespace *cgroup_ns;
};
|
nsenter 函数的实现原理
openNfqueue
函数的实现过程如下:
- 将当前线程的
nsproxy->net_ns
指向目标网络 namespace
- 创建
netlink socket
,使得 socket fd
引用目标网络 namespace
- 将当前线程的
nsproxy->net_ns
指向原来的网络 namespace
nsenter 的源代码分析
以 openNfqueue
Go 函数为例,翻开内核源代码看看 nsenter
涉及的源代码:
1
2
3
4
|
// https://github.com/kata-containers/runtime/blob/master/virtcontainers/pkg/nsenter/nsenter.go#L101
if err := unix.Setns(int(nsFile.Fd()), nsFlag); err != nil {
return fmt.Errorf("Error switching to ns %v: %v", nsFile.Name(), err)
}
|
最终调用的是 setns
系统调用。
setns 系统调用
setns
系统调用的文档:setns(2)。
接着,暴力翻看 setns
系统调用的源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
SYSCALL_DEFINE2(setns, int, fd, int, flags) // kernel/nsproxy.c
|-->file = fget(fd);
|-->ns = get_proc_ns(file_inode(file)); // 根据 fd 获取对应的 namespace
|-->prepare_nsset(flags, &nsset);
|-->validate_ns(&nsset, ns);
| |-->ns->ops->install(nsset, ns);
| \
| \
| \
| |-->netns_install(struct nsproxy *nsproxy, struct ns_common *ns) // net/core/net_namespace.c
| |-->put_net(nsproxy->net_ns); // 减少一个网络 namespace 引用
| |-->nsproxy->net_ns = get_net(net); // 增加一个网络 namespace 引用,并且变更 nsproxy->net_ns 为目标网络 namespace
|-->commit_nsset(&nsset);
|-->switch_task_namespaces(me, nsset->nsproxy);
|-->p->nsproxy = new; // 变更当前 task_struct->nsproxy 为新的 nsproxy,新的 nsproxy->net_ns 引用了目标网络 namespace
|
setns
实现的功能就是将当前线程引用的 namespace
引用目标 namespace
。
socket 系统调用
跟踪 nfqueue.NewNFQueue(nfqueueNum)
函数的实现,最终会调用 syscall.Socket
系统调用去创建一个 socket fd
。
1
2
3
4
5
6
7
8
|
// https://github.com/subgraph/go-nfnetlink/blob/master/nfnl_sock.go#L69
func NewNetlinkSocket(bus int) (*NetlinkSocket, error) {
fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_RAW, bus)
if err != nil {
return nil, err
}
...
}
|
socket
系统调用的文档:socket(2)。
暴力翻看 socket
系统调用的源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) // net/socket.c
|-->__sys_socket(family, type, protocol);
|-->sock_create(family, type, protocol, &sock);
| |-->__sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0); // 拿到当前的 task_struct->nsproxy->net_ns 所引用的网络 namespace
| |-->pf = rcu_dereference(net_families[family]);
| |-->pf->create(net, sock, protocol, kern); // 以 netlink socket 为例,看下创建 socket 的时候是 socket 哪个属性引用网络 namespace 的
| /
| /
| /
| |-->netlink_create(struct net *net, struct socket *sock, int protocol, int kern) // net/netlink/af_netlink.c
| |-->__netlink_create(net, sock, cb_mutex, protocol, kern);
| |-->sk = sk_alloc(net, PF_NETLINK, GFP_KERNEL, &netlink_proto, kern); // net/core/sock.c
| | |-->sock_net_set(sk, net); // include/net/sock.h
| | |-->write_pnet(&sk->sk_net, net); // include/net/net_namespace.h
| | |-->pnet->net = net;
| |-->sock_init_data(sock, sk);
| |-->sock->sk = sk; // net/core/sock.c
|-->sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); // 保存 struct socket 跟 fd 的映射关系
|
最终,一个 socket fd
引用网络 namespace
的地方是 struct socket->struct sock->sk_net.net
。
nsenter 函数退出时
查看 NsEnter
函数:
1
2
3
4
5
6
7
8
9
|
// https://github.com/kata-containers/runtime/blob/master/virtcontainers/pkg/nsenter/nsenter.go#L168
for nsType, pair := range targetNSList {
// Switch to targeted namespace.
if err := setNS(pair.targetNS, nsType); err != nil {
return fmt.Errorf("error switching to ns %v: %v", pair.targetNS.Name(), err)
}
// Switch back to initial namespace after closure return.
defer setNS(pair.threadNS, nsType)
}
|
从这段代码可知,在函数退出时(defer setNS
),会将当前线程发生变更的 namespace
恢复回原来的 namespace
。setNS
函数是对 setns
系统调用的封装。
小结
至此,nsenter
再无秘密。
揭开 nsenter
的神秘面纱后,发现 nsenter
在项目中的用武之地还真不少,就是因为它带给用户程序处理跨 namespace
的能力。
总结
经历了几次带着问题来查看源代码的过程,既能提升阅读源代码的能力,也扎实了计算机基础知识;特别是在阅读源代码解决了问题之后的那感觉,真美好。
阅读内核源代码的工具推荐: