昨天我們簡單介紹了 net
結構與 network namespace
的關係。在我們使用 ip
指令或 docker
等工具來建立新的 network namespace
之前,所有的進程都位於一個初始的 network namespace
中。今天我們要探討這個初始的 network namespace
是如何出現的。
除了網路子系統本身,其他的 Kernel 模組,甚至是我們自己撰寫的 Kernel module,也可能需要在 network namespace
被建立或刪除時執行某些操作。為此,Linux Kernel 設計了一個機制來實現這樣的需求。
// include/net/net_namespace.h
struct pernet_operations {
struct list_head list;
int (*init)(struct net *net);
void (*pre_exit)(struct net *net);
void (*exit)(struct net *net);
void (*exit_batch)(struct list_head *net_exit_list);
unsigned int *id;
size_t size;
};
Linux Kernel 定義了 pernet_operations
結構,裡面包含了 init
和 exit
等函數指標,讓 Kernel 模組可以自行撰寫初始化和結束函數,並向網路子系統透過 register_pernet_subsys
或 register_pernet_device
來進行註冊。兩者的差異在於,subsys
的優先度較高,因此所有 subsys
的 init
函數會先被執行,接著才會執行 device
的函數。
從 pernet_operations
的定義中,我們可以看到熟悉的 list_head
,這表示 pernet_operations
是以 list 的方式維護在網路子系統中的。
static int __init net_dev_init(void) // 網路設備子系統初始化
{
...
if (register_pernet_device(&loopback_net_ops)) // 向網路命名空間子系統註冊 loopback_net_ops
goto out;
...
}
// drivers/net/loopback.c
struct pernet_operations __net_initdata loopback_net_ops = {
.init = loopback_net_init,
};
static __net_init int loopback_net_init(struct net *net)
{
struct net_device *dev;
...
dev = alloc_netdev(0, "lo", NET_NAME_PREDICTABLE, loopback_setup); // 建立loopback inteface
...
dev_net_set(dev, net);
err = register_netdev(dev);
net->loopback_dev = dev;
}
這邊舉一個例子,當 network namespace
被建立時,網路子系統會透過註冊一個處理函數 loopback_net_ops
來建立 loopback
介面。
理解了 pernet_operations
的運作方式後,我們可以開始探索第一個 network namespace
的初始化過程。
第一個 network namespace
被稱為 init_net
,它同樣也是對應到一個 net
結構。不同於其他透過動態分配方式產生的 network namespace
,init_net
是直接宣告在 Linux Kernel 的原始碼中。
// net/core/net_namespace.c#L48
struct net init_net;
接著我們就要來看init_net是怎麼被初始化的。開機後,當Linux被載入開始執行後,會執行start_kernel
函數,該函數會依序呼叫各個子系統的方法來初始化Linux kernel。
// source/init/main.c
asmlinkage __visible __init __no_sanitize_address __noreturn __no_stack_protector
void start_kernel(void)
{
...
sched_init();
...
init_IRQ();
softirq_init();
...
net_ns_init(); // 在這裡初始化網路命名空間
...
signals_init();
}
那 net_ns_init 的工作當然就是要來初始化 init_net結構的數值拉。
// net/core/net_namespace.c
void __init net_ns_init(void) {
...
if (setup_net(&init_net, &init_user_ns))
panic("Could not setup the initial network namespace");
...
init_net_initialized = true;
if (register_pernet_subsys(&net_ns_ops))
panic("Could not register network namespace subsystems");
...
}
可以看到 net_ns_init
函數呼叫了 setup_net
,並將 init_net
作為參數。setup_net
是初始化 net
結構的通用函數,無論是 init_net
還是後續建立的 network namespace
,都會使用該函數來進行初始化。完成後,init_net_initialized
會被設為 true
。此外,net_ns_init
還註冊了一組由網路子系統自己定義的 pernet_operations
,即 net_ns_ops
。
// net/core/net_namespace.c
static struct pernet_operations __net_initdata net_ns_ops = {
.init = net_ns_net_init,
.exit = net_ns_net_exit,
};
static __net_init int net_ns_net_init(struct net *net)
{
#ifdef CONFIG_NET_NS
net->ns.ops = &netns_operations;
#endif
return ns_alloc_inum(&net->ns);
}
昨天提到net->ns
是 ns_common
結構體,而 net->ns.inum 是該 namespace
的唯一識別碼。net_ns_ops
會負責初始化 net->ns
,其中,透過 ns_alloc_inum
函數來設置 net->ns.inum
。
// net/core/net_namespace.c
/*
* setup_net runs the initializers for the network namespace object.
*/
static __net_init int setup_net(struct net *net, struct user_namespace *user_ns
{
const struct pernet_operations *ops, *saved_ops;
...
list_for_each_entry(ops, &pernet_list, list) {
error = ops_init(ops, net); // ops->init ? ops->init(ops, net) : 0
if (error < 0)
goto out_undo;
}
...
}
接著我們看到 setup_net 函數,可以看到他使用了一個特殊的list_for_each_entry(ops, &pernet_list, list)
macro,這個跟container_of相關,是專門針對 list_head
結構的 macro
,功能就是遍歷一個list_head的list,然後把值拿出來用。
pernet_list
儲存了所有註冊的 pernet_operations
,因此 setup_net
會依次取出每個 pernet_operations
,並呼叫其 init
函數來完成初始化。
今天我們探討了 Linux 網路子系統如何透過 pernet_operations
提供一個可擴充的 network namespace
初始化機制,以及第一個 network namespace
——init_net
是如何生成的。