從今天開始的幾天,我們將探索 Linux 中的 file descriptor、system call、虛擬檔案系統 (VFS)、proc
檔案系統,以及 Network namespace 之間的關係。
今天,我們會先從應用的角度來看看這些系統是如何被串聯在一起的,接著再深入介紹它們的實現細節。
在第三天介紹網路命名空間時,我們就有提到一個例子。
int main() {
int fd = open("/proc/123/ns/net", O_RDONLY | O_CLOEXEC); // 打開Process 123 的 network namespace 檔案
setns(fd, CLONE_NEWNET); // 切換當前進程的 network namespace 為Process 123 的 namespace
/* 執行其他操作 */
return 0;
}
這個例子中,首先透過 open system call 打開 process 123 的 network namespace 檔案,然後拿到一個 file descriptor。接著,透過 setns system call 將當前 process 的 network namespace 切換到 process 123 所使用的 namespace。
這裡引出幾個問題:
proc
檔案系統是如何輸出 process 的資訊?/proc/123/ns/net
這個檔案是如何與 Network namespace 關聯的?setns
如何從 file descriptor 知道要切換到哪個 Network namespace?ip netns exec
指令解析另一個常見的例子是 ip netns exec
指令。我們通常會先使用 ip netns add
指令建立一個新的 Network namespace,然後使用 ip netns exec
指令進入該 namespace,執行特定命令。
ip netns add net1
ip netns exec net1 <command> ...
我們一樣透過追蹤 ip 指令的程式碼,了解 ip 指令是如何實現的 namespace 切換的。ip 指令是在 iproute2 這個專案中。
// iproute2: ip/ipnetns.c
static int netns_exec(int argc, char **argv)
{
fork();
do_switch(arg[0]);
execvp(cmd, argv)
}
static int do_switch(void *arg)
{
char *netns = arg;
vrf_reset();
return netns_switch(netns);
}
以上是簡化後的程式碼,netns_exec
的邏輯非常簡單,首先 fork 建立一個新的 process,接著透過 do_switch
函數將這個新 process 切換到指定的 Network namespace,然後再使用execvp函數執行命令。這時候 do_switch 函數接收的是我們取的namespace的名子 ("net1")。
接著我們來看看 do_switch
呼叫的 netns_switch
函數的具體實現:
// iproute2: lib/namespace.c
int netns_switch(char *name)
{
char net_path[PATH_MAX];
int netns_fd;
/* 重點 */
snprintf(net_path, sizeof(net_path), "%s/%s", NETNS_RUN_DIR, name);
netns_fd = open(net_path, O_RDONLY | O_CLOEXEC);
setns(netns_fd, CLONE_NEWNET);
close(netns_fd);
/* 重點 */
unshare(CLONE_NEWNS);
mount("", "/", "none", MS_SLAVE | MS_REC, NULL);
umount2("/sys", MNT_DETACH);
mount(name, "/sys", "sysfs", mountflags, NULL)
bind_etc(name);
return 0;
}
netns_switch
的邏輯比較複雜,除了切換 network namespace
以外,也還需要對檔案系統做一些處理,但我們就只關注 network namespace
切換的部分。和前面的範例一樣,netns_switch
打開了 /var/run/netns/<namespace name>
檔案,並使用 setns
system call 切換到該 namespace
。
不過這時候的檔案就不是 /proc
下面的檔案了,而是 NETNS_RUN_DIR
下面的檔案,也就是 /var/run/netns
這個路徑。
經常使用 ip netns
指令的人都知道,/var/run/netns
是 ip netns
保存建立的 network namespace
檔案的路徑,所以 ip netns exec net1
就是打開 /var/run/netns/net1
。
另外像是,如果要使用 ip netns exec
指令去訪問一個 docker container
的 network namespace
,有兩種方式可以將 network namespace
檔案複製到 /var/run/netns
中,並進行操作:
SandboxKey
檔案: 透過 docker inspect
找到 container 的 SandboxKey
,其指向的檔案位於 /var/run/docker/netns/
,我們可以將該檔案複製到 /var/run/netns
中進行操作。> docker inspect xxxxx
{
...
"SandboxKey": "/var/run/docker/netns/d0358da4a049",
...
}
> ln -sfT /var/run/docker/netns/d0358da4a049 /var/run/netns/d0358da4a049
> ip netns exec d0358da4a049 <command> ...
/proc/$pid/ns/net
link 到 /var/run/netns/
中,這樣也可以透過 ip netns
指令進行操作。pid=$(docker inspect -f '{{.State.Pid}}' ${container_id})
mkdir -p /var/run/netns/
ln -sfT /proc/$pid/ns/net /var/run/netns/$container_id
ip netns add
的實現本質上來說,ip netns add
之後建立出來的 /var/run/netns/<name>
檔案,其實也是從 /proc
來的。我們可以追蹤一下 ip netns add
的 source code:
static int netns_add(int argc, char **argv, bool create)
{
...
snprintf(netns_path, sizeof(netns_path), "%s/%s", NETNS_RUN_DIR, name);
...
unshare(CLONE_NEWNET)
...
strcpy(proc_path, "/proc/self/ns/net");
...
mount(proc_path, netns_path, "none", MS_BIND, NULL);
...
}
首先,netns_path
一樣被設置為 /var/run/netns/<name>
,接著呼叫 unshare
函數。unshare
system call 會建立一個新的 network namespace
,同時自身這個 process 會進入新的這個 network namespace
,所以 /proc/self/ns/net
會指向到新的 network namespace
。接著透過 mount bind
的方式,將 /proc/self/ns/net
掛載到 /var/run/netns/<name>
。
因此,可以得知 ip netns add
產生的 namespace
檔案也是從 /proc
來的,這讓我們了解了 proc
在管理 network namespace
中的重要角色。
透過這幾個範例,我們可以看到 Linux 系統如何透過 proc
檔案系統與利用 system call(例如 open
、setns
和 unshare
)來進行 network namespace
的切換與管理。
接下來的幾天,我們會繼續深入探討這些系統底層的運作機制。