1 概述
在linux协议栈中引入网络命名空间,是为了支持网络协议栈的多个实例,而这些协议栈的隔离就是通过命名空间来实现的,一个net namespace为进程提供一个完全独立的网络协议栈的视图,包括网络设备接口、ipv4和ipv6协议栈、ip路由表、防火墙规则、sockets等。一个net namespace提供了一份独立的网络环境,就跟独立的系统一样。一个物理设备只能存在于一个net namespace中,但可以从一个net namespace移动到另一个net namespace。网络系统在初始化的时候会初始化一个初始网络命名空间,即init_net命名空间。后续创建的net namespace命名空间会和init_net一起通过list项组织起来,且每个网络设备都对应一个命名空间,同一命名空间下的网络设备通过dev_base_head组织在一起(如图1所示)。当一个netnamespace被销毁时,物理设备会被自动移回到init net namespace。
2 重要数据结构
2.1 struct net
struct net {
atomic_t passive; /* To decided when the network
* namespace should be freed.
*/
atomic_t count; /*To decided when the network
* namespace should be shut down.
*/
#ifdef NETNS_REFCNT_DEBUG
atomic_t use_count; /* To track references we
* destroy on demand
*/
#endif
spinlock_t rules_mod_lock;
//网络命名空间是扁平结构,采用链表连接
structlist_head list; /* list of network namespaces */
//链入全局cleanup_list链表,用于表示要释放的net
structlist_head cleanup_list; /* namespaces on death row */
//连接到exit_list链表的都会被执行pernet_operations的exit函数
structlist_head exit_list; /* Use only net_mutex */
structproc_dir_entry *proc_net; //对应/proc/net
structproc_dir_entry *proc_net_stat; //对应/proc/net/stat
#ifdef CONFIG_SYSCTL
structctl_table_set sysctls;
#endif
structsock *rtnl; /* rtnetlink socket */
structsock *genl_sock;
structlist_head dev_base_head; // netnamespace中网络设备链表
structhlist_head *dev_name_head;//netnamespace中网络设备名链表
structhlist_head *dev_index_head; //netnamespace中网络设备索引链表
unsignedint dev_base_seq; /* protected by rtnl_mutex */
/*core fib_rules */ //路由、ip相关部分
structlist_head rules_ops;
structnet_device *loopback_dev; /* The loopback */
structnetns_core core;
structnetns_mib mib;
structnetns_packet packet;
structnetns_unix unx;
structnetns_ipv4 ipv4;
#if IS_ENABLED(CONFIG_IPV6)
structnetns_ipv6 ipv6;
#endif
#if defined(CONFIG_IP_DCCP) ||defined(CONFIG_IP_DCCP_MODULE)
structnetns_dccp dccp;
#endif
#ifdef CONFIG_NETFILTER
structnetns_xt xt;
#if defined(CONFIG_NF_CONNTRACK) ||defined(CONFIG_NF_CONNTRACK_MODULE)
structnetns_ct ct;
#endif
structsock *nfnl;
structsock *nfnl_stash;
#endif
#ifdef CONFIG_WEXT_CORE
structsk_buff_head wext_nlevents;
#endif
structnet_generic __rcu *gen;
/*Note : following structs are cache line aligned */
#ifdef CONFIG_XFRM
structnetns_xfrm xfrm;
#endif
structnetns_ipvs *ipvs;
};
struct net表示网络命名空间的结构体,网络命名空间就是将网络中需要局部化的信息,如设备、路由和ipv4协议等放入该结构中,每个命名空间通过空间内的相关信息进行数据报文传送。
2.2 struct pernet_operations
struct pernet_operations {
structlist_head list; //用于链入全局链表pernet_list
int(*init)(struct net *net); //网络命名空间每个子系统的初始化函数
void(*exit)(struct net *net); //网络命名空间每个子系统的退出函数
void(*exit_batch)(struct list_head *net_exit_list); //也是退出函数
int*id; //本实例序号,标识在structnet中gen的ptr数组中位置,通过idr树管理
size_tsize; //本结构大小
};
struct pernet_operations定义了net namespace每个子系统的操作函数集。当创建一个新的net namespace时,都会调用其中的init函数进行初始化。同理,当销毁一个net namespace时,会调用其中的exit函数退出。
3 网络命名空间初始化
网络模块的初始化中,不同的模块可能用到不同的内核初始化宏定义。比如:
1) pure_initcall(net_ns_init);——网络命名空间初始化
2) fs_initcall(af_unix_init);——unix模块初始化
3) module_init(arptable_filter_init);——arp表初始化
4) subsys_initcall(net_dev_init);——网络设备的初始化
对于这些宏定义,到底哪个先执行,哪个后执行呢?展开这些宏定义如下表:
在内核初始化的过程中,会按照.initcall0.init-.initcall7.init的顺序执行,即对于网络命名空间而言,其初始化函数net_ns_init()会在其各个子系统的注册函数前执行。其代码具体如下:
该函数主要做了一下几件事:
1) 创建网络命名空间的内存池(以后创建新的网络命名空间时,申请新的struct net结构的内存就是用这个内存池中的内存)
2) 创建工作队列
3) 初始化init_net(全局网络命名空间)的gen变量
4) 调用setup_net()初始化init_net的所有子系统(因为net_ns_init比网络命名空间各个子系统的注册时间更早,所以setup_net并没有做实质性工作)
5) 把init_net链接到系统全局链表net_namespace_list中
再来看看setup_net函数,关键操作如下:
list_for_each_entry(ops, &pernet_list,list) {
error = ops_init(ops, net);
if (error < 0)
gotoout_undo;
}
pernet_list是系统的全局链表,在register_pernet_subsys时就会把ops挂到这个链表上。这个函数的主要功能就是遍历已注册的网络命名空间的所有子系统,调用其初始化函数初始化每一个网络命名空间子系统。
4 注册网络命名空间子系统
网络命名空间子系统的注册可以分为两类,一类是通过register_pernet_subsys注册,一类是通过register_pernet_device注册。
4.1.1 register_pernet_subsys
内核中网络很多地方用到了该注册函数,代表了把网络系统的全局资源进行局部化。系统启动时,与网络相关的初始化中,会调用很多的register_pernet_subsys()函数,初始化很多网络子系统,比如路由子系统、防火墙、netlink等等。每当初始化一个网络子系统,就会利用该函数将子系统相应的函数操作集加入全局链表first_device中。
其中first_device有指向了另一个全局变量pernet_list,二者定义如下:
static LIST_HEAD(pernet_list);
static struct list_head *first_device =&pernet_list;
4.1.2 register_pernet_device
register_pernet_device用于注册设备相关的网络命名空间子系统。
这个函数和register_pernet_subsys类似,不同之处在于如果first_device指向pernet_list,则改变first_device的指针,使其指向链表中第一个设备相关的网络命名空间子系统的ops。如图所示:
所有网络命名空间子系统的ops都会链接到图中的双向链表中。用了上面两个不同的注册函数的目的在于,在链表中将ops归类:把subsys相关的放到一起,把device相关的放到一起,并让全局变量first_device指向第一个device相关的ops。图中的pernet_list是双向链表的头节点。整个操作系统中,双向链表pernet_list中的ops会有100来个。
register_pernet_subsys和register_pernet_device除了将网络空间子系统ops加入全局链表中,还会对现有命名空间的子系统进行初始化:
register_pernet_operations ->__register_pernet_operations:
list_add_tail(&ops->list, list);
if(ops->init || (ops->id && ops->size)) {
for_each_net(net){
error= ops_init(ops, net);
if(error)
gotoout_undo;
list_add_tail(&net->exit_list,&net_exit_list);
}
}
在__register_pernet_operations中把注册的ops挂载到全局链表,并遍历系统中现有的网络命名空间进行子系统初始化操作,之所有要遍历所有的子系统,是因为操作系统中很多都是模块化的,在系统运行一段时候后才加载,这样有些命名空间在子系统加载前就创建好了,因此要对现存的所有网络命名空间进行初始化操作(init_net的子系统的初始化也在这进行)。
5 创建网络命名空间
在执行clone或者unshare系统调用时,如果有传入CLONE_NEWNET参数,即新建网络命名空间时,会调用copy_net_ns函数创建新的网络命名空间:
在创建网络命名空间时,和网络命名空间初始化时做的事情相似,主要就是做了以下几件事:
1. 先分配一个struct net结构(系统初始化时是使用全局结构struct net init_net)
2. 通过调用setup_net,调用网络命名空间中的各个子系统的初始化函数初始化struct net结构
3. 将新的net命名空间加入到网络命名空间全局链表net_namespace_list中
6 获取网络命名空间
一个网络命名空间提供了一份独立的网络环境,包括网络设备、进程、sockets等,因此也可以从这些资源中,获取相应的网络命名空间。
6.1.1 通过进程获取网络命名空间
命名空间在内核里被抽象成数据结构struct nsproxy,每个进程的task_struct结构中有一个struct nsproxy指针,指向进程所属的命名空间。函数get_net_ns_by_pid()便是通过进程的pid获取taks_struct结构,获取进程所在的网络命名空间:
tsk = find_task_by_vpid(pid);
if(tsk) {
structnsproxy *nsproxy;
nsproxy= task_nsproxy(tsk);
if(nsproxy)
net= get_net(nsproxy->net_ns);
}
6.1.2 通过套接字获取网络命名空间
对于和网络命名空间相关的文件记录其所属的命名空间,所以可以根据其获取网络命名空间。
struct net *get_net_ns_by_fd(int fd)
{
structproc_inode *ei;
structfile *file;
structnet *net;
file= proc_ns_fget(fd);
if(IS_ERR(file))
returnERR_CAST(file);
ei= PROC_I(file->f_dentry->d_inode);
if(ei->ns_ops == &netns_operations)
net= get_net(ei->ns);
else
net= ERR_PTR(-EINVAL);
fput(file);
returnnet;
}
6.1.3 通过socket获取网络命名空间
创建套接字的流程基本如下:
sock_create() --> __sock_create(current->nsproxy->net_ns,) --> pf-->create()-->sk_alloc(net, )-->sock_net_set()
在创建套接字时会通过调用协议对应注册的操作结构的pf-->create()函数,将当前进程所在的网络命名空间传递下去,并在sk_alloc()中赋给sk->sk_net指针, 因此可以通过struct sock结构获取网络命名空间,具体如下:
struct net *sock_net(const struct sock *sk)
{
returnread_pnet(&sk->sk_net);
}
协议对应的sock操作函数,是在每个协议初始化时进行注册的,比如ipv4,其sock操作函数集如下:
static const struct net_proto_familyinet_family_ops = {
.family= PF_INET,
.create= inet_create,
.owner = THIS_MODULE,
};
在协议初始化时:
static int __init inet_init(void)
{
……
sock_register(&inet_family_ops);
……
}
会通过sock_register()将操作函数集注册到全局数组net_families中,通过协议族便可以直接从数组中获取对应的sock操作函数结构。
6.1.4 通过网络设备获取网络命名空间
在创建/注册/初始化各种网络设备的时候,会将网络设备与其所属的网络命名空间相关联:
void dev_net_set(struct net_device *dev,struct net *net)
{
#ifdef CONFIG_NET_NS
release_net(dev->nd_net);
dev->nd_net= hold_net(net);
#endif
}
因此也可以通过strcutnet_device结构获取网络命名空间:
struct net *dev_net(const struct net_device*dev)
{
returnread_pnet(&dev->nd_net);
}