昨天我們探討了 setns
的整個實作流程,並發現 process 123 使用的 network namespace 結構 (net
) 的 ns_common
會保存在當我們打開 /proc/123/ns/net
時,透過 file->f_inode->i_private
取得。我們今天就要進一步了解,這個關聯是如何建立的。
/proc/<pid>/ns
目錄的生成機制首先,我們需要檢視 proc 檔案系統是如何生成 /proc/<pid>/ns
目錄的。
// proc/base.c
static const struct pid_entry tgid_base_stuff[] = {
DIR("task", S_IRUGO|S_IXUGO, proc_task_inode_operations, proc_task_operations),
DIR("fd", S_IRUSR|S_IXUSR, proc_fd_inode_operations, proc_fd_operations),
DIR("map_files", S_IRUSR|S_IXUSR, proc_map_files_inode_operations, proc_map_files_operations),
DIR("fdinfo", S_IRUGO|S_IXUGO, proc_fdinfo_inode_operations, proc_fdinfo_operations),
DIR("ns", S_IRUSR|S_IXUGO, proc_ns_dir_inode_operations, proc_ns_dir_operations),
#ifdef CONFIG_NET
DIR("net", S_IRUGO|S_IXUGO, proc_net_inode_operations, proc_net_operations),
#endif
REG("comm", S_IRUGO|S_IWUSR, proc_pid_set_comm_operations),
...
可以看到,ns
是一個目錄,且其 inode_operations
是 proc_ns_dir_inode_operations
,file_operations
則是 proc_ns_dir_operations
。
因此,當我們打開 /proc/123/ns/net
時,會呼叫 proc_ns_dir_inode_operations.lookup
來建立/proc/123/ns/net
的 dentry 和 inode。
// fs/proc/namespaces.c
static struct dentry *proc_ns_dir_lookup(struct inode *dir,
struct dentry *dentry, unsigned int flags)
{
struct task_struct *task = get_proc_task(dir); // 取得 process 123 的 task_struct
const struct proc_ns_operations **entry, **last;
unsigned int len = dentry->d_name.len;
struct dentry *res = ERR_PTR(-ENOENT);
last = &ns_entries[ARRAY_SIZE(ns_entries)];
for (entry = ns_entries; entry < last; entry++) {
if (strlen((*entry)->name) != len)
continue;
if (!memcmp(dentry->d_name.name, (*entry)->name, len)) // 找到 name 為 net 的 entry
break;
}
if (entry == last)
goto out;
res = proc_ns_instantiate(dentry, task, *entry); // 執行 proc_ns_instantiate
out:
put_task_struct(task);
out_no_task:
return res;
}
這段 lookup
函數相當複雜,我們可以分段解析:
/proc/123/ns
的 inode 和 /proc/123/ns/net
的空 dentry。get_proc_task
取得 process 123 的 task_struct
,並將其保存到 task
。ns_entries
陣列中找到一個名稱為 net
的 entry。/proc/123/ns/net
的空 dentry、task_struct
和找到的 entry
傳遞給 proc_ns_instantiate
處理。static const struct proc_ns_operations *ns_entries[] = {
#ifdef CONFIG_NET_NS
&netns_operations, // network namespace
#endif
#ifdef CONFIG_UTS_NS
&utsns_operations,
#endif
#ifdef CONFIG_IPC_NS
&ipcns_operations,
#endif
#ifdef CONFIG_PID_NS
&pidns_operations,
&pidns_for_children_operations,
#endif
#ifdef CONFIG_USER_NS
&userns_operations,
#endif
&mntns_operations,
#ifdef CONFIG_CGROUPS
&cgroupns_operations,
#endif
#ifdef CONFIG_TIME_NS
&timens_operations,
&timens_for_children_operations,
#endif
};
這裡的 entry
就是昨天介紹過的 proc_ns_operations
,而 netns_operations
則是專門用於處理 network namespace 的操作結構。
// net/core/net_namespace.c
const struct proc_ns_operations netns_operations = {
.name = "net",
.type = CLONE_NEWNET,
.get = netns_get,
.put = netns_put,
.install = netns_install,
.owner = netns_owner,
};
其中 netns_operations
,就是昨天介紹由 network namespace 系統定義的,當然他的 name 欄位就對應到 net
。
回到 proc_ns_dir_lookup
,最終呼叫 proc_ns_instantiate
,將 /proc/123/ns/net
的 dentry、process 123 和 netns_operations
傳入進行進一步處理。
// fs/proc/namespaces.c
static struct dentry *proc_ns_instantiate(struct dentry *dentry,
struct task_struct *task, const void *ptr)
{
const struct proc_ns_operations *ns_ops = ptr; // netns_operations
struct inode *inode;
struct proc_inode *ei;
inode = proc_pid_make_inode(dentry->d_sb, task, S_IFLNK | S_IRWXUGO); // 軟連結
if (!inode)
return ERR_PTR(-ENOENT);
ei = PROC_I(inode);
inode->i_op = &proc_ns_link_inode_operations; // 設置 inode_operations
ei->ns_ops = ns_ops;
pid_update_inode(task, inode);
d_set_d_op(dentry, &pid_dentry_operations);
return d_splice_alias(inode, dentry);
}
proc_ns_instantiate 實現的是 lookup 函數的邏輯,我們可以關注幾個重點:
S_IFLNK
(symbolic link)軟連結檔案。i_op
(inode_operations) 是 proc_ns_link_inode_operations
。ns_ops
(即 netns_operations
)會被保存到 ei->ns_ops
,struct proc_inode {
struct pid *pid;
unsigned int fd;
union proc_op op;
struct proc_dir_entry *pde;
struct ctl_table_header *sysctl;
struct ctl_table *sysctl_entry;
struct hlist_node sibling_inodes;
const struct proc_ns_operations *ns_ops;
struct inode vfs_inode;
} __randomize_layout;
proc_inode 是 proc 設計的一個擴充的 inode 結構,用來保存 proc 檔案系統需要的 process 資訊 (task_struct) 等,也將 VFS 的 inode 結構內嵌在 proc_inode 裡面。當透過 proc_pid_make_inode 申請一個 inode 時,實際上建立的是 proc_inode 實例,然後返回子結構 vfs_inode。所以 PROC_I macro 可以透過 container_of 拿到外層的 proc_inode 結構。
所以可以發現 /proc/123/ns/net
是一個 symbolic link 檔案,對於軟連結檔案,你透過 open system call 打開時,最終 file 結構指向不是打開的那個檔案,而是連結的目標檔案。
在 Linux 的檔案系統中,symbolic link 是一種特殊的檔案類型,透過它可以連結到其他檔案。對於 symbolic link 檔案,相關的 inode 具有兩個重要的函數:readlink
和 get_link
。
static const struct inode_operations proc_ns_link_inode_operations = {
.readlink = proc_ns_readlink,
.get_link = proc_ns_get_link,
.setattr = proc_setattr,
};
int (*readlink) (struct dentry *, char __user *,int);
const char * (*get_link) (struct dentry *, struct inode *, struct delayed_call *);
readlink
的功能是取得該 symbolic link 所指向的檔案名稱。
> ls /proc/1/ns -l
total 0
lrwxrwxrwx 1 root root 0 九 19 22:02 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 九 19 22:02 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 九 19 22:02 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0 九 19 22:02 net -> 'net:[4026531992]'
lrwxrwxrwx 1 root root 0 九 19 22:02 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 九 19 22:02 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 九 19 22:02 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 九 19 22:02 uts -> 'uts:[4026531838]'
ls 指令看到的 net:[4026531992]
,就致 net 這個 symbolic link 檔案連結過去的檔案名稱。
接下來,我們來探討 /proc/123/ns/net
inode 使用的 proc_ns_readlink
函數是如何取得 symbolic link 的目標檔案名稱。
static int proc_ns_readlink(struct dentry *dentry, char __user *buffer, int buflen)
{
struct inode *inode = d_inode(dentry);
const struct proc_ns_operations *ns_ops = PROC_I(inode)->ns_ops;
struct task_struct *task;
char name[50];
int res = -EACCES;
task = get_proc_task(inode); // 從 inode 拿到 proc_inode,然後取得 process 123 的 task_struct
...
res = ns_get_name(name, sizeof(name), task, ns_ops);
...
}
在這段程式碼中,proc_ns_readlink
函數首先從 inode 取得與該 inode 關聯的 task_struct
,並呼叫 ns_get_name
函數來取得目標檔案的名稱。ns_get_name
函數的定義位於 fs/nsfs.c
。
終於我們要進入到今天的另外一個主角 NameSpace File System (NSFS)。這個是 Linux 專門針對 /proc/123/ns/net
這類指向 namespace 結構的檔案專門設計的檔案系統。
特別注意,我使用的 kernel版本是 6.6.47,從 6.6.47 到最新的 6.11 中間,nsfs 檔案系統的程式碼有大幅度修改,函數名稱和實作內容都有蠻大的變化。
NSFS 是一種特殊的虛擬檔案系統,具有以下幾個特性:
inode
的 i_private
會指向該 namespace 實例的 ns_common
結構體。/proc/<pid>/ns/*
軟連結對應的檔案。這個掛起來的檔案系統,只是在 kernel 有對應的 vfsmount 掛載資料結構存在,但沒有掛載到 VFS 檔案樹上面,所以並沒有路徑可以直接找到這個預設的 NSFS 檔案系統,mount 指令也看不到。<namespace type>:<inode number>
,使用 proc_ns_operations.name
作為 namespace type,然後使用 ns_common
結構的 inum
作為 inode numberls /proc/123/ns/net
會顯示檔名 net:[4026531992]
。下圖說明了打開 /proc/123/ns/net
後, NSFS 的運作方式:
在這張圖中,我們可以看到 /proc/123/ns/net
是一個 symbolic link 檔案,透過 open
系統呼叫會 "follow" 到 net:[4026531992]
,並取得對應的 ns_common
結構。
前面說到 proc_ns_readlink
會呼叫 ns_get_name
函數來取得 NSFS 檔案的名稱:
// fs/nsfs.c
int ns_get_name(char *buf, size_t size, struct task_struct *task,
const struct proc_ns_operations *ns_ops) // 傳入 netns_operations
{
struct ns_common *ns;
int res = -ENOENT;
const char *name;
ns = ns_ops->get(task); // proc_ns_operations GET API
if (ns) {
name = ns_ops->real_ns_name ? : ns_ops->name;
res = snprintf(buf, size, "%s:[%u]", name, ns->inum); // net:[4026531992]
ns_ops->put(ns);
}
return res;
}
從這段程式碼可以看出,NSFS 的檔案名稱是動態產生的,而非從某個 dentry 讀取出來的。這裡使用 ns_ops.name
取得 namespace 的類型名稱 (例如 net
),並使用 ns_ops->get
取得對應的 ns_common
結構,最終將 ns->inum
作為 inode number。
我們可以從這裡發現這真的是一個純虛擬的檔案系統,他的檔案名稱直接是造出來的,而不是從某個 dentry 讀出來的,這邊會透過輸入的 ns_ops.name 拿到 namespace 的類型名稱 (net),然後透過其面介紹的ns_ops->get 拿到對應namepsace類型的ns_common結構,這邊是使用net的ns_ops,所以就會拿到 network namespace的ns_common,然後就真的用 ns->inum當作inode number了。
透過前面的探討,我們瞭解到,當我們打開 /proc/123/ns/net
時,/proc/123/ns/net
會軟連結到 NSFS 檔案系統中,代表 prcess 123 使用的 network namespace 的檔案。明天,我們將進一步探討打開 /proc/123/ns/net
時,系統是如何將 file 結構連結到 NSFS 檔案的,並瞭解 NSFS 檔案的 dentry 與 inode 是如何產生的。