本文是翻譯一篇國(guó)外博客的內(nèi)容:https://www.toptal.com/linux/separation-anxiety-isolating-your-system-with-linux-namespaces
隨著Docker捌蚊、Linux Containers等工具的出現(xiàn)渗饮,把Linux進(jìn)程隔離到進(jìn)程所屬的系統(tǒng)環(huán)境中變的很容易恢暖。讓一組程序在不需要虛擬機(jī)的情況下盯蝴,運(yùn)行在一個(gè)真實(shí)的Linux機(jī)器上并且這些程序不會(huì)互相影響變的完全有可能禁谦。這些工具對(duì)于PaaS供應(yīng)商來(lái)說(shuō)如虎添翼显设。但是到底發(fā)生了什么在這種現(xiàn)象下面?
這些工具依賴了一些Linux kernel的功能和組件涩盾。一些其中的功能是最近一段時(shí)間才發(fā)布的十气,并且需要給kernel打補(bǔ)丁才能使用。其中的一個(gè)重要的組建Linux namespaces是2.6.24版本中的一個(gè)功能春霍,這個(gè)版本在2008年發(fā)布砸西。
已經(jīng)熟悉chroot的人已經(jīng)有了一些關(guān)于Linux namespaces能做什么和怎么樣使用namespaces有了一個(gè)基本的認(rèn)知。chroot允許進(jìn)程作為root去訪問(wèn)任意目錄(不會(huì)影響其他的進(jìn)程), Linux namespaces允許操作系統(tǒng)的其他方面也變的可以獨(dú)立的被修改址儒,包括進(jìn)程樹(shù)芹枷、網(wǎng)絡(luò)接口,進(jìn)程間通信資源等离福。
為什么使用Namespaces隔離進(jìn)程
在只有一個(gè)用戶的計(jì)算機(jī)中杖狼,一個(gè)單獨(dú)的系統(tǒng)環(huán)境就可以滿足。但是在一個(gè)想要運(yùn)行多個(gè)服務(wù)的服務(wù)器中妖爷,各個(gè)服務(wù)是隔離運(yùn)行的是有必要的蝶涩,這樣會(huì)更加安全和穩(wěn)定。想象一種情況絮识,有一個(gè)服務(wù)器中運(yùn)行著多個(gè)服務(wù)绿聘,其中一個(gè)服務(wù)被入侵了,在這種情況下次舌,這個(gè)入侵者就能夠利用這個(gè)服務(wù)并以自己的方式去入侵其他服務(wù)熄攘,并且有可能對(duì)整個(gè)服務(wù)器都造成影響。Namespaces隔離可以提供一個(gè)安全的環(huán)境去排除這種風(fēng)險(xiǎn)彼念。
像Docker這種命名空間工具可以很好的控制進(jìn)程對(duì)系統(tǒng)資源的使用挪圾,PaaS提供商讓這些工具變的很流行。像Heroku和Google App Engine使用這些工具去隔離和運(yùn)行多個(gè)web服務(wù)在同一個(gè)真實(shí)的硬件上逐沙。這些工具允許他們?nèi)ミ\(yùn)行每一個(gè)程序并且不用擔(dān)心其中的程序使用了太多系統(tǒng)資源或者干擾運(yùn)行在同一臺(tái)機(jī)器上的程序活著發(fā)生沖突哲思。當(dāng)這些進(jìn)程隔離時(shí),甚至可以讓每個(gè)隔離環(huán)境擁有一套不同的依賴軟件集合吩案。
如果你已經(jīng)使用過(guò)了像Docker這樣的工具棚赔,你已經(jīng)知道了這些工具是有著把進(jìn)程隔離到一個(gè)小的containers的能力。運(yùn)行在Docker Container中的進(jìn)程就像運(yùn)行在虛擬機(jī)上徘郭,不過(guò)container是比虛擬機(jī)輕量許多靠益。一個(gè)典型的虛擬機(jī)是在你的操作系統(tǒng)上模仿硬件層,并且虛擬機(jī)會(huì)運(yùn)行另外一個(gè)操作系統(tǒng)在虛擬硬件層上残揉。這樣你可以運(yùn)行進(jìn)程在一個(gè)虛擬機(jī)內(nèi)胧后,并且完全和真正的操作系統(tǒng)隔離。但是虛擬機(jī)非常重抱环!從另一個(gè)方面講绩卤,Docker Container使用了一些真實(shí)的操作系統(tǒng)中重要的功能途样,包括napespaces,并確保類似于虛擬機(jī)的隔離級(jí)別濒憋,Docker Container并沒(méi)有模擬仿真硬件何暇,也不會(huì)讓一臺(tái)機(jī)器上運(yùn)行另一個(gè)操作系統(tǒng). 這讓Docker container變得很輕量。
Process Namespace 進(jìn)程命名空間
從歷史上看凛驮,Linux內(nèi)核一直維護(hù)著一個(gè)進(jìn)程樹(shù)裆站。這個(gè)樹(shù)包含了每一個(gè)指向運(yùn)行在父子層上的進(jìn)程的引用。給一個(gè)進(jìn)程足夠的權(quán)限和并滿足一些條件黔夭,這個(gè)進(jìn)程就可以訪問(wèn)其他進(jìn)程通過(guò)附著一個(gè)追蹤器宏胯,甚至可以殺死其它進(jìn)程。
根據(jù)Linux namespaces的介紹本姥,實(shí)現(xiàn)多個(gè)嵌套進(jìn)程樹(shù)變得有可能肩袍。每一個(gè)進(jìn)程樹(shù)可以擁有整個(gè)隔離的進(jìn)程集合。這樣能夠保證屬于某個(gè)進(jìn)程樹(shù)的進(jìn)程不會(huì)被訪問(wèn)或殺死婚惫,事實(shí)上氛赐,甚至無(wú)法知道其它兄弟或父進(jìn)程里面是否有進(jìn)程。
每一次Linux系統(tǒng)在啟動(dòng)的時(shí)候先舷,它都會(huì)從一個(gè)進(jìn)程ID是1的進(jìn)程開(kāi)始艰管。這個(gè)進(jìn)程是進(jìn)程樹(shù)的根,并且它通過(guò)執(zhí)行正確的維護(hù)工作和開(kāi)始正確的守護(hù)程序/服務(wù)初始化剩下的系統(tǒng)蒋川。所有的其它進(jìn)程都會(huì)在這個(gè)根進(jìn)程下運(yùn)行牲芋。進(jìn)程空間允許使用自己PID 1進(jìn)程衍生出新的進(jìn)程樹(shù)。執(zhí)行此操作的進(jìn)程保留在原始樹(shù)中的父進(jìn)程空間中捺球,但它使子進(jìn)程成為其自己的進(jìn)程樹(shù)的根缸浦。
在PID命名空間的隔離下,在子命名空間下的進(jìn)程沒(méi)有辦法知道父進(jìn)程的存在氮兵。然后裂逐,在父命名空間的進(jìn)程能夠看到在子命名空間的全部?jī)?nèi)容, 就好像這些進(jìn)程是在父命名空間內(nèi)。
創(chuàng)建一個(gè)內(nèi)嵌子命名空間是可能的胆剧,一個(gè)進(jìn)程開(kāi)始一個(gè)子進(jìn)程在一個(gè)新的PID命名空間絮姆,并且這個(gè)子進(jìn)程創(chuàng)建出另外的進(jìn)程在新的PID命名空間醉冤,以此類推秩霍。
根據(jù)PID namespaces的介紹,單個(gè)進(jìn)程能夠關(guān)聯(lián)多個(gè)進(jìn)程蚁阳,每個(gè)進(jìn)程空間對(duì)應(yīng)一個(gè)進(jìn)程铃绒。在Linux的源碼中,我們能看到一個(gè)名字是pid的結(jié)構(gòu)體螺捐,這個(gè)結(jié)構(gòu)體原本是用來(lái)追蹤單個(gè)PID颠悬,現(xiàn)在通過(guò)使用一個(gè)名字是upid的結(jié)構(gòu)體用來(lái)追蹤多個(gè)PIDs矮燎。
struct upid {
int nr; // the PID value
struct pid_namespace *ns; // namespace where this PID is relevant
// ...
};
struct pid {
// ...
int level; // number of upids
struct upid numbers[0]; // array of upids
};
想要?jiǎng)?chuàng)建一個(gè)新的PID命名空間,必須的調(diào)用clone()這個(gè)系統(tǒng)調(diào)用赔癌,并且傳入一個(gè)特殊的標(biāo)示CLONE_NEWPID诞外。然后一些在下面討論的其它命名空間可以使用unshare()系統(tǒng)調(diào)用創(chuàng)建,一個(gè)PID namespace只能在一個(gè)新的進(jìn)程被啟動(dòng)之后使用clone()創(chuàng)建灾票。一旦clone()被調(diào)用并傳入這個(gè)標(biāo)識(shí)峡谊,這個(gè)新進(jìn)程立即在一個(gè)新的PID namespace中運(yùn)行并且在一個(gè)新的進(jìn)程樹(shù)下面。這個(gè)過(guò)程是可以使用一個(gè)簡(jiǎn)單的C語(yǔ)言來(lái)演示的:
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
static char child_stack[1048576];
static int child_fn() {
printf("PID: %ld\n", (long)getpid());
return 0;
}
int main() {
pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_NEWPID | SIGCHLD, NULL);
printf("clone() = %ld\n", (long)child_pid);
waitpid(child_pid, NULL, 0);
return 0;
}
編譯并且使用root軟件運(yùn)行這個(gè)權(quán)限刊苍,你將會(huì)看到類似下面的輸出:
clone() = 5304
PID: 1
這個(gè)是1的PID是child_fn打印出來(lái)的.
雖然上面關(guān)于namespaces的代碼比起在其它語(yǔ)言中的“Hello, world”并沒(méi)有長(zhǎng)多少既们,但是在這背后很多事情會(huì)發(fā)生。clone()函數(shù)就如果你想象的那樣正什,會(huì)通過(guò)克隆當(dāng)前的進(jìn)程并且開(kāi)始執(zhí)行在child_fn()中的代碼創(chuàng)建一個(gè)新的進(jìn)程. 然后啥纸,在做這些的同時(shí),新進(jìn)程會(huì)從原來(lái)的進(jìn)程樹(shù)中分離婴氮,并且系統(tǒng)會(huì)為這個(gè)新進(jìn)程創(chuàng)建一個(gè)新的進(jìn)程樹(shù)斯棒。
用下面的函數(shù)去替換static int child_fn()函數(shù),可以把在被隔離的空間的父進(jìn)程的PID打印出來(lái):
static int child_fn() {
println("Parent PIDL %ld\n", (long)getpid());
return 0;
}
運(yùn)行這個(gè)程序之后會(huì)看到下面的結(jié)果:
clone() = 11449
Parent PID: 0
注意這個(gè)在隔離空間的父進(jìn)程PID是0莹妒,表明這個(gè)進(jìn)程沒(méi)有父進(jìn)程名船。嘗試再一次運(yùn)行下面同樣的程序,這次旨怠,刪除在clone)()里的CLONE_NEWPID標(biāo)識(shí)
pid_t child_pid = clone(child_fn, child_stack+1048576, SIGCHLD, NULL);
這次渠驼,你看到的父進(jìn)程PID不再是0:
clone() = 11561
Parent PID: 11560
這只是我們這個(gè)博客里的第一步,這些進(jìn)程并沒(méi)有被限制訪問(wèn)其它共享資源鉴腻。例如迷扇,網(wǎng)絡(luò)接口:如果被上面代碼創(chuàng)建出的子進(jìn)程監(jiān)聽(tīng)80借口,它將會(huì)阻止所有其它在這個(gè)系統(tǒng)上的進(jìn)程去監(jiān)聽(tīng)爽哎。
Linux Network Namespace
Network namespaces在這變得很有用蜓席。一個(gè)網(wǎng)絡(luò)命名空間允許每個(gè)進(jìn)程去監(jiān)聽(tīng)一組完全不同的網(wǎng)絡(luò)接口,甚至每個(gè)網(wǎng)絡(luò)命名空間的環(huán)回接口也不同课锌。
使用CLONE_NEWNET和clone()函數(shù)可以把一個(gè)進(jìn)程隔離到它自己的網(wǎng)絡(luò)命名空間厨内。
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
static char child_stack[1048576];
static int child_fn() {
printf("New `net` Namespace:\n");
system("ip link");
printf("\n\n");
return 0;
}
int main() {
printf("Original `net` Namespace:\n");
system("ip link");
printf("\n\n");
pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_NEWPID | CLONE_NEWNET | SIGCHLD, NULL);
waitpid(child_pid, NULL, 0);
return 0;
}
Output:
Original `net` Namespace:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp4s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 00:24:8c:a1:ac:e7 brd ff:ff:ff:ff:ff:ff
New `net` Namespace:
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
這里發(fā)生了什么? 物理以太網(wǎng)設(shè)備 enp4s0 屬于全局網(wǎng)絡(luò)命名空間渺贤,正如在此命名空間運(yùn)行的“ip”工具所指示的那樣雏胃。然后,在新的網(wǎng)絡(luò)命名空間中的物理接口是不可用的志鞍。而且瞭亮,在原本網(wǎng)絡(luò)命名空間的回環(huán)設(shè)備是激活的,但是在子網(wǎng)絡(luò)命名空間是不可用的固棚。
為了在子命令空間中提供一個(gè)有用的網(wǎng)絡(luò)接口统翩,有必要去創(chuàng)建額外的虛擬網(wǎng)絡(luò)接口仙蚜,這個(gè)接口可以橫跨多個(gè)命名空間。一旦完成這些厂汗,就可以去創(chuàng)建以太網(wǎng)橋了委粉,甚至可以在命名空間之間路由數(shù)據(jù)包。最后娶桦,為了讓這些功能工作艳丛,一個(gè)rounting process必須運(yùn)行在全局網(wǎng)絡(luò)命名空間去接收來(lái)自物理接口的流量并且通過(guò)合適的虛擬接口把流量路由到正確的子網(wǎng)絡(luò)命名空間√宋桑或許你現(xiàn)在能明白了為什么類似于Docker的幫忙做了全部繁重的事情的工具如此受歡迎氮双!
想要手動(dòng)做到這些,你可以創(chuàng)建一對(duì)虛擬以太網(wǎng)霎匈,它通過(guò)在父命名空間運(yùn)行一條命令來(lái)連接父子命名空間戴差。
ip link add name veth0 type veth peer name veth1 netns <pid>
這里,pid應(yīng)該被在子命名空間的進(jìn)程ID替換铛嘱,這個(gè)子命名空間可以被父命名空間看到暖释。運(yùn)行這個(gè)命令會(huì)建立一個(gè)類似于管道的連接在兩個(gè)命名空間之間。父命名空間維護(hù)著veth0墨吓,子命名空間維護(hù)著veth1球匕。任何從一端進(jìn)入的流量都會(huì)從另一端出來(lái),就像連接兩個(gè)真實(shí)節(jié)點(diǎn)的真實(shí)網(wǎng)卡那樣帖烘。因此亮曹,這邊的虛擬以太網(wǎng)連接必須被分配一個(gè)IP地址。
Mount Namespace
Linux為系統(tǒng)所有的掛載點(diǎn)維護(hù)了一個(gè)數(shù)據(jù)結(jié)構(gòu)秘症。它包含了一些類似下面的信息:那個(gè)磁盤分區(qū)被掛載了照卦,它們被掛載到了哪里,它們是否是可讀的等等乡摹。使用 Linux 命名空間役耕,可以克隆這種數(shù)據(jù)結(jié)構(gòu),以便不同命名空間下的進(jìn)程可以更改掛載點(diǎn)聪廉,而不會(huì)相互影響瞬痘。
創(chuàng)建一個(gè)隔離的掛載命名空間與執(zhí)行chroot()有著相似的效果。chroot()是一個(gè)不錯(cuò)的方法板熊,但是它并不能完全的隔離框全,并且它的作用只局限在root掛載點(diǎn)。創(chuàng)建一個(gè)隔離的掛載命名空間允許每一個(gè)隔離的進(jìn)程有完全不同的關(guān)于整個(gè)系統(tǒng)掛載點(diǎn)的視圖邻邮。這允許你為每個(gè)隔離的進(jìn)程設(shè)置不同的根掛載點(diǎn)以及被指定的其他掛載點(diǎn)竣况。小心的使用本教材克婶,你可以避免任何系統(tǒng)底層信息的泄漏筒严。
想要達(dá)到這種效果需要給clone()函數(shù)穿一個(gè)CLONE_NEWNS的標(biāo)識(shí)丹泉。
clone(child_fn, child_stack+1048576, CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | SIGCHLD, NULL)
在剛開(kāi)始的時(shí)候,子進(jìn)程能看到跟父進(jìn)程完全一樣的掛載點(diǎn)鸭蛙。但是子進(jìn)程位于一個(gè)新的掛載命名空間時(shí)摹恨,這個(gè)子進(jìn)程能掛載或者取消掛載任何想要掛載的內(nèi)容,并且這個(gè)改變不會(huì)影響到在這個(gè)系統(tǒng)中的父命名空間以及其他掛載命名空間娶视。例如晒哄,如果父進(jìn)程有一個(gè)特殊的磁盤分區(qū)掛載到了根節(jié)點(diǎn),這個(gè)隔離的進(jìn)程剛開(kāi)始將會(huì)看到相同的掛載到root節(jié)點(diǎn)的磁盤分區(qū)肪获,但是寝凌,當(dāng)隔離進(jìn)程嘗試將根分區(qū)更改為其他分區(qū)時(shí),隔離掛載命名空間的好處是顯而易見(jiàn)的孝赫,因?yàn)楦闹粫?huì)影響隔離掛載命名空間较木。
其它命名空間
還有一個(gè)其它的命名空間,user namespace青柄, IPC namespace伐债, UTS namespace, 進(jìn)程可以被隔離到其中致开。user namespace允許一個(gè)進(jìn)程在namespace中擁有root的權(quán)限峰锁,同時(shí)不能訪問(wèn)在命名空間之外的進(jìn)程。被IPC namespace隔離的進(jìn)程可以有自己的進(jìn)程間通信資源双戳,例如System V IPC 和 POSIX 消息虹蒋。UTS 命名空間隔離了系統(tǒng)的兩個(gè)特定標(biāo)識(shí)符:節(jié)點(diǎn)名和域名。
這有一個(gè)小例子展示UTS namespace是如何隔離的
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/utsname.h>
#include <sys/wait.h>
#include <unistd.h>
static char child_stack[1048576];
static void print_nodename() {
struct utsname utsname;
uname(&utsname);
printf("%s\n", utsname.nodename);
}
static int child_fn() {
printf("New UTS namespace nodename: ");
print_nodename();
printf("Changing nodename inside new UTS namespace\n");
sethostname("GLaDOS", 6);
printf("New UTS namespace nodename: ");
print_nodename();
return 0;
}
int main() {
printf("Original UTS namespace nodename: ");
print_nodename();
pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_NEWUTS | SIGCHLD, NULL);
sleep(1);
printf("Original UTS namespace nodename: ");
print_nodename();
waitpid(child_pid, NULL, 0);
return 0;
}
這個(gè)程序產(chǎn)生下面的輸出:
Original UTS namespace nodename: XT
New UTS namespace nodename: XT
Changing nodename inside new UTS namespace
New UTS namespace nodename: GLaDOS
Original UTS namespace nodename: XT
在這里飒货, child_fn() 打印節(jié)點(diǎn)名千诬,將其更改為其他內(nèi)容,然后再次打印膏斤。 更改自然僅發(fā)生在新的 UTS 命名空間內(nèi)徐绑。
Cross-Namespace Communication 跨Namespace通信
建立一種在父子命名空間的通信是有必要的。這可能是為了在隔離的環(huán)境中進(jìn)行配置工作莫辨,或者只是保留從外部查看該環(huán)境狀況的能力傲茄。一種能夠做到這樣的方法是運(yùn)行一個(gè)SSH后臺(tái)程序在namespace中。你可以有一個(gè)隔離的SSH后臺(tái)程序在每一個(gè)網(wǎng)絡(luò)命名空間中沮榜。但是有多個(gè)SSH后臺(tái)程序運(yùn)行會(huì)使用很多重要的資源例如內(nèi)存盘榨。擁有一個(gè)特殊的“init”過(guò)程再次被證明是一個(gè)好主意的地方。
“init”進(jìn)程可以在父命名空間和子命名空間之間建立通信通道蟆融。該通道可以基于 UNIX 套接字草巡,甚至可以使用 TCP。想要?jiǎng)?chuàng)建一個(gè)跨越兩個(gè)不同掛載命名空間的UNIX套接字型酥,你需要先創(chuàng)建一個(gè)子進(jìn)程山憨,然后創(chuàng)建UNIX套接字查乒,然后把子進(jìn)程隔離到一個(gè)分割的掛載命名空間。但是怎么能先創(chuàng)建進(jìn)程之后再隔離它郁竟?Linux提供了unshare()玛迄。這個(gè)特殊的系統(tǒng)調(diào)用允許一個(gè)進(jìn)程將自己與原始命名空間隔離,而不是讓父進(jìn)程讓子進(jìn)程隔離棚亩。例如蓖议,下面的代碼有著跟上面提到的網(wǎng)絡(luò)命名空間同樣的效果:
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
static char child_stack[1048576];
static int child_fn() {
// calling unshare() from inside the init process lets you create a new namespace after a new process has been spawned
unshare(CLONE_NEWNET);
printf("New `net` Namespace:\n");
system("ip link");
printf("\n\n");
return 0;
}
int main() {
printf("Original `net` Namespace:\n");
system("ip link");
printf("\n\n");
pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_NEWPID | SIGCHLD, NULL);
waitpid(child_pid, NULL, 0);
return 0;
}
并且,由于“init”進(jìn)程是你自己設(shè)計(jì)的讥蟆,你可以先讓它做所有必要的工作勒虾,然后在執(zhí)行目標(biāo)子進(jìn)程之前將它自己與系統(tǒng)的其余部分隔離開(kāi)來(lái)。
結(jié)論
本教程只是對(duì)如何在 Linux 中使用命名空間的概述瘸彤。 它應(yīng)該讓您對(duì) Linux 開(kāi)發(fā)人員如何開(kāi)始實(shí)施系統(tǒng)隔離有一個(gè)基本的了解从撼,這是 Docker 或 Linux 容器等工具架構(gòu)的一個(gè)組成部分。在大多數(shù)情況下钧栖,使用現(xiàn)有的工具是一個(gè)很好的選擇低零,例如Docker,這些工具已經(jīng)眾所周知并經(jīng)過(guò)測(cè)試拯杠。 但在某些情況下掏婶,擁有你自己的、自定義的進(jìn)程隔離機(jī)制可能是有意義的潭陪,在這種情況下雄妥,本命名空間教程將極大地幫助你。
除了我在本文中介紹的之外依溯,還有更多的事情發(fā)生老厌,并且你可能希望通過(guò)更多方式來(lái)限制目標(biāo)進(jìn)程以增加安全性和隔離性。 不管如何黎炉,希望這可以作為一個(gè)有用的起點(diǎn)枝秤,對(duì)于那些有興趣了解更多關(guān)于 Linux 的命名空間隔離如何真正起作用的人。